@tldraw/sync-core 5.3.0-internal.fba91ed55f6c → 5.3.0-next.4123e97459a9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +3 -154
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/InMemorySyncStorage.js +20 -55
- package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/SQLiteSyncStorage.js +52 -115
- package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
- package/dist-cjs/lib/TLSocketRoom.js +2 -27
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +1 -6
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +10 -160
- package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
- package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
- package/dist-cjs/lib/protocol.js.map +2 -2
- package/dist-esm/index.d.mts +3 -154
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/InMemorySyncStorage.mjs +20 -55
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/SQLiteSyncStorage.mjs +52 -115
- package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/TLSocketRoom.mjs +2 -27
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +1 -6
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +10 -160
- package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
- package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/protocol.mjs.map +2 -2
- package/package.json +6 -6
- package/src/index.ts +0 -3
- package/src/lib/InMemorySyncStorage.ts +21 -74
- package/src/lib/RoomSession.ts +2 -7
- package/src/lib/SQLiteSyncStorage.test.ts +2 -29
- package/src/lib/SQLiteSyncStorage.ts +59 -158
- package/src/lib/TLSocketRoom.ts +3 -61
- package/src/lib/TLSyncClient.test.ts +2 -9
- package/src/lib/TLSyncClient.ts +3 -15
- package/src/lib/TLSyncRoom.ts +10 -294
- package/src/lib/TLSyncStorage.ts +0 -8
- package/src/lib/protocol.ts +0 -17
- package/src/test/TLSocketRoom.test.ts +2 -37
- package/src/test/TLSyncRoom.test.ts +0 -25
- package/src/test/TestServer.ts +4 -14
- package/src/test/storageContractSuite.ts +0 -79
- package/src/test/upgradeDowngrade.test.ts +2 -144
- package/src/test/objectStore.test.ts +0 -630
|
@@ -109,41 +109,14 @@ class TLSyncRoom {
|
|
|
109
109
|
storage;
|
|
110
110
|
serializedSchema;
|
|
111
111
|
documentTypes;
|
|
112
|
-
/**
|
|
113
|
-
* Record types served by the object-store lane. Object records ride the same wire messages
|
|
114
|
-
* as document records but are gated by the session's `objectAccess` instead of `isReadonly`,
|
|
115
|
-
* and are excluded from `documentTypes` so hosts can persist them in a separate lane.
|
|
116
|
-
*/
|
|
117
|
-
objectTypes;
|
|
118
112
|
presenceType;
|
|
119
113
|
log;
|
|
120
114
|
schema;
|
|
121
|
-
authorizeRecord;
|
|
122
115
|
sessionIdleTimeout;
|
|
123
|
-
/**
|
|
124
|
-
* The authorizer registered for a record type, widened to the room's record union. Each entry in
|
|
125
|
-
* `authorizeRecord` is typed to its specific record; the cast here is the one place we can't
|
|
126
|
-
* statically correlate a runtime `typeName` with its record type, so it lives in the library
|
|
127
|
-
* rather than in every consumer.
|
|
128
|
-
*/
|
|
129
|
-
authorizerFor(typeName) {
|
|
130
|
-
const authorize = this.authorizeRecord?.[typeName];
|
|
131
|
-
if (!authorize) return void 0;
|
|
132
|
-
return (args) => {
|
|
133
|
-
try {
|
|
134
|
-
return authorize(args);
|
|
135
|
-
} catch (e) {
|
|
136
|
-
this.log?.error?.("record authorizer threw; rejecting the write", e);
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
116
|
constructor(opts) {
|
|
142
117
|
this.schema = opts.schema;
|
|
143
118
|
this.log = opts.log;
|
|
144
119
|
this.onPresenceChange = opts.onPresenceChange;
|
|
145
|
-
this.onCommittedChanges = opts.onCommittedChanges;
|
|
146
|
-
this.authorizeRecord = opts.authorizeRecord;
|
|
147
120
|
this.storage = opts.storage;
|
|
148
121
|
this.sessionIdleTimeout = opts.clientTimeout ?? import_RoomSession.SESSION_IDLE_TIMEOUT;
|
|
149
122
|
(0, import_utils.assert)(
|
|
@@ -151,17 +124,8 @@ class TLSyncRoom {
|
|
|
151
124
|
"TLSyncRoom is supposed to run either on Cloudflare Workersor on a 18+ version of Node.js, which both support the native structuredClone API"
|
|
152
125
|
);
|
|
153
126
|
this.serializedSchema = JSON.parse(JSON.stringify(this.schema.serialize()));
|
|
154
|
-
this.objectTypes = new Set(opts.objectTypes ?? []);
|
|
155
|
-
for (const typeName of this.objectTypes) {
|
|
156
|
-
const type = (0, import_utils.getOwnProperty)(this.schema.types, typeName);
|
|
157
|
-
(0, import_utils.assert)(type, `TLSyncRoom: object type '${typeName}' is not registered in the schema`);
|
|
158
|
-
(0, import_utils.assert)(
|
|
159
|
-
type.scope === "document",
|
|
160
|
-
`TLSyncRoom: object type '${typeName}' must have scope 'document', got '${type.scope}'`
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
127
|
this.documentTypes = new Set(
|
|
164
|
-
Object.values(this.schema.types).filter((t) => t.scope === "document"
|
|
128
|
+
Object.values(this.schema.types).filter((t) => t.scope === "document").map((t) => t.typeName)
|
|
165
129
|
);
|
|
166
130
|
const presenceTypes = new Set(
|
|
167
131
|
Object.values(this.schema.types).filter((t) => t.scope === "presence")
|
|
@@ -172,12 +136,6 @@ class TLSyncRoom {
|
|
|
172
136
|
);
|
|
173
137
|
}
|
|
174
138
|
this.presenceType = presenceTypes.values().next()?.value ?? null;
|
|
175
|
-
if (this.presenceType && this.authorizeRecord) {
|
|
176
|
-
(0, import_utils.assert)(
|
|
177
|
-
!(0, import_utils.getOwnProperty)(this.authorizeRecord, this.presenceType.typeName),
|
|
178
|
-
`TLSyncRoom: authorizeRecord['${this.presenceType.typeName}'] is a presence type; presence records are not authorized`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
139
|
const { documentClock } = this.storage.transaction((txn) => {
|
|
182
140
|
this.schema.migrateStorage(txn);
|
|
183
141
|
});
|
|
@@ -304,7 +262,6 @@ class TLSyncRoom {
|
|
|
304
262
|
cancellationTime: Date.now(),
|
|
305
263
|
meta: session.meta,
|
|
306
264
|
isReadonly: session.isReadonly,
|
|
307
|
-
objectAccess: session.objectAccess,
|
|
308
265
|
requiresLegacyRejection: session.requiresLegacyRejection,
|
|
309
266
|
supportsStringAppend: session.supportsStringAppend
|
|
310
267
|
});
|
|
@@ -396,7 +353,7 @@ class TLSyncRoom {
|
|
|
396
353
|
* @internal
|
|
397
354
|
*/
|
|
398
355
|
handleNewSession(opts) {
|
|
399
|
-
const { sessionId, socket, meta, isReadonly
|
|
356
|
+
const { sessionId, socket, meta, isReadonly } = opts;
|
|
400
357
|
const existing = this.sessions.get(sessionId);
|
|
401
358
|
this.sessions.set(sessionId, {
|
|
402
359
|
state: import_RoomSession.RoomSessionState.AwaitingConnectMessage,
|
|
@@ -406,7 +363,6 @@ class TLSyncRoom {
|
|
|
406
363
|
sessionStartTime: Date.now(),
|
|
407
364
|
meta,
|
|
408
365
|
isReadonly: isReadonly ?? false,
|
|
409
|
-
objectAccess: objectAccess ?? "write",
|
|
410
366
|
// this gets set later during handleConnectMessage
|
|
411
367
|
requiresLegacyRejection: false,
|
|
412
368
|
supportsStringAppend: true
|
|
@@ -426,7 +382,6 @@ class TLSyncRoom {
|
|
|
426
382
|
socket,
|
|
427
383
|
meta,
|
|
428
384
|
isReadonly,
|
|
429
|
-
objectAccess,
|
|
430
385
|
serializedSchema,
|
|
431
386
|
presenceId,
|
|
432
387
|
presenceRecord,
|
|
@@ -447,7 +402,6 @@ class TLSyncRoom {
|
|
|
447
402
|
outstandingDataMessages: [],
|
|
448
403
|
meta,
|
|
449
404
|
isReadonly,
|
|
450
|
-
objectAccess: objectAccess ?? "write",
|
|
451
405
|
requiresLegacyRejection,
|
|
452
406
|
supportsStringAppend
|
|
453
407
|
});
|
|
@@ -708,7 +662,6 @@ class TLSyncRoom {
|
|
|
708
662
|
supportsStringAppend: session.supportsStringAppend,
|
|
709
663
|
meta: session.meta,
|
|
710
664
|
isReadonly: session.isReadonly,
|
|
711
|
-
objectAccess: session.objectAccess,
|
|
712
665
|
requiresLegacyRejection: session.requiresLegacyRejection
|
|
713
666
|
});
|
|
714
667
|
this._unsafe_sendMessage(session.sessionId, msg);
|
|
@@ -753,8 +706,7 @@ class TLSyncRoom {
|
|
|
753
706
|
schema: this.schema.serialize(),
|
|
754
707
|
serverClock: txn.getClock(),
|
|
755
708
|
diff: { ...presenceDiff.value, ...docDiff },
|
|
756
|
-
isReadonly: session.isReadonly
|
|
757
|
-
objectAccess: session.objectAccess
|
|
709
|
+
isReadonly: session.isReadonly
|
|
758
710
|
};
|
|
759
711
|
});
|
|
760
712
|
this.lastDocumentClock = documentClock;
|
|
@@ -788,18 +740,13 @@ class TLSyncRoom {
|
|
|
788
740
|
(0, import_utils.exhaustiveSwitchError)(op[0]);
|
|
789
741
|
}
|
|
790
742
|
};
|
|
791
|
-
const addDocument = (storage, changes2, id, _state
|
|
743
|
+
const addDocument = (storage, changes2, id, _state) => {
|
|
792
744
|
const res = session ? this.schema.migratePersistedRecord(_state, session.serializedSchema, "up") : { type: "success", value: _state };
|
|
793
745
|
if (res.type === "error") {
|
|
794
746
|
throw new import_TLSyncClient.TLSyncError(res.reason, import_TLSyncClient.TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
|
|
795
747
|
}
|
|
796
|
-
|
|
748
|
+
const { value: state } = res;
|
|
797
749
|
const doc = storage.get(id);
|
|
798
|
-
if (authorize) {
|
|
799
|
-
const result2 = authorize(doc ?? null, state);
|
|
800
|
-
if (!result2) return import_utils.Result.ok(void 0);
|
|
801
|
-
if (!doc) state = result2;
|
|
802
|
-
}
|
|
803
750
|
if (doc) {
|
|
804
751
|
const recordType = (0, import_utils.assertExists)((0, import_utils.getOwnProperty)(this.schema.types, doc.typeName));
|
|
805
752
|
const diff = (0, import_recordDiff.diffAndValidateRecord)(doc, state, recordType);
|
|
@@ -815,7 +762,7 @@ class TLSyncRoom {
|
|
|
815
762
|
}
|
|
816
763
|
return import_utils.Result.ok(void 0);
|
|
817
764
|
};
|
|
818
|
-
const patchDocument = (storage, changes2, id, patch
|
|
765
|
+
const patchDocument = (storage, changes2, id, patch) => {
|
|
819
766
|
const doc = storage.get(id);
|
|
820
767
|
if (!doc) return;
|
|
821
768
|
const recordType = (0, import_utils.assertExists)((0, import_utils.getOwnProperty)(this.schema.types, doc.typeName));
|
|
@@ -826,7 +773,6 @@ class TLSyncRoom {
|
|
|
826
773
|
if (downgraded.value === doc) {
|
|
827
774
|
const diff = (0, import_recordDiff.applyAndDiffRecord)(doc, patch, recordType, legacyAppendMode);
|
|
828
775
|
if (diff) {
|
|
829
|
-
if (authorize && !authorize(doc, diff[1])) return;
|
|
830
776
|
storage.set(id, diff[1]);
|
|
831
777
|
propagateOp(changes2, id, [import_diff.RecordOpType.Patch, diff[0]], doc, diff[1]);
|
|
832
778
|
}
|
|
@@ -838,7 +784,6 @@ class TLSyncRoom {
|
|
|
838
784
|
}
|
|
839
785
|
const diff = (0, import_recordDiff.diffAndValidateRecord)(doc, upgraded.value, recordType, legacyAppendMode);
|
|
840
786
|
if (diff) {
|
|
841
|
-
if (authorize && !authorize(doc, upgraded.value)) return;
|
|
842
787
|
storage.set(id, upgraded.value);
|
|
843
788
|
propagateOp(changes2, id, [import_diff.RecordOpType.Patch, diff], doc, upgraded.value);
|
|
844
789
|
}
|
|
@@ -873,89 +818,21 @@ class TLSyncRoom {
|
|
|
873
818
|
}
|
|
874
819
|
}
|
|
875
820
|
}
|
|
876
|
-
|
|
877
|
-
if (message.diff) {
|
|
821
|
+
if (message.diff && !session?.isReadonly) {
|
|
878
822
|
for (const [id, op] of (0, import_utils.objectMapEntriesIterable)(message.diff)) {
|
|
879
823
|
switch (op[0]) {
|
|
880
824
|
case import_diff.RecordOpType.Put: {
|
|
881
|
-
if (!
|
|
882
|
-
if (!this.documentTypes.has(op[1].typeName) && !this.objectTypes.has(op[1].typeName)) {
|
|
825
|
+
if (!this.documentTypes.has(op[1].typeName)) {
|
|
883
826
|
throw new import_TLSyncClient.TLSyncError(
|
|
884
827
|
"invalid record",
|
|
885
828
|
import_TLSyncClient.TLSyncErrorCloseEventReason.INVALID_RECORD
|
|
886
829
|
);
|
|
887
830
|
}
|
|
888
|
-
|
|
889
|
-
let authorize;
|
|
890
|
-
if (session && this.authorizeRecord) {
|
|
891
|
-
const prev = txn.get(id) ?? null;
|
|
892
|
-
if (prev && prev.typeName !== record.typeName) {
|
|
893
|
-
this.log?.warn?.(
|
|
894
|
-
"skipping put that changes typeName",
|
|
895
|
-
`${prev.typeName} -> ${record.typeName}`,
|
|
896
|
-
id,
|
|
897
|
-
"session:",
|
|
898
|
-
session.sessionId
|
|
899
|
-
);
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
const authorizePut = this.authorizerFor(record.typeName);
|
|
903
|
-
if (authorizePut) {
|
|
904
|
-
authorize = (prevRec, next) => {
|
|
905
|
-
const result2 = authorizePut(
|
|
906
|
-
prevRec ? {
|
|
907
|
-
session: { sessionId: session.sessionId, meta: session.meta },
|
|
908
|
-
type: "update",
|
|
909
|
-
prev: prevRec,
|
|
910
|
-
next
|
|
911
|
-
} : {
|
|
912
|
-
session: { sessionId: session.sessionId, meta: session.meta },
|
|
913
|
-
type: "create",
|
|
914
|
-
prev: null,
|
|
915
|
-
next
|
|
916
|
-
}
|
|
917
|
-
);
|
|
918
|
-
if (!result2) {
|
|
919
|
-
this.log?.warn?.(
|
|
920
|
-
"authorizer vetoed put",
|
|
921
|
-
record.typeName,
|
|
922
|
-
id,
|
|
923
|
-
"session:",
|
|
924
|
-
session.sessionId
|
|
925
|
-
);
|
|
926
|
-
return null;
|
|
927
|
-
}
|
|
928
|
-
return prevRec ? next : result2;
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
addDocument(txn, docChanges, id, record, authorize);
|
|
831
|
+
addDocument(txn, docChanges, id, op[1]);
|
|
933
832
|
break;
|
|
934
833
|
}
|
|
935
834
|
case import_diff.RecordOpType.Patch: {
|
|
936
|
-
|
|
937
|
-
if (!doc) continue;
|
|
938
|
-
if (!canWrite(doc.typeName)) continue;
|
|
939
|
-
const authorizePatch = session && this.authorizerFor(doc.typeName);
|
|
940
|
-
const authorize = authorizePatch ? (prev, next) => {
|
|
941
|
-
const result2 = authorizePatch({
|
|
942
|
-
session: { sessionId: session.sessionId, meta: session.meta },
|
|
943
|
-
type: "update",
|
|
944
|
-
prev,
|
|
945
|
-
next
|
|
946
|
-
});
|
|
947
|
-
if (!result2) {
|
|
948
|
-
this.log?.warn?.(
|
|
949
|
-
"authorizer vetoed patch",
|
|
950
|
-
doc.typeName,
|
|
951
|
-
id,
|
|
952
|
-
"session:",
|
|
953
|
-
session.sessionId
|
|
954
|
-
);
|
|
955
|
-
}
|
|
956
|
-
return result2;
|
|
957
|
-
} : void 0;
|
|
958
|
-
patchDocument(txn, docChanges, id, op[1], authorize);
|
|
835
|
+
patchDocument(txn, docChanges, id, op[1]);
|
|
959
836
|
break;
|
|
960
837
|
}
|
|
961
838
|
case import_diff.RecordOpType.Remove: {
|
|
@@ -963,23 +840,6 @@ class TLSyncRoom {
|
|
|
963
840
|
if (!doc) {
|
|
964
841
|
continue;
|
|
965
842
|
}
|
|
966
|
-
if (!canWrite(doc.typeName)) continue;
|
|
967
|
-
const authorizeRemove = session && this.authorizerFor(doc.typeName);
|
|
968
|
-
if (authorizeRemove && !authorizeRemove({
|
|
969
|
-
session: { sessionId: session.sessionId, meta: session.meta },
|
|
970
|
-
type: "delete",
|
|
971
|
-
prev: doc,
|
|
972
|
-
next: null
|
|
973
|
-
})) {
|
|
974
|
-
this.log?.warn?.(
|
|
975
|
-
"authorizer vetoed delete",
|
|
976
|
-
doc.typeName,
|
|
977
|
-
id,
|
|
978
|
-
"session:",
|
|
979
|
-
session.sessionId
|
|
980
|
-
);
|
|
981
|
-
continue;
|
|
982
|
-
}
|
|
983
843
|
txn.delete(id);
|
|
984
844
|
propagateOp(docChanges, id, op, doc, void 0);
|
|
985
845
|
break;
|
|
@@ -1054,16 +914,6 @@ class TLSyncRoom {
|
|
|
1054
914
|
this.onPresenceChange?.();
|
|
1055
915
|
});
|
|
1056
916
|
}
|
|
1057
|
-
if (result.docChanges.diffs && this.onCommittedChanges) {
|
|
1058
|
-
const diff = result.docChanges.diffs.diff;
|
|
1059
|
-
queueMicrotask(() => {
|
|
1060
|
-
try {
|
|
1061
|
-
this.onCommittedChanges?.({ diff, documentClock });
|
|
1062
|
-
} catch (e) {
|
|
1063
|
-
this.log?.error?.("onCommittedChanges threw", e);
|
|
1064
|
-
}
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
917
|
}
|
|
1068
918
|
/**
|
|
1069
919
|
* Handle the event when a client disconnects. Cleans up the session and
|