@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 +46 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +26 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +214 -56
- package/dist/index.mjs.map +1 -1
- package/dist/{router-D_wddPdy.d.mts → router-B9emOwKj.d.mts} +13 -110
- package/dist/{router-D_wddPdy.d.mts.map → router-B9emOwKj.d.mts.map} +1 -1
- package/dist/router-CZnnFmXh.mjs +39 -0
- package/dist/router-CZnnFmXh.mjs.map +1 -0
- package/package.json +3 -4
- package/dist/router-C4eYfMV7.mjs +0 -39
- package/dist/router-C4eYfMV7.mjs.map +0 -1
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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":";;;;;iBAmBgB,kBAAA,CACd,GAAA,EAAK,cAAA,EACL,KAAA,EAAO,SAAA,EACP,WAAA,GAAa,iBAAA,GACZ,UAAA;AAAA,iBA4Da,iBAAA,CACd,GAAA,EAAK,cAAA,EACL,UAAA,UACA,GAAA,mBACA,WAAA,GAAa,iBAAA,GACZ,MAAA;;;KC3ES,eAAA,GAAkB,WAAW;AAAA,UAExB,oBAAA,WAA+B,MAAA,SAAe,eAAA;EAC7D,OAAA,IAAW,KAAA,CAAM,CAAA;AAAA;AAAA,UAGF,cAAA;EACf,IAAA,WAAe,MAAA,SAAe,eAAA,GAC5B,KAAA,aACG,QAAA,EAAU,KAAA,YACZ,oBAAA,CAAqB,CAAA;AAAA;AAAA,UAGT,sBAAA;EACf,SAAA;EACA,OAAA,GAAU,KAAA,CAAM,KAAA,WAAgB,qBAAA;AAAA;AAAA,iBAGlB,eAAA,CAAgB,OAAA,GAAS,sBAAA,GAA8B,uBAAuB;AAAA,iBAU9E,sBAAA,CACd,GAAA,EAAK,cAAA,EACL,WAAA,GAAa,iBAAsB;AAAA,iBAkDrB,oBAAA,CACd,GAAA,EAAK,cAAA,EACL,KAAA,EAAO,KAAA,EACP,WAAA,GAAa,iBAAA;;;
|
|
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-
|
|
2
|
-
import { and, compileReadExpression, compileReadExpression as compileReadExpression$1, eq, field, gt, gte, inArray, lt, lte, matchesReadPredicate, matchesReadPredicate as matchesReadPredicate$1, ne, not, or,
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
895
|
-
|
|
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:
|
|
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
|
|
1097
|
+
function sendResult(ws, id, payload) {
|
|
1025
1098
|
sendServerMessage(ws, {
|
|
1099
|
+
version: 1,
|
|
1026
1100
|
id,
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
data: { json: result }
|
|
1030
|
-
}
|
|
1101
|
+
type: "result",
|
|
1102
|
+
payload
|
|
1031
1103
|
});
|
|
1032
1104
|
}
|
|
1033
|
-
function
|
|
1105
|
+
function sendProtocolError(ws, id, error) {
|
|
1034
1106
|
sendServerMessage(ws, {
|
|
1107
|
+
version: 1,
|
|
1035
1108
|
id,
|
|
1036
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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.
|
|
1258
|
-
|
|
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
|