@korajs/store 1.0.0-beta.0 → 1.0.0-beta.1
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.d.cts +1 -1
- package/dist/adapters/better-sqlite3.d.ts +1 -1
- package/dist/adapters/indexeddb.d.cts +1 -1
- package/dist/adapters/indexeddb.d.ts +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +1 -1
- package/dist/adapters/sqlite-wasm.d.ts +1 -1
- package/dist/{backup-5XZ7THNJ.js → backup-ZV4Y424B.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-ETDJEH2I.js → chunk-6NKLDBTY.js} +11 -1
- package/dist/{chunk-ETDJEH2I.js.map → chunk-6NKLDBTY.js.map} +1 -1
- package/dist/{chunk-EIVXWF4B.js → chunk-FFYA7J5H.js} +2 -2
- package/dist/chunk-W4MTTNEZ.js +55 -0
- package/dist/chunk-W4MTTNEZ.js.map +1 -0
- package/dist/{chunk-5R4QMY3Z.js → chunk-X57MWW35.js} +2 -2
- package/dist/{chunk-OUQDC5Z6.js → chunk-ZLWAYQEQ.js} +28 -12
- package/dist/chunk-ZLWAYQEQ.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-AJ7RORNK.js} +3 -3
- package/dist/index.cjs +632 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +397 -6
- package/dist/index.d.ts +397 -6
- package/dist/index.js +561 -26
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +25 -5
- 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/{types-CyjXwrjW.d.cts → types-5h4Hqcwc.d.ts} +10 -3
- package/dist/{types-DAxCladl.d.ts → types-DfPiPwKr.d.cts} +10 -3
- package/dist/{types-CJOVKxLw.d.ts → types-k6AZgiH4.d.cts} +6 -1
- package/dist/{types-CJOVKxLw.d.cts → types-k6AZgiH4.d.ts} +6 -1
- package/package.json +13 -2
- package/dist/chunk-OUQDC5Z6.js.map +0 -1
- /package/dist/{backup-5XZ7THNJ.js.map → backup-ZV4Y424B.js.map} +0 -0
- /package/dist/{chunk-EIVXWF4B.js.map → chunk-FFYA7J5H.js.map} +0 -0
- /package/dist/{chunk-5R4QMY3Z.js.map → chunk-X57MWW35.js.map} +0 -0
- /package/dist/{export-audit-VPGQWNR6.js.map → export-audit-AJ7RORNK.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaDefinition } from '@korajs/core';
|
|
2
|
-
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-
|
|
2
|
+
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-k6AZgiH4.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Storage adapter backed by better-sqlite3 for Node.js environments.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaDefinition } from '@korajs/core';
|
|
2
|
-
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-
|
|
2
|
+
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-k6AZgiH4.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Storage adapter backed by better-sqlite3 for Node.js environments.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KoraEventEmitter, SchemaDefinition } from '@korajs/core';
|
|
2
|
-
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-
|
|
2
|
+
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-k6AZgiH4.cjs';
|
|
3
3
|
import { W as WorkerBridge } from '../sqlite-wasm-channel-DUDPclTE.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KoraEventEmitter, SchemaDefinition } from '@korajs/core';
|
|
2
|
-
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-
|
|
2
|
+
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-k6AZgiH4.js';
|
|
3
3
|
import { W as WorkerBridge } from '../sqlite-wasm-channel-DUDPclTE.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaDefinition } from '@korajs/core';
|
|
2
|
-
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-
|
|
2
|
+
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-k6AZgiH4.cjs';
|
|
3
3
|
import { W as WorkerBridge } from '../sqlite-wasm-channel-DUDPclTE.cjs';
|
|
4
4
|
export { a as WebWorkerBridge, b as WorkerRequest, c as WorkerResponse } from '../sqlite-wasm-channel-DUDPclTE.cjs';
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaDefinition } from '@korajs/core';
|
|
2
|
-
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-
|
|
2
|
+
import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-k6AZgiH4.js';
|
|
3
3
|
import { W as WorkerBridge } from '../sqlite-wasm-channel-DUDPclTE.js';
|
|
4
4
|
export { a as WebWorkerBridge, b as WorkerRequest, c as WorkerResponse } from '../sqlite-wasm-channel-DUDPclTE.js';
|
|
5
5
|
|
|
@@ -3,12 +3,12 @@ import {
|
|
|
3
3
|
readBackupManifest,
|
|
4
4
|
restoreBackup,
|
|
5
5
|
verifyBackupChecksum
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-X57MWW35.js";
|
|
7
|
+
import "./chunk-6NKLDBTY.js";
|
|
8
8
|
export {
|
|
9
9
|
exportBackup,
|
|
10
10
|
readBackupManifest,
|
|
11
11
|
restoreBackup,
|
|
12
12
|
verifyBackupChecksum
|
|
13
13
|
};
|
|
14
|
-
//# sourceMappingURL=backup-
|
|
14
|
+
//# sourceMappingURL=backup-ZV4Y424B.js.map
|
package/dist/blob-fs.cjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/blob-fs.ts
|
|
21
|
+
var blob_fs_exports = {};
|
|
22
|
+
__export(blob_fs_exports, {
|
|
23
|
+
FilesystemBlobStore: () => FilesystemBlobStore
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(blob_fs_exports);
|
|
26
|
+
|
|
27
|
+
// src/blob/filesystem-blob-store.ts
|
|
28
|
+
var import_node_crypto = require("crypto");
|
|
29
|
+
var import_promises = require("fs/promises");
|
|
30
|
+
var import_node_path = require("path");
|
|
31
|
+
var import_core2 = require("@korajs/core");
|
|
32
|
+
|
|
33
|
+
// src/blob/content-addressed-blob-store.ts
|
|
34
|
+
var import_core = require("@korajs/core");
|
|
35
|
+
var BlobIntegrityError = class extends Error {
|
|
36
|
+
constructor(expectedHash, actualHash) {
|
|
37
|
+
super(`Blob integrity check failed: content under hash ${expectedHash} hashes to ${actualHash}`);
|
|
38
|
+
this.expectedHash = expectedHash;
|
|
39
|
+
this.actualHash = actualHash;
|
|
40
|
+
this.name = "BlobIntegrityError";
|
|
41
|
+
}
|
|
42
|
+
expectedHash;
|
|
43
|
+
actualHash;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/blob/filesystem-blob-store.ts
|
|
47
|
+
var FilesystemBlobStore = class {
|
|
48
|
+
constructor(dir) {
|
|
49
|
+
this.dir = dir;
|
|
50
|
+
}
|
|
51
|
+
dir;
|
|
52
|
+
pathFor(hash) {
|
|
53
|
+
return (0, import_node_path.join)(this.dir, hash.slice(0, 2), hash);
|
|
54
|
+
}
|
|
55
|
+
async put(bytes, metadata = {}) {
|
|
56
|
+
const ref = await (0, import_core2.createBlobRef)(bytes, metadata);
|
|
57
|
+
const target = this.pathFor(ref.hash);
|
|
58
|
+
if (await this.exists(target)) {
|
|
59
|
+
return ref;
|
|
60
|
+
}
|
|
61
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(target), { recursive: true });
|
|
62
|
+
const out = new Uint8Array(bytes.byteLength);
|
|
63
|
+
out.set(bytes);
|
|
64
|
+
const tmp = `${target}.${(0, import_node_crypto.randomUUID)()}.tmp`;
|
|
65
|
+
await (0, import_promises.writeFile)(tmp, out);
|
|
66
|
+
await (0, import_promises.rename)(tmp, target);
|
|
67
|
+
return ref;
|
|
68
|
+
}
|
|
69
|
+
async get(hash) {
|
|
70
|
+
let contents;
|
|
71
|
+
try {
|
|
72
|
+
contents = await (0, import_promises.readFile)(this.pathFor(hash));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (isNotFound(error)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
const bytes = new Uint8Array(contents);
|
|
80
|
+
const actual = await (0, import_core2.hashBlob)(bytes);
|
|
81
|
+
if (actual !== hash) {
|
|
82
|
+
throw new BlobIntegrityError(hash, actual);
|
|
83
|
+
}
|
|
84
|
+
return bytes;
|
|
85
|
+
}
|
|
86
|
+
async has(hash) {
|
|
87
|
+
return this.exists(this.pathFor(hash));
|
|
88
|
+
}
|
|
89
|
+
async delete(hash) {
|
|
90
|
+
const target = this.pathFor(hash);
|
|
91
|
+
if (!await this.exists(target)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
await (0, import_promises.rm)(target);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
async size() {
|
|
98
|
+
let count = 0;
|
|
99
|
+
let shards;
|
|
100
|
+
try {
|
|
101
|
+
shards = await (0, import_promises.readdir)(this.dir);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (isNotFound(error)) {
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
for (const shard of shards) {
|
|
109
|
+
try {
|
|
110
|
+
const entries = await (0, import_promises.readdir)((0, import_node_path.join)(this.dir, shard));
|
|
111
|
+
count += entries.filter((name) => !name.endsWith(".tmp")).length;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!isNotFound(error)) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return count;
|
|
119
|
+
}
|
|
120
|
+
async list() {
|
|
121
|
+
let shards;
|
|
122
|
+
try {
|
|
123
|
+
shards = await (0, import_promises.readdir)(this.dir);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (isNotFound(error)) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const hashes = [];
|
|
131
|
+
for (const shard of shards) {
|
|
132
|
+
try {
|
|
133
|
+
const entries = await (0, import_promises.readdir)((0, import_node_path.join)(this.dir, shard));
|
|
134
|
+
for (const name of entries) {
|
|
135
|
+
if (!name.endsWith(".tmp")) {
|
|
136
|
+
hashes.push(name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (!isNotFound(error)) {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return hashes;
|
|
146
|
+
}
|
|
147
|
+
async exists(path) {
|
|
148
|
+
try {
|
|
149
|
+
await (0, import_promises.stat)(path);
|
|
150
|
+
return true;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (isNotFound(error)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
function isNotFound(error) {
|
|
160
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
161
|
+
}
|
|
162
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
163
|
+
0 && (module.exports = {
|
|
164
|
+
FilesystemBlobStore
|
|
165
|
+
});
|
|
166
|
+
//# sourceMappingURL=blob-fs.cjs.map
|
|
@@ -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":[]}
|
|
@@ -207,6 +207,9 @@ function serializeValue(value, descriptor) {
|
|
|
207
207
|
case "boolean":
|
|
208
208
|
return value ? 1 : 0;
|
|
209
209
|
case "array":
|
|
210
|
+
case "object":
|
|
211
|
+
case "json":
|
|
212
|
+
case "blob":
|
|
210
213
|
return JSON.stringify(value);
|
|
211
214
|
case "richtext":
|
|
212
215
|
return encodeRichtext(value);
|
|
@@ -223,6 +226,13 @@ function deserializeValue(value, descriptor) {
|
|
|
223
226
|
return JSON.parse(value);
|
|
224
227
|
}
|
|
225
228
|
return value;
|
|
229
|
+
case "object":
|
|
230
|
+
case "json":
|
|
231
|
+
case "blob":
|
|
232
|
+
if (typeof value === "string") {
|
|
233
|
+
return JSON.parse(value);
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
226
236
|
case "richtext":
|
|
227
237
|
return decodeRichtext(value);
|
|
228
238
|
default:
|
|
@@ -243,4 +253,4 @@ export {
|
|
|
243
253
|
deserializeOperation,
|
|
244
254
|
deserializeOperationWithCollection
|
|
245
255
|
};
|
|
246
|
-
//# sourceMappingURL=chunk-
|
|
256
|
+
//# sourceMappingURL=chunk-6NKLDBTY.js.map
|
|
@@ -1 +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\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\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 '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
|
+
{"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\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":[]}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-MYSJN3DF.js";
|
|
6
6
|
import {
|
|
7
7
|
deserializeOperationWithCollection
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6NKLDBTY.js";
|
|
9
9
|
|
|
10
10
|
// src/audit/export-audit.ts
|
|
11
11
|
function encodeSection(name, content) {
|
|
@@ -185,4 +185,4 @@ export {
|
|
|
185
185
|
readAuditExportManifest,
|
|
186
186
|
verifyAuditExportChecksum
|
|
187
187
|
};
|
|
188
|
-
//# sourceMappingURL=chunk-
|
|
188
|
+
//# sourceMappingURL=chunk-FFYA7J5H.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/blob/content-addressed-blob-store.ts
|
|
2
|
+
import { createBlobRef, hashBlob } from "@korajs/core";
|
|
3
|
+
var BlobIntegrityError = class extends Error {
|
|
4
|
+
constructor(expectedHash, actualHash) {
|
|
5
|
+
super(`Blob integrity check failed: content under hash ${expectedHash} hashes to ${actualHash}`);
|
|
6
|
+
this.expectedHash = expectedHash;
|
|
7
|
+
this.actualHash = actualHash;
|
|
8
|
+
this.name = "BlobIntegrityError";
|
|
9
|
+
}
|
|
10
|
+
expectedHash;
|
|
11
|
+
actualHash;
|
|
12
|
+
};
|
|
13
|
+
var MemoryBlobStore = class {
|
|
14
|
+
blobs = /* @__PURE__ */ new Map();
|
|
15
|
+
async put(bytes, metadata = {}) {
|
|
16
|
+
const ref = await createBlobRef(bytes, metadata);
|
|
17
|
+
if (!this.blobs.has(ref.hash)) {
|
|
18
|
+
const stored = new Uint8Array(bytes.byteLength);
|
|
19
|
+
stored.set(bytes);
|
|
20
|
+
this.blobs.set(ref.hash, stored);
|
|
21
|
+
}
|
|
22
|
+
return ref;
|
|
23
|
+
}
|
|
24
|
+
async get(hash) {
|
|
25
|
+
const stored = this.blobs.get(hash);
|
|
26
|
+
if (stored === void 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const actualHash = await hashBlob(stored);
|
|
30
|
+
if (actualHash !== hash) {
|
|
31
|
+
throw new BlobIntegrityError(hash, actualHash);
|
|
32
|
+
}
|
|
33
|
+
const out = new Uint8Array(stored.byteLength);
|
|
34
|
+
out.set(stored);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
async has(hash) {
|
|
38
|
+
return this.blobs.has(hash);
|
|
39
|
+
}
|
|
40
|
+
async delete(hash) {
|
|
41
|
+
return this.blobs.delete(hash);
|
|
42
|
+
}
|
|
43
|
+
async size() {
|
|
44
|
+
return this.blobs.size;
|
|
45
|
+
}
|
|
46
|
+
async list() {
|
|
47
|
+
return [...this.blobs.keys()];
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
BlobIntegrityError,
|
|
53
|
+
MemoryBlobStore
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=chunk-W4MTTNEZ.js.map
|