@lsync/server 0.0.2 → 0.0.3

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/README.md CHANGED
@@ -8,3 +8,49 @@ pnpm add @lsync/server @lsync/definitions
8
8
 
9
9
  The package exports the reusable Durable Object, worker handler, collection configuration helpers,
10
10
  and the `@lsync/server/client` subpath for server-side clients.
11
+
12
+ SQLite JSON collections use server-sequence last-write-wins conflict resolution. `previousValue`
13
+ is available to access rules and history consumers, but is not a compare-and-swap precondition.
14
+ Mutation IDs are recorded durably, so retrying the same update is idempotent; reusing an ID with a
15
+ different payload is rejected.
16
+
17
+ ## Safeguards
18
+
19
+ Each WebSocket may subscribe to at most 100 collection scopes by default. Override or disable the
20
+ cap when building the shard options:
21
+
22
+ ```ts
23
+ const options = CollectionShardDurableObject.from(collections)
24
+ .limits({ maxSubscriptionsPerWebSocket: 250 }) // or false
25
+ .build();
26
+ ```
27
+
28
+ Cloudflare's managed Rate Limiting binding can protect WebSocket message handling:
29
+
30
+ ```ts
31
+ import { cloudflareRateLimiter } from "@lsync/server";
32
+
33
+ const options = CollectionShardDurableObject.from(collections)
34
+ .rateLimit(
35
+ cloudflareRateLimiter({
36
+ key: ({ auth, clientId }) => String(auth.userId ?? clientId),
37
+ }),
38
+ )
39
+ .build();
40
+ ```
41
+
42
+ Configure the matching binding in `wrangler.toml` (the period must be 10 or 60 seconds):
43
+
44
+ ```toml
45
+ [[ratelimits]]
46
+ name = "RATE_LIMITER"
47
+ namespace_id = "1001"
48
+
49
+ [ratelimits.simple]
50
+ limit = 100
51
+ period = 60
52
+ ```
53
+
54
+ The managed limiter is intentionally permissive, eventually consistent, and local to a Cloudflare
55
+ location. Use it for abuse protection rather than billing or exact accounting. Prefer an
56
+ authenticated user or tenant key; `clientId` is only the fallback.
package/dist/client.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as CollectionSubscriptionInput, B as ReadOrderBy, C as CollectionApiDefinition, E as CollectionConfigs, H as ReadQuery, J as SyncChangesQuery, K as SequencedBatch, L as ReadCursor, M as OperationType, N as PushResult, R as ReadFilter, T as CollectionConfig, U as ReadResult, V as ReadPredicate, X as SyncChangesResult, Y as SyncChangesQueryInput, Z as Update, _ as ApiOutput, b as Batch, d as ApiCallInput, f as ApiContract, g as ApiInput, h as ApiHandlerContext, j as CollectionSubscriptionResult, k as CollectionSubscription, l as ApiCall, m as ApiHandlerArgs, n as Router, p as ApiHandler, q as SequencedUpdate, r as router, t as Context, u as ApiCallArgs, v as ApiPath, w as CollectionApiHandlers, x as Broadcast, y as ApiRoute, z as ReadFilterOperator } from "./router-D_wddPdy.mjs";
1
+ import { A as CollectionSubscriptionInput, B as ReadOrderBy, C as CollectionApiDefinition, E as CollectionConfigs, H as ReadQuery, J as SyncChangesQuery, K as SequencedBatch, L as ReadCursor, M as OperationType, N as PushResult, R as ReadFilter, T as CollectionConfig, U as ReadResult, V as ReadPredicate, X as SyncChangesResult, Y as SyncChangesQueryInput, Z as Update, _ as ApiOutput, b as Batch, d as ApiCallInput, f as ApiContract, g as ApiInput, h as ApiHandlerContext, j as CollectionSubscriptionResult, k as CollectionSubscription, l as ApiCall, m as ApiHandlerArgs, n as Router, p as ApiHandler, q as SequencedUpdate, r as router, t as Context, u as ApiCallArgs, v as ApiPath, w as CollectionApiHandlers, x as Broadcast, y as ApiRoute, z as ReadFilterOperator } from "./router-B9emOwKj.mjs";
2
2
  export { type ApiCall, type ApiCallArgs, type ApiCallInput, type ApiContract, type ApiHandler, type ApiHandlerArgs, type ApiHandlerContext, type ApiInput, type ApiOutput, type ApiPath, type ApiRoute, type Batch, type Broadcast, type CollectionApiDefinition, type CollectionApiHandlers, type CollectionConfig, type CollectionConfigs, type CollectionSubscription, type CollectionSubscriptionInput, type CollectionSubscriptionResult, type Context, type OperationType, type PushResult, type ReadCursor, type ReadFilter, type ReadFilterOperator, type ReadOrderBy, type ReadPredicate, type ReadQuery, type ReadResult, type Router, type SequencedBatch, type SequencedUpdate, type SyncChangesQuery, type SyncChangesQueryInput, type SyncChangesResult, type Update, router };
