@korajs/store 1.0.0-beta.0 → 1.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +1 -1
- package/dist/adapters/better-sqlite3.d.ts +1 -1
- package/dist/adapters/better-sqlite3.js +1 -1
- package/dist/adapters/indexeddb.cjs +523 -148
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +4 -2
- package/dist/adapters/indexeddb.d.ts +4 -2
- package/dist/adapters/indexeddb.js +55 -11
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs +207 -187
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +208 -188
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +474 -140
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +45 -8
- package/dist/adapters/sqlite-wasm.d.ts +45 -8
- package/dist/adapters/sqlite-wasm.js +3 -3
- package/dist/{backup-5XZ7THNJ.js → backup-5S7K2Z35.js} +3 -3
- package/dist/blob-fs.cjs +166 -0
- package/dist/blob-fs.cjs.map +1 -0
- package/dist/blob-fs.d.cts +29 -0
- package/dist/blob-fs.d.ts +29 -0
- package/dist/blob-fs.js +128 -0
- package/dist/blob-fs.js.map +1 -0
- package/dist/{chunk-EIVXWF4B.js → chunk-CYO3YKFL.js} +4 -3
- package/dist/chunk-CYO3YKFL.js.map +1 -0
- package/dist/{chunk-ETDJEH2I.js → chunk-GSJ7EEUC.js} +12 -1
- package/dist/chunk-GSJ7EEUC.js.map +1 -0
- package/dist/{chunk-5R4QMY3Z.js → chunk-HS6DBMST.js} +13 -10
- package/dist/chunk-HS6DBMST.js.map +1 -0
- package/dist/chunk-RJIEKEN4.js +751 -0
- package/dist/chunk-RJIEKEN4.js.map +1 -0
- package/dist/{chunk-QL2XVZRN.js → chunk-TUR7RGEP.js} +12 -1
- package/dist/{chunk-QL2XVZRN.js.map → chunk-TUR7RGEP.js.map} +1 -1
- package/dist/{chunk-IOEFNHHP.js → chunk-VDBJQ75K.js} +2 -2
- package/dist/chunk-VDBJQ75K.js.map +1 -0
- package/dist/chunk-W4MTTNEZ.js +55 -0
- package/dist/chunk-W4MTTNEZ.js.map +1 -0
- package/dist/{chunk-OUQDC5Z6.js → chunk-YT7MRFCP.js} +55 -35
- package/dist/chunk-YT7MRFCP.js.map +1 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
- package/dist/{export-audit-VPGQWNR6.js → export-audit-ODGQTQIK.js} +3 -3
- package/dist/index.cjs +923 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +443 -6
- package/dist/index.d.ts +443 -6
- package/dist/index.js +737 -49
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +69 -43
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +3 -3
- package/dist/sqlite-wasm-channel-GGHA2BXU.js +12 -0
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.cts → sqlite-wasm-channel-L6o3bWQK.d.cts} +1 -1
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.ts → sqlite-wasm-channel-L6o3bWQK.d.ts} +1 -1
- package/dist/{types-CyjXwrjW.d.cts → types-BOO9EPt1.d.ts} +10 -3
- package/dist/{types-CJOVKxLw.d.ts → types-BslDOTk0.d.cts} +32 -2
- package/dist/{types-CJOVKxLw.d.cts → types-BslDOTk0.d.ts} +32 -2
- package/dist/{types-DAxCladl.d.ts → types-C_GDavG-.d.cts} +10 -3
- package/package.json +13 -12
- package/dist/adapters/sqlite-wasm-shared-host.cjs +0 -68
- package/dist/adapters/sqlite-wasm-shared-host.cjs.map +0 -1
- package/dist/adapters/sqlite-wasm-shared-host.d.cts +0 -2
- package/dist/adapters/sqlite-wasm-shared-host.d.ts +0 -2
- package/dist/adapters/sqlite-wasm-shared-host.js +0 -66
- package/dist/adapters/sqlite-wasm-shared-host.js.map +0 -1
- package/dist/chunk-5R4QMY3Z.js.map +0 -1
- package/dist/chunk-EIVXWF4B.js.map +0 -1
- package/dist/chunk-ETDJEH2I.js.map +0 -1
- package/dist/chunk-IOEFNHHP.js.map +0 -1
- package/dist/chunk-OUQDC5Z6.js.map +0 -1
- package/dist/chunk-RBPVEAVM.js +0 -425
- package/dist/chunk-RBPVEAVM.js.map +0 -1
- package/dist/sqlite-wasm-channel-H43KEZLM.js +0 -12
- /package/dist/{backup-5XZ7THNJ.js.map → backup-5S7K2Z35.js.map} +0 -0
- /package/dist/{export-audit-VPGQWNR6.js.map → export-audit-ODGQTQIK.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-H43KEZLM.js.map → sqlite-wasm-channel-GGHA2BXU.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/blob-fs.ts","../src/blob/filesystem-blob-store.ts","../src/blob/content-addressed-blob-store.ts"],"sourcesContent":["// Node-only entry point for @korajs/store/blob-fs.\n// Kept as a separate subpath so the filesystem backend (which imports node:fs)\n// never enters a browser bundle's module graph.\nexport { FilesystemBlobStore } from './blob/filesystem-blob-store'\n","import { randomUUID } from 'node:crypto'\nimport { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { type BlobRef, type BlobRefMetadata, createBlobRef, hashBlob } from '@korajs/core'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * A persistent {@link ContentAddressedBlobStore} backed by the filesystem.\n *\n * Node-only (exported from `@korajs/store/blob-fs` so it never enters a browser\n * bundle). Blobs are stored at `<dir>/<hash[0:2]>/<hash>`, sharded by hash prefix\n * so a single directory never holds millions of entries. Writes are atomic (write\n * to a temp file, then rename) so a crash mid-write cannot leave a half-written\n * blob under a valid hash. Reads verify the bytes hash to the requested key.\n *\n * This is the same content-addressed contract as {@link MemoryBlobStore}, so it\n * drops into the blob transfer path (chunk staging, blob destination) unchanged.\n */\nexport class FilesystemBlobStore implements ContentAddressedBlobStore {\n\tconstructor(private readonly dir: string) {}\n\n\tprivate pathFor(hash: string): string {\n\t\treturn join(this.dir, hash.slice(0, 2), hash)\n\t}\n\n\tasync put(bytes: Uint8Array, metadata: BlobRefMetadata = {}): Promise<BlobRef> {\n\t\tconst ref = await createBlobRef(bytes, metadata)\n\t\tconst target = this.pathFor(ref.hash)\n\n\t\t// Dedup: identical content already stored is a no-op.\n\t\tif (await this.exists(target)) {\n\t\t\treturn ref\n\t\t}\n\n\t\tawait mkdir(dirname(target), { recursive: true })\n\t\t// Copy into an exactly-sized buffer so a subarray view writes its own bytes.\n\t\tconst out = new Uint8Array(bytes.byteLength)\n\t\tout.set(bytes)\n\t\t// Atomic write: temp file then rename, so a crash never leaves a partial\n\t\t// blob under a hash that readers would then trust.\n\t\tconst tmp = `${target}.${randomUUID()}.tmp`\n\t\tawait writeFile(tmp, out)\n\t\tawait rename(tmp, target)\n\t\treturn ref\n\t}\n\n\tasync get(hash: string): Promise<Uint8Array | null> {\n\t\tlet contents: Buffer\n\t\ttry {\n\t\t\tcontents = await readFile(this.pathFor(hash))\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t\tconst bytes = new Uint8Array(contents)\n\t\tconst actual = await hashBlob(bytes)\n\t\tif (actual !== hash) {\n\t\t\tthrow new BlobIntegrityError(hash, actual)\n\t\t}\n\t\treturn bytes\n\t}\n\n\tasync has(hash: string): Promise<boolean> {\n\t\treturn this.exists(this.pathFor(hash))\n\t}\n\n\tasync delete(hash: string): Promise<boolean> {\n\t\tconst target = this.pathFor(hash)\n\t\tif (!(await this.exists(target))) {\n\t\t\treturn false\n\t\t}\n\t\tawait rm(target)\n\t\treturn true\n\t}\n\n\tasync size(): Promise<number> {\n\t\tlet count = 0\n\t\tlet shards: string[]\n\t\ttry {\n\t\t\tshards = await readdir(this.dir)\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t\tfor (const shard of shards) {\n\t\t\ttry {\n\t\t\t\tconst entries = await readdir(join(this.dir, shard))\n\t\t\t\tcount += entries.filter((name) => !name.endsWith('.tmp')).length\n\t\t\t} catch (error) {\n\t\t\t\tif (!isNotFound(error)) {\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn count\n\t}\n\n\tasync list(): Promise<string[]> {\n\t\tlet shards: string[]\n\t\ttry {\n\t\t\tshards = await readdir(this.dir)\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t\tconst hashes: string[] = []\n\t\tfor (const shard of shards) {\n\t\t\ttry {\n\t\t\t\tconst entries = await readdir(join(this.dir, shard))\n\t\t\t\tfor (const name of entries) {\n\t\t\t\t\tif (!name.endsWith('.tmp')) {\n\t\t\t\t\t\thashes.push(name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (!isNotFound(error)) {\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn hashes\n\t}\n\n\tprivate async exists(path: string): Promise<boolean> {\n\t\ttry {\n\t\t\tawait stat(path)\n\t\t\treturn true\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n}\n\nfunction isNotFound(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === 'object' && error !== null && (error as { code?: string }).code === 'ENOENT'\n\t)\n}\n","import { type BlobRef, type BlobRefMetadata, createBlobRef, hashBlob } from '@korajs/core'\n\n/**\n * Thrown when a blob read fails its integrity check: the bytes stored under a\n * hash do not actually hash to that value (corruption or tampering).\n */\nexport class BlobIntegrityError extends Error {\n\tconstructor(\n\t\treadonly expectedHash: string,\n\t\treadonly actualHash: string,\n\t) {\n\t\tsuper(`Blob integrity check failed: content under hash ${expectedHash} hashes to ${actualHash}`)\n\t\tthis.name = 'BlobIntegrityError'\n\t}\n}\n\n/**\n * A content-addressed store for the bytes behind `blob` fields.\n *\n * Blobs are keyed by the SHA-256 hash of their content, so identical content is\n * stored exactly once (deduplication) and a peer never needs to transfer bytes\n * it already holds. Reads are integrity-checked: the returned bytes are verified\n * to hash to the requested key.\n */\nexport interface ContentAddressedBlobStore {\n\t/**\n\t * Store bytes and return their content-addressed reference. Storing content\n\t * that already exists is a no-op that returns the same reference (dedup).\n\t */\n\tput(bytes: Uint8Array, metadata?: BlobRefMetadata): Promise<BlobRef>\n\t/**\n\t * Retrieve the bytes for a hash, or null if absent. The returned bytes are\n\t * verified to hash to the requested value; a mismatch throws\n\t * {@link BlobIntegrityError} rather than returning corrupt data.\n\t */\n\tget(hash: string): Promise<Uint8Array | null>\n\t/** Whether the store holds content for the given hash. */\n\thas(hash: string): Promise<boolean>\n\t/** Remove the content for a hash. Returns whether anything was removed. */\n\tdelete(hash: string): Promise<boolean>\n\t/** Number of distinct blobs held. */\n\tsize(): Promise<number>\n\t/**\n\t * List the hashes of every blob currently held. Used by garbage collection to\n\t * find content no live reference points to. Order is unspecified.\n\t */\n\tlist(): Promise<string[]>\n}\n\n/**\n * In-memory {@link ContentAddressedBlobStore}. The reference backend used for\n * tests and for environments without a persistent blob backend yet. Persistent\n * backends (OPFS client-side, filesystem/S3 server-side) implement the same\n * interface.\n */\nexport class MemoryBlobStore implements ContentAddressedBlobStore {\n\tprivate readonly blobs = new Map<string, Uint8Array>()\n\n\tasync put(bytes: Uint8Array, metadata: BlobRefMetadata = {}): Promise<BlobRef> {\n\t\tconst ref = await createBlobRef(bytes, metadata)\n\t\tif (!this.blobs.has(ref.hash)) {\n\t\t\t// Copy so later mutation of the caller's buffer cannot corrupt the store.\n\t\t\tconst stored = new Uint8Array(bytes.byteLength)\n\t\t\tstored.set(bytes)\n\t\t\tthis.blobs.set(ref.hash, stored)\n\t\t}\n\t\treturn ref\n\t}\n\n\tasync get(hash: string): Promise<Uint8Array | null> {\n\t\tconst stored = this.blobs.get(hash)\n\t\tif (stored === undefined) {\n\t\t\treturn null\n\t\t}\n\t\tconst actualHash = await hashBlob(stored)\n\t\tif (actualHash !== hash) {\n\t\t\tthrow new BlobIntegrityError(hash, actualHash)\n\t\t}\n\t\t// Return a copy so callers cannot mutate the stored bytes.\n\t\tconst out = new Uint8Array(stored.byteLength)\n\t\tout.set(stored)\n\t\treturn out\n\t}\n\n\tasync has(hash: string): Promise<boolean> {\n\t\treturn this.blobs.has(hash)\n\t}\n\n\tasync delete(hash: string): Promise<boolean> {\n\t\treturn this.blobs.delete(hash)\n\t}\n\n\tasync size(): Promise<number> {\n\t\treturn this.blobs.size\n\t}\n\n\tasync list(): Promise<string[]> {\n\t\treturn [...this.blobs.keys()]\n\t}\n}\n"],"mappings":"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","names":["import_core"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BlobRefMetadata, BlobRef } from '@korajs/core';
|
|
2
|
+
import { C as ContentAddressedBlobStore } from './content-addressed-blob-store-Dcj3SB9r.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A persistent {@link ContentAddressedBlobStore} backed by the filesystem.
|
|
6
|
+
*
|
|
7
|
+
* Node-only (exported from `@korajs/store/blob-fs` so it never enters a browser
|
|
8
|
+
* bundle). Blobs are stored at `<dir>/<hash[0:2]>/<hash>`, sharded by hash prefix
|
|
9
|
+
* so a single directory never holds millions of entries. Writes are atomic (write
|
|
10
|
+
* to a temp file, then rename) so a crash mid-write cannot leave a half-written
|
|
11
|
+
* blob under a valid hash. Reads verify the bytes hash to the requested key.
|
|
12
|
+
*
|
|
13
|
+
* This is the same content-addressed contract as {@link MemoryBlobStore}, so it
|
|
14
|
+
* drops into the blob transfer path (chunk staging, blob destination) unchanged.
|
|
15
|
+
*/
|
|
16
|
+
declare class FilesystemBlobStore implements ContentAddressedBlobStore {
|
|
17
|
+
private readonly dir;
|
|
18
|
+
constructor(dir: string);
|
|
19
|
+
private pathFor;
|
|
20
|
+
put(bytes: Uint8Array, metadata?: BlobRefMetadata): Promise<BlobRef>;
|
|
21
|
+
get(hash: string): Promise<Uint8Array | null>;
|
|
22
|
+
has(hash: string): Promise<boolean>;
|
|
23
|
+
delete(hash: string): Promise<boolean>;
|
|
24
|
+
size(): Promise<number>;
|
|
25
|
+
list(): Promise<string[]>;
|
|
26
|
+
private exists;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { FilesystemBlobStore };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BlobRefMetadata, BlobRef } from '@korajs/core';
|
|
2
|
+
import { C as ContentAddressedBlobStore } from './content-addressed-blob-store-Dcj3SB9r.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A persistent {@link ContentAddressedBlobStore} backed by the filesystem.
|
|
6
|
+
*
|
|
7
|
+
* Node-only (exported from `@korajs/store/blob-fs` so it never enters a browser
|
|
8
|
+
* bundle). Blobs are stored at `<dir>/<hash[0:2]>/<hash>`, sharded by hash prefix
|
|
9
|
+
* so a single directory never holds millions of entries. Writes are atomic (write
|
|
10
|
+
* to a temp file, then rename) so a crash mid-write cannot leave a half-written
|
|
11
|
+
* blob under a valid hash. Reads verify the bytes hash to the requested key.
|
|
12
|
+
*
|
|
13
|
+
* This is the same content-addressed contract as {@link MemoryBlobStore}, so it
|
|
14
|
+
* drops into the blob transfer path (chunk staging, blob destination) unchanged.
|
|
15
|
+
*/
|
|
16
|
+
declare class FilesystemBlobStore implements ContentAddressedBlobStore {
|
|
17
|
+
private readonly dir;
|
|
18
|
+
constructor(dir: string);
|
|
19
|
+
private pathFor;
|
|
20
|
+
put(bytes: Uint8Array, metadata?: BlobRefMetadata): Promise<BlobRef>;
|
|
21
|
+
get(hash: string): Promise<Uint8Array | null>;
|
|
22
|
+
has(hash: string): Promise<boolean>;
|
|
23
|
+
delete(hash: string): Promise<boolean>;
|
|
24
|
+
size(): Promise<number>;
|
|
25
|
+
list(): Promise<string[]>;
|
|
26
|
+
private exists;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { FilesystemBlobStore };
|
package/dist/blob-fs.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlobIntegrityError
|
|
3
|
+
} from "./chunk-W4MTTNEZ.js";
|
|
4
|
+
|
|
5
|
+
// src/blob/filesystem-blob-store.ts
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
import { createBlobRef, hashBlob } from "@korajs/core";
|
|
10
|
+
var FilesystemBlobStore = class {
|
|
11
|
+
constructor(dir) {
|
|
12
|
+
this.dir = dir;
|
|
13
|
+
}
|
|
14
|
+
dir;
|
|
15
|
+
pathFor(hash) {
|
|
16
|
+
return join(this.dir, hash.slice(0, 2), hash);
|
|
17
|
+
}
|
|
18
|
+
async put(bytes, metadata = {}) {
|
|
19
|
+
const ref = await createBlobRef(bytes, metadata);
|
|
20
|
+
const target = this.pathFor(ref.hash);
|
|
21
|
+
if (await this.exists(target)) {
|
|
22
|
+
return ref;
|
|
23
|
+
}
|
|
24
|
+
await mkdir(dirname(target), { recursive: true });
|
|
25
|
+
const out = new Uint8Array(bytes.byteLength);
|
|
26
|
+
out.set(bytes);
|
|
27
|
+
const tmp = `${target}.${randomUUID()}.tmp`;
|
|
28
|
+
await writeFile(tmp, out);
|
|
29
|
+
await rename(tmp, target);
|
|
30
|
+
return ref;
|
|
31
|
+
}
|
|
32
|
+
async get(hash) {
|
|
33
|
+
let contents;
|
|
34
|
+
try {
|
|
35
|
+
contents = await readFile(this.pathFor(hash));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (isNotFound(error)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
const bytes = new Uint8Array(contents);
|
|
43
|
+
const actual = await hashBlob(bytes);
|
|
44
|
+
if (actual !== hash) {
|
|
45
|
+
throw new BlobIntegrityError(hash, actual);
|
|
46
|
+
}
|
|
47
|
+
return bytes;
|
|
48
|
+
}
|
|
49
|
+
async has(hash) {
|
|
50
|
+
return this.exists(this.pathFor(hash));
|
|
51
|
+
}
|
|
52
|
+
async delete(hash) {
|
|
53
|
+
const target = this.pathFor(hash);
|
|
54
|
+
if (!await this.exists(target)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
await rm(target);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
async size() {
|
|
61
|
+
let count = 0;
|
|
62
|
+
let shards;
|
|
63
|
+
try {
|
|
64
|
+
shards = await readdir(this.dir);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (isNotFound(error)) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
for (const shard of shards) {
|
|
72
|
+
try {
|
|
73
|
+
const entries = await readdir(join(this.dir, shard));
|
|
74
|
+
count += entries.filter((name) => !name.endsWith(".tmp")).length;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!isNotFound(error)) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return count;
|
|
82
|
+
}
|
|
83
|
+
async list() {
|
|
84
|
+
let shards;
|
|
85
|
+
try {
|
|
86
|
+
shards = await readdir(this.dir);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (isNotFound(error)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const hashes = [];
|
|
94
|
+
for (const shard of shards) {
|
|
95
|
+
try {
|
|
96
|
+
const entries = await readdir(join(this.dir, shard));
|
|
97
|
+
for (const name of entries) {
|
|
98
|
+
if (!name.endsWith(".tmp")) {
|
|
99
|
+
hashes.push(name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (!isNotFound(error)) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return hashes;
|
|
109
|
+
}
|
|
110
|
+
async exists(path) {
|
|
111
|
+
try {
|
|
112
|
+
await stat(path);
|
|
113
|
+
return true;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (isNotFound(error)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
function isNotFound(error) {
|
|
123
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
FilesystemBlobStore
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=blob-fs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/blob/filesystem-blob-store.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { type BlobRef, type BlobRefMetadata, createBlobRef, hashBlob } from '@korajs/core'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * A persistent {@link ContentAddressedBlobStore} backed by the filesystem.\n *\n * Node-only (exported from `@korajs/store/blob-fs` so it never enters a browser\n * bundle). Blobs are stored at `<dir>/<hash[0:2]>/<hash>`, sharded by hash prefix\n * so a single directory never holds millions of entries. Writes are atomic (write\n * to a temp file, then rename) so a crash mid-write cannot leave a half-written\n * blob under a valid hash. Reads verify the bytes hash to the requested key.\n *\n * This is the same content-addressed contract as {@link MemoryBlobStore}, so it\n * drops into the blob transfer path (chunk staging, blob destination) unchanged.\n */\nexport class FilesystemBlobStore implements ContentAddressedBlobStore {\n\tconstructor(private readonly dir: string) {}\n\n\tprivate pathFor(hash: string): string {\n\t\treturn join(this.dir, hash.slice(0, 2), hash)\n\t}\n\n\tasync put(bytes: Uint8Array, metadata: BlobRefMetadata = {}): Promise<BlobRef> {\n\t\tconst ref = await createBlobRef(bytes, metadata)\n\t\tconst target = this.pathFor(ref.hash)\n\n\t\t// Dedup: identical content already stored is a no-op.\n\t\tif (await this.exists(target)) {\n\t\t\treturn ref\n\t\t}\n\n\t\tawait mkdir(dirname(target), { recursive: true })\n\t\t// Copy into an exactly-sized buffer so a subarray view writes its own bytes.\n\t\tconst out = new Uint8Array(bytes.byteLength)\n\t\tout.set(bytes)\n\t\t// Atomic write: temp file then rename, so a crash never leaves a partial\n\t\t// blob under a hash that readers would then trust.\n\t\tconst tmp = `${target}.${randomUUID()}.tmp`\n\t\tawait writeFile(tmp, out)\n\t\tawait rename(tmp, target)\n\t\treturn ref\n\t}\n\n\tasync get(hash: string): Promise<Uint8Array | null> {\n\t\tlet contents: Buffer\n\t\ttry {\n\t\t\tcontents = await readFile(this.pathFor(hash))\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t\tconst bytes = new Uint8Array(contents)\n\t\tconst actual = await hashBlob(bytes)\n\t\tif (actual !== hash) {\n\t\t\tthrow new BlobIntegrityError(hash, actual)\n\t\t}\n\t\treturn bytes\n\t}\n\n\tasync has(hash: string): Promise<boolean> {\n\t\treturn this.exists(this.pathFor(hash))\n\t}\n\n\tasync delete(hash: string): Promise<boolean> {\n\t\tconst target = this.pathFor(hash)\n\t\tif (!(await this.exists(target))) {\n\t\t\treturn false\n\t\t}\n\t\tawait rm(target)\n\t\treturn true\n\t}\n\n\tasync size(): Promise<number> {\n\t\tlet count = 0\n\t\tlet shards: string[]\n\t\ttry {\n\t\t\tshards = await readdir(this.dir)\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t\tfor (const shard of shards) {\n\t\t\ttry {\n\t\t\t\tconst entries = await readdir(join(this.dir, shard))\n\t\t\t\tcount += entries.filter((name) => !name.endsWith('.tmp')).length\n\t\t\t} catch (error) {\n\t\t\t\tif (!isNotFound(error)) {\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn count\n\t}\n\n\tasync list(): Promise<string[]> {\n\t\tlet shards: string[]\n\t\ttry {\n\t\t\tshards = await readdir(this.dir)\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t\tconst hashes: string[] = []\n\t\tfor (const shard of shards) {\n\t\t\ttry {\n\t\t\t\tconst entries = await readdir(join(this.dir, shard))\n\t\t\t\tfor (const name of entries) {\n\t\t\t\t\tif (!name.endsWith('.tmp')) {\n\t\t\t\t\t\thashes.push(name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (!isNotFound(error)) {\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn hashes\n\t}\n\n\tprivate async exists(path: string): Promise<boolean> {\n\t\ttry {\n\t\t\tawait stat(path)\n\t\t\treturn true\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n}\n\nfunction isNotFound(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === 'object' && error !== null && (error as { code?: string }).code === 'ENOENT'\n\t)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -5,9 +5,10 @@ import {
|
|
|
5
5
|
} from "./chunk-MYSJN3DF.js";
|
|
6
6
|
import {
|
|
7
7
|
deserializeOperationWithCollection
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-GSJ7EEUC.js";
|
|
9
9
|
|
|
10
10
|
// src/audit/export-audit.ts
|
|
11
|
+
import { quoteIdent } from "@korajs/core";
|
|
11
12
|
function encodeSection(name, content) {
|
|
12
13
|
const nameBytes = new TextEncoder().encode(name);
|
|
13
14
|
const header = new Uint8Array(8);
|
|
@@ -67,7 +68,7 @@ async function readAllOperations(adapter, schema, collectionFilter) {
|
|
|
67
68
|
const allOps = [];
|
|
68
69
|
for (const collectionName of collections) {
|
|
69
70
|
const rows = await adapter.query(
|
|
70
|
-
`SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
|
|
71
|
+
`SELECT * FROM ${quoteIdent(`_kora_ops_${collectionName}`)} ORDER BY sequence_number ASC`
|
|
71
72
|
);
|
|
72
73
|
for (const row of rows) {
|
|
73
74
|
allOps.push(deserializeOperationWithCollection(row, collectionName));
|
|
@@ -185,4 +186,4 @@ export {
|
|
|
185
186
|
readAuditExportManifest,
|
|
186
187
|
verifyAuditExportChecksum
|
|
187
188
|
};
|
|
188
|
-
//# sourceMappingURL=chunk-
|
|
189
|
+
//# sourceMappingURL=chunk-CYO3YKFL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audit/export-audit.ts"],"sourcesContent":["import type { Operation, SchemaDefinition } from '@korajs/core'\nimport { quoteIdent } from '@korajs/core'\nimport { deserializeOperationWithCollection } from '../serialization/serializer'\nimport type { StorageAdapter } from '../types'\nimport type { OperationRow } from '../types'\nimport { deserializeAuditJson, serializeAuditJson } from './audit-json'\nimport { readAuditTraces } from './audit-trace-store'\nimport type {\n\tAuditExportManifest,\n\tAuditExportOptions,\n\tAuditExportPayload,\n\tAuditTraceQuery,\n\tPersistedAuditTrace,\n} from './types'\n\nfunction encodeSection(name: string, content: Uint8Array): Uint8Array {\n\tconst nameBytes = new TextEncoder().encode(name)\n\tconst header = new Uint8Array(8)\n\tconst view = new DataView(header.buffer)\n\tview.setUint32(0, nameBytes.length, true)\n\tview.setUint32(4, content.length, true)\n\n\tconst result = new Uint8Array(header.length + nameBytes.length + content.length)\n\tresult.set(header, 0)\n\tresult.set(nameBytes, 8)\n\tresult.set(content, 8 + nameBytes.length)\n\treturn result\n}\n\nfunction encodeJsonSection(name: string, data: unknown): Uint8Array {\n\treturn encodeSection(name, new TextEncoder().encode(serializeAuditJson(data)))\n}\n\nfunction encodeNdjsonSection(name: string, records: unknown[]): Uint8Array {\n\tconst lines = `${records.map((record) => serializeAuditJson(record)).join('\\n')}\\n`\n\treturn encodeSection(name, new TextEncoder().encode(lines))\n}\n\nfunction readUint32(view: DataView, offset: number): number {\n\treturn view.getUint32(offset, true)\n}\n\ninterface Section {\n\tname: string\n\tcontent: Uint8Array\n}\n\nfunction parseSections(data: Uint8Array): Section[] {\n\tconst sections: Section[] = []\n\tconst view = new DataView(data.buffer, data.byteOffset, data.byteLength)\n\tlet offset = 0\n\n\twhile (offset + 8 <= data.byteLength) {\n\t\tconst nameLen = readUint32(view, offset)\n\t\tconst contentLen = readUint32(view, offset + 4)\n\t\toffset += 8\n\n\t\tif (offset + nameLen + contentLen > data.byteLength) {\n\t\t\tbreak\n\t\t}\n\n\t\tconst name = new TextDecoder().decode(data.slice(offset, offset + nameLen))\n\t\toffset += nameLen\n\n\t\tconst content = data.slice(offset, offset + contentLen)\n\t\toffset += contentLen\n\n\t\tsections.push({ name, content })\n\t}\n\n\treturn sections\n}\n\nfunction findSection(sections: Section[], name: string): Uint8Array | null {\n\tfor (const section of sections) {\n\t\tif (section.name === name) {\n\t\t\treturn section.content\n\t\t}\n\t}\n\treturn null\n}\n\nasync function computeSha256(data: Uint8Array<ArrayBuffer>): Promise<string> {\n\tconst hashBuffer = await crypto.subtle.digest('SHA-256', data)\n\treturn Array.from(new Uint8Array(hashBuffer))\n\t\t.map((byte) => byte.toString(16).padStart(2, '0'))\n\t\t.join('')\n}\n\nasync function readAllOperations(\n\tadapter: StorageAdapter,\n\tschema: SchemaDefinition,\n\tcollectionFilter?: string[],\n): Promise<Operation[]> {\n\tconst collections = collectionFilter\n\t\t? Object.keys(schema.collections).filter((name) => collectionFilter.includes(name))\n\t\t: Object.keys(schema.collections)\n\n\tconst allOps: Operation[] = []\n\tfor (const collectionName of collections) {\n\t\tconst rows = await adapter.query<OperationRow>(\n\t\t\t`SELECT * FROM ${quoteIdent(`_kora_ops_${collectionName}`)} ORDER BY sequence_number ASC`,\n\t\t)\n\n\t\tfor (const row of rows) {\n\t\t\tallOps.push(deserializeOperationWithCollection(row, collectionName))\n\t\t}\n\t}\n\n\treturn allOps\n}\n\nfunction filterOperationsByCollections(\n\toperations: Operation[],\n\tcollectionFilter?: string[],\n): Operation[] {\n\tif (!collectionFilter || collectionFilter.length === 0) {\n\t\treturn operations\n\t}\n\tconst allowed = new Set(collectionFilter)\n\treturn operations.filter((op) => allowed.has(op.collection))\n}\n\n/**\n * Export operations and persisted merge traces as a portable binary audit bundle.\n */\nexport async function exportAudit(\n\tadapter: StorageAdapter,\n\tschema: SchemaDefinition,\n\tnodeId: string,\n\tschemaVersion: number,\n\toptions?: AuditExportOptions,\n): Promise<Uint8Array> {\n\tconst onProgress = options?.onProgress ?? (() => {})\n\tconst traceQuery: AuditTraceQuery = {\n\t\tcollections: options?.collections,\n\t\tsince: options?.since,\n\t\tuntil: options?.until,\n\t}\n\n\tonProgress({ phase: 'reading', progress: 0, message: 'Reading operations' })\n\tconst operations = filterOperationsByCollections(\n\t\tawait readAllOperations(adapter, schema, options?.collections),\n\t\toptions?.collections,\n\t)\n\n\tonProgress({ phase: 'reading', progress: 0.4, message: 'Reading merge traces' })\n\tconst mergeTraces = await readAuditTraces(adapter, traceQuery)\n\n\tonProgress({ phase: 'writing', progress: 0.7, message: 'Encoding audit export' })\n\tconst sections: Uint8Array[] = []\n\tlet contentForChecksum = new Uint8Array(0)\n\n\tconst addSection = (section: Uint8Array): void => {\n\t\tsections.push(section)\n\t\tconst combined = new Uint8Array(contentForChecksum.length + section.length)\n\t\tcombined.set(contentForChecksum, 0)\n\t\tcombined.set(section, contentForChecksum.length)\n\t\tcontentForChecksum = combined\n\t}\n\n\taddSection(encodeNdjsonSection('operations', operations))\n\taddSection(encodeNdjsonSection('merge_traces', mergeTraces))\n\n\tonProgress({ phase: 'verifying', progress: 0.9, message: 'Computing checksum' })\n\tconst checksum = await computeSha256(contentForChecksum as Uint8Array<ArrayBuffer>)\n\n\tconst manifest: AuditExportManifest = {\n\t\tversion: 1,\n\t\texportedAt: Date.now(),\n\t\tnodeId,\n\t\tschemaVersion,\n\t\toperationCount: operations.length,\n\t\tmergeTraceCount: mergeTraces.length,\n\t\tchecksum,\n\t}\n\n\tconst manifestSection = encodeJsonSection('manifest', manifest)\n\tconst checksumSection = encodeSection('checksum', new TextEncoder().encode(checksum))\n\n\tonProgress({ phase: 'writing', progress: 1, message: 'Finalizing' })\n\n\tconst parts = [manifestSection, ...sections, checksumSection]\n\tconst finalOutput = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0))\n\tlet offset = 0\n\tfor (const part of parts) {\n\t\tfinalOutput.set(part, offset)\n\t\toffset += part.length\n\t}\n\n\treturn finalOutput\n}\n\n/**\n * Decode an audit export binary into structured data.\n */\nexport function decodeAuditExport(data: Uint8Array): AuditExportPayload {\n\tconst sections = parseSections(data)\n\tconst manifest = JSON.parse(\n\t\tnew TextDecoder().decode(findSection(sections, 'manifest') ?? new Uint8Array()),\n\t) as AuditExportManifest\n\n\tconst operationsContent = findSection(sections, 'operations')\n\tconst mergeTracesContent = findSection(sections, 'merge_traces')\n\n\tconst operations = parseNdjsonSection<Operation>(operationsContent)\n\tconst mergeTraces = parseNdjsonSection<PersistedAuditTrace>(mergeTracesContent)\n\n\treturn { manifest, operations, mergeTraces }\n}\n\n/**\n * Read only the manifest from an audit export without decoding all sections.\n */\nexport function readAuditExportManifest(data: Uint8Array): AuditExportManifest {\n\tconst sections = parseSections(data)\n\tconst manifestContent = findSection(sections, 'manifest')\n\tif (!manifestContent) {\n\t\tthrow new Error('Invalid audit export: missing manifest section')\n\t}\n\treturn JSON.parse(new TextDecoder().decode(manifestContent)) as AuditExportManifest\n}\n\n/**\n * Verify the checksum of an audit export file.\n */\nexport async function verifyAuditExportChecksum(data: Uint8Array): Promise<boolean> {\n\tconst sections = parseSections(data)\n\tconst manifest = readAuditExportManifest(data)\n\tconst contentSections = sections.filter(\n\t\t(section) => section.name !== 'manifest' && section.name !== 'checksum',\n\t)\n\n\tlet content = new Uint8Array(0)\n\tfor (const section of contentSections) {\n\t\tconst encoded = encodeSection(section.name, section.content)\n\t\tconst combined = new Uint8Array(content.length + encoded.length)\n\t\tcombined.set(content, 0)\n\t\tcombined.set(encoded, content.length)\n\t\tcontent = combined\n\t}\n\n\tconst checksum = await computeSha256(content as Uint8Array<ArrayBuffer>)\n\treturn checksum === manifest.checksum\n}\n\nfunction parseNdjsonSection<T>(content: Uint8Array | null): T[] {\n\tif (!content || content.length === 0) {\n\t\treturn []\n\t}\n\tconst text = new TextDecoder().decode(content).trim()\n\tif (text.length === 0) {\n\t\treturn []\n\t}\n\treturn text.split('\\n').map((line) => deserializeAuditJson<T>(line))\n}\n"],"mappings":"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","names":[]}
|
|
@@ -42,6 +42,7 @@ function encodeRichtext(value) {
|
|
|
42
42
|
}
|
|
43
43
|
if (typeof value === "string") {
|
|
44
44
|
const doc = new Y.Doc();
|
|
45
|
+
doc.clientID = 0;
|
|
45
46
|
doc.getText(TEXT_KEY).insert(0, value);
|
|
46
47
|
return Y.encodeStateAsUpdate(doc);
|
|
47
48
|
}
|
|
@@ -207,6 +208,9 @@ function serializeValue(value, descriptor) {
|
|
|
207
208
|
case "boolean":
|
|
208
209
|
return value ? 1 : 0;
|
|
209
210
|
case "array":
|
|
211
|
+
case "object":
|
|
212
|
+
case "json":
|
|
213
|
+
case "blob":
|
|
210
214
|
return JSON.stringify(value);
|
|
211
215
|
case "richtext":
|
|
212
216
|
return encodeRichtext(value);
|
|
@@ -223,6 +227,13 @@ function deserializeValue(value, descriptor) {
|
|
|
223
227
|
return JSON.parse(value);
|
|
224
228
|
}
|
|
225
229
|
return value;
|
|
230
|
+
case "object":
|
|
231
|
+
case "json":
|
|
232
|
+
case "blob":
|
|
233
|
+
if (typeof value === "string") {
|
|
234
|
+
return JSON.parse(value);
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
226
237
|
case "richtext":
|
|
227
238
|
return decodeRichtext(value);
|
|
228
239
|
default:
|
|
@@ -243,4 +254,4 @@ export {
|
|
|
243
254
|
deserializeOperation,
|
|
244
255
|
deserializeOperationWithCollection
|
|
245
256
|
};
|
|
246
|
-
//# sourceMappingURL=chunk-
|
|
257
|
+
//# sourceMappingURL=chunk-GSJ7EEUC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/serialization/richtext-serializer.ts","../src/serialization/op-data-encoding.ts","../src/serialization/serializer.ts"],"sourcesContent":["import * as Y from 'yjs'\nimport {\n\ttype KoraBytesValue,\n\tdecodeRichtextOpDataValue,\n\tisKoraBytesValue,\n\tisLegacyNumericByteObject,\n} from './op-data-encoding'\n\nconst TEXT_KEY = 'content'\n\nexport type RichtextInput = string | Uint8Array | ArrayBuffer | null | undefined\n\n/**\n * Encodes richtext values into Yjs document updates.\n *\n * Also accepts the tagged `{ $koraBytes }` form that binary richtext values\n * take inside `op.data` (and, for old dev databases, the pre-fix numeric-key\n * object shape), so every consumer that writes op-data values to columns\n * inherits the decoding. Plain strings keep their exact historical behavior:\n * encoded as a fresh Yjs document containing the string.\n */\nexport function encodeRichtext(value: RichtextInput | KoraBytesValue): Uint8Array | null {\n\tif (value === null || value === undefined) {\n\t\treturn null\n\t}\n\n\tif (typeof value === 'string') {\n\t\tconst doc = new Y.Doc()\n\t\t// Deterministic clientId so the same string encodes to the same bytes on every\n\t\t// device (a plain string is a replacement, not a collaborative actor). A random\n\t\t// clientId would make a string-derived richtext column diverge byte-wise across\n\t\t// replicas even when the text is identical.\n\t\tdoc.clientID = 0\n\t\tdoc.getText(TEXT_KEY).insert(0, value)\n\t\treturn Y.encodeStateAsUpdate(doc)\n\t}\n\n\tif (value instanceof Uint8Array) {\n\t\treturn value\n\t}\n\n\tif (value instanceof ArrayBuffer) {\n\t\treturn new Uint8Array(value)\n\t}\n\n\tif (isKoraBytesValue(value) || isLegacyNumericByteObject(value)) {\n\t\tconst decoded = decodeRichtextOpDataValue(value)\n\t\t// Both accepted shapes decode to bytes; strings were handled above.\n\t\treturn decoded instanceof Uint8Array ? decoded : null\n\t}\n\n\tthrow new Error('Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.')\n}\n\n/**\n * Decodes driver-provided richtext values into Uint8Array.\n */\nexport function decodeRichtext(value: unknown): Uint8Array | null {\n\tif (value === null || value === undefined) {\n\t\treturn null\n\t}\n\n\tif (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {\n\t\treturn new Uint8Array(value)\n\t}\n\n\tif (value instanceof Uint8Array) {\n\t\treturn value\n\t}\n\n\tif (value instanceof ArrayBuffer) {\n\t\treturn new Uint8Array(value)\n\t}\n\n\tthrow new Error(\n\t\t'Richtext storage value must be Uint8Array, ArrayBuffer, Buffer, null, or undefined.',\n\t)\n}\n\n/**\n * Reads plain text from a richtext Yjs state update.\n */\nexport function richtextToPlainText(value: RichtextInput): string {\n\tconst encoded = encodeRichtext(value)\n\tif (!encoded) return ''\n\n\tconst doc = new Y.Doc()\n\tY.applyUpdate(doc, encoded)\n\treturn doc.getText(TEXT_KEY).toString()\n}\n\n/**\n * Compares two richtext values by decoded plain text.\n * Yjs update bytes are not stable across separate encodes of the same string.\n */\nexport function richtextStatesEqual(a: unknown, b: unknown): boolean {\n\ttry {\n\t\treturn richtextToPlainText(a as RichtextInput) === richtextToPlainText(b as RichtextInput)\n\t} catch {\n\t\treturn false\n\t}\n}\n","import {\n\ttype KoraBytesValue,\n\tbase64ToBytes,\n\tbytesToBase64,\n\tdecodeBytesFromOpData,\n\tencodeBytesForOpData,\n\tisKoraBytesValue,\n\tisLegacyNumericByteObject,\n} from '@korajs/core'\nimport type { FieldDescriptor } from '@korajs/core'\n\n/**\n * The canonical binary op-data encoding lives in `@korajs/core` because\n * `op.data` is a core concept shared by store (creation, persistence) and merge\n * (CRDT merge). These re-exports keep existing store-internal import paths\n * working while the single source of truth is the core module.\n */\nexport type { KoraBytesValue }\nexport { base64ToBytes, bytesToBase64, isKoraBytesValue, isLegacyNumericByteObject }\n\n/** @deprecated Use {@link encodeRichtextForOpData}; kept for existing imports. */\nexport const encodeRichtextForOpData = encodeBytesForOpData\n/** @deprecated Use {@link decodeRichtextOpDataValue}; kept for existing imports. */\nexport const decodeRichtextOpDataValue = decodeBytesFromOpData\n\n/**\n * Apply {@link encodeBytesForOpData} to every richtext-typed field in an\n * operation's data payload. Called at operation creation, BEFORE the payload\n * is content-hashed, so the hash input, persisted JSON, wire payload, and the\n * value the merge engine sees are the identical canonical value. Non-richtext\n * fields are untouched.\n */\nexport function encodeRichtextFieldsForOpData(\n\tdata: Record<string, unknown>,\n\tfields: Record<string, FieldDescriptor>,\n): Record<string, unknown> {\n\tconst result: Record<string, unknown> = { ...data }\n\tfor (const [key, value] of Object.entries(data)) {\n\t\tif (fields[key]?.kind !== 'richtext') {\n\t\t\tcontinue\n\t\t}\n\t\tif (typeof value === 'string' || value instanceof Uint8Array || value instanceof ArrayBuffer) {\n\t\t\tresult[key] = encodeBytesForOpData(value)\n\t\t}\n\t}\n\treturn result\n}\n\n/**\n * Reverse of {@link encodeRichtextFieldsForOpData}: decode tagged (or legacy\n * numeric-key) richtext values in an op-data payload back to record-shaped\n * values (string or Uint8Array). Null/undefined and plain strings pass\n * through, preserving pre-fix behavior exactly.\n */\nexport function decodeRichtextFieldsFromOpData(\n\tdata: Record<string, unknown>,\n\tfields: Record<string, FieldDescriptor>,\n): Record<string, unknown> {\n\tconst result: Record<string, unknown> = { ...data }\n\tfor (const [key, value] of Object.entries(data)) {\n\t\tif (fields[key]?.kind !== 'richtext' || value === null || value === undefined) {\n\t\t\tcontinue\n\t\t}\n\t\tresult[key] = decodeBytesFromOpData(value)\n\t}\n\treturn result\n}\n","import { HybridLogicalClock } from '@korajs/core'\nimport type { CollectionDefinition, FieldDescriptor, Operation } from '@korajs/core'\nimport type { CollectionRecord, OperationRow, RawCollectionRow } from '../types'\nimport { decodeRichtext, encodeRichtext } from './richtext-serializer'\n\n/**\n * Serialize a JS record to SQL-compatible values for INSERT/UPDATE.\n * Transforms: boolean → 0/1, array → JSON string, richtext → Yjs binary update.\n *\n * @param data - The record data with JS-native types\n * @param fields - The field descriptors from the schema\n * @returns An object with SQL-compatible values\n */\nexport function serializeRecord(\n\tdata: Record<string, unknown>,\n\tfields: Record<string, FieldDescriptor>,\n): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {}\n\tfor (const [key, value] of Object.entries(data)) {\n\t\tconst descriptor = fields[key]\n\t\tif (!descriptor) {\n\t\t\tresult[key] = value\n\t\t\tcontinue\n\t\t}\n\t\tresult[key] = serializeValue(value, descriptor)\n\t}\n\treturn result\n}\n\n/**\n * Deserialize a SQL row to JS-native types for the application layer.\n * Transforms: 0/1 → boolean, JSON string → array, strips _deleted, maps _created_at/_updated_at.\n *\n * @param row - The raw SQL row\n * @param fields - The field descriptors from the schema\n * @returns A CollectionRecord with JS-native types\n */\nexport function deserializeRecord(\n\trow: RawCollectionRow,\n\tfields: Record<string, FieldDescriptor>,\n): CollectionRecord {\n\tconst result: CollectionRecord = {\n\t\tid: row.id,\n\t\tcreatedAt: row._created_at,\n\t\tupdatedAt: row._updated_at,\n\t}\n\n\tfor (const [key, descriptor] of Object.entries(fields)) {\n\t\tconst rawValue = row[key]\n\t\tif (rawValue === undefined || rawValue === null) {\n\t\t\tresult[key] = rawValue ?? null\n\t\t\tcontinue\n\t\t}\n\t\tresult[key] = deserializeValue(rawValue, descriptor)\n\t}\n\n\treturn result\n}\n\n/**\n * Internal key used to embed atomicOps metadata in the data JSON column.\n * This avoids adding a new column to the ops table (no migration needed).\n */\nconst ATOMIC_OPS_KEY = '__kora_atomic_ops__'\n\n/**\n * Internal key used to embed transactionId in the data JSON column.\n */\nconst TX_ID_KEY = '__kora_tx_id__'\n\n/**\n * Internal key used to embed mutationName in the data JSON column.\n */\nconst MUTATION_NAME_KEY = '__kora_mutation__'\n\n/**\n * Serialize an Operation to a row for the operations log table.\n *\n * @param op - The operation to serialize\n * @returns An OperationRow suitable for SQL INSERT\n */\nexport function serializeOperation(op: Operation): OperationRow {\n\tconst hasMetadata = op.transactionId !== undefined || op.mutationName !== undefined\n\tlet dataPayload: Record<string, unknown> | null = null\n\tif (op.data) {\n\t\t// Embed metadata in the data JSON when present\n\t\tdataPayload = { ...op.data }\n\t\tif (op.atomicOps !== undefined && Object.keys(op.atomicOps).length > 0) {\n\t\t\tdataPayload[ATOMIC_OPS_KEY] = op.atomicOps\n\t\t}\n\t\tif (op.transactionId !== undefined) {\n\t\t\tdataPayload[TX_ID_KEY] = op.transactionId\n\t\t}\n\t\tif (op.mutationName !== undefined) {\n\t\t\tdataPayload[MUTATION_NAME_KEY] = op.mutationName\n\t\t}\n\t} else if (hasMetadata) {\n\t\t// For delete operations (data is null), we still need to store metadata\n\t\tdataPayload = {}\n\t\tif (op.transactionId !== undefined) {\n\t\t\tdataPayload[TX_ID_KEY] = op.transactionId\n\t\t}\n\t\tif (op.mutationName !== undefined) {\n\t\t\tdataPayload[MUTATION_NAME_KEY] = op.mutationName\n\t\t}\n\t}\n\n\treturn {\n\t\tid: op.id,\n\t\tnode_id: op.nodeId,\n\t\ttype: op.type,\n\t\trecord_id: op.recordId,\n\t\tdata: dataPayload ? JSON.stringify(dataPayload) : null,\n\t\tprevious_data: op.previousData ? JSON.stringify(op.previousData) : null,\n\t\ttimestamp: HybridLogicalClock.serialize(op.timestamp),\n\t\tsequence_number: op.sequenceNumber,\n\t\tcausal_deps: JSON.stringify(op.causalDeps),\n\t\tschema_version: op.schemaVersion,\n\t}\n}\n\n/**\n * Deserialize a row from the operations log table back to an Operation.\n *\n * @param row - The raw operation row from SQL\n * @returns The deserialized Operation object\n */\nexport function deserializeOperation(row: OperationRow): Operation {\n\tlet data: Record<string, unknown> | null = null\n\tlet atomicOps: Record<string, unknown> | undefined\n\tlet transactionId: string | undefined\n\tlet mutationName: string | undefined\n\n\tif (row.data) {\n\t\tconst parsed = JSON.parse(row.data) as Record<string, unknown>\n\t\t// Extract embedded metadata keys\n\t\tif (ATOMIC_OPS_KEY in parsed) {\n\t\t\tatomicOps = parsed[ATOMIC_OPS_KEY] as Record<string, unknown>\n\t\t}\n\t\tif (TX_ID_KEY in parsed) {\n\t\t\ttransactionId = parsed[TX_ID_KEY] as string\n\t\t}\n\t\tif (MUTATION_NAME_KEY in parsed) {\n\t\t\tmutationName = parsed[MUTATION_NAME_KEY] as string\n\t\t}\n\t\t// Remove metadata keys from data\n\t\tconst { [ATOMIC_OPS_KEY]: _a, [TX_ID_KEY]: _t, [MUTATION_NAME_KEY]: _m, ...rest } = parsed\n\t\tdata = Object.keys(rest).length > 0 ? rest : null\n\t}\n\n\treturn {\n\t\tid: row.id,\n\t\tnodeId: row.node_id,\n\t\ttype: row.type as Operation['type'],\n\t\tcollection: '', // Collection name is derived from the table name by the caller\n\t\trecordId: row.record_id,\n\t\tdata,\n\t\tpreviousData: row.previous_data\n\t\t\t? (JSON.parse(row.previous_data) as Record<string, unknown>)\n\t\t\t: null,\n\t\ttimestamp: HybridLogicalClock.deserialize(row.timestamp),\n\t\tsequenceNumber: row.sequence_number,\n\t\tcausalDeps: JSON.parse(row.causal_deps) as string[],\n\t\tschemaVersion: row.schema_version,\n\t\t...(atomicOps !== undefined ? { atomicOps: atomicOps as Operation['atomicOps'] } : {}),\n\t\t...(transactionId !== undefined ? { transactionId } : {}),\n\t\t...(mutationName !== undefined ? { mutationName } : {}),\n\t}\n}\n\n/**\n * Deserialize an operation row with collection name already known.\n */\nexport function deserializeOperationWithCollection(\n\trow: OperationRow,\n\tcollection: string,\n): Operation {\n\tconst op = deserializeOperation(row)\n\treturn { ...op, collection }\n}\n\nfunction serializeValue(value: unknown, descriptor: FieldDescriptor): unknown {\n\tif (value === null || value === undefined) {\n\t\treturn null\n\t}\n\n\tswitch (descriptor.kind) {\n\t\tcase 'boolean':\n\t\t\treturn value ? 1 : 0\n\t\tcase 'array':\n\t\tcase 'object':\n\t\tcase 'json':\n\t\tcase 'blob':\n\t\t\treturn JSON.stringify(value)\n\t\tcase 'richtext':\n\t\t\t// May be a record-shaped value (string/bytes) or an op-data value\n\t\t\t// (tagged { $koraBytes }) — encodeRichtext accepts every form.\n\t\t\treturn encodeRichtext(value as Parameters<typeof encodeRichtext>[0])\n\t\tdefault:\n\t\t\treturn value\n\t}\n}\n\nfunction deserializeValue(value: unknown, descriptor: FieldDescriptor): unknown {\n\tswitch (descriptor.kind) {\n\t\tcase 'boolean':\n\t\t\treturn value === 1 || value === true\n\t\tcase 'array':\n\t\t\tif (typeof value === 'string') {\n\t\t\t\treturn JSON.parse(value) as unknown[]\n\t\t\t}\n\t\t\treturn value\n\t\tcase 'object':\n\t\tcase 'json':\n\t\tcase 'blob':\n\t\t\tif (typeof value === 'string') {\n\t\t\t\treturn JSON.parse(value) as unknown\n\t\t\t}\n\t\t\treturn value\n\t\tcase 'richtext':\n\t\t\treturn decodeRichtext(value)\n\t\tdefault:\n\t\t\treturn value\n\t}\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deserializeOperationWithCollection
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GSJ7EEUC.js";
|
|
4
4
|
|
|
5
5
|
// src/backup/backup.ts
|
|
6
|
-
import "@korajs/core";
|
|
6
|
+
import { quoteIdent } from "@korajs/core";
|
|
7
7
|
function encodeSection(name, content) {
|
|
8
8
|
const nameBytes = new TextEncoder().encode(name);
|
|
9
9
|
const header = new Uint8Array(8);
|
|
@@ -242,7 +242,7 @@ async function restoreBackup(adapter, schema, data, options) {
|
|
|
242
242
|
schema_version: op.schemaVersion
|
|
243
243
|
};
|
|
244
244
|
await tx.execute(
|
|
245
|
-
`INSERT OR IGNORE INTO _kora_ops_${collection} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
245
|
+
`INSERT OR IGNORE INTO ${quoteIdent(`_kora_ops_${collection}`)} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
246
246
|
[
|
|
247
247
|
opRow.id,
|
|
248
248
|
opRow.node_id,
|
|
@@ -261,8 +261,8 @@ async function restoreBackup(adapter, schema, data, options) {
|
|
|
261
261
|
} else {
|
|
262
262
|
await adapter.transaction(async (tx) => {
|
|
263
263
|
for (const collection of Object.keys(schema.collections)) {
|
|
264
|
-
await tx.execute(`DELETE FROM _kora_ops_${collection}`);
|
|
265
|
-
await tx.execute(`DELETE FROM ${collection}`);
|
|
264
|
+
await tx.execute(`DELETE FROM ${quoteIdent(`_kora_ops_${collection}`)}`);
|
|
265
|
+
await tx.execute(`DELETE FROM ${quoteIdent(collection)}`);
|
|
266
266
|
}
|
|
267
267
|
await tx.execute("DELETE FROM _kora_version_vector");
|
|
268
268
|
await tx.execute("DELETE FROM _kora_meta");
|
|
@@ -282,7 +282,7 @@ async function restoreBackup(adapter, schema, data, options) {
|
|
|
282
282
|
for (const op of operations) {
|
|
283
283
|
const collection = op.collection;
|
|
284
284
|
await tx.execute(
|
|
285
|
-
`INSERT INTO _kora_ops_${collection} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
285
|
+
`INSERT INTO ${quoteIdent(`_kora_ops_${collection}`)} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
286
286
|
[
|
|
287
287
|
op.id,
|
|
288
288
|
op.nodeId,
|
|
@@ -314,6 +314,7 @@ async function restoreBackup(adapter, schema, data, options) {
|
|
|
314
314
|
for (const record of records) {
|
|
315
315
|
const keys = Object.keys(record);
|
|
316
316
|
const placeholders = keys.map(() => "?").join(", ");
|
|
317
|
+
const quotedKeys = keys.map((k) => quoteIdent(k)).join(", ");
|
|
317
318
|
const values = keys.map((k) => {
|
|
318
319
|
const v = record[k];
|
|
319
320
|
if (typeof v === "boolean") return v ? 1 : 0;
|
|
@@ -321,7 +322,7 @@ async function restoreBackup(adapter, schema, data, options) {
|
|
|
321
322
|
return v;
|
|
322
323
|
});
|
|
323
324
|
await tx.execute(
|
|
324
|
-
`INSERT OR REPLACE INTO ${col} (${
|
|
325
|
+
`INSERT OR REPLACE INTO ${quoteIdent(col)} (${quotedKeys}) VALUES (${placeholders})`,
|
|
325
326
|
values
|
|
326
327
|
);
|
|
327
328
|
}
|
|
@@ -354,7 +355,7 @@ async function readAllOperations(adapter, schema) {
|
|
|
354
355
|
const allOps = [];
|
|
355
356
|
for (const collectionName of Object.keys(schema.collections)) {
|
|
356
357
|
const rows = await adapter.query(
|
|
357
|
-
`SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
|
|
358
|
+
`SELECT * FROM ${quoteIdent(`_kora_ops_${collectionName}`)} ORDER BY sequence_number ASC`
|
|
358
359
|
);
|
|
359
360
|
for (const row of rows) {
|
|
360
361
|
allOps.push(deserializeOperationWithCollection(row, collectionName));
|
|
@@ -363,7 +364,9 @@ async function readAllOperations(adapter, schema) {
|
|
|
363
364
|
return allOps;
|
|
364
365
|
}
|
|
365
366
|
async function readCollectionRecords(adapter, collection) {
|
|
366
|
-
return adapter.query(
|
|
367
|
+
return adapter.query(
|
|
368
|
+
`SELECT * FROM ${quoteIdent(collection)} WHERE _deleted = 0`
|
|
369
|
+
);
|
|
367
370
|
}
|
|
368
371
|
async function computeSha256(data) {
|
|
369
372
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -377,4 +380,4 @@ export {
|
|
|
377
380
|
verifyBackupChecksum,
|
|
378
381
|
restoreBackup
|
|
379
382
|
};
|
|
380
|
-
//# sourceMappingURL=chunk-
|
|
383
|
+
//# sourceMappingURL=chunk-HS6DBMST.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/backup/backup.ts"],"sourcesContent":["import { generateUUIDv7, quoteIdent } from '@korajs/core'\nimport type { Operation, SchemaDefinition, VersionVector } from '@korajs/core'\nimport { deserializeOperationWithCollection } from '../serialization/serializer'\nimport type { OperationRow, StorageAdapter } from '../types'\nimport type {\n\tBackupManifest,\n\tBackupOptions,\n\tBackupProgress,\n\tRestoreOptions,\n\tRestoreResult,\n} from './types'\n\n// ── Section I/O helpers ──────────────────────────────────────────────────────\n\nfunction encodeSection(name: string, content: Uint8Array): Uint8Array {\n\tconst nameBytes = new TextEncoder().encode(name)\n\tconst header = new Uint8Array(8)\n\tconst dv = new DataView(header.buffer)\n\tdv.setUint32(0, nameBytes.length, true)\n\tdv.setUint32(4, content.length, true)\n\n\tconst result = new Uint8Array(header.length + nameBytes.length + content.length)\n\tresult.set(header, 0)\n\tresult.set(nameBytes, 8)\n\tresult.set(content, 8 + nameBytes.length)\n\treturn result\n}\n\nfunction encodeJsonSection(name: string, data: unknown): Uint8Array {\n\tconst json = JSON.stringify(data)\n\treturn encodeSection(name, new TextEncoder().encode(json))\n}\n\nfunction encodeNdjsonSection(name: string, records: unknown[]): Uint8Array {\n\tconst lines = `${records.map((r) => JSON.stringify(r)).join('\\n')}\\n`\n\treturn encodeSection(name, new TextEncoder().encode(lines))\n}\n\nfunction readUint32(view: DataView, offset: number): number {\n\treturn view.getUint32(offset, true)\n}\n\ninterface Section {\n\tname: string\n\tcontent: Uint8Array\n}\n\nfunction parseSections(data: Uint8Array): Section[] {\n\tconst sections: Section[] = []\n\tconst dv = new DataView(data.buffer, data.byteOffset, data.byteLength)\n\tlet offset = 0\n\n\twhile (offset + 8 <= data.byteLength) {\n\t\tconst nameLen = readUint32(dv, offset)\n\t\tconst contentLen = readUint32(dv, offset + 4)\n\t\toffset += 8\n\n\t\tif (offset + nameLen + contentLen > data.byteLength) break\n\n\t\tconst name = new TextDecoder().decode(data.slice(offset, offset + nameLen))\n\t\toffset += nameLen\n\n\t\tconst content = data.slice(offset, offset + contentLen)\n\t\toffset += contentLen\n\n\t\tsections.push({ name, content })\n\t}\n\n\treturn sections\n}\n\nfunction findSection(sections: Section[], name: string): Uint8Array | null {\n\tfor (const s of sections) {\n\t\tif (s.name === name) return s.content\n\t}\n\treturn null\n}\n\nfunction parseJsonSection<T>(sections: Section[], name: string): T | null {\n\tconst content = findSection(sections, name)\n\tif (!content) return null\n\treturn JSON.parse(new TextDecoder().decode(content)) as T\n}\n\n/**\n * Export format version. Bump on breaking changes.\n */\nconst BACKUP_VERSION = 1\n\n/**\n * Export a backup from a store.\n *\n * Reads all data via the adapter and produces a portable binary backup.\n *\n * @param adapter - The storage adapter\n * @param schema - The schema definition\n * @param nodeId - Current node ID\n * @param schemaVersion - Schema version\n * @param options - Backup options\n * @returns Backup as a Uint8Array\n */\nexport async function exportBackup(\n\tadapter: StorageAdapter,\n\tschema: SchemaDefinition,\n\tnodeId: string,\n\tschemaVersion: number,\n\toptions?: BackupOptions,\n): Promise<Uint8Array> {\n\tconst onProgress = options?.onProgress ?? (() => {})\n\tconst includeRecords = options?.includeRecords ?? true\n\tconst collectionFilter = options?.collections\n\tconst collections = collectionFilter\n\t\t? Object.keys(schema.collections).filter((c) => collectionFilter.includes(c))\n\t\t: Object.keys(schema.collections)\n\n\tonProgress({ phase: 'reading', progress: 0, message: 'Reading version vector' })\n\tconst versionVector = await readVersionVector(adapter)\n\tonProgress({ phase: 'reading', progress: 0.1, message: 'Reading metadata' })\n\tconst meta = await readMeta(adapter)\n\tonProgress({ phase: 'reading', progress: 0.2, message: 'Reading operations' })\n\tconst operations = await readAllOperations(adapter, schema)\n\tonProgress({ phase: 'reading', progress: 0.5, message: 'Reading records' })\n\n\tconst sections: Uint8Array[] = []\n\tlet allContentForChecksum = new Uint8Array(0)\n\n\t// Helper: add a section and update running checksum content\n\tconst addSection = (name: string, data: Uint8Array) => {\n\t\tsections.push(data)\n\t\tconst newLen = allContentForChecksum.length + data.length\n\t\tconst combined = new Uint8Array(newLen)\n\t\tcombined.set(allContentForChecksum, 0)\n\t\tcombined.set(data, allContentForChecksum.length)\n\t\tallContentForChecksum = combined\n\t}\n\n\t// Version vector section\n\tconst vvObj: Record<string, number> = {}\n\tfor (const [nid, seq] of versionVector) {\n\t\tvvObj[nid] = seq\n\t}\n\taddSection('version_vector', encodeJsonSection('version_vector', vvObj))\n\n\t// Meta section\n\tconst metaObj: Record<string, string> = {}\n\tfor (const row of meta) {\n\t\tmetaObj[row.key] = row.value\n\t}\n\taddSection('meta', encodeJsonSection('meta', metaObj))\n\n\t// Operations section\n\tconst opLines = `${operations.map((op) => JSON.stringify(op)).join('\\n')}\\n`\n\taddSection('operations', encodeSection('operations', new TextEncoder().encode(opLines)))\n\n\t// Records sections (optional)\n\tlet totalRecords = 0\n\tif (includeRecords) {\n\t\tfor (const col of collections) {\n\t\t\tconst records = await readCollectionRecords(adapter, col)\n\t\t\tif (records.length > 0) {\n\t\t\t\tconst lines = `${records.map((r) => JSON.stringify(r)).join('\\n')}\\n`\n\t\t\t\tconst sectionName = `records:${col}`\n\t\t\t\taddSection(sectionName, encodeSection(sectionName, new TextEncoder().encode(lines)))\n\t\t\t\ttotalRecords += records.length\n\t\t\t}\n\t\t}\n\t}\n\tonProgress({ phase: 'reading', progress: 0.9, message: 'Computing checksum' })\n\n\t// Compute SHA-256 checksum over all content sections\n\tconst checksumHex = await computeSha256(allContentForChecksum)\n\tonProgress({ phase: 'writing', progress: 1, message: 'Finalizing' })\n\n\t// Build manifest\n\tconst manifest: BackupManifest = {\n\t\tversion: BACKUP_VERSION,\n\t\tcreatedAt: Date.now(),\n\t\tnodeId,\n\t\tschemaVersion,\n\t\toperationCount: operations.length,\n\t\tcollections,\n\t\tincludesRecords: includeRecords,\n\t\tchecksum: checksumHex,\n\t}\n\n\t// Final output: header + manifest + all content sections + checksum section\n\tconst header = new Uint8Array(0)\n\n\t// Merge manifest + all sections + checksum section\n\tconst manifestSection = encodeJsonSection('manifest', manifest)\n\tconst checksumSection = encodeSection('checksum', new TextEncoder().encode(checksumHex))\n\n\tconst totalLen = manifestSection.length + allContentForChecksum.length + checksumSection.length\n\tconst result = new Uint8Array(totalLen)\n\tlet pos = 0\n\tresult.set(manifestSection, pos)\n\tpos += manifestSection.length\n\tresult.set(allContentForChecksum, pos)\n\tpos += allContentForChecksum.length\n\tresult.set(checksumSection, pos)\n\n\treturn result\n}\n\n/**\n * Read backup manifest without loading the entire backup.\n * Parses only the first section (which is always the manifest).\n *\n * @param data - The raw backup data\n * @returns The backup manifest\n */\nexport function readBackupManifest(data: Uint8Array): BackupManifest {\n\tconst sections = parseSections(data)\n\tconst manifest = parseJsonSection<BackupManifest>(sections, 'manifest')\n\tif (!manifest) {\n\t\tthrow new Error('Invalid backup: manifest section not found')\n\t}\n\treturn manifest\n}\n\n/**\n * Validate the backup checksum.\n *\n * @param data - The raw backup data\n * @returns True if the checksum is valid\n */\nexport async function verifyBackupChecksum(data: Uint8Array): Promise<boolean> {\n\tconst sections = parseSections(data)\n\tconst manifest = parseJsonSection<BackupManifest>(sections, 'manifest')\n\tif (!manifest) return false\n\n\tconst storedChecksum = manifest.checksum\n\tif (!storedChecksum) return false\n\n\t// Recompute checksum over content sections (everything after manifest, before checksum)\n\tconst manifestSection = findSection(sections, 'manifest')\n\tif (!manifestSection) return false\n\n\t// Find the checksum section\n\tconst checksumSection = findSection(sections, 'checksum')\n\n\t// The content spans from end of manifest section to start of checksum section\n\tconst checksumBytes = checksumSection\n\t\t? new TextEncoder().encode(manifest.checksum)\n\t\t: new Uint8Array(0)\n\n\t// Rebuild all content sections and hash them\n\tconst contentSections = sections.filter((s) => s.name !== 'manifest' && s.name !== 'checksum')\n\n\tlet contentForHash = new Uint8Array(0)\n\tfor (const section of contentSections) {\n\t\tconst sectionBytes = encodeSection(section.name, section.content)\n\t\tconst newLen = contentForHash.length + sectionBytes.length\n\t\tconst combined = new Uint8Array(newLen)\n\t\tcombined.set(contentForHash, 0)\n\t\tcombined.set(sectionBytes, contentForHash.length)\n\t\tcontentForHash = combined\n\t}\n\n\tconst computed = await computeSha256(contentForHash)\n\treturn computed === storedChecksum\n}\n\n/**\n * Restore a backup into a store.\n *\n * @param adapter - The storage adapter\n * @param schema - The schema definition\n * @param data - The backup data\n * @param options - Restore options\n * @returns Result of the restore operation\n */\nexport async function restoreBackup(\n\tadapter: StorageAdapter,\n\tschema: SchemaDefinition,\n\tdata: Uint8Array,\n\toptions?: RestoreOptions,\n): Promise<RestoreResult> {\n\tconst startTime = Date.now()\n\tconst onProgress = options?.onProgress ?? (() => {})\n\tconst collectionFilter = options?.collections\n\n\tonProgress({ phase: 'verifying', progress: 0, message: 'Parsing backup' })\n\n\tconst sections = parseSections(data)\n\tconst manifest = parseJsonSection<BackupManifest>(sections, 'manifest')\n\tif (!manifest) {\n\t\treturn {\n\t\t\toperationsRestored: 0,\n\t\t\trecordsRestored: 0,\n\t\t\tsuccess: false,\n\t\t\terror: 'Invalid backup: manifest section not found',\n\t\t\tduration: Date.now() - startTime,\n\t\t}\n\t}\n\n\t// Validate format version\n\tif (manifest.version !== BACKUP_VERSION) {\n\t\treturn {\n\t\t\toperationsRestored: 0,\n\t\t\trecordsRestored: 0,\n\t\t\tsuccess: false,\n\t\t\terror: `Unsupported backup version: ${manifest.version}`,\n\t\t\tduration: Date.now() - startTime,\n\t\t}\n\t}\n\n\tonProgress({ phase: 'verifying', progress: 0.2, message: 'Verifying checksum' })\n\n\t// Verify checksum\n\tconst valid = await verifyBackupChecksum(data)\n\tif (!valid) {\n\t\treturn {\n\t\t\toperationsRestored: 0,\n\t\t\trecordsRestored: 0,\n\t\t\tsuccess: false,\n\t\t\terror: 'Backup checksum mismatch: data may be corrupted',\n\t\t\tduration: Date.now() - startTime,\n\t\t}\n\t}\n\n\tonProgress({ phase: 'restoring', progress: 0.3, message: 'Loading operations' })\n\n\t// Parse operations\n\tconst opsContent = findSection(sections, 'operations')\n\tlet operations: Operation[] = []\n\tif (opsContent) {\n\t\tconst text = new TextDecoder().decode(opsContent)\n\t\tconst lines = text\n\t\t\t.trim()\n\t\t\t.split('\\n')\n\t\t\t.filter((l) => l.length > 0)\n\t\toperations = lines.map((line) => JSON.parse(line) as Operation)\n\t}\n\n\t// Filter collections if requested\n\tif (collectionFilter) {\n\t\toperations = operations.filter((op) => collectionFilter.includes(op.collection))\n\t}\n\n\t// Load version vector\n\tconst vvData = parseJsonSection<Record<string, number>>(sections, 'version_vector')\n\n\t// Load meta\n\tconst metaData = parseJsonSection<Record<string, string>>(sections, 'meta')\n\n\tonProgress({ phase: 'restoring', progress: 0.4, message: 'Applying operations' })\n\n\tif (options?.merge) {\n\t\t// Merge mode: replay operations through applyRemoteOperation.\n\t\t// This won't work directly since adapter doesn't have applyRemoteOperation.\n\t\t// Instead, we import the raw data into the adapter.\n\t\tawait adapter.transaction(async (tx) => {\n\t\t\t// Import version vector\n\t\t\tif (vvData) {\n\t\t\t\tfor (const [nid, seq] of Object.entries(vvData)) {\n\t\t\t\t\tawait tx.execute(\n\t\t\t\t\t\t'INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)',\n\t\t\t\t\t\t[nid, seq],\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Import meta\n\t\t\tif (metaData) {\n\t\t\t\tfor (const [key, value] of Object.entries(metaData)) {\n\t\t\t\t\tawait tx.execute('INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)', [\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t])\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Import operations into _kora_ops_ tables\n\t\t\tfor (const op of operations) {\n\t\t\t\tconst collection = op.collection\n\t\t\t\tconst opRow = {\n\t\t\t\t\tid: op.id,\n\t\t\t\t\tnode_id: op.nodeId,\n\t\t\t\t\ttype: op.type,\n\t\t\t\t\trecord_id: op.recordId,\n\t\t\t\t\tdata: op.data !== null ? JSON.stringify(op.data) : null,\n\t\t\t\t\tprevious_data: op.previousData !== null ? JSON.stringify(op.previousData) : null,\n\t\t\t\t\ttimestamp: JSON.stringify(op.timestamp),\n\t\t\t\t\tsequence_number: op.sequenceNumber,\n\t\t\t\t\tcausal_deps: JSON.stringify(op.causalDeps),\n\t\t\t\t\tschema_version: op.schemaVersion,\n\t\t\t\t}\n\n\t\t\t\tawait tx.execute(\n\t\t\t\t\t`INSERT OR IGNORE INTO ${quoteIdent(`_kora_ops_${collection}`)} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t\t\t\t\t[\n\t\t\t\t\t\topRow.id,\n\t\t\t\t\t\topRow.node_id,\n\t\t\t\t\t\topRow.type,\n\t\t\t\t\t\topRow.record_id,\n\t\t\t\t\t\topRow.data,\n\t\t\t\t\t\topRow.previous_data,\n\t\t\t\t\t\topRow.timestamp,\n\t\t\t\t\t\topRow.sequence_number,\n\t\t\t\t\t\topRow.causal_deps,\n\t\t\t\t\t\topRow.schema_version,\n\t\t\t\t\t],\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t} else {\n\t\t// Replace mode: clear existing data and bulk import\n\t\tawait adapter.transaction(async (tx) => {\n\t\t\t// Clear existing data\n\t\t\tfor (const collection of Object.keys(schema.collections)) {\n\t\t\t\tawait tx.execute(`DELETE FROM ${quoteIdent(`_kora_ops_${collection}`)}`)\n\t\t\t\tawait tx.execute(`DELETE FROM ${quoteIdent(collection)}`)\n\t\t\t}\n\t\t\tawait tx.execute('DELETE FROM _kora_version_vector')\n\t\t\tawait tx.execute('DELETE FROM _kora_meta')\n\n\t\t\t// Import version vector\n\t\t\tif (vvData) {\n\t\t\t\tfor (const [nid, seq] of Object.entries(vvData)) {\n\t\t\t\t\tawait tx.execute(\n\t\t\t\t\t\t'INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)',\n\t\t\t\t\t\t[nid, seq],\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Import meta\n\t\t\tif (metaData) {\n\t\t\t\tfor (const [key, value] of Object.entries(metaData)) {\n\t\t\t\t\tawait tx.execute('INSERT INTO _kora_meta (key, value) VALUES (?, ?)', [key, value])\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Import operations\n\t\t\tfor (const op of operations) {\n\t\t\t\tconst collection = op.collection\n\t\t\t\tawait tx.execute(\n\t\t\t\t\t`INSERT INTO ${quoteIdent(`_kora_ops_${collection}`)} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t\t\t\t\t[\n\t\t\t\t\t\top.id,\n\t\t\t\t\t\top.nodeId,\n\t\t\t\t\t\top.type,\n\t\t\t\t\t\top.recordId,\n\t\t\t\t\t\top.data !== null ? JSON.stringify(op.data) : null,\n\t\t\t\t\t\top.previousData !== null ? JSON.stringify(op.previousData) : null,\n\t\t\t\t\t\tJSON.stringify(op.timestamp),\n\t\t\t\t\t\top.sequenceNumber,\n\t\t\t\t\t\tJSON.stringify(op.causalDeps),\n\t\t\t\t\t\top.schemaVersion,\n\t\t\t\t\t],\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n\n\tonProgress({ phase: 'restoring', progress: 0.8, message: 'Restoring records' })\n\n\t// Restore records sections (optional)\n\tlet recordsRestored = 0\n\tif (manifest.includesRecords) {\n\t\tfor (const col of manifest.collections) {\n\t\t\tif (collectionFilter && !collectionFilter.includes(col)) continue\n\n\t\t\tconst sectionContent = findSection(sections, `records:${col}`)\n\t\t\tif (!sectionContent) continue\n\n\t\t\tconst text = new TextDecoder().decode(sectionContent)\n\t\t\tconst lines = text\n\t\t\t\t.trim()\n\t\t\t\t.split('\\n')\n\t\t\t\t.filter((l) => l.length > 0)\n\t\t\tconst records = lines.map((line) => JSON.parse(line) as Record<string, unknown>)\n\n\t\t\tif (records.length === 0) continue\n\n\t\t\tawait adapter.transaction(async (tx) => {\n\t\t\t\tfor (const record of records) {\n\t\t\t\t\tconst keys = Object.keys(record)\n\t\t\t\t\tconst placeholders = keys.map(() => '?').join(', ')\n\t\t\t\t\tconst quotedKeys = keys.map((k) => quoteIdent(k)).join(', ')\n\t\t\t\t\tconst values = keys.map((k) => {\n\t\t\t\t\t\tconst v = record[k]\n\t\t\t\t\t\tif (typeof v === 'boolean') return v ? 1 : 0\n\t\t\t\t\t\tif (Array.isArray(v) || (typeof v === 'object' && v !== null)) return JSON.stringify(v)\n\t\t\t\t\t\treturn v\n\t\t\t\t\t})\n\n\t\t\t\t\tawait tx.execute(\n\t\t\t\t\t\t`INSERT OR REPLACE INTO ${quoteIdent(col)} (${quotedKeys}) VALUES (${placeholders})`,\n\t\t\t\t\t\tvalues,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\trecordsRestored += records.length\n\t\t}\n\t}\n\n\tonProgress({ phase: 'restoring', progress: 1, message: 'Done' })\n\n\treturn {\n\t\toperationsRestored: operations.length,\n\t\trecordsRestored,\n\t\tsuccess: true,\n\t\tduration: Date.now() - startTime,\n\t}\n}\n\n// ── Reading helpers ──────────────────────────────────────────────────────────\n\nasync function readVersionVector(adapter: StorageAdapter): Promise<Map<string, number>> {\n\tconst rows = await adapter.query<{ node_id: string; sequence_number: number }>(\n\t\t'SELECT node_id, sequence_number FROM _kora_version_vector',\n\t)\n\tconst vv = new Map<string, number>()\n\tfor (const row of rows) {\n\t\tvv.set(row.node_id, row.sequence_number)\n\t}\n\treturn vv\n}\n\ninterface MetaRow {\n\tkey: string\n\tvalue: string\n}\n\nasync function readMeta(adapter: StorageAdapter): Promise<MetaRow[]> {\n\treturn adapter.query<MetaRow>('SELECT key, value FROM _kora_meta')\n}\n\nasync function readAllOperations(\n\tadapter: StorageAdapter,\n\tschema: SchemaDefinition,\n): Promise<Operation[]> {\n\tconst allOps: Operation[] = []\n\n\tfor (const collectionName of Object.keys(schema.collections)) {\n\t\tconst rows = await adapter.query<OperationRow>(\n\t\t\t`SELECT * FROM ${quoteIdent(`_kora_ops_${collectionName}`)} ORDER BY sequence_number ASC`,\n\t\t)\n\n\t\tfor (const row of rows) {\n\t\t\tallOps.push(deserializeOperationWithCollection(row, collectionName))\n\t\t}\n\t}\n\n\treturn allOps\n}\n\nasync function readCollectionRecords(\n\tadapter: StorageAdapter,\n\tcollection: string,\n): Promise<Record<string, unknown>[]> {\n\treturn adapter.query<Record<string, unknown>>(\n\t\t`SELECT * FROM ${quoteIdent(collection)} WHERE _deleted = 0`,\n\t)\n}\n\n// ── Crypto helpers ───────────────────────────────────────────────────────────\n\nasync function computeSha256(data: Uint8Array<ArrayBuffer>): Promise<string> {\n\tconst hashBuffer = await crypto.subtle.digest('SHA-256', data)\n\tconst hashArray = Array.from(new Uint8Array(hashBuffer))\n\treturn hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')\n}\n"],"mappings":"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","names":[]}
|