222 lines
6.7 KiB
TypeScript
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);
|
|
});
|