package/dist/client.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as router } from "./router-C4eYfMV7.mjs";
1
+ import { t as router } from "./router-CZnnFmXh.mjs";
2
2
  export { router };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as CollectionSubscriptionInput, B as ReadOrderBy, C as CollectionApiDefinition, D as CollectionInvalidation, E as CollectionConfigs, F as ReadAccessHandler, G as SQLiteJsonStorageConfig, H as ReadQuery, I as ReadAccessResult, J as SyncChangesQuery, K as SequencedBatch$1, L as ReadCursor, M as OperationType, N as PushResult, O as CollectionStorageConfig, P as ReadAccessContext, Q as WebSocketAttachment, R as ReadFilter, S as CollectionAccessConfig, T as CollectionConfig, U as ReadResult, V as ReadPredicate, W as SQLiteJsonIndexConfig, X as SyncChangesResult, Y as SyncChangesQueryInput, Z as Update, _ as ApiOutput, a as AccessReference, b as Batch, c as ApiAccessHandler, d as ApiCallInput, f as ApiContract, g as ApiInput, h as ApiHandlerContext, i as AccessAuth, j as CollectionSubscriptionResult, k as CollectionSubscription, l as ApiCall, m as ApiHandlerArgs, n as Router, o as AccessReferenceResolver, p as ApiHandler, q as SequencedUpdate, r as router, s as ApiAccessContext, t as Context, u as ApiCallArgs, v as ApiPath, w as CollectionApiHandlers, x as Broadcast, y as ApiRoute, z as ReadFilterOperator } from "./router-D_wddPdy.mjs";
1
+ import { A as CollectionSubscriptionInput, B as ReadOrderBy, C as CollectionApiDefinition, D as CollectionInvalidation, E as CollectionConfigs, F as ReadAccessHandler, G as SQLiteJsonStorageConfig, H as ReadQuery, I as ReadAccessResult, J as SyncChangesQuery, K as SequencedBatch$1, L as ReadCursor, M as OperationType, N as PushResult, O as CollectionStorageConfig, P as ReadAccessContext, Q as WebSocketAttachment, R as ReadFilter, S as CollectionAccessConfig, T as CollectionConfig, U as ReadResult, V as ReadPredicate, W as SQLiteJsonIndexConfig, X as SyncChangesResult, Y as SyncChangesQueryInput, Z as Update, _ as ApiOutput, a as AccessReference, b as Batch, c as ApiAccessHandler, d as ApiCallInput, f as ApiContract, g as ApiInput, h as ApiHandlerContext, i as AccessAuth, j as CollectionSubscriptionResult, k as CollectionSubscription, l as ApiCall, m as ApiHandlerArgs, n as Router, o as AccessReferenceResolver, p as ApiHandler, q as SequencedUpdate, r as router, s as ApiAccessContext, t as Context, u as ApiCallArgs, v as ApiPath, w as CollectionApiHandlers, x as Broadcast, y as ApiRoute, z as ReadFilterOperator } from "./router-B9emOwKj.mjs";
2
2
  import { FuncExpression, ReadExpression, ReadExpressionField, ReadExpressionInput, ReadExpressionRow, ReadExpressionRow as ReadExpressionRow$1, RefExpression, ValExpression, and, compileReadExpression, eq, field, gt, gte, inArray, lt, lte, matchesReadPredicate, ne, not, or, readExpressionReferences, readExpressionRow, val } from "@lsync/transport";
3
3
  import { CollectionAtPath, CollectionDefinitions, CollectionPath, CollectionSet } from "@lsync/definitions";
4
4
 
@@ -23,6 +23,7 @@ declare function ensureSQLiteJsonTables(sql: SqlStorageLike, collections?: Colle
23
23
  declare function applySQLiteJsonBatch(sql: SqlStorageLike, batch: Batch, collections?: CollectionConfigs): void;
24
24
  //#endregion
25
25
  //#region src/history.d.ts
26
+ declare const SQLITE_JSON_CONFLICT_POLICY: "server-sequence-last-write-wins";
26
27
  interface HistoryOptions {
27
28
  maxChanges?: number;
28
29
  }
@@ -34,6 +35,15 @@ declare function persistSQLiteJsonBatchWithHistory(sql: SqlStorageLike, batch: B
34
35
  declare function readSQLiteJsonChanges(sql: SqlStorageLike, query: SyncChangesQuery, collections?: CollectionConfigs): SyncChangesResult;
35
36
  //#endregion
36
37
  //#region src/durable-object-types.d.ts
38
+ interface RateLimitContext {
39
+ auth: AccessAuth;
40
+ clientId: string;
41
+ env: Env;
42
+ }
43
+ type RateLimitHandler = (context: RateLimitContext) => boolean | Promise<boolean>;
44
+ interface CollectionShardLimits {
45
+ maxSubscriptionsPerWebSocket?: number | false;
46
+ }
37
47
  interface Env {
38
48
  SYNC_SHARDS: DurableObjectNamespace;
39
49
  }
@@ -44,6 +54,8 @@ interface CollectionShardOptions {
44
54
  request: Request;
45
55
  }) => AccessAuth | Promise<AccessAuth>;
46
56
  history?: HistoryOptions;
57
+ limits?: CollectionShardLimits;
58
+ rateLimit?: RateLimitHandler;
47
59
  }
48
60
  //#endregion
