Files

222 lines
6.7 KiB
TypeScript

import { expect, test } from "../support/browser/strict-browser-test.ts";
test("runs put, open, remove and reconcile through a native DedicatedWorker, OPFS and IndexedDB", async ({
page,
}) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const runtimeModulePath =
"/src/adapters/storage/opfs/browser-opfs-runtime.ts";
const policyModulePath =
"/src/adapters/storage/opfs/opfs-policy.ts";
const journalModulePath =
"/src/adapters/storage/opfs/indexeddb-opfs-journal.ts";
const {
createBrowserOpfsRuntime,
inspectBrowserOpfsSupport,
} = (await import(
/* @vite-ignore */ runtimeModulePath
)) as typeof import("../../src/adapters/storage/opfs/browser-opfs-runtime.ts");
const {
DEFAULT_OPFS_RUNTIME_POLICY,
resolveOpfsRuntimePolicy,
} = (await import(
/* @vite-ignore */ policyModulePath
)) as typeof import("../../src/adapters/storage/opfs/opfs-policy.ts");
const { opfsJournalDatabaseName } = (await import(
/* @vite-ignore */ journalModulePath
)) as typeof import("../../src/adapters/storage/opfs/indexeddb-opfs-journal.ts");
const policy = resolveOpfsRuntimePolicy();
const support = inspectBrowserOpfsSupport(policy);
if (!support.ok) {
return {
nativeSupport: false as const,
capabilities: support,
};
}
const suffix = crypto.randomUUID().replaceAll("-", "");
const scope = {
namespace: "durable-objects",
authorityToken: `authority_${suffix}`,
namespaceToken: `namespace_${suffix}`,
partitionToken: `partition_${suffix}`,
} as const;
const storagePolicy = {
owner: "browser-capability",
namespace: scope.namespace,
classification: "PERSONAL",
authority: "LOCAL_FIRST",
accountScope: "OPAQUE_PARTITION",
retention: { kind: "EXPLICIT_DELETE" },
softBudgetBytes: 1024 * 1024,
hardBudgetBytes: 2 * 1024 * 1024,
evictionPriority: "USER_AUTHORED",
logoutAction: "EXPORT_THEN_PURGE",
accountDeletionAction: "PURGE_PARTITION",
pressureAction: "RETAIN",
unavailableFallback: "EXPORT_REQUIRED",
} as const;
const runtime = createBrowserOpfsRuntime({
workerUrl: new URL(
"/tests/browser-capabilities/opfs-test.worker.ts",
location.href,
),
workerName: `opfs-capability-${suffix}`,
scope,
storagePolicy,
policy,
});
const bytes = new Uint8Array([3, 1, 4, 1, 5, 9]);
let capabilities: unknown;
let put: unknown;
let opened:
| Awaited<ReturnType<typeof runtime.objects.open>>
| undefined;
let readBytes: number[] = [];
let removed: unknown;
let afterRemove: unknown;
let reconciled: unknown;
try {
capabilities = await runtime.objects.capabilities();
put = await runtime.objects.put({
objectId: "object_browser_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: {
byteLength: bytes.byteLength,
async *stream(signal: AbortSignal) {
if (signal.aborted) {
yield {
ok: false as const,
error: {
code: "ABORTED" as const,
operation: "OBJECT_WRITE" as const,
retryable: false,
recovery: "NONE" as const,
},
};
return;
}
yield { ok: true as const, value: Uint8Array.from(bytes) };
},
},
});
opened = await runtime.objects.open({
objectId: "object_browser_12345678",
});
if (
opened &&
typeof opened === "object" &&
"ok" in opened &&
opened.ok
) {
for await (const chunk of opened.value.source.stream(
new AbortController().signal,
)) {
if (!chunk.ok) throw new Error(chunk.error.code);
readBytes.push(...chunk.value);
}
}
removed = await runtime.objects.remove({
objectId: "object_browser_12345678",
expectedGeneration: 1,
});
afterRemove = await runtime.objects.open({
objectId: "object_browser_12345678",
});
reconciled = await runtime.maintenance.reconcile({
budgetMs: 5_000,
maxTransactions: 100,
});
} finally {
runtime.close();
}
const deletion = await new Promise<string>((resolve) => {
const request = indexedDB.deleteDatabase(
opfsJournalDatabaseName(scope.authorityToken),
);
request.onsuccess = () => resolve("DELETED");
request.onerror = () =>
resolve(request.error?.name ?? "UNKNOWN_ERROR");
request.onblocked = () => resolve("BLOCKED");
});
let opfsCleanup = "ABSENT";
try {
const originRoot = await navigator.storage.getDirectory();
const runtimeRoot = await originRoot.getDirectoryHandle(
DEFAULT_OPFS_RUNTIME_POLICY.rootDirectoryName,
);
const authorities = await runtimeRoot.getDirectoryHandle(
"authorities",
);
await authorities.removeEntry(scope.authorityToken, {
recursive: true,
});
opfsCleanup = "DELETED";
} catch (error) {
if (!(error instanceof DOMException) || error.name !== "NotFoundError") {
throw error;
}
}
return {
nativeSupport: true as const,
capabilities,
put,
opened:
opened &&
typeof opened === "object" &&
"ok" in opened &&
opened.ok
? {
ok: true,
generation: opened.value.descriptor.generation,
}
: opened,
readBytes,
removed,
afterRemove,
reconciled,
deletion,
opfsCleanup,
};
});
if (!result.nativeSupport) {
expect(result.capabilities).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED" },
});
return;
}
expect(result.capabilities).toMatchObject({
ok: true,
value: {
available: true,
dedicatedWorkerRequired: true,
crossContextMutationLockAvailable: true,
},
});
expect(result.put).toMatchObject({
ok: true,
value: { generation: 1, byteLength: 6 },
});
expect(result.opened).toEqual({ ok: true, generation: 1 });
expect(result.readBytes).toEqual([3, 1, 4, 1, 5, 9]);
expect(result.removed).toEqual({ ok: true });
expect(result.afterRemove).toMatchObject({
ok: false,
error: { code: "NOT_FOUND" },
});
expect(result.reconciled).toMatchObject({
ok: true,
value: {
inspectedTransactions: 0,
orphanGcStatus: "COMPLETED",
},
});
expect(result.deletion).toBe("DELETED");
expect(["DELETED", "ABSENT"]).toContain(result.opfsCleanup);
});