@lsync/server 0.0.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/index.mjs ADDED
@@ -0,0 +1,1346 @@
1
+ import { n as webSocketAttachmentSchema, t as router } from "./router-C4eYfMV7.mjs";
2
+ import { and, compileReadExpression, compileReadExpression as compileReadExpression$1, eq, field, gt, gte, inArray, lt, lte, matchesReadPredicate, matchesReadPredicate as matchesReadPredicate$1, ne, not, or, parseClientRpcRequest, readExpressionReferences, readExpressionReferences as readExpressionReferences$1, readExpressionRow, readExpressionRow as readExpressionRow$1, readRpcId, sendServerMessage, val } from "@lsync/transport";
3
+ import sqlTag, { join, raw } from "sql-template-tag";
4
+ import { z } from "zod";
5
+ //#region src/access-expression.ts
6
+ function accessRows(referenceNames) {
7
+ return {
8
+ references: readExpressionReferences$1(referenceNames),
9
+ row: readExpressionRow$1()
10
+ };
11
+ }
12
+ function reduceAccessExpression(expression, references) {
13
+ const dependencies = [];
14
+ const reduced = reduce(expression, references, dependencies);
15
+ if (isVal(reduced)) return {
16
+ allow: reduced.value === true,
17
+ dependencies
18
+ };
19
+ return {
20
+ allow: true,
21
+ dependencies,
22
+ predicate: compileReadExpression$1(reduced)
23
+ };
24
+ }
25
+ function reduce(expression, references, dependencies) {
26
+ const unwrapped = unwrap(expression);
27
+ if (isVal(unwrapped)) return unwrapped;
28
+ if (isRef(unwrapped)) return reduceRef(unwrapped, references, dependencies);
29
+ if (unwrapped.name === "and") return reduceAnd(unwrapped, references, dependencies);
30
+ if (unwrapped.name === "or") return reduceOr(unwrapped, references, dependencies);
31
+ if (unwrapped.name === "not") {
32
+ const child = reduceExpressionArg(unwrapped.args[0], references, dependencies);
33
+ return isVal(child) ? val$1(child.value !== true) : func("not", child);
34
+ }
35
+ const args = unwrapped.args.map((arg) => reduceExpressionArg(arg, references, dependencies));
36
+ if (args.every(isVal)) return val$1(evaluate(unwrapped.name, args.map((arg) => arg.value)));
37
+ return func(unwrapped.name, ...args);
38
+ }
39
+ function reduceRef(expression, references, dependencies) {
40
+ if (expression.source !== "reference") return expression;
41
+ const name = expression.name;
42
+ const reference = name ? references.get(name) : void 0;
43
+ if (!name || !reference) return val$1(void 0);
44
+ const field = expression.path.join(".");
45
+ dependencies.push({
46
+ collection: reference.collection,
47
+ field,
48
+ key: reference.key,
49
+ name
50
+ });
51
+ return val$1(valueAtPath$1(reference.value, expression.path));
52
+ }
53
+ function reduceAnd(expression, references, dependencies) {
54
+ const reduced = expression.args.map((arg) => reduceExpressionArg(arg, references, dependencies));
55
+ if (reduced.some((arg) => isVal(arg) && arg.value !== true)) return val$1(false);
56
+ const remaining = reduced.filter((arg) => !isVal(arg));
57
+ if (remaining.length === 0) return val$1(true);
58
+ return remaining.length === 1 ? remaining[0] : func("and", ...remaining);
59
+ }
60
+ function reduceOr(expression, references, dependencies) {
61
+ const reduced = expression.args.map((arg) => reduceExpressionArg(arg, references, dependencies));
62
+ if (reduced.some((arg) => isVal(arg) && arg.value === true)) return val$1(true);
63
+ const remaining = reduced.filter((arg) => !isVal(arg));
64
+ if (remaining.length === 0) return val$1(false);
65
+ return remaining.length === 1 ? remaining[0] : func("or", ...remaining);
66
+ }
67
+ function reduceExpressionArg(value, references, dependencies) {
68
+ return isExpression(value) ? reduce(value, references, dependencies) : val$1(value);
69
+ }
70
+ function evaluate(name, args) {
71
+ const [left, right] = args;
72
+ switch (name) {
73
+ case "eq": return left === right;
74
+ case "not_eq": return left !== right;
75
+ case "gt": return compare(left, right, (a, b) => a > b);
76
+ case "gte": return compare(left, right, (a, b) => a >= b);
77
+ case "lt": return compare(left, right, (a, b) => a < b);
78
+ case "lte": return compare(left, right, (a, b) => a <= b);
79
+ case "in": return Array.isArray(right) && right.includes(left);
80
+ default: throw new Error(`Unsupported read access operator: ${name}`);
81
+ }
82
+ }
83
+ function compare(left, right, compareValues) {
84
+ if (typeof left === "number" && typeof right === "number") return compareValues(left, right);
85
+ if (typeof left === "string" && typeof right === "string") return compareValues(left, right);
86
+ return false;
87
+ }
88
+ function func(name, ...args) {
89
+ return {
90
+ type: "func",
91
+ name,
92
+ args
93
+ };
94
+ }
95
+ function val$1(value) {
96
+ return {
97
+ type: "val",
98
+ value
99
+ };
100
+ }
101
+ function valueAtPath$1(value, path) {
102
+ return path.reduce((current, segment) => {
103
+ if (typeof current !== "object" || current === null) return void 0;
104
+ return current[String(segment)];
105
+ }, value);
106
+ }
107
+ function unwrap(expression) {
108
+ return "expression" in expression ? unwrap(expression.expression) : expression;
109
+ }
110
+ function isExpression(value) {
111
+ return typeof value === "object" && value !== null && ("type" in value || "expression" in value);
112
+ }
113
+ function isVal(value) {
114
+ return unwrap(value).type === "val";
115
+ }
116
+ function isRef(value) {
117
+ return unwrap(value).type === "ref";
118
+ }
119
+ //#endregion
120
+ //#region src/collections.ts
121
+ function resolveCollection(collectionName, collections = {}) {
122
+ const exact = collections[collectionName];
123
+ if (exact) return {
124
+ name: collectionName,
125
+ collection: exact,
126
+ params: {},
127
+ scope: collectionScope(collectionName)
128
+ };
129
+ const best = Object.entries(collections).flatMap(([name, collection], index) => {
130
+ const match = matchCollectionPattern(name, collectionName);
131
+ return match ? [{
132
+ name,
133
+ collection,
134
+ params: match.params,
135
+ staticSegments: match.staticSegments,
136
+ segments: match.segments,
137
+ index
138
+ }] : [];
139
+ }).sort(comparePatternMatches)[0];
140
+ if (!best) return;
141
+ return {
142
+ name: best.name,
143
+ collection: best.collection,
144
+ params: best.params,
145
+ scope: collectionScope(collectionName)
146
+ };
147
+ }
148
+ function collectionScope(collectionName) {
149
+ return `/${splitCollectionPath(collectionName).map(normalizeSegment).join("/")}/`;
150
+ }
151
+ function isCollectionPattern(collectionName) {
152
+ return collectionName.split("/").some((segment) => /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(segment));
153
+ }
154
+ function collectionApiHandlers(collections = {}) {
155
+ const handlers = {};
156
+ for (const [collectionPath, collection] of Object.entries(collections)) for (const [name, definition] of Object.entries(collection.api ?? {})) {
157
+ const path = defaultCollectionApiPath(collectionPath, name);
158
+ if (handlers[path]) throw new Error(`Duplicate API path: ${path}`);
159
+ handlers[path] = (args) => definition.handler({
160
+ ...args,
161
+ input: definition.input.parse(args.input)
162
+ });
163
+ }
164
+ return handlers;
165
+ }
166
+ function defaultCollectionApiPath(collectionPath, name) {
167
+ const prefix = splitCollectionPath(collectionPath).filter((segment) => !collectionPathParamName(segment)).join(".");
168
+ return prefix ? `${prefix}.${name}` : name;
169
+ }
170
+ function matchCollectionPattern(pattern, collectionName) {
171
+ if (!isCollectionPattern(pattern)) return;
172
+ const patternSegments = splitCollectionPath(pattern);
173
+ const collectionSegments = splitCollectionPath(collectionName);
174
+ if (patternSegments.length !== collectionSegments.length) return;
175
+ const params = {};
176
+ let staticSegments = 0;
177
+ for (const [index, segment] of patternSegments.entries()) {
178
+ const param = segment.match(/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/);
179
+ if (param?.[1]) {
180
+ params[param[1]] = decodeURIComponent(collectionSegments[index] ?? "");
181
+ continue;
182
+ }
183
+ if (segment !== collectionSegments[index]) return;
184
+ staticSegments += 1;
185
+ }
186
+ return {
187
+ params,
188
+ staticSegments,
189
+ segments: patternSegments.length
190
+ };
191
+ }
192
+ function comparePatternMatches(left, right) {
193
+ return right.staticSegments - left.staticSegments || right.segments - left.segments || left.index - right.index;
194
+ }
195
+ function splitCollectionPath(path) {
196
+ const trimmed = path.trim().replace(/^\/+|\/+$/g, "");
197
+ if (!trimmed) return [];
198
+ return trimmed.split("/");
199
+ }
200
+ function collectionPathParamName(segment) {
201
+ return segment.match(/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/)?.[1] ?? segment.match(/^:([A-Za-z_][A-Za-z0-9_]*)$/)?.[1];
202
+ }
203
+ function normalizeSegment(segment) {
204
+ return encodeURIComponent(decodeURIComponent(segment));
205
+ }
206
+ //#endregion
207
+ //#region src/access.ts
208
+ function authorizeReadQuery(query, collections, auth, store) {
209
+ const decision = readAccessDecision(query.collection, collections, auth, store);
210
+ if (!decision.allow) return void 0;
211
+ if (!decision.predicate) return query;
212
+ const predicate = combinePredicates$1(decision.predicate, query.predicate);
213
+ return predicate ? {
214
+ ...query,
215
+ predicate
216
+ } : query;
217
+ }
218
+ function visibleUpdateForAuth(update, collections, auth, store) {
219
+ const decision = readAccessDecision(update.collection, collections, auth, store);
220
+ if (!decision.allow) return void 0;
221
+ if (!decision.predicate) return update;
222
+ const before = update.previousValue ? matchesReadPredicate$1(update.previousValue, decision.predicate) : false;
223
+ const after = update.value ? matchesReadPredicate$1(update.value, decision.predicate) : false;
224
+ if (!before && !after) return void 0;
225
+ if (before && !after) {
226
+ const { previousValue, value, ...base } = update;
227
+ return {
228
+ ...base,
229
+ type: "delete"
230
+ };
231
+ }
232
+ return !before && after && update.type === "update" ? {
233
+ ...update,
234
+ type: "insert"
235
+ } : update;
236
+ }
237
+ function authorizeWriteBatch(batch, collections, auth, store) {
238
+ for (const update of batch.updates) {
239
+ if (writeAccessDecision(update, collections, auth, store).allow) continue;
240
+ throw new Error(`Access denied for ${update.type} on ${update.collection}`);
241
+ }
242
+ }
243
+ function authorizeApiCall(handler, name, input, auth) {
244
+ if (!handler) return;
245
+ const row = asRecord(input) ?? {};
246
+ const decision = reduceAccessExpression(handler({
247
+ auth,
248
+ input: readExpressionRow$1()
249
+ }), /* @__PURE__ */ new Map());
250
+ if (!(decision.allow && (!decision.predicate || matchesReadPredicate$1(row, decision.predicate)))) throw new Error(`Access denied for API: ${name}`);
251
+ }
252
+ function readAccessDecision(collection, collections, auth, store) {
253
+ const resolved = resolveCollection(collection, collections);
254
+ if (!resolved) return {
255
+ allow: true,
256
+ dependencies: []
257
+ };
258
+ const access = resolved?.collection.access;
259
+ if (!access?.read) return {
260
+ allow: true,
261
+ dependencies: []
262
+ };
263
+ const references = accessReferences(collection, collections, auth);
264
+ const rows = accessRows([...references.keys()]);
265
+ return reduceAccessExpression(access.read({
266
+ auth,
267
+ collection: resolved.scope,
268
+ references: rows.references,
269
+ params: resolved.params,
270
+ pattern: resolved.name,
271
+ row: rows.row
272
+ }), new Map([...references].map(([name, reference]) => [name, {
273
+ ...reference,
274
+ value: store.read(reference.collection, reference.key)
275
+ }])));
276
+ }
277
+ function writeAccessDecision(update, collections, auth, store) {
278
+ const resolved = resolveCollection(update.collection, collections);
279
+ if (!resolved) return {
280
+ allow: true,
281
+ dependencies: []
282
+ };
283
+ const access = resolved.collection.access;
284
+ const handler = writeAccessHandler(access, update.type);
285
+ if (!handler) return {
286
+ allow: true,
287
+ dependencies: []
288
+ };
289
+ const row = writeAccessRow(update, store);
290
+ if (!row) return {
291
+ allow: false,
292
+ dependencies: []
293
+ };
294
+ const references = accessReferences(update.collection, collections, auth);
295
+ const rows = accessRows([...references.keys()]);
296
+ const decision = reduceAccessExpression(handler({
297
+ auth,
298
+ collection: resolved.scope,
299
+ references: rows.references,
300
+ params: resolved.params,
301
+ pattern: resolved.name,
302
+ row: rows.row
303
+ }), new Map([...references].map(([name, reference]) => [name, {
304
+ ...reference,
305
+ value: store.read(reference.collection, reference.key)
306
+ }])));
307
+ if (!decision.allow || !decision.predicate) return decision;
308
+ return matchesReadPredicate$1(row, decision.predicate) ? decision : {
309
+ ...decision,
310
+ allow: false
311
+ };
312
+ }
313
+ function accessReferences(collection, collections, auth) {
314
+ const resolved = resolveCollection(collection, collections);
315
+ const references = resolved?.collection.access?.references;
316
+ if (!resolved || !references) return /* @__PURE__ */ new Map();
317
+ return new Map(Object.entries(references).map(([name, resolver]) => [name, normalizeAccessReference(resolver({
318
+ auth,
319
+ collection: resolved.scope,
320
+ params: resolved.params,
321
+ pattern: resolved.name
322
+ }))]));
323
+ }
324
+ function normalizeAccessReference(reference) {
325
+ if (typeof reference !== "string") return reference;
326
+ const normalized = reference.trim().replace(/^\/+|\/+$/g, "");
327
+ const segments = normalized ? normalized.split("/") : [];
328
+ const key = segments.at(-1);
329
+ if (!key || segments.length < 2) throw new Error(`Invalid access reference path: ${reference}`);
330
+ return {
331
+ collection: `/${segments.slice(0, -1).map(encodeURIComponent).join("/")}/`,
332
+ key: decodeURIComponent(key)
333
+ };
334
+ }
335
+ function combinePredicates$1(left, right) {
336
+ if (!left) return right;
337
+ if (!right) return left;
338
+ return {
339
+ type: "and",
340
+ predicates: [left, right]
341
+ };
342
+ }
343
+ function writeAccessHandler(access, type) {
344
+ if (!access) return void 0;
345
+ return access[type] ?? access.write ?? access.read;
346
+ }
347
+ function writeAccessRow(update, store) {
348
+ if (update.type === "insert") return asRecord(update.value);
349
+ const existing = asRecord(update.previousValue) ?? store.read(update.collection, update.key);
350
+ if (update.type === "delete") return existing;
351
+ const next = asRecord(update.value);
352
+ return existing && next ? {
353
+ ...existing,
354
+ ...next
355
+ } : next ?? existing;
356
+ }
357
+ function asRecord(value) {
358
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
359
+ }
360
+ //#endregion
361
+ //#region src/collection-normalize.ts
362
+ function normalizeCollection(collection, collections) {
363
+ const configured = resolveCollection(collection, collections);
364
+ if (configured) return configured.scope;
365
+ if (collections && Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${collection}`);
366
+ return collectionScope(collection);
367
+ }
368
+ //#endregion
369
+ //#region src/sqlite-sql.ts
370
+ function execSql(storage, statement) {
371
+ return storage.exec(statement.sql, ...statement.values);
372
+ }
373
+ function identifierSql(identifier) {
374
+ return raw(`"${identifier.replaceAll(`"`, `""`)}"`);
375
+ }
376
+ function rawSql(value) {
377
+ return raw(value);
378
+ }
379
+ //#endregion
380
+ //#region src/sqlite-json-read.ts
381
+ function readSQLiteJsonRows(sql, query, collections = {}) {
382
+ ensureSQLiteJsonTables(sql, collections);
383
+ const resolved = resolveCollection(query.collection, collections);
384
+ if (!resolved) {
385
+ if (Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${query.collection}`);
386
+ throw new Error(`Cannot read unconfigured collection: ${query.collection}`);
387
+ }
388
+ const storage = sqliteJsonStorage(resolved.collection.storage);
389
+ if (!storage) throw new Error(`Collection is not readable with SQLite JSON storage: ${query.collection}`);
390
+ const table = tableName$1(resolved.name, storage);
391
+ const filters = query.filters ?? [];
392
+ const predicate = query.predicate;
393
+ const orderBy = query.orderBy ?? [];
394
+ const limit = query.limit ?? 1e3;
395
+ const scope = { scope: resolved.scope };
396
+ if (query.cursor) {
397
+ const currentPredicate = combinePredicates(predicate, query.cursor.whereCurrent);
398
+ const fromPredicate = combinePredicates(predicate, query.cursor.whereFrom);
399
+ const currentRows = selectRows(sql, table, {
400
+ filters,
401
+ orderBy,
402
+ ...scope,
403
+ ...currentPredicate ? { predicate: currentPredicate } : {}
404
+ });
405
+ const fromRows = selectRows(sql, table, {
406
+ filters,
407
+ limit,
408
+ orderBy,
409
+ ...scope,
410
+ ...fromPredicate ? { predicate: fromPredicate } : {}
411
+ });
412
+ return { rows: uniqueRows([...currentRows, ...fromRows]).map((row) => JSON.parse(row.value)) };
413
+ }
414
+ return { rows: selectRows(sql, table, {
415
+ filters,
416
+ limit,
417
+ offset: query.offset ?? 0,
418
+ orderBy,
419
+ ...scope,
420
+ ...predicate ? { predicate } : {}
421
+ }).map((row) => JSON.parse(row.value)) };
422
+ }
423
+ function readSQLiteJsonRow(sql, collection, key, collections = {}) {
424
+ ensureSQLiteJsonTables(sql, collections);
425
+ const resolved = resolveCollection(collection, collections);
426
+ if (!resolved) {
427
+ if (Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${collection}`);
428
+ throw new Error(`Cannot read unconfigured collection: ${collection}`);
429
+ }
430
+ if (resolved.collection.storage?.kind !== "sqlite-json") throw new Error(`Collection is not readable with SQLite JSON storage: ${collection}`);
431
+ const row = execSql(sql, sqlTag`
432
+ SELECT value FROM ${identifierSql(tableName$1(resolved.name, resolved.collection.storage))}
433
+ WHERE path = ${resolved.scope} AND key = ${String(key)}
434
+ `).toArray()[0];
435
+ return row ? JSON.parse(row.value) : void 0;
436
+ }
437
+ function selectRows(sql, table, options) {
438
+ const where = [sqlTag`path = ${options.scope}`];
439
+ where.push(...options.filters.map((filter) => filterSql(filter)));
440
+ if (options.predicate) where.push(predicateSql(options.predicate));
441
+ const order = options.orderBy.length > 0 ? join(options.orderBy.map((item) => orderBySql(item))) : sqlTag`key`;
442
+ return execSql(sql, sqlTag`
443
+ SELECT key, value FROM ${identifierSql(table)}
444
+ WHERE ${join(where, " AND ")}
445
+ ORDER BY ${order}
446
+ LIMIT ${options.limit ?? 1e3} OFFSET ${options.offset ?? 0}
447
+ `).toArray();
448
+ }
449
+ function uniqueRows(rows) {
450
+ const seen = /* @__PURE__ */ new Set();
451
+ const unique = [];
452
+ for (const row of rows) if (!seen.has(row.key)) {
453
+ seen.add(row.key);
454
+ unique.push(row);
455
+ }
456
+ return unique;
457
+ }
458
+ function tableName$1(collectionName, storage) {
459
+ return storage.tableName ?? collectionName;
460
+ }
461
+ function jsonPath$1(field) {
462
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(field)) throw new Error(`Invalid JSON index field: ${field}`);
463
+ return `$.${field}`;
464
+ }
465
+ function orderBySql(orderBy) {
466
+ const direction = orderBy.direction === "desc" ? "DESC" : "ASC";
467
+ return sqlTag`json_extract(value, ${jsonPath$1(orderBy.field)}) ${directionSql(direction)}`;
468
+ }
469
+ function predicateSql(predicate) {
470
+ if (predicate.type === "comparison") return filterSql(predicate);
471
+ const joiner = predicate.type === "and" ? " AND " : " OR ";
472
+ return sqlTag`(${join(predicate.predicates.map((child) => predicateSql(child)), joiner)})`;
473
+ }
474
+ function combinePredicates(left, right) {
475
+ if (!left) return right;
476
+ if (!right) return left;
477
+ return {
478
+ type: "and",
479
+ predicates: [left, right]
480
+ };
481
+ }
482
+ function filterSql(filter) {
483
+ const field = sqlTag`json_extract(value, ${jsonPath$1(filter.field)})`;
484
+ if (filter.op === "in") {
485
+ if (!Array.isArray(filter.value) || filter.value.length === 0) throw new Error("The in filter requires a non-empty array value");
486
+ return sqlTag`${field} IN (${join(filter.value.map(toSqlJsonValue))})`;
487
+ }
488
+ const value = toSqlJsonValue(filter.value);
489
+ if (value === null) {
490
+ if (filter.op === "eq") return sqlTag`${field} IS ${value}`;
491
+ if (filter.op === "ne") return sqlTag`${field} IS NOT ${value}`;
492
+ }
493
+ switch (filter.op) {
494
+ case "eq": return sqlTag`${field} = ${value}`;
495
+ case "ne": return sqlTag`${field} != ${value}`;
496
+ case "gt": return sqlTag`${field} > ${value}`;
497
+ case "gte": return sqlTag`${field} >= ${value}`;
498
+ case "lt": return sqlTag`${field} < ${value}`;
499
+ case "lte": return sqlTag`${field} <= ${value}`;
500
+ }
501
+ }
502
+ function directionSql(direction) {
503
+ return rawSql(direction);
504
+ }
505
+ function toSqlJsonValue(value) {
506
+ if (typeof value === "boolean") return value ? 1 : 0;
507
+ if (value === null || typeof value === "string" || typeof value === "number" || value instanceof ArrayBuffer) return value;
508
+ throw new Error(`Unsupported SQLite JSON filter value: ${typeof value}`);
509
+ }
510
+ //#endregion
511
+ //#region src/storage.ts
512
+ function sqliteJsonTable(options = {}) {
513
+ return {
514
+ kind: "sqlite-json",
515
+ ...options.tableName ? { tableName: options.tableName } : {},
516
+ indexes: (options.indexes ?? []).map((index) => Array.isArray(index) ? { fields: index } : index)
517
+ };
518
+ }
519
+ function ensureSQLiteJsonTables(sql, collections = {}) {
520
+ for (const [collectionName, collection] of Object.entries(collections)) {
521
+ const storage = sqliteJsonStorage(collection.storage);
522
+ if (!storage) continue;
523
+ const table = tableName(collectionName, storage);
524
+ const result = execSql(sql, sqlTag`
525
+ CREATE TABLE IF NOT EXISTS ${identifierSql(table)} (
526
+ key TEXT NOT NULL,
527
+ path TEXT NOT NULL,
528
+ value TEXT NOT NULL,
529
+ version INTEGER NOT NULL,
530
+ created_at TEXT NOT NULL,
531
+ updated_at TEXT NOT NULL,
532
+ PRIMARY KEY (path, key)
533
+ )
534
+ `);
535
+ for (const index of storage.indexes) execSql(sql, createIndexSql(table, index));
536
+ if (result.rowsWritten > 0) {
537
+ if (collection.initialData) for (const data of collection.initialData) {
538
+ const key = String(data.id);
539
+ const path = collectionScope(collectionName);
540
+ const value = JSON.stringify(data);
541
+ const version = 1;
542
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
543
+ const updatedAt = createdAt;
544
+ execSql(sql, sqlTag`
545
+ INSERT INTO ${identifierSql(table)} (key, path, value, version, created_at, updated_at)
546
+ VALUES (${key}, ${path}, ${value}, ${version}, ${createdAt}, ${updatedAt})
547
+ `);
548
+ }
549
+ }
550
+ }
551
+ }
552
+ function applySQLiteJsonBatch(sql, batch, collections = {}) {
553
+ ensureSQLiteJsonTables(sql, collections);
554
+ for (const update of batch.updates) {
555
+ const resolved = resolveCollection(update.collection, collections);
556
+ const storage = resolved ? sqliteJsonStorage(resolved.collection.storage) : void 0;
557
+ if (!resolved || !storage) continue;
558
+ const table = tableName(resolved.name, storage);
559
+ const key = String(update.key);
560
+ const scope = resolved.scope;
561
+ const updatedAt = new Date(update.createdAt).toISOString();
562
+ if (update.type === "delete") {
563
+ execSql(sql, sqlTag`DELETE FROM ${identifierSql(table)} WHERE ${whereKeySql(scope, key)}`);
564
+ continue;
565
+ }
566
+ if (update.type === "insert") {
567
+ execSql(sql, sqlTag`
568
+ INSERT INTO ${identifierSql(table)} (key, path, value, version, created_at, updated_at)
569
+ VALUES (${key}, ${scope}, ${stringifyRow(update.value)}, 1, ${updatedAt}, ${updatedAt})
570
+ ON CONFLICT(path, key) DO UPDATE SET
571
+ path = excluded.path,
572
+ value = excluded.value,
573
+ version = ${identifierSql(table)}.version + 1,
574
+ updated_at = excluded.updated_at
575
+ `);
576
+ continue;
577
+ }
578
+ const existing = execSql(sql, sqlTag`SELECT value FROM ${identifierSql(table)} WHERE ${whereKeySql(scope, key)}`).toArray()[0];
579
+ if (!existing) throw new Error(`Cannot update missing ${update.collection} row: ${key}`);
580
+ const merged = mergeRows(JSON.parse(existing.value), update.value);
581
+ execSql(sql, sqlTag`
582
+ UPDATE ${identifierSql(table)}
583
+ SET value = ${JSON.stringify(merged)}, version = version + 1, updated_at = ${updatedAt}
584
+ WHERE ${whereKeySql(scope, key)}
585
+ `);
586
+ }
587
+ }
588
+ function sqliteJsonStorage(storage) {
589
+ if (!storage) return sqliteJsonTable();
590
+ return storage.kind === "sqlite-json" ? storage : void 0;
591
+ }
592
+ function whereKeySql(scope, key) {
593
+ return sqlTag`path = ${scope} AND key = ${key}`;
594
+ }
595
+ function tableName(collectionName, storage) {
596
+ return storage.tableName ?? collectionName;
597
+ }
598
+ function createIndexSql(table, index) {
599
+ const name = index.name ?? `${table}_${index.fields.join("_")}_idx`;
600
+ const fields = index.fields.map((field) => rawSql(`json_extract(value, '$.${jsonPath(field)}')`));
601
+ return sqlTag`
602
+ CREATE INDEX IF NOT EXISTS ${identifierSql(name)}
603
+ ON ${identifierSql(table)} (${join(fields)})
604
+ `;
605
+ }
606
+ function jsonPath(field) {
607
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(field)) throw new Error(`Invalid JSON index field: ${field}`);
608
+ return field;
609
+ }
610
+ function stringifyRow(value) {
611
+ if (!isRecord(value)) throw new Error("SQLite JSON storage requires object row values");
612
+ return JSON.stringify(value);
613
+ }
614
+ function mergeRows(existing, changes) {
615
+ if (!isRecord(existing) || !isRecord(changes)) throw new Error("SQLite JSON storage can only merge object update values");
616
+ return {
617
+ ...existing,
618
+ ...changes
619
+ };
620
+ }
621
+ function isRecord(value) {
622
+ return typeof value === "object" && value !== null && !Array.isArray(value);
623
+ }
624
+ //#endregion
625
+ //#region src/collection-builder.ts
626
+ function collectionConfigsBuilder() {
627
+ return new CollectionConfigsBuilder({});
628
+ }
629
+ function collectionBuilder() {
630
+ return new CollectionBuilder({});
631
+ }
632
+ const Collections = {
633
+ builder: collectionConfigsBuilder,
634
+ collection: collectionBuilder
635
+ };
636
+ var CollectionConfigsBuilder = class CollectionConfigsBuilder {
637
+ collections;
638
+ constructor(collections) {
639
+ this.collections = Object.freeze({ ...collections });
640
+ }
641
+ collection(name, configure) {
642
+ const builder = configure(new CollectionBuilder({ path: rootPathForName(name) }));
643
+ return new CollectionConfigsBuilder({
644
+ ...this.collections,
645
+ ...builder.entries()
646
+ });
647
+ }
648
+ path(path, configure) {
649
+ const builder = configure(new CollectionBuilder({ path }));
650
+ return new CollectionConfigsBuilder({
651
+ ...this.collections,
652
+ ...builder.entries()
653
+ });
654
+ }
655
+ build() {
656
+ return { ...this.collections };
657
+ }
658
+ };
659
+ var CollectionBuilder = class CollectionBuilder {
660
+ state;
661
+ constructor(state) {
662
+ this.state = freezeState(state);
663
+ }
664
+ name(name) {
665
+ return this.next({ path: rootPathForName(name) });
666
+ }
667
+ path(path) {
668
+ return this.next({ path });
669
+ }
670
+ schema(schema) {
671
+ return this.next({ schema });
672
+ }
673
+ access(access) {
674
+ return this.next({ access });
675
+ }
676
+ storage(storage) {
677
+ return this.next({ storage });
678
+ }
679
+ index(fieldOrIndex, ...fields) {
680
+ return this.next({ indexes: [...this.state.indexes ?? [], normalizeIndex(fieldOrIndex, fields)] });
681
+ }
682
+ initialData(...initialData) {
683
+ return this.next({ initialData });
684
+ }
685
+ api(name, input, handler) {
686
+ return this.next({ api: {
687
+ ...this.state.api,
688
+ [name]: freezeApiDefinition({
689
+ input,
690
+ handler
691
+ })
692
+ } });
693
+ }
694
+ child(name, configure) {
695
+ const path = this.requirePath("CollectionBuilder.child");
696
+ const builder = configure(new CollectionBuilder({ path: childPathForName(path, parentIdParamForName(path), name) }));
697
+ return this.next({ children: {
698
+ ...this.state.children,
699
+ [name]: builder
700
+ } });
701
+ }
702
+ buildConfig() {
703
+ if (!this.state.schema) throw new Error("CollectionBuilder requires a schema");
704
+ const config = { schema: this.state.schema };
705
+ if (this.state.access) config.access = this.state.access;
706
+ config.storage = storageWithIndexes(this.state.storage, this.state.indexes ?? []);
707
+ if (this.state.initialData) config.initialData = [...this.state.initialData];
708
+ if (this.state.api) config.api = { ...this.state.api };
709
+ return config;
710
+ }
711
+ build() {
712
+ return this.entries();
713
+ }
714
+ entries() {
715
+ const entries = { [this.requirePath("CollectionBuilder.build")]: this.buildConfig() };
716
+ for (const child of Object.values(this.state.children ?? {})) Object.assign(entries, child.entries());
717
+ return entries;
718
+ }
719
+ next(updates) {
720
+ return new CollectionBuilder({
721
+ ...this.state,
722
+ ...updates
723
+ });
724
+ }
725
+ requirePath(caller) {
726
+ if (!this.state.path) throw new Error(`${caller} requires a name or path`);
727
+ return this.state.path;
728
+ }
729
+ };
730
+ function freezeState(state) {
731
+ const next = {};
732
+ if (state.path) next.path = state.path;
733
+ if (state.schema) next.schema = state.schema;
734
+ if (state.access) next.access = state.access;
735
+ if (state.storage) next.storage = state.storage;
736
+ if (state.indexes) next.indexes = state.indexes.map((index) => ({ ...index }));
737
+ if (state.initialData) next.initialData = [...state.initialData];
738
+ if (state.api) next.api = Object.freeze({ ...state.api });
739
+ if (state.children) next.children = Object.freeze({ ...state.children });
740
+ return Object.freeze(next);
741
+ }
742
+ function freezeApiDefinition(definition) {
743
+ return Object.freeze({ ...definition });
744
+ }
745
+ function normalizeIndex(fieldOrIndex, fields) {
746
+ if (typeof fieldOrIndex === "string") return { fields: [fieldOrIndex, ...fields] };
747
+ return Array.isArray(fieldOrIndex) ? { fields: fieldOrIndex } : fieldOrIndex;
748
+ }
749
+ function storageWithIndexes(storage, indexes) {
750
+ const current = storage ?? sqliteJsonTable();
751
+ if (current.kind !== "sqlite-json") return current;
752
+ return {
753
+ ...current,
754
+ indexes: [...current.indexes, ...indexes]
755
+ };
756
+ }
757
+ function rootPathForName(name) {
758
+ return `/${encodeURIComponent(name)}/`;
759
+ }
760
+ function childPathForName(parentPath, parentParam, name) {
761
+ return `${parentPath.replace(/\/+$/g, "")}/{${parentParam}}/${encodeURIComponent(name)}/`;
762
+ }
763
+ function parentIdParamForName(parentPath) {
764
+ return `${singularize(parentPath.trim().replace(/^\/+|\/+$/g, "").split("/").filter((part) => !part.startsWith("{") && !part.startsWith(":")).at(-1) ?? "parent")}Id`;
765
+ }
766
+ function singularize(value) {
767
+ return value.endsWith("s") ? value.slice(0, -1) : value;
768
+ }
769
+ //#endregion
770
+ //#region src/definition-builder.ts
771
+ function collectionShardOptionsFrom(definitions) {
772
+ return new CollectionShardDefinitionBuilder(definitions.collections, {});
773
+ }
774
+ var CollectionShardDefinitionBuilder = class CollectionShardDefinitionBuilder {
775
+ definitions;
776
+ overrides;
777
+ constructor(definitions, overrides) {
778
+ this.definitions = definitions;
779
+ this.overrides = overrides;
780
+ }
781
+ collection(path, configure) {
782
+ const override = configure(new ServerCollectionBuilder({})).toOverride();
783
+ return new CollectionShardDefinitionBuilder(this.definitions, {
784
+ ...this.overrides,
785
+ [path]: override
786
+ });
787
+ }
788
+ build() {
789
+ const collections = {};
790
+ for (const [name, definition] of Object.entries(this.definitions)) addDefinition(collections, definition, name, this.overrides);
791
+ return { collections };
792
+ }
793
+ };
794
+ var ServerCollectionBuilder = class ServerCollectionBuilder {
795
+ override;
796
+ constructor(override) {
797
+ this.override = override;
798
+ }
799
+ index(...fields) {
800
+ return new ServerCollectionBuilder({
801
+ ...this.override,
802
+ indexes: [...this.override.indexes ?? [], fields]
803
+ });
804
+ }
805
+ initialData(data) {
806
+ return new ServerCollectionBuilder({
807
+ ...this.override,
808
+ initialData: data
809
+ });
810
+ }
811
+ reference(name, resolver) {
812
+ return new ServerCollectionBuilder({
813
+ ...this.override,
814
+ references: {
815
+ ...this.override.references,
816
+ [name]: resolver
817
+ }
818
+ });
819
+ }
820
+ access(kind, handler) {
821
+ return new ServerCollectionBuilder({
822
+ ...this.override,
823
+ access: {
824
+ ...this.override.access,
825
+ [kind]: handler
826
+ }
827
+ });
828
+ }
829
+ api(name, handler) {
830
+ return new ServerCollectionBuilder({
831
+ ...this.override,
832
+ api: {
833
+ ...this.override.api,
834
+ [name]: handler
835
+ }
836
+ });
837
+ }
838
+ toOverride() {
839
+ return this.override;
840
+ }
841
+ };
842
+ function addDefinition(collections, definition, dotPath, overrides) {
843
+ const override = overrides[dotPath] ?? {};
844
+ let builder = collectionBuilder().path(definition.path).schema(definition.schema);
845
+ for (const fields of override.indexes ?? []) builder = builder.index(fields);
846
+ if (override.initialData) builder = builder.initialData(...override.initialData);
847
+ const access = accessConfig(override);
848
+ if (access) builder = builder.access(access);
849
+ for (const [name, handler] of Object.entries(override.api ?? {})) {
850
+ const apiDefinition = definition.api[name];
851
+ const apiAccess = override.access?.[`api.${name}`];
852
+ const apiPath = apiDefinition?.path ?? defaultCollectionApiPath(definition.path, name);
853
+ const input = apiDefinition?.input ?? z.undefined();
854
+ builder = builder.api(name, input, async (args) => {
855
+ authorizeApiCall(apiAccess, apiPath, args.input, args.auth);
856
+ return handler(args);
857
+ });
858
+ }
859
+ Object.assign(collections, builder.build());
860
+ for (const [name, child] of Object.entries(definition.children)) addDefinition(collections, child, `${dotPath}.${name}`, overrides);
861
+ }
862
+ function accessConfig(override) {
863
+ const access = override.access ?? {};
864
+ const apiAccess = Object.fromEntries(Object.entries(access).filter(([kind]) => kind.startsWith("api.")).map(([kind, handler]) => [kind.slice(4), handler]));
865
+ const config = {};
866
+ if (override.references) config.references = override.references;
867
+ if (access.read) config.read = access.read;
868
+ if (access.write) config.write = access.write;
869
+ if (access.insert) config.insert = access.insert;
870
+ if (access.update) config.update = access.update;
871
+ if (access.delete) config.delete = access.delete;
872
+ if (Object.keys(apiAccess).length > 0) config.api = apiAccess;
873
+ return Object.keys(config).length > 0 ? config : void 0;
874
+ }
875
+ //#endregion
876
+ //#region src/durable-object-rpc.ts
877
+ function callRouter(caller, request) {
878
+ if (request.method === "query" && request.params.path === "read") return caller.read(request.params.input.json);
879
+ if (request.method === "query" && request.params.path === "changes") return caller.changes(request.params.input.json);
880
+ if (request.method === "subscribe") return caller.subscribe(request.params.input.json);
881
+ if (request.method === "unsubscribe") return caller.unsubscribe(request.params.input.json);
882
+ if (request.method !== "mutation") throw new Error("Unsupported operation");
883
+ if (request.params.path === "push") return caller.push(request.params.input.json);
884
+ if (request.params.path === "api") return caller.api(request.params.input.json);
885
+ throw new Error("Unsupported operation");
886
+ }
887
+ //#endregion
888
+ //#region src/history-watermarks.ts
889
+ const COLLECTION_WATERMARKS_TABLE = "_lsync_collection_watermarks";
890
+ function ensureCollectionWatermarkTable(sql) {
891
+ sql.exec(`
892
+ CREATE TABLE IF NOT EXISTS ${quoteIdentifier$1(COLLECTION_WATERMARKS_TABLE)} (
893
+ scope TEXT PRIMARY KEY,
894
+ collection TEXT NOT NULL,
895
+ latest_sequence INTEGER NOT NULL
896
+ )
897
+ `);
898
+ }
899
+ function recordCollectionWatermark(sql, collection, scope, sequence) {
900
+ sql.exec(`
901
+ INSERT INTO ${quoteIdentifier$1(COLLECTION_WATERMARKS_TABLE)}
902
+ (scope, collection, latest_sequence)
903
+ VALUES (?, ?, ?)
904
+ ON CONFLICT(scope) DO UPDATE SET
905
+ collection = excluded.collection,
906
+ latest_sequence = excluded.latest_sequence
907
+ `, scope, collection, sequence);
908
+ }
909
+ function readCollectionWatermark(sql, scope) {
910
+ return sql.exec(`
911
+ SELECT latest_sequence FROM ${quoteIdentifier$1(COLLECTION_WATERMARKS_TABLE)}
912
+ WHERE scope = ?
913
+ `, scope).toArray()[0]?.latest_sequence ?? 0;
914
+ }
915
+ function quoteIdentifier$1(identifier) {
916
+ return `"${identifier.replaceAll(`"`, `""`)}"`;
917
+ }
918
+ //#endregion
919
+ //#region src/history.ts
920
+ const CHANGES_TABLE = "_lsync_changes";
921
+ function persistSQLiteJsonBatchWithHistory(sql, batch, collections = {}, options = {}) {
922
+ ensureSQLiteJsonTables(sql, collections);
923
+ ensureChangesTable(sql);
924
+ applySQLiteJsonBatch(sql, batch, collections);
925
+ const updates = batch.updates.map((update) => appendChange(sql, update, collections));
926
+ const watermark = updates.at(-1)?.sequence ?? readWatermark(sql);
927
+ pruneChanges(sql, watermark, options.maxChanges ?? 1e4);
928
+ return {
929
+ updates,
930
+ watermark
931
+ };
932
+ }
933
+ function readSQLiteJsonChanges(sql, query, collections = {}) {
934
+ ensureChangesTable(sql);
935
+ const requested = Object.entries(query.collections).map(([collection, sequence]) => ({
936
+ collection,
937
+ sequence,
938
+ scope: scopeForCollection(collection, collections)
939
+ }));
940
+ const watermark = readWatermark(sql);
941
+ const staleCollections = requested.flatMap((item) => isCursorStale(sql, item.scope, item.sequence) ? [item.collection] : []);
942
+ if (staleCollections.length > 0) return {
943
+ type: "resyncRequired",
944
+ collections: staleCollections,
945
+ watermark
946
+ };
947
+ if (requested.length === 0) return {
948
+ type: "changes",
949
+ updates: [],
950
+ watermark,
951
+ hasMore: false
952
+ };
953
+ const limit = query.limit ?? 1e3;
954
+ const rows = selectChanges(sql, requested, limit + 1);
955
+ return {
956
+ type: "changes",
957
+ updates: rows.slice(0, limit).map(rowToUpdate),
958
+ watermark,
959
+ hasMore: rows.length > limit
960
+ };
961
+ }
962
+ function ensureChangesTable(sql) {
963
+ sql.exec(`
964
+ CREATE TABLE IF NOT EXISTS ${quoteIdentifier(CHANGES_TABLE)} (
965
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
966
+ update_id TEXT NOT NULL,
967
+ collection TEXT NOT NULL,
968
+ scope TEXT NOT NULL,
969
+ "key" TEXT NOT NULL,
970
+ type TEXT NOT NULL,
971
+ value TEXT,
972
+ previous_value TEXT,
973
+ client_id TEXT,
974
+ client_created_at INTEGER NOT NULL,
975
+ server_created_at TEXT NOT NULL
976
+ )
977
+ `);
978
+ sql.exec(`
979
+ CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${CHANGES_TABLE}_scope_sequence_idx`)}
980
+ ON ${quoteIdentifier(CHANGES_TABLE)} (scope, sequence)
981
+ `);
982
+ ensureCollectionWatermarkTable(sql);
983
+ }
984
+ function appendChange(sql, update, collections) {
985
+ const serverCreatedAt = (/* @__PURE__ */ new Date()).toISOString();
986
+ const scope = scopeForCollection(update.collection, collections);
987
+ const row = sql.exec(`
988
+ INSERT INTO ${quoteIdentifier(CHANGES_TABLE)}
989
+ (update_id, collection, scope, "key", type, value, previous_value, client_id,
990
+ client_created_at, server_created_at)
991
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
992
+ RETURNING sequence
993
+ `, update.id, update.collection, scope, JSON.stringify(update.key), update.type, update.value === void 0 ? null : JSON.stringify(update.value), update.previousValue === void 0 ? null : JSON.stringify(update.previousValue), update.clientId ?? null, update.createdAt, serverCreatedAt).toArray()[0];
994
+ if (!row) throw new Error("Unable to append sync history row");
995
+ recordCollectionWatermark(sql, update.collection, scope, row.sequence);
996
+ return {
997
+ ...update,
998
+ sequence: row.sequence,
999
+ serverCreatedAt
1000
+ };
1001
+ }
1002
+ function selectChanges(sql, requested, limit) {
1003
+ const bindings = requested.flatMap((item) => [item.scope, item.sequence]);
1004
+ bindings.push(limit);
1005
+ return sql.exec(`
1006
+ SELECT sequence, update_id, collection, "key", type, value, previous_value, client_id,
1007
+ client_created_at, server_created_at
1008
+ FROM ${quoteIdentifier(CHANGES_TABLE)}
1009
+ WHERE ${requested.map(() => "(scope = ? AND sequence > ?)").join(" OR ")}
1010
+ ORDER BY sequence
1011
+ LIMIT ?
1012
+ `, ...bindings).toArray();
1013
+ }
1014
+ function rowToUpdate(row) {
1015
+ const update = {
1016
+ id: row.update_id,
1017
+ collection: row.collection,
1018
+ key: JSON.parse(row.key),
1019
+ type: row.type,
1020
+ createdAt: row.client_created_at,
1021
+ sequence: row.sequence,
1022
+ serverCreatedAt: row.server_created_at
1023
+ };
1024
+ if (row.value !== null) update.value = JSON.parse(row.value);
1025
+ if (row.previous_value !== null) update.previousValue = JSON.parse(row.previous_value);
1026
+ if (row.client_id !== null) update.clientId = row.client_id;
1027
+ return update;
1028
+ }
1029
+ function isCursorStale(sql, scope, sequence) {
1030
+ const latest = readCollectionWatermark(sql, scope);
1031
+ if (latest === 0) return false;
1032
+ const oldest = sql.exec(`SELECT MIN(sequence) as sequence FROM ${quoteIdentifier(CHANGES_TABLE)} WHERE scope = ?`, scope).toArray()[0]?.sequence;
1033
+ if (oldest === null || oldest === void 0) return sequence < latest;
1034
+ return sequence < oldest - 1;
1035
+ }
1036
+ function readWatermark(sql) {
1037
+ return sql.exec(`SELECT COALESCE(MAX(sequence), 0) as watermark FROM ${quoteIdentifier(CHANGES_TABLE)}`).toArray()[0]?.watermark ?? 0;
1038
+ }
1039
+ function pruneChanges(sql, watermark, maxChanges) {
1040
+ if (!Number.isFinite(maxChanges) || maxChanges < 0) return;
1041
+ sql.exec(`DELETE FROM ${quoteIdentifier(CHANGES_TABLE)} WHERE sequence <= ?`, watermark - maxChanges);
1042
+ }
1043
+ function scopeForCollection(collection, collections) {
1044
+ const resolved = resolveCollection(collection, collections);
1045
+ if (resolved) return resolved.scope;
1046
+ if (Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${collection}`);
1047
+ return collectionScope(collection);
1048
+ }
1049
+ function quoteIdentifier(identifier) {
1050
+ return `"${identifier.replaceAll(`"`, `""`)}"`;
1051
+ }
1052
+ //#endregion
1053
+ //#region src/messages.ts
1054
+ function sendRpcResult(ws, id, result) {
1055
+ sendServerMessage(ws, {
1056
+ id,
1057
+ result: {
1058
+ type: "data",
1059
+ data: { json: result }
1060
+ }
1061
+ });
1062
+ }
1063
+ function sendRpcError(ws, id, error) {
1064
+ sendServerMessage(ws, {
1065
+ id,
1066
+ error: { message: error instanceof Error ? error.message : "Unknown error" }
1067
+ });
1068
+ }
1069
+ //#endregion
1070
+ //#region src/socket-attachment.ts
1071
+ function webSocketAttachment(ws) {
1072
+ const result = webSocketAttachmentSchema.safeParse(ws.deserializeAttachment());
1073
+ return result.success ? result.data : void 0;
1074
+ }
1075
+ function webSocketAuth(ws) {
1076
+ const attachment = webSocketAttachment(ws);
1077
+ if (!attachment) return {};
1078
+ return {
1079
+ clientId: attachment.clientId,
1080
+ ...attachment.auth
1081
+ };
1082
+ }
1083
+ function updateSocketSubscriptions(ws, collection, normalize, update) {
1084
+ const attachment = webSocketAttachment(ws);
1085
+ if (!attachment) throw new Error("Missing WebSocket attachment");
1086
+ const normalized = normalize(collection);
1087
+ const subscriptions = new Set(attachment.subscriptions);
1088
+ update(subscriptions, normalized);
1089
+ const next = [...subscriptions];
1090
+ ws.serializeAttachment({
1091
+ ...attachment,
1092
+ subscriptions: next
1093
+ });
1094
+ return {
1095
+ collection: normalized,
1096
+ subscriptions: next
1097
+ };
1098
+ }
1099
+ //#endregion
1100
+ //#region src/access-invalidation.ts
1101
+ function invalidatesDependency(update, dependencies) {
1102
+ return dependencies.some((dependency) => {
1103
+ if (collectionScope(update.collection) !== collectionScope(dependency.collection)) return false;
1104
+ if (String(update.key) !== String(dependency.key)) return false;
1105
+ return fieldChanged(update.previousValue, update.value, dependency.field);
1106
+ });
1107
+ }
1108
+ function fieldChanged(previous, next, field) {
1109
+ if (!previous || !next) return true;
1110
+ return valueAtPath(previous, field) !== valueAtPath(next, field);
1111
+ }
1112
+ function valueAtPath(value, field) {
1113
+ return field.split(".").reduce((current, segment) => {
1114
+ if (typeof current !== "object" || current === null) return void 0;
1115
+ return current[segment];
1116
+ }, value);
1117
+ }
1118
+ //#endregion
1119
+ //#region src/subscription-invalidation.ts
1120
+ function subscribedInvalidations(subscriptions, batch, collections, auth, store) {
1121
+ const invalidations = /* @__PURE__ */ new Set();
1122
+ for (const collection of subscriptions) {
1123
+ const decision = readAccessDecision(collection, collections, auth, store);
1124
+ if (decision.dependencies.length === 0) continue;
1125
+ if (batch.updates.some((update) => invalidatesDependency(update, decision.dependencies))) invalidations.add(collection);
1126
+ }
1127
+ return [...invalidations].map((collection) => ({ collection }));
1128
+ }
1129
+ //#endregion
1130
+ //#region src/validation.ts
1131
+ function validateBatch(batch, collections) {
1132
+ for (const update of batch.updates) {
1133
+ const schema = schemaFor(update.collection, collections);
1134
+ if (!schema || update.type === "delete") continue;
1135
+ if (update.type === "update" && schema instanceof z.ZodObject) {
1136
+ schema.partial().parse(update.value);
1137
+ continue;
1138
+ }
1139
+ schema.parse(update.value);
1140
+ }
1141
+ }
1142
+ function schemaFor(collection, collections) {
1143
+ const configured = resolveCollection(collection, collections);
1144
+ if (!configured) {
1145
+ if (collections && Object.keys(collections).length > 0) throw new Error(`Unknown collection: ${collection}`);
1146
+ return;
1147
+ }
1148
+ return configured.collection.schema;
1149
+ }
1150
+ //#endregion
1151
+ //#region src/durable-object.ts
1152
+ var CollectionShardDurableObject = class {
1153
+ state;
1154
+ env;
1155
+ options;
1156
+ static from = collectionShardOptionsFrom;
1157
+ apiHandlers;
1158
+ constructor(state, env, options = {}) {
1159
+ this.state = state;
1160
+ this.env = env;
1161
+ this.options = options;
1162
+ this.apiHandlers = collectionApiHandlers(options.collections);
1163
+ }
1164
+ async fetch(request) {
1165
+ if (request.headers.get("Upgrade") !== "websocket") return new Response("Expected WebSocket upgrade", { status: 426 });
1166
+ const pair = new WebSocketPair();
1167
+ const client = pair[0];
1168
+ const server = pair[1];
1169
+ const clientId = new URL(request.url).searchParams.get("clientId") ?? crypto.randomUUID();
1170
+ const auth = await this.options.authenticate?.({
1171
+ clientId,
1172
+ request
1173
+ }) ?? { clientId };
1174
+ this.state.acceptWebSocket(server);
1175
+ server.serializeAttachment({
1176
+ clientId,
1177
+ connectedAt: Date.now(),
1178
+ auth,
1179
+ subscriptions: []
1180
+ });
1181
+ return new Response(null, {
1182
+ status: 101,
1183
+ webSocket: client
1184
+ });
1185
+ }
1186
+ async webSocketMessage(ws, message) {
1187
+ try {
1188
+ const request = parseClientRpcRequest(message);
1189
+ const result = await callRouter(router.createCaller({
1190
+ shardId: this.shardId(),
1191
+ validate: (input) => this.validate(input),
1192
+ persist: (input) => this.persist(input),
1193
+ publish: (input) => this.publish(input),
1194
+ mutate: (input) => this.mutate(input, webSocketAuth(ws)),
1195
+ subscribe: (input) => this.subscribe(ws, input),
1196
+ unsubscribe: (input) => this.unsubscribe(ws, input),
1197
+ read: (input) => this.read(input, webSocketAuth(ws)),
1198
+ changes: (input) => this.readChanges(input, webSocketAuth(ws)),
1199
+ callApi: (input) => this.callApi(ws, input)
1200
+ }), request);
1201
+ sendRpcResult(ws, request.id, result);
1202
+ } catch (error) {
1203
+ sendRpcError(ws, readRpcId(message), error);
1204
+ }
1205
+ }
1206
+ publish(batch) {
1207
+ const payload = {
1208
+ type: "updates",
1209
+ shardId: this.shardId(),
1210
+ updates: batch.updates,
1211
+ watermark: batch.watermark
1212
+ };
1213
+ for (const socket of this.state.getWebSockets()) {
1214
+ const updates = this.subscribedUpdates(socket, batch);
1215
+ const invalidations = this.subscribedInvalidations(socket, batch);
1216
+ if (updates.length === 0 && invalidations.length === 0) continue;
1217
+ sendServerMessage(socket, {
1218
+ method: "subscription",
1219
+ params: {
1220
+ path: "updates",
1221
+ input: { json: {
1222
+ ...payload,
1223
+ ...invalidations.length > 0 ? { invalidations } : {},
1224
+ updates
1225
+ } }
1226
+ }
1227
+ });
1228
+ }
1229
+ }
1230
+ validate(batch) {
1231
+ validateBatch(batch, this.options.collections);
1232
+ }
1233
+ persist(batch) {
1234
+ return this.state.storage.transactionSync(() => persistSQLiteJsonBatchWithHistory(this.state.storage.sql, batch, this.options.collections, this.options.history));
1235
+ }
1236
+ read(query, auth = {}) {
1237
+ const authorized = authorizeReadQuery(query, this.options.collections, auth, this.accessStore());
1238
+ return authorized ? readSQLiteJsonRows(this.state.storage.sql, authorized, this.options.collections) : { rows: [] };
1239
+ }
1240
+ readChanges(query, auth = {}) {
1241
+ const result = readSQLiteJsonChanges(this.state.storage.sql, query, this.options.collections);
1242
+ if (result.type === "resyncRequired") return result;
1243
+ return {
1244
+ ...result,
1245
+ updates: result.updates.flatMap((update) => {
1246
+ const visible = visibleUpdateForAuth(update, this.options.collections, auth, this.accessStore());
1247
+ return visible ? [{
1248
+ ...visible,
1249
+ sequence: update.sequence,
1250
+ serverCreatedAt: update.serverCreatedAt
1251
+ }] : [];
1252
+ })
1253
+ };
1254
+ }
1255
+ subscribe(ws, input) {
1256
+ return this.updateSubscriptions(ws, input.collection, (subscriptions, collection) => {
1257
+ subscriptions.add(collection);
1258
+ });
1259
+ }
1260
+ unsubscribe(ws, input) {
1261
+ return this.updateSubscriptions(ws, input.collection, (subscriptions, collection) => {
1262
+ subscriptions.delete(collection);
1263
+ });
1264
+ }
1265
+ async callApi(ws, call) {
1266
+ const handler = this.apiHandlers[call.path];
1267
+ if (!handler) throw new Error(`Unknown API path: ${call.path}`);
1268
+ const clientId = this.clientId(ws);
1269
+ const auth = webSocketAuth(ws);
1270
+ const args = {
1271
+ shardId: this.shardId(),
1272
+ input: call.input,
1273
+ auth,
1274
+ validate: (input) => this.validate(input),
1275
+ persist: (input) => this.persist(input),
1276
+ publish: (input) => this.publish(input),
1277
+ subscribe: (input) => this.subscribe(ws, input),
1278
+ unsubscribe: (input) => this.unsubscribe(ws, input),
1279
+ mutate: (input) => this.mutate(input, auth),
1280
+ read: (input) => this.read(input, auth),
1281
+ changes: (input) => this.readChanges(input, auth)
1282
+ };
1283
+ if (clientId) args.clientId = clientId;
1284
+ return handler(args);
1285
+ }
1286
+ mutate(batch, auth = {}) {
1287
+ this.validate(batch);
1288
+ authorizeWriteBatch(batch, this.options.collections, auth, this.accessStore());
1289
+ const persisted = this.persist(batch);
1290
+ this.publish(persisted);
1291
+ return {
1292
+ accepted: batch.updates.length,
1293
+ watermark: persisted.watermark
1294
+ };
1295
+ }
1296
+ clientId(ws) {
1297
+ return webSocketAttachment(ws)?.clientId;
1298
+ }
1299
+ updateSubscriptions(ws, collection, update) {
1300
+ return updateSocketSubscriptions(ws, collection, (input) => normalizeCollection(input, this.options.collections), update);
1301
+ }
1302
+ subscribedUpdates(ws, batch) {
1303
+ const attachment = webSocketAttachment(ws);
1304
+ if (!attachment || attachment.subscriptions.length === 0) return [];
1305
+ const subscriptions = new Set(attachment.subscriptions);
1306
+ const auth = webSocketAuth(ws);
1307
+ return batch.updates.flatMap((update) => {
1308
+ if (!subscriptions.has(normalizeCollection(update.collection, this.options.collections))) return [];
1309
+ const visible = visibleUpdateForAuth(update, this.options.collections, auth, this.accessStore());
1310
+ return visible ? [{
1311
+ ...visible,
1312
+ sequence: update.sequence,
1313
+ serverCreatedAt: update.serverCreatedAt
1314
+ }] : [];
1315
+ });
1316
+ }
1317
+ subscribedInvalidations(ws, batch) {
1318
+ const attachment = webSocketAttachment(ws);
1319
+ if (!attachment || attachment.subscriptions.length === 0) return [];
1320
+ return subscribedInvalidations(attachment.subscriptions, batch, this.options.collections, webSocketAuth(ws), this.accessStore());
1321
+ }
1322
+ accessStore() {
1323
+ return { read: (collection, key) => readSQLiteJsonRow(this.state.storage.sql, collection, key, this.options.collections) };
1324
+ }
1325
+ shardId() {
1326
+ return this.state.id.toString();
1327
+ }
1328
+ };
1329
+ //#endregion
1330
+ //#region src/worker.ts
1331
+ function createWorkerHandler(options = {}) {
1332
+ const binding = options.binding ?? "SYNC_SHARDS";
1333
+ const routePattern = options.routePattern ?? /^\/sync\/([^/]+)$/;
1334
+ return { fetch(request, env) {
1335
+ const match = new URL(request.url).pathname.match(routePattern);
1336
+ if (!match?.[1]) return new Response("Not found", { status: 404 });
1337
+ const namespace = env[binding];
1338
+ if (!namespace) return new Response(`Missing Durable Object binding: ${String(binding)}`, { status: 500 });
1339
+ const id = namespace.idFromName(decodeURIComponent(match[1]));
1340
+ return namespace.get(id).fetch(request);
1341
+ } };
1342
+ }
1343
+ //#endregion
1344
+ export { CollectionBuilder, CollectionConfigsBuilder, CollectionShardDefinitionBuilder, CollectionShardDurableObject, Collections, ServerCollectionBuilder, and, applySQLiteJsonBatch, authorizeApiCall, authorizeReadQuery, authorizeWriteBatch, collectionApiHandlers, collectionBuilder, collectionConfigsBuilder, collectionScope, compileReadExpression, createWorkerHandler, defaultCollectionApiPath, ensureSQLiteJsonTables, eq, field, gt, gte, inArray, isCollectionPattern, lt, lte, matchesReadPredicate, ne, not, or, persistSQLiteJsonBatchWithHistory, readAccessDecision, readExpressionReferences, readExpressionRow, readSQLiteJsonChanges, readSQLiteJsonRow, readSQLiteJsonRows, resolveCollection, router, sqliteJsonTable, val, validateBatch, visibleUpdateForAuth };
1345
+
1346
+ //# sourceMappingURL=index.mjs.map