@lsync/server 0.0.1 → 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 -55
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +214 -86
- 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/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 ")}
|
|
@@ -623,39 +623,9 @@ function isRecord(value) {
|
|
|
623
623
|
}
|
|
624
624
|
//#endregion
|
|
625
625
|
//#region src/collection-builder.ts
|
|
626
|
-
function collectionConfigsBuilder() {
|
|
627
|
-
return new CollectionConfigsBuilder({});
|
|
628
|
-
}
|
|
629
626
|
function collectionBuilder() {
|
|
630
627
|
return new CollectionBuilder({});
|
|
631
628
|
}
|
|
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
629
|
var CollectionBuilder = class CollectionBuilder {
|
|
660
630
|
state;
|
|
661
631
|
constructor(state) {
|
|
@@ -769,26 +739,43 @@ function singularize(value) {
|
|
|
769
739
|
//#endregion
|
|
770
740
|
//#region src/definition-builder.ts
|
|
771
741
|
function collectionShardOptionsFrom(definitions) {
|
|
772
|
-
return new CollectionShardDefinitionBuilder(definitions.collections, {});
|
|
742
|
+
return new CollectionShardDefinitionBuilder(definitions.collections, {}, {});
|
|
773
743
|
}
|
|
774
744
|
var CollectionShardDefinitionBuilder = class CollectionShardDefinitionBuilder {
|
|
775
745
|
definitions;
|
|
776
746
|
overrides;
|
|
777
|
-
|
|
747
|
+
options;
|
|
748
|
+
constructor(definitions, overrides, options) {
|
|
778
749
|
this.definitions = definitions;
|
|
779
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
|
+
});
|
|
780
764
|
}
|
|
781
765
|
collection(path, configure) {
|
|
782
766
|
const override = configure(new ServerCollectionBuilder({})).toOverride();
|
|
783
767
|
return new CollectionShardDefinitionBuilder(this.definitions, {
|
|
784
768
|
...this.overrides,
|
|
785
769
|
[path]: override
|
|
786
|
-
});
|
|
770
|
+
}, this.options);
|
|
787
771
|
}
|
|
788
772
|
build() {
|
|
789
773
|
const collections = {};
|
|
790
774
|
for (const [name, definition] of Object.entries(this.definitions)) addDefinition(collections, definition, name, this.overrides);
|
|
791
|
-
return {
|
|
775
|
+
return {
|
|
776
|
+
...this.options,
|
|
777
|
+
collections
|
|
778
|
+
};
|
|
792
779
|
}
|
|
793
780
|
};
|
|
794
781
|
var ServerCollectionBuilder = class ServerCollectionBuilder {
|
|
@@ -874,22 +861,25 @@ function accessConfig(override) {
|
|
|
874
861
|
}
|
|
875
862
|
//#endregion
|
|
876
863
|
//#region src/durable-object-rpc.ts
|
|
877
|
-
function callRouter(caller, request) {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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";
|
|
886
876
|
}
|
|
887
877
|
//#endregion
|
|
888
878
|
//#region src/history-watermarks.ts
|
|
889
879
|
const COLLECTION_WATERMARKS_TABLE = "_lsync_collection_watermarks";
|
|
890
880
|
function ensureCollectionWatermarkTable(sql) {
|
|
891
881
|
sql.exec(`
|
|
892
|
-
CREATE TABLE IF NOT EXISTS ${quoteIdentifier$
|
|
882
|
+
CREATE TABLE IF NOT EXISTS ${quoteIdentifier$2(COLLECTION_WATERMARKS_TABLE)} (
|
|
893
883
|
scope TEXT PRIMARY KEY,
|
|
894
884
|
collection TEXT NOT NULL,
|
|
895
885
|
latest_sequence INTEGER NOT NULL
|
|
@@ -898,7 +888,7 @@ function ensureCollectionWatermarkTable(sql) {
|
|
|
898
888
|
}
|
|
899
889
|
function recordCollectionWatermark(sql, collection, scope, sequence) {
|
|
900
890
|
sql.exec(`
|
|
901
|
-
INSERT INTO ${quoteIdentifier$
|
|
891
|
+
INSERT INTO ${quoteIdentifier$2(COLLECTION_WATERMARKS_TABLE)}
|
|
902
892
|
(scope, collection, latest_sequence)
|
|
903
893
|
VALUES (?, ?, ?)
|
|
904
894
|
ON CONFLICT(scope) DO UPDATE SET
|
|
@@ -908,21 +898,66 @@ function recordCollectionWatermark(sql, collection, scope, sequence) {
|
|
|
908
898
|
}
|
|
909
899
|
function readCollectionWatermark(sql, scope) {
|
|
910
900
|
return sql.exec(`
|
|
911
|
-
SELECT latest_sequence FROM ${quoteIdentifier$
|
|
901
|
+
SELECT latest_sequence FROM ${quoteIdentifier$2(COLLECTION_WATERMARKS_TABLE)}
|
|
912
902
|
WHERE scope = ?
|
|
913
903
|
`, scope).toArray()[0]?.latest_sequence ?? 0;
|
|
914
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
|
+
}
|
|
915
943
|
function quoteIdentifier$1(identifier) {
|
|
916
944
|
return `"${identifier.replaceAll(`"`, `""`)}"`;
|
|
917
945
|
}
|
|
918
946
|
//#endregion
|
|
919
947
|
//#region src/history.ts
|
|
920
948
|
const CHANGES_TABLE = "_lsync_changes";
|
|
949
|
+
const SQLITE_JSON_CONFLICT_POLICY = "server-sequence-last-write-wins";
|
|
921
950
|
function persistSQLiteJsonBatchWithHistory(sql, batch, collections = {}, options = {}) {
|
|
922
951
|
ensureSQLiteJsonTables(sql, collections);
|
|
923
952
|
ensureChangesTable(sql);
|
|
924
|
-
|
|
925
|
-
|
|
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);
|
|
926
961
|
const watermark = updates.at(-1)?.sequence ?? readWatermark(sql);
|
|
927
962
|
pruneChanges(sql, watermark, options.maxChanges ?? 1e4);
|
|
928
963
|
return {
|
|
@@ -952,11 +987,19 @@ function readSQLiteJsonChanges(sql, query, collections = {}) {
|
|
|
952
987
|
};
|
|
953
988
|
const limit = query.limit ?? 1e3;
|
|
954
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;
|
|
955
997
|
return {
|
|
956
998
|
type: "changes",
|
|
957
|
-
updates:
|
|
999
|
+
updates: page.map(rowToUpdate),
|
|
958
1000
|
watermark,
|
|
959
|
-
hasMore: rows.length > limit
|
|
1001
|
+
hasMore: rows.length > limit,
|
|
1002
|
+
cursors
|
|
960
1003
|
};
|
|
961
1004
|
}
|
|
962
1005
|
function ensureChangesTable(sql) {
|
|
@@ -1003,7 +1046,7 @@ function selectChanges(sql, requested, limit) {
|
|
|
1003
1046
|
const bindings = requested.flatMap((item) => [item.scope, item.sequence]);
|
|
1004
1047
|
bindings.push(limit);
|
|
1005
1048
|
return sql.exec(`
|
|
1006
|
-
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,
|
|
1007
1050
|
client_created_at, server_created_at
|
|
1008
1051
|
FROM ${quoteIdentifier(CHANGES_TABLE)}
|
|
1009
1052
|
WHERE ${requested.map(() => "(scope = ? AND sequence > ?)").join(" OR ")}
|
|
@@ -1051,21 +1094,69 @@ function quoteIdentifier(identifier) {
|
|
|
1051
1094
|
}
|
|
1052
1095
|
//#endregion
|
|
1053
1096
|
//#region src/messages.ts
|
|
1054
|
-
function
|
|
1097
|
+
function sendResult(ws, id, payload) {
|
|
1055
1098
|
sendServerMessage(ws, {
|
|
1099
|
+
version: 1,
|
|
1056
1100
|
id,
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
data: { json: result }
|
|
1060
|
-
}
|
|
1101
|
+
type: "result",
|
|
1102
|
+
payload
|
|
1061
1103
|
});
|
|
1062
1104
|
}
|
|
1063
|
-
function
|
|
1105
|
+
function sendProtocolError(ws, id, error) {
|
|
1064
1106
|
sendServerMessage(ws, {
|
|
1107
|
+
version: 1,
|
|
1065
1108
|
id,
|
|
1066
|
-
|
|
1109
|
+
type: "error",
|
|
1110
|
+
error: protocolErrorFrom(error)
|
|
1067
1111
|
});
|
|
1068
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
|
+
}
|
|
1069
1160
|
//#endregion
|
|
1070
1161
|
//#region src/socket-attachment.ts
|
|
1071
1162
|
function webSocketAttachment(ws) {
|
|
@@ -1097,6 +1188,38 @@ function updateSocketSubscriptions(ws, collection, normalize, update) {
|
|
|
1097
1188
|
};
|
|
1098
1189
|
}
|
|
1099
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
|
|
1100
1223
|
//#region src/access-invalidation.ts
|
|
1101
1224
|
function invalidatesDependency(update, dependencies) {
|
|
1102
1225
|
return dependencies.some((dependency) => {
|
|
@@ -1185,7 +1308,8 @@ var CollectionShardDurableObject = class {
|
|
|
1185
1308
|
}
|
|
1186
1309
|
async webSocketMessage(ws, message) {
|
|
1187
1310
|
try {
|
|
1188
|
-
|
|
1311
|
+
await enforceRateLimit(ws, this.env, this.options.rateLimit);
|
|
1312
|
+
const request = parseClientMessage(message);
|
|
1189
1313
|
const result = await callRouter(router.createCaller({
|
|
1190
1314
|
shardId: this.shardId(),
|
|
1191
1315
|
validate: (input) => this.validate(input),
|
|
@@ -1198,9 +1322,17 @@ var CollectionShardDurableObject = class {
|
|
|
1198
1322
|
changes: (input) => this.readChanges(input, webSocketAuth(ws)),
|
|
1199
1323
|
callApi: (input) => this.callApi(ws, input)
|
|
1200
1324
|
}), request);
|
|
1201
|
-
|
|
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);
|
|
1202
1334
|
} catch (error) {
|
|
1203
|
-
|
|
1335
|
+
sendProtocolError(ws, readMessageId(message), error);
|
|
1204
1336
|
}
|
|
1205
1337
|
}
|
|
1206
1338
|
publish(batch) {
|
|
@@ -1212,17 +1344,16 @@ var CollectionShardDurableObject = class {
|
|
|
1212
1344
|
};
|
|
1213
1345
|
for (const socket of this.state.getWebSockets()) {
|
|
1214
1346
|
const updates = this.subscribedUpdates(socket, batch);
|
|
1215
|
-
const
|
|
1347
|
+
const attachment = webSocketAttachment(socket);
|
|
1348
|
+
const invalidations = attachment ? subscribedInvalidations(attachment.subscriptions, batch, this.options.collections, webSocketAuth(socket), this.accessStore()) : [];
|
|
1216
1349
|
if (updates.length === 0 && invalidations.length === 0) continue;
|
|
1217
1350
|
sendServerMessage(socket, {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
updates
|
|
1225
|
-
} }
|
|
1351
|
+
version: 1,
|
|
1352
|
+
type: "updates",
|
|
1353
|
+
payload: {
|
|
1354
|
+
...payload,
|
|
1355
|
+
...invalidations.length > 0 ? { invalidations } : {},
|
|
1356
|
+
updates
|
|
1226
1357
|
}
|
|
1227
1358
|
});
|
|
1228
1359
|
}
|
|
@@ -1253,6 +1384,7 @@ var CollectionShardDurableObject = class {
|
|
|
1253
1384
|
};
|
|
1254
1385
|
}
|
|
1255
1386
|
subscribe(ws, input) {
|
|
1387
|
+
assertSubscriptionAllowed(ws, normalizeCollection(input.collection, this.options.collections), this.options.limits);
|
|
1256
1388
|
return this.updateSubscriptions(ws, input.collection, (subscriptions, collection) => {
|
|
1257
1389
|
subscriptions.add(collection);
|
|
1258
1390
|
});
|
|
@@ -1284,8 +1416,9 @@ var CollectionShardDurableObject = class {
|
|
|
1284
1416
|
return handler(args);
|
|
1285
1417
|
}
|
|
1286
1418
|
mutate(batch, auth = {}) {
|
|
1287
|
-
this.
|
|
1288
|
-
|
|
1419
|
+
const pending = deduplicateMutations(this.state.storage.sql, batch);
|
|
1420
|
+
this.validate(pending);
|
|
1421
|
+
authorizeWriteBatch(pending, this.options.collections, auth, this.accessStore());
|
|
1289
1422
|
const persisted = this.persist(batch);
|
|
1290
1423
|
this.publish(persisted);
|
|
1291
1424
|
return {
|
|
@@ -1314,11 +1447,6 @@ var CollectionShardDurableObject = class {
|
|
|
1314
1447
|
}] : [];
|
|
1315
1448
|
});
|
|
1316
1449
|
}
|
|
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
1450
|
accessStore() {
|
|
1323
1451
|
return { read: (collection, key) => readSQLiteJsonRow(this.state.storage.sql, collection, key, this.options.collections) };
|
|
1324
1452
|
}
|
|
@@ -1341,6 +1469,6 @@ function createWorkerHandler(options = {}) {
|
|
|
1341
1469
|
} };
|
|
1342
1470
|
}
|
|
1343
1471
|
//#endregion
|
|
1344
|
-
export {
|
|
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 };
|
|
1345
1473
|
|
|
1346
1474
|
//# sourceMappingURL=index.mjs.map
|