89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
/**
|
||
* 测试共享 — 真实语义 OPFS mock
|
||
*
|
||
* 模拟 OPFS 的完整语义:
|
||
* - createWritable({ keepExistingData }) + position 追加写
|
||
* - 文件内容以 ArrayBuffer 存储(跨 backend 实例共享于同一 Map)
|
||
* - entries() 迭代、removeEntry 删除
|
||
*
|
||
* 用法:
|
||
* const files = installOPFSMock(new Map());
|
||
* const backend = new OPFSBackend();
|
||
* await backend.open('any');
|
||
*/
|
||
|
||
export interface MockOpfsFile {
|
||
content: ArrayBuffer;
|
||
}
|
||
|
||
export function installOPFSMock(files: Map<string, MockOpfsFile>): {
|
||
files: Map<string, MockOpfsFile>;
|
||
dir: {
|
||
getFileHandle: (name: string, opts?: { create?: boolean }) => Promise<{
|
||
getFile: () => Promise<{ size: number; arrayBuffer: () => Promise<ArrayBuffer> }>;
|
||
createWritable: (wOpts?: { keepExistingData?: boolean }) => Promise<{
|
||
write: (arg: ArrayBuffer | { type: string; position: number; data: ArrayBuffer }) => Promise<void>;
|
||
close: () => Promise<void>;
|
||
}>;
|
||
}>;
|
||
entries: () => AsyncGenerator<[string]>;
|
||
removeEntry: (name: string) => Promise<void>;
|
||
};
|
||
} {
|
||
const getFileHandle = async (name: string, opts?: { create?: boolean }) => {
|
||
if (!files.has(name)) {
|
||
if (!opts?.create) throw new Error(`NotFoundError: ${name}`);
|
||
files.set(name, { content: new ArrayBuffer(0) });
|
||
}
|
||
const entry = files.get(name)!;
|
||
return {
|
||
getFile: async () => ({ size: entry.content.byteLength, arrayBuffer: async () => entry.content }),
|
||
createWritable: async (wOpts?: { keepExistingData?: boolean }) => {
|
||
const w: {
|
||
write: (arg: ArrayBuffer | { type: string; position: number; data: ArrayBuffer }) => Promise<void>;
|
||
close: () => Promise<void>;
|
||
} = {
|
||
write: async (arg) => {
|
||
const keepExisting = wOpts?.keepExistingData ?? false;
|
||
const chunk = (arg as { type?: string }).type === 'write'
|
||
? { data: (arg as { data: ArrayBuffer }).data, position: (arg as { position: number }).position }
|
||
: { data: arg as ArrayBuffer, position: keepExisting ? entry.content.byteLength : 0 };
|
||
const merged = new Uint8Array(chunk.position + chunk.data.byteLength);
|
||
if (keepExisting || chunk.position > 0) {
|
||
merged.set(new Uint8Array(entry.content), 0);
|
||
}
|
||
merged.set(new Uint8Array(chunk.data), chunk.position);
|
||
entry.content = merged.buffer;
|
||
},
|
||
close: async () => { /* no-op */ },
|
||
};
|
||
return w;
|
||
},
|
||
};
|
||
};
|
||
|
||
const dir = {
|
||
getFileHandle,
|
||
entries: async function* () {
|
||
for (const [name] of files) yield [name];
|
||
},
|
||
removeEntry: async (name: string) => {
|
||
files.delete(name);
|
||
},
|
||
};
|
||
|
||
Object.defineProperty(globalThis, 'navigator', {
|
||
value: {
|
||
storage: {
|
||
getDirectory: async () => ({
|
||
getDirectoryHandle: async (_name: string, _opts?: unknown) => dir,
|
||
}),
|
||
},
|
||
},
|
||
configurable: true,
|
||
writable: true,
|
||
});
|
||
|
||
return { files, dir };
|
||
}
|