Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions apps/mobile/src/connection/environment-owned-data-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Persistence } from "@t3tools/client-runtime/platform";
import { EnvironmentId } from "@t3tools/contracts";
import { describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Logger from "effect/Logger";
import { vi } from "vite-plus/test";

const harness = vi.hoisted(() => ({
clearOutbox: vi.fn<(environmentId: string) => Promise<void>>(),
clearDrafts: vi.fn<(environmentId: string) => Promise<void>>(),
clearComposerErrors: vi.fn(),
}));

vi.mock("../state/thread-outbox-removal", () => ({
clearThreadOutboxEnvironment: harness.clearOutbox,
}));
vi.mock("../state/use-composer-drafts", () => ({
clearComposerDraftsEnvironment: harness.clearDrafts,
}));
vi.mock("../state/thread-composer-error", () => ({
clearThreadComposerErrorsForEnvironment: harness.clearComposerErrors,
}));

import * as EnvironmentOwnedDataCleanup from "./environment-owned-data-cleanup";

const ENVIRONMENT_ID = EnvironmentId.make("environment-1");

describe("EnvironmentOwnedDataCleanup", () => {
it.effect("waits for the other clears and logs a bounded warning when one rejects", () => {
const warnings: Array<unknown> = [];
let releaseDrafts = () => {};
let draftsCleared = false;
// The drafts clear finishes only after the outbox failure is logged.
const logger = Logger.make(({ message }) => {
warnings.push(message);
releaseDrafts();
});
return Effect.gen(function* () {
harness.clearOutbox.mockRejectedValue(new Error("Some queued messages could not be read."));
harness.clearDrafts.mockImplementation(() =>
new Promise<void>((resolve) => {
releaseDrafts = resolve;
}).then(() => {
draftsCleared = true;
}),
);

const cleanup = yield* Persistence.EnvironmentOwnedDataCleanup;
yield* cleanup.clear(ENVIRONMENT_ID);

expect(draftsCleared).toBe(true);
expect(warnings).toEqual([
[
"Could not clear mobile environment-owned data.",
{
environmentId: ENVIRONMENT_ID,
operation: "outbox",
errorType: "error",
errorName: "Error",
stack: expect.any(String),
},
],
]);
}).pipe(
Effect.provide(
Layer.merge(
EnvironmentOwnedDataCleanup.layer,
Logger.layer([logger], { mergeWithExisting: false }),
),
),
);
});
});
48 changes: 48 additions & 0 deletions apps/mobile/src/connection/environment-owned-data-cleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
import { Persistence } from "@t3tools/client-runtime/platform";
import type { EnvironmentId } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";

import { clearThreadComposerErrorsForEnvironment } from "../state/thread-composer-error";
import { clearThreadOutboxEnvironment } from "../state/thread-outbox-removal";
import { clearComposerDraftsEnvironment } from "../state/use-composer-drafts";

class EnvironmentOwnedDataClearError extends Schema.TaggedError<EnvironmentOwnedDataClearError>()(
"EnvironmentOwnedDataClearError",
{ cause: Schema.Defect() },
) {}

export const layer = Layer.succeed(
Persistence.EnvironmentOwnedDataCleanup,
Persistence.EnvironmentOwnedDataCleanup.of({
clear: Effect.fn("Persistence.EnvironmentOwnedDataCleanup.clear")(function (environmentId) {
// Each clear settles on its own: a failure must not interrupt a clear that is still running.
const settle = (
operation: "outbox" | "drafts",
clear: (environmentId: EnvironmentId) => Promise<void>,
) =>
Effect.tryPromise({
try: () => clear(environmentId),
catch: (cause) => new EnvironmentOwnedDataClearError({ cause }),
}).pipe(
Effect.catch((error) =>
Effect.logWarning("Could not clear mobile environment-owned data.", {
environmentId,
operation,
...safeErrorLogAttributes(error.cause),
}),
),
);
return Effect.all(
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
[
settle("outbox", clearThreadOutboxEnvironment),
settle("drafts", clearComposerDraftsEnvironment),
Effect.sync(() => clearThreadComposerErrorsForEnvironment(environmentId)),
],
{ concurrency: "unbounded", discard: true },
);
}),
}),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
30 changes: 4 additions & 26 deletions apps/mobile/src/connection/platform.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
ClientCapabilities,
PlatformConnectionSource,
Persistence,
} from "@t3tools/client-runtime/platform";
import { ClientCapabilities, PlatformConnectionSource } from "@t3tools/client-runtime/platform";
import {
ConnectionBlockedError,
ConnectionTransientError,
Expand All @@ -25,10 +21,8 @@ import { authClientMetadata } from "../lib/authClientMetadata";
import * as Runtime from "../lib/runtime";
import * as MobileStorage from "../persistence/mobile-storage";
import { appAtomRegistry } from "../state/atom-registry";
import { clearThreadOutboxEnvironment } from "../state/thread-outbox-removal";
import { clearComposerDraftsEnvironment } from "../state/use-composer-drafts";
import { clearThreadComposerErrorsForEnvironment } from "../state/thread-composer-error";
import { mobileApplicationActiveWakeup } from "./app-state-wakeups";
import * as EnvironmentOwnedDataCleanup from "./environment-owned-data-cleanup";
import * as ConnectionStorage from "./storage";

function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "online" {
Expand Down Expand Up @@ -255,30 +249,14 @@ const layerPlatformConnectionSource = Layer.succeed(
const layerProvidedConnectionStorage = ConnectionStorage.layer.pipe(Layer.provide(Runtime.layer));
const layerProvidedCapabilities = layerCapabilities.pipe(Layer.provide(Runtime.layer));

const layerEnvironmentOwnedDataCleanup = Layer.succeed(
Persistence.EnvironmentOwnedDataCleanup,
Persistence.EnvironmentOwnedDataCleanup.of({
clear: Effect.fn("Persistence.EnvironmentOwnedDataCleanup.clear")((environmentId) =>
Effect.all(
[
Effect.promise(() => clearThreadOutboxEnvironment(environmentId)),
Effect.promise(() => clearComposerDraftsEnvironment(environmentId)),
Effect.sync(() => clearThreadComposerErrorsForEnvironment(environmentId)),
],
{ concurrency: "unbounded", discard: true },
),
),
}),
);

type ConnectionPlatformLayerSource =
| typeof layerProvidedConnectionStorage
| typeof Runtime.layer
| typeof layerConnectivity
| typeof layerWakeups
| typeof layerProvidedCapabilities
| typeof layerPlatformConnectionSource
| typeof layerEnvironmentOwnedDataCleanup;
| typeof EnvironmentOwnedDataCleanup.layer;

export const layer: Layer.Layer<
Layer.Success<ConnectionPlatformLayerSource>,
Expand All @@ -291,5 +269,5 @@ export const layer: Layer.Layer<
layerWakeups,
layerProvidedCapabilities,
layerPlatformConnectionSource,
layerEnvironmentOwnedDataCleanup,
EnvironmentOwnedDataCleanup.layer,
);
Loading