49
61
  //#region src/definition-builder.d.ts
@@ -51,7 +63,10 @@ declare function collectionShardOptionsFrom<TCollections extends CollectionDefin
51
63
  declare class CollectionShardDefinitionBuilder<TCollections extends CollectionDefinitions> {
52
64
  private readonly definitions;
53
65
  private readonly overrides;
54
- constructor(definitions: TCollections, overrides: Record<string, ServerCollectionOverride>);
66
+ private readonly options;
67
+ constructor(definitions: TCollections, overrides: Record<string, ServerCollectionOverride>, options: Omit<CollectionShardOptions, "collections">);
68
+ limits(limits: CollectionShardLimits): CollectionShardDefinitionBuilder<TCollections>;
69
+ rateLimit(rateLimit: RateLimitHandler): CollectionShardDefinitionBuilder<TCollections>;
55
70
  collection<TPath extends CollectionPath<TCollections>>(path: TPath, configure: (collection: ServerCollectionBuilder<CollectionAtPath<TCollections, TPath>>) => ServerCollectionBuilder<CollectionAtPath<TCollections, TPath>>): CollectionShardDefinitionBuilder<TCollections>;
56
71
  build(): CollectionShardOptions;
57
72
  }
@@ -97,7 +112,6 @@ declare class CollectionShardDurableObject implements DurableObject {
97
112
  private clientId;
98
113
  private updateSubscriptions;
99
114
  private subscribedUpdates;
100
- private subscribedInvalidations;
101
115
  private accessStore;
102
116
  private shardId;
103
117
  }
@@ -111,6 +125,14 @@ declare function createWorkerHandler(options?: WorkerOptions): {
111
125
  fetch(request: Request, env: Env): Promise<Response> | Response;
112
126
  };
113
127
  //#endregion
128
+ //#region src/safeguards.d.ts
129
+ declare const DEFAULT_MAX_SUBSCRIPTIONS_PER_WEBSOCKET = 100;
130
+ interface CloudflareRateLimiterOptions {
131
+ binding?: string;
132
+ key?: (context: RateLimitContext) => string;
133
+ }
134
+ declare function cloudflareRateLimiter(options?: CloudflareRateLimiterOptions): RateLimitHandler;
135
+ //#endregion
114
136
  //#region src/collections.d.ts
115
137
  interface ResolvedCollection {
116
138
  name: string;
@@ -150,5 +172,5 @@ declare function readAccessDecision(collection: string, collections: CollectionC
150
172
  //#region src/validation.d.ts
151
173
  declare function validateBatch(batch: Batch, collections?: CollectionConfigs): void;
152
174
  //#endregion
153
- export { type AccessAuth, type AccessReference, type AccessReferenceResolver, type AccessStore, type ApiAccessContext, type ApiAccessHandler, type ApiCall, type ApiCallArgs, type ApiCallInput, type ApiContract, type ApiHandler, type ApiHandlerArgs, type ApiHandlerContext, type ApiInput, type ApiOutput, type ApiPath, type ApiRoute, type Batch, type Broadcast, type CollectionAccessConfig, type CollectionApiDefinition, type CollectionApiHandlers, type CollectionConfig, type CollectionConfigs, type CollectionInvalidation, CollectionShardDurableObject, type CollectionShardOptions, type CollectionStorageConfig, type CollectionSubscription, type CollectionSubscriptionInput, type CollectionSubscriptionResult, type Context, type Env, type FuncExpression, type HistoryOptions, type OperationType, type PushResult, type ReadAccessContext, type ReadAccessDecision, type ReadAccessHandler, type ReadAccessResult, type ReadCursor, type ReadExpression, type ReadExpressionField, type ReadExpressionInput, type ReadExpressionRow, type ReadFilter, type ReadFilterOperator, type ReadOrderBy, type ReadPredicate, type ReadQuery, type ReadResult, type RefExpression, type ResolvedCollection, type Router, type SQLiteJsonIndexConfig, type SQLiteJsonStorageConfig, type SQLiteJsonTableOptions, type SequencedBatch, type SequencedUpdate, type SqlStorageCursorLike, type SqlStorageLike, type SqlStorageValue, type SyncChangesQuery, type SyncChangesQueryInput, type SyncChangesResult, type Update, type ValExpression, type WebSocketAttachment, type WorkerOptions, and, applySQLiteJsonBatch, authorizeApiCall, authorizeReadQuery, authorizeWriteBatch, collectionApiHandlers, collectionScope, collectionShardOptionsFrom, 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 };
175
+ export { type AccessAuth, type AccessReference, type AccessReferenceResolver, type AccessStore, type ApiAccessContext, type ApiAccessHandler, type ApiCall, type ApiCallArgs, type ApiCallInput, type ApiContract, type ApiHandler, type ApiHandlerArgs, type ApiHandlerContext, type ApiInput, type ApiOutput, type ApiPath, type ApiRoute, type Batch, type Broadcast, type CloudflareRateLimiterOptions, type CollectionAccessConfig, type CollectionApiDefinition, type CollectionApiHandlers, type CollectionConfig, type CollectionConfigs, type CollectionInvalidation, CollectionShardDurableObject, type CollectionShardLimits, type CollectionShardOptions, type CollectionStorageConfig, type CollectionSubscription, type CollectionSubscriptionInput, type CollectionSubscriptionResult, type Context, DEFAULT_MAX_SUBSCRIPTIONS_PER_WEBSOCKET, type Env, type FuncExpression, type HistoryOptions, type OperationType, type PushResult, type RateLimitContext, type RateLimitHandler, type ReadAccessContext, type ReadAccessDecision, type ReadAccessHandler, type ReadAccessResult, type ReadCursor, type ReadExpression, type ReadExpressionField, type ReadExpressionInput, type ReadExpressionRow, type ReadFilter, type ReadFilterOperator, type ReadOrderBy, type ReadPredicate, type ReadQuery, type ReadResult, type RefExpression, type ResolvedCollection, type Router, SQLITE_JSON_CONFLICT_POLICY, type SQLiteJsonIndexConfig, type SQLiteJsonStorageConfig, type SQLiteJsonTableOptions, type SequencedBatch, type SequencedUpdate, type SqlStorageCursorLike, type SqlStorageLike, type SqlStorageValue, type SyncChangesQuery, type SyncChangesQueryInput, type SyncChangesResult, type Update, type ValExpression, type WebSocketAttachment, type WorkerOptions, and, applySQLiteJsonBatch, authorizeApiCall, authorizeReadQuery, authorizeWriteBatch, cloudflareRateLimiter, collectionApiHandlers, collectionScope, collectionShardOptionsFrom, 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 };
154
176
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/sqlite-json-read.ts","../src/storage.ts","../src/history.ts","../src/durable-object-types.ts","../src/definition-builder.ts","../src/durable-object.ts","../src/worker.ts","../src/collections.ts","../src/access-expression.ts","../src/access.ts","../src/validation.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/sqlite-json-read.ts","../src/storage.ts","../src/history.ts","../src/durable-object-types.ts","../src/definition-builder.ts","../src/durable-object.ts","../src/worker.ts","../src/safeguards.ts","../src/collections.ts","../src/access-expression.ts","../src/access.ts","../src/validation.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
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";
1
+ import { n as webSocketAttachmentSchema, t as router } from "./router-CZnnFmXh.mjs";
2
+ import { ProtocolError, and, compileReadExpression, compileReadExpression as compileReadExpression$1, eq, field, gt, gte, inArray, lt, lte, matchesReadPredicate, matchesReadPredicate as matchesReadPredicate$1, ne, not, or, parseClientMessage, protocolErrorCodeSchema, readExpressionReferences, readExpressionReferences as readExpressionReferences$1, readExpressionRow, readExpressionRow as readExpressionRow$1, readMessageId, sendServerMessage, val } from "@lsync/transport";
3
3
  import sqlTag, { join, raw } from "sql-template-tag";
4
4
  import { z } from "zod";
5
5
  //#region src/access-expression.ts
@@ -242,7 +242,7 @@ function authorizeWriteBatch(batch, collections, auth, store) {
242
242
  }
243
243
  function authorizeApiCall(handler, name, input, auth) {
244
244
  if (!handler) return;
245
- const row = asRecord(input) ?? {};
245
+ const row = asRecord$1(input) ?? {};
246
246
  const decision = reduceAccessExpression(handler({
247
247
  auth,
248
248
  input: readExpressionRow$1()
@@ -345,16 +345,16 @@ function writeAccessHandler(access, type) {
345
345
  return access[type] ?? access.write ?? access.read;
346
346
  }
347
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);
348
+ if (update.type === "insert") return asRecord$1(update.value);
349
+ const existing = asRecord$1(update.previousValue) ?? store.read(update.collection, update.key);
350
350
  if (update.type === "delete") return existing;
351
- const next = asRecord(update.value);
351
+ const next = asRecord$1(update.value);
352
352
  return existing && next ? {
353
353
  ...existing,
354
354
  ...next
355
355
  } : next ?? existing;
356
356
  }
357
- function asRecord(value) {
357
+ function asRecord$1(value) {
358
358
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
359
359
  }
360
360
  //#endregion
@@ -438,7 +438,7 @@ function selectRows(sql, table, options) {
438
438
  const where = [sqlTag`path = ${options.scope}`];
439
439
  where.push(...options.filters.map((filter) => filterSql(filter)));
440
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`;
441
+ const order = options.orderBy.length > 0 ? sqlTag`${join(options.orderBy.map((item) => orderBySql(item)))}, key` : sqlTag`key`;
442
442
  return execSql(sql, sqlTag`
443
443
  SELECT key, value FROM ${identifierSql(table)}
444
444
  WHERE ${join(where, " AND ")}
@@ -739,26 +739,43 @@ function singularize(value) {
739
739
  //#endregion
740
740
  //#region src/definition-builder.ts
741
741
  function collectionShardOptionsFrom(definitions) {
742
- return new CollectionShardDefinitionBuilder(definitions.collections, {});
742
+ return new CollectionShardDefinitionBuilder(definitions.collections, {}, {});
743
743
  }
744
744
  var CollectionShardDefinitionBuilder = class CollectionShardDefinitionBuilder {
745
745
  definitions;
746
746
  overrides;
747
- constructor(definitions, overrides) {
747
+ options;
748
+ constructor(definitions, overrides, options) {
748
749
  this.definitions = definitions;
749
750
  this.overrides = overrides;
751
+ this.options = options;
752
+ }
753
+ limits(limits) {
754
+ return new CollectionShardDefinitionBuilder(this.definitions, this.overrides, {
755
+ ...this.options,
756
+ limits
757
+ });
758
+ }
759
+ rateLimit(rateLimit) {
760
+ return new CollectionShardDefinitionBuilder(this.definitions, this.overrides, {
761
+ ...this.options,
762
+ rateLimit
763
+ });
750
764
  }
751
765
  collection(path, configure) {
752
766
  const override = configure(new ServerCollectionBuilder({})).toOverride();
753
767
  return new CollectionShardDefinitionBuilder(this.definitions, {
754
768
  ...this.overrides,
755
769
  [path]: override
756
- });
770
+ }, this.options);
757
771
  }
758
772
  build() {
759
773
  const collections = {};
760
774
  for (const [name, definition] of Object.entries(this.definitions)) addDefinition(collections, definition, name, this.overrides);
761
- return { collections };
775
+ return {
776
+ ...this.options,
777
+ collections
778
+ };
762
779
  }
763
780
  };
764
781
  var ServerCollectionBuilder = class ServerCollectionBuilder {
@@ -844,22 +861,25 @@ function accessConfig(override) {
844
861
  }
845
862
  //#endregion
846
863
  //#region src/durable-object-rpc.ts
847
- function callRouter(caller, request) {
848
- if (request.method === "query" && request.params.path === "read") return caller.read(request.params.input.json);
849
- if (request.method === "query" && request.params.path === "changes") return caller.changes(request.params.input.json);
850
- if (request.method === "subscribe") return caller.subscribe(request.params.input.json);
851
- if (request.method === "unsubscribe") return caller.unsubscribe(request.params.input.json);
852
- if (request.method !== "mutation") throw new Error("Unsupported operation");
853
- if (request.params.path === "push") return caller.push(request.params.input.json);
854
- if (request.params.path === "api") return caller.api(request.params.input.json);
855
- throw new Error("Unsupported operation");
864
+ async function callRouter(caller, request) {
865
+ switch (request.type) {
866
+ case "read": return caller.read(request.input);
867
+ case "push": return caller.push(request.input);
868
+ case "changes": return caller.changes(request.input);
869
+ case "api": return caller.api(request.input);
870
+ case "subscribe": return caller.subscribe(request.input);
871
+ case "unsubscribe": return caller.unsubscribe(request.input);
872
+ }
873
+ }
874
+ function isResyncRequired(result) {
875
+ return typeof result === "object" && result !== null && result.type === "resyncRequired";
856
876
  }
857
877
  //#endregion
858
878
  //#region src/history-watermarks.ts
859
879
  const COLLECTION_WATERMARKS_TABLE = "_lsync_collection_watermarks";
860
880
  function ensureCollectionWatermarkTable(sql) {
861
881
  sql.exec(`
862
- CREATE TABLE IF NOT EXISTS ${quoteIdentifier$1(COLLECTION_WATERMARKS_TABLE)} (
882
+ CREATE TABLE IF NOT EXISTS ${quoteIdentifier$2(COLLECTION_WATERMARKS_TABLE)} (
863
883
  scope TEXT PRIMARY KEY,
864
884
  collection TEXT NOT NULL,
865
885
  latest_sequence INTEGER NOT NULL
@@ -868,7 +888,7 @@ function ensureCollectionWatermarkTable(sql) {
868
888
  }
869
889
  function recordCollectionWatermark(sql, collection, scope, sequence) {
870
890
  sql.exec(`
871
- INSERT INTO ${quoteIdentifier$1(COLLECTION_WATERMARKS_TABLE)}
891
+ INSERT INTO ${quoteIdentifier$2(COLLECTION_WATERMARKS_TABLE)}
872
892
  (scope, collection, latest_sequence)
873
893
  VALUES (?, ?, ?)
874
894
  ON CONFLICT(scope) DO UPDATE SET
@@ -878,21 +898,66 @@ function recordCollectionWatermark(sql, collection, scope, sequence) {
878
898
  }
879
899
  function readCollectionWatermark(sql, scope) {
880
900
  return sql.exec(`
881
- SELECT latest_sequence FROM ${quoteIdentifier$1(COLLECTION_WATERMARKS_TABLE)}
901
+ SELECT latest_sequence FROM ${quoteIdentifier$2(COLLECTION_WATERMARKS_TABLE)}
882
902
  WHERE scope = ?
883
903
  `, scope).toArray()[0]?.latest_sequence ?? 0;
884
904
  }
905
+ function quoteIdentifier$2(identifier) {
906
+ return `"${identifier.replaceAll(`"`, `""`)}"`;
907
+ }
908
+ //#endregion
909
+ //#region src/mutation-dedup.ts
910
+ const APPLIED_UPDATES_TABLE = "_lsync_applied_updates";
911
+ function deduplicateMutations(sql, batch) {
912
+ ensureAppliedUpdatesTable(sql);
913
+ const seen = /* @__PURE__ */ new Map();
914
+ return { updates: batch.updates.filter((update) => {
915
+ const payload = JSON.stringify(update);
916
+ const priorPayload = seen.get(update.id) ?? readAppliedPayload(sql, update.id);
917
+ if (priorPayload === void 0) {
918
+ seen.set(update.id, payload);
919
+ return true;
920
+ }
921
+ if (priorPayload !== payload) throw new Error(`Mutation id reused with a different payload: ${update.id}`);
922
+ return false;
923
+ }) };
924
+ }
925
+ function recordAppliedMutations(sql, updates) {
926
+ for (const update of updates) {
927
+ const { sequence, serverCreatedAt, ...original } = update;
928
+ sql.exec(`INSERT INTO ${quoteIdentifier$1(APPLIED_UPDATES_TABLE)} (update_id, payload, sequence) VALUES (?, ?, ?)`, update.id, JSON.stringify(original), sequence);
929
+ }
930
+ }
931
+ function ensureAppliedUpdatesTable(sql) {
932
+ sql.exec(`
933
+ CREATE TABLE IF NOT EXISTS ${quoteIdentifier$1(APPLIED_UPDATES_TABLE)} (
934
+ update_id TEXT PRIMARY KEY,
935
+ payload TEXT NOT NULL,
936
+ sequence INTEGER NOT NULL
937
+ )
938
+ `);
939
+ }
940
+ function readAppliedPayload(sql, updateId) {
941
+ return sql.exec(`SELECT payload FROM ${quoteIdentifier$1(APPLIED_UPDATES_TABLE)} WHERE update_id = ?`, updateId).toArray()[0]?.payload;
942
+ }
885
943
  function quoteIdentifier$1(identifier) {
886
944
  return `"${identifier.replaceAll(`"`, `""`)}"`;
887
945
  }
888
946
  //#endregion
889
947
  //#region src/history.ts
890
948
  const CHANGES_TABLE = "_lsync_changes";
949
+ const SQLITE_JSON_CONFLICT_POLICY = "server-sequence-last-write-wins";
891
950
  function persistSQLiteJsonBatchWithHistory(sql, batch, collections = {}, options = {}) {
892
951
  ensureSQLiteJsonTables(sql, collections);
893
952
  ensureChangesTable(sql);
894
- applySQLiteJsonBatch(sql, batch, collections);
895
- const updates = batch.updates.map((update) => appendChange(sql, update, collections));
953
+ const pending = deduplicateMutations(sql, batch);
954
+ if (pending.updates.length === 0) return {
955
+ updates: [],
956
+ watermark: readWatermark(sql)
957
+ };
958
+ applySQLiteJsonBatch(sql, pending, collections);
959
+ const updates = pending.updates.map((update) => appendChange(sql, update, collections));
960
+ recordAppliedMutations(sql, updates);
896
961
  const watermark = updates.at(-1)?.sequence ?? readWatermark(sql);
897
962
  pruneChanges(sql, watermark, options.maxChanges ?? 1e4);
898
963
  return {
@@ -922,11 +987,19 @@ function readSQLiteJsonChanges(sql, query, collections = {}) {
922
987
  };
923
988
  const limit = query.limit ?? 1e3;
924
989
  const rows = selectChanges(sql, requested, limit + 1);
990
+ const page = rows.slice(0, limit);
991
+ const cursors = Object.fromEntries(requested.map((item) => [item.collection, item.sequence]));
992
+ for (const row of page) {
993
+ const item = requested.find((candidate) => candidate.scope === row.scope);
994
+ if (item) cursors[item.collection] = row.sequence;
995
+ }
996
+ if (rows.length <= limit) for (const item of requested) cursors[item.collection] = watermark;
925
997
  return {
926
998
  type: "changes",
927
- updates: rows.slice(0, limit).map(rowToUpdate),
999
+ updates: page.map(rowToUpdate),
928
1000
  watermark,
929
- hasMore: rows.length > limit
1001
+ hasMore: rows.length > limit,
1002
+ cursors
930
1003
  };
931
1004
  }
932
1005
  function ensureChangesTable(sql) {
@@ -973,7 +1046,7 @@ function selectChanges(sql, requested, limit) {
973
1046
  const bindings = requested.flatMap((item) => [item.scope, item.sequence]);
974
1047
  bindings.push(limit);
975
1048
  return sql.exec(`
976
- SELECT sequence, update_id, collection, "key", type, value, previous_value, client_id,
1049
+ SELECT sequence, update_id, collection, scope, "key", type, value, previous_value, client_id,
977
1050
  client_created_at, server_created_at
978
1051
  FROM ${quoteIdentifier(CHANGES_TABLE)}
979
1052
  WHERE ${requested.map(() => "(scope = ? AND sequence > ?)").join(" OR ")}
@@ -1021,21 +1094,69 @@ function quoteIdentifier(identifier) {
1021
1094
  }
1022
1095
  //#endregion
1023
1096
  //#region src/messages.ts
1024
- function sendRpcResult(ws, id, result) {
1097
+ function sendResult(ws, id, payload) {
1025
1098
  sendServerMessage(ws, {
1099
+ version: 1,
1026
1100
  id,
1027
- result: {
1028
- type: "data",
1029
- data: { json: result }
1030
- }
1101
+ type: "result",
1102
+ payload
1031
1103
  });
1032
1104
  }
1033
- function sendRpcError(ws, id, error) {
1105
+ function sendProtocolError(ws, id, error) {
1034
1106
  sendServerMessage(ws, {
1107
+ version: 1,
1035
1108
  id,
1036
- error: { message: error instanceof Error ? error.message : "Unknown error" }
1109
+ type: "error",
1110
+ error: protocolErrorFrom(error)
1037
1111
  });
1038
1112
  }
1113
+ function protocolErrorFrom(error) {
1114
+ if (error instanceof ProtocolError) return {
1115
+ code: error.code,
1116
+ message: error.message,
1117
+ ...error.details !== void 0 ? { details: error.details } : {}
1118
+ };
1119
+ if (error instanceof z.ZodError) return {
1120
+ code: "VALIDATION_FAILED",
1121
+ message: "The request failed validation",
1122
+ details: error.issues
1123
+ };
1124
+ const message = error instanceof Error ? error.message : "Unknown protocol error";
1125
+ if (message.toLowerCase().includes("access denied")) return {
1126
+ code: "AUTH_DENIED",
1127
+ message
1128
+ };
1129
+ if (message.toLowerCase().includes("unknown collection")) return {
1130
+ code: "VALIDATION_FAILED",
1131
+ message
1132
+ };
1133
+ if (message.toLowerCase().includes("maximum subscriptions")) return {
1134
+ code: "CONFLICT",
1135
+ message
1136
+ };
1137
+ if (message.toLowerCase().includes("rate limit")) return {
1138
+ code: "RATE_LIMITED",
1139
+ message
1140
+ };
1141
+ if (message.toLowerCase().includes("unknown api path")) return {
1142
+ code: "NOT_FOUND",
1143
+ message
1144
+ };
1145
+ const candidate = asRecord(error);
1146
+ const code = protocolErrorCodeSchema.safeParse(candidate?.code);
1147
+ if (code.success) return {
1148
+ code: code.data,
1149
+ message,
1150
+ ...candidate?.details !== void 0 ? { details: candidate.details } : {}
1151
+ };
1152
+ return {
1153
+ code: "INTERNAL_ERROR",
1154
+ message
1155
+ };
1156
+ }
1157
+ function asRecord(value) {
1158
+ return typeof value === "object" && value !== null ? value : void 0;
1159
+ }
1039
1160
  //#endregion
1040
1161
  //#region src/socket-attachment.ts
1041
1162
  function webSocketAttachment(ws) {
@@ -1067,6 +1188,38 @@ function updateSocketSubscriptions(ws, collection, normalize, update) {
1067
1188
  };
1068
1189
  }
1069
1190
  //#endregion
1191
+ //#region src/safeguards.ts
1192
+ const DEFAULT_MAX_SUBSCRIPTIONS_PER_WEBSOCKET = 100;
1193
+ function assertSubscriptionAllowed(ws, collection, limits) {
1194
+ const attachment = webSocketAttachment(ws);
1195
+ if (!attachment) throw new Error("Missing WebSocket attachment");
1196
+ const maximum = limits?.maxSubscriptionsPerWebSocket ?? 100;
1197
+ if (maximum === false || attachment.subscriptions.includes(collection)) return;
1198
+ assertPositiveInteger(maximum, "maxSubscriptionsPerWebSocket");
1199
+ if (attachment.subscriptions.length >= maximum) throw new Error(`Maximum subscriptions per WebSocket exceeded (${maximum})`);
1200
+ }
1201
+ async function enforceRateLimit(ws, env, rateLimit) {
1202
+ if (!rateLimit) return;
1203
+ const attachment = webSocketAttachment(ws);
1204
+ if (!await rateLimit({
1205
+ auth: webSocketAuth(ws),
1206
+ clientId: attachment?.clientId ?? "unknown",
1207
+ env
1208
+ })) throw new Error("Rate limit exceeded");
1209
+ }
1210
+ function cloudflareRateLimiter(options = {}) {
1211
+ const bindingName = options.binding ?? "RATE_LIMITER";
1212
+ return async (context) => {
1213
+ const binding = context.env[bindingName];
1214
+ if (!binding?.limit) throw new Error(`Missing Cloudflare rate limit binding: ${bindingName}`);
1215
+ const key = options.key?.(context) ?? context.clientId;
1216
+ return (await binding.limit({ key })).success;
1217
+ };
1218
+ }
1219
+ function assertPositiveInteger(value, name) {
1220
+ if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer or false`);
1221
+ }
1222
+ //#endregion
1070
1223
  //#region src/access-invalidation.ts
1071
1224
  function invalidatesDependency(update, dependencies) {
1072
1225
  return dependencies.some((dependency) => {
@@ -1155,7 +1308,8 @@ var CollectionShardDurableObject = class {
1155
1308
  }
1156
1309
  async webSocketMessage(ws, message) {
1157
1310
  try {
1158
- const request = parseClientRpcRequest(message);
1311
+ await enforceRateLimit(ws, this.env, this.options.rateLimit);
1312
+ const request = parseClientMessage(message);
1159
1313
  const result = await callRouter(router.createCaller({
1160
1314
  shardId: this.shardId(),
1161
1315
  validate: (input) => this.validate(input),
@@ -1168,9 +1322,17 @@ var CollectionShardDurableObject = class {
1168
1322
  changes: (input) => this.readChanges(input, webSocketAuth(ws)),
1169
1323
  callApi: (input) => this.callApi(ws, input)
1170
1324
  }), request);
1171
- sendRpcResult(ws, request.id, result);
1325
+ if (request.type === "changes" && isResyncRequired(result)) {
1326
+ sendProtocolError(ws, request.id, {
1327
+ code: "RESYNC_REQUIRED",
1328
+ message: "The requested change history is no longer available",
1329
+ details: result
1330
+ });
1331
+ return;
1332
+ }
1333
+ sendResult(ws, request.id, result);
1172
1334
  } catch (error) {
1173
- sendRpcError(ws, readRpcId(message), error);
1335
+ sendProtocolError(ws, readMessageId(message), error);
1174
1336
  }
1175
1337
  }
1176
1338
  publish(batch) {
@@ -1182,17 +1344,16 @@ var CollectionShardDurableObject = class {
1182
1344
  };
1183
1345
  for (const socket of this.state.getWebSockets()) {
1184
1346
  const updates = this.subscribedUpdates(socket, batch);
1185
- const invalidations = this.subscribedInvalidations(socket, batch);
1347
+ const attachment = webSocketAttachment(socket);
1348
+ const invalidations = attachment ? subscribedInvalidations(attachment.subscriptions, batch, this.options.collections, webSocketAuth(socket), this.accessStore()) : [];
1186
1349
  if (updates.length === 0 && invalidations.length === 0) continue;
1187
1350
  sendServerMessage(socket, {
1188
- method: "subscription",
1189
- params: {
1190
- path: "updates",
1191
- input: { json: {
1192
- ...payload,
1193
- ...invalidations.length > 0 ? { invalidations } : {},
1194
- updates
1195
- } }
1351
+ version: 1,
1352
+ type: "updates",
1353
+ payload: {
1354
+ ...payload,
1355
+ ...invalidations.length > 0 ? { invalidations } : {},
1356
+ updates
1196
1357
  }
1197
1358
  });
1198
1359
  }
@@ -1223,6 +1384,7 @@ var CollectionShardDurableObject = class {
1223
1384
  };
1224
1385
  }
1225
1386
  subscribe(ws, input) {
1387
+ assertSubscriptionAllowed(ws, normalizeCollection(input.collection, this.options.collections), this.options.limits);
1226
1388
  return this.updateSubscriptions(ws, input.collection, (subscriptions, collection) => {
1227
1389
  subscriptions.add(collection);
1228
1390
  });
@@ -1254,8 +1416,9 @@ var CollectionShardDurableObject = class {
1254
1416
  return handler(args);
1255
1417
  }
1256
1418
  mutate(batch, auth = {}) {
1257
- this.validate(batch);
1258
- authorizeWriteBatch(batch, this.options.collections, auth, this.accessStore());
1419
+ const pending = deduplicateMutations(this.state.storage.sql, batch);
1420
+ this.validate(pending);
1421
+ authorizeWriteBatch(pending, this.options.collections, auth, this.accessStore());
1259
1422
  const persisted = this.persist(batch);
1260
1423
  this.publish(persisted);
1261
1424
  return {
@@ -1284,11 +1447,6 @@ var CollectionShardDurableObject = class {
1284
1447
  }] : [];
1285
1448
  });
1286
1449
  }
1287
- subscribedInvalidations(ws, batch) {
1288
- const attachment = webSocketAttachment(ws);
1289
- if (!attachment || attachment.subscriptions.length === 0) return [];
1290
- return subscribedInvalidations(attachment.subscriptions, batch, this.options.collections, webSocketAuth(ws), this.accessStore());
1291
- }
1292
1450
  accessStore() {
1293
1451
  return { read: (collection, key) => readSQLiteJsonRow(this.state.storage.sql, collection, key, this.options.collections) };
1294
1452
  }
@@ -1311,6 +1469,6 @@ function createWorkerHandler(options = {}) {
1311
1469
  } };
1312
1470
  }
1313
1471
  //#endregion
1314
- export { CollectionShardDurableObject, and, applySQLiteJsonBatch, authorizeApiCall, authorizeReadQuery, authorizeWriteBatch, collectionApiHandlers, collectionScope, collectionShardOptionsFrom, 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 };
1472
+ export { CollectionShardDurableObject, DEFAULT_MAX_SUBSCRIPTIONS_PER_WEBSOCKET, SQLITE_JSON_CONFLICT_POLICY, and, applySQLiteJsonBatch, authorizeApiCall, authorizeReadQuery, authorizeWriteBatch, cloudflareRateLimiter, collectionApiHandlers, collectionScope, collectionShardOptionsFrom, 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 };
1315
1473
 
1316
1474
  //# sourceMappingURL=index.mjs.map