@verdant-web/server 2.1.3 → 3.0.0

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.
Files changed (198) hide show
  1. package/dist/esm/ClientConnection.d.ts +1 -0
  2. package/dist/esm/ClientConnection.js.map +1 -1
  3. package/dist/esm/Presence.js.map +1 -1
  4. package/dist/esm/Profiles.js.map +1 -1
  5. package/dist/esm/ReplicaKeepaliveTimers.js.map +1 -1
  6. package/dist/esm/Server.d.ts +15 -11
  7. package/dist/esm/Server.js +29 -30
  8. package/dist/esm/Server.js.map +1 -1
  9. package/dist/esm/ServerLibrary.d.ts +10 -17
  10. package/dist/esm/ServerLibrary.js +151 -127
  11. package/dist/esm/ServerLibrary.js.map +1 -1
  12. package/dist/esm/ServerLibrary.test.js +369 -0
  13. package/dist/esm/ServerLibrary.test.js.map +1 -0
  14. package/dist/esm/TokenVerifier.js.map +1 -1
  15. package/dist/esm/files/FileStorage.js.map +1 -1
  16. package/dist/esm/storage/Storage.d.ts +67 -0
  17. package/dist/esm/storage/Storage.js +2 -0
  18. package/dist/esm/storage/Storage.js.map +1 -0
  19. package/dist/esm/storage/index.d.ts +2 -0
  20. package/dist/esm/storage/index.js +3 -0
  21. package/dist/esm/storage/index.js.map +1 -0
  22. package/dist/esm/storage/sql/SqlBaselines.d.ts +22 -0
  23. package/dist/esm/storage/sql/SqlBaselines.js +101 -0
  24. package/dist/esm/storage/sql/SqlBaselines.js.map +1 -0
  25. package/dist/esm/storage/sql/SqlFileMetadata.d.ts +18 -0
  26. package/dist/esm/storage/sql/SqlFileMetadata.js +69 -0
  27. package/dist/esm/storage/sql/SqlFileMetadata.js.map +1 -0
  28. package/dist/esm/storage/sql/SqlOperations.d.ts +19 -0
  29. package/dist/esm/storage/sql/SqlOperations.js +104 -0
  30. package/dist/esm/storage/sql/SqlOperations.js.map +1 -0
  31. package/dist/esm/storage/sql/SqlReplicas.d.ts +32 -0
  32. package/dist/esm/storage/sql/SqlReplicas.js +163 -0
  33. package/dist/esm/storage/sql/SqlReplicas.js.map +1 -0
  34. package/dist/esm/storage/sql/migrations/v1.d.ts +3 -0
  35. package/dist/esm/storage/sql/migrations/v1.js +59 -0
  36. package/dist/esm/storage/sql/migrations/v1.js.map +1 -0
  37. package/dist/esm/storage/sql/migrations.d.ts +5 -0
  38. package/dist/esm/storage/sql/migrations.js +3 -0
  39. package/dist/esm/storage/sql/migrations.js.map +1 -0
  40. package/dist/esm/storage/sql/sqlStorage.d.ts +5 -0
  41. package/dist/esm/storage/sql/sqlStorage.js +43 -0
  42. package/dist/esm/storage/sql/sqlStorage.js.map +1 -0
  43. package/dist/esm/storage/sql/tables.d.ts +40 -0
  44. package/dist/esm/storage/sql/tables.js +2 -0
  45. package/dist/esm/storage/sql/tables.js.map +1 -0
  46. package/dist/esm/types.d.ts +14 -5
  47. package/package.json +20 -14
  48. package/src/Server.ts +37 -43
  49. package/src/ServerLibrary.test.ts +447 -0
  50. package/src/ServerLibrary.ts +239 -163
  51. package/src/storage/Storage.ts +118 -0
  52. package/src/storage/index.ts +2 -0
  53. package/src/storage/sql/SqlBaselines.ts +141 -0
  54. package/src/storage/sql/SqlFileMetadata.ts +94 -0
  55. package/src/storage/sql/SqlOperations.ts +149 -0
  56. package/src/storage/sql/SqlReplicas.ts +234 -0
  57. package/src/storage/sql/migrations/v1.ts +64 -0
  58. package/src/storage/sql/migrations.ts +3 -0
  59. package/src/storage/sql/sqlStorage.ts +59 -0
  60. package/src/storage/sql/tables.ts +45 -0
  61. package/src/types.ts +15 -4
  62. package/bin/server.mjs +0 -115
  63. package/dist/cjs/Baselines.d.ts +0 -16
  64. package/dist/cjs/Baselines.js +0 -128
  65. package/dist/cjs/Baselines.js.map +0 -1
  66. package/dist/cjs/ClientConnection.d.ts +0 -20
  67. package/dist/cjs/ClientConnection.js +0 -139
  68. package/dist/cjs/ClientConnection.js.map +0 -1
  69. package/dist/cjs/MessageSender.d.ts +0 -5
  70. package/dist/cjs/MessageSender.js +0 -3
  71. package/dist/cjs/MessageSender.js.map +0 -1
  72. package/dist/cjs/OperationHistory.d.ts +0 -36
  73. package/dist/cjs/OperationHistory.js +0 -165
  74. package/dist/cjs/OperationHistory.js.map +0 -1
  75. package/dist/cjs/Presence.d.ts +0 -15
  76. package/dist/cjs/Presence.js +0 -60
  77. package/dist/cjs/Presence.js.map +0 -1
  78. package/dist/cjs/Profiles.d.ts +0 -10
  79. package/dist/cjs/Profiles.js +0 -20
  80. package/dist/cjs/Profiles.js.map +0 -1
  81. package/dist/cjs/ReplicaKeepaliveTimers.d.ts +0 -17
  82. package/dist/cjs/ReplicaKeepaliveTimers.js +0 -31
  83. package/dist/cjs/ReplicaKeepaliveTimers.js.map +0 -1
  84. package/dist/cjs/Replicas.d.ts +0 -25
  85. package/dist/cjs/Replicas.js +0 -196
  86. package/dist/cjs/Replicas.js.map +0 -1
  87. package/dist/cjs/Server.d.ts +0 -167
  88. package/dist/cjs/Server.js +0 -552
  89. package/dist/cjs/Server.js.map +0 -1
  90. package/dist/cjs/ServerLibrary.d.ts +0 -65
  91. package/dist/cjs/ServerLibrary.js +0 -505
  92. package/dist/cjs/ServerLibrary.js.map +0 -1
  93. package/dist/cjs/TokenProvider.d.ts +0 -19
  94. package/dist/cjs/TokenProvider.js +0 -27
  95. package/dist/cjs/TokenProvider.js.map +0 -1
  96. package/dist/cjs/TokenVerifier.d.ts +0 -16
  97. package/dist/cjs/TokenVerifier.js +0 -40
  98. package/dist/cjs/TokenVerifier.js.map +0 -1
  99. package/dist/cjs/files/FileMetadata.d.ts +0 -28
  100. package/dist/cjs/files/FileMetadata.js +0 -69
  101. package/dist/cjs/files/FileMetadata.js.map +0 -1
  102. package/dist/cjs/files/FileStorage.d.ts +0 -35
  103. package/dist/cjs/files/FileStorage.js +0 -72
  104. package/dist/cjs/files/FileStorage.js.map +0 -1
  105. package/dist/cjs/index.d.ts +0 -10
  106. package/dist/cjs/index.js +0 -12
  107. package/dist/cjs/index.js.map +0 -1
  108. package/dist/cjs/migrations/v0.d.ts +0 -1
  109. package/dist/cjs/migrations/v0.js +0 -30
  110. package/dist/cjs/migrations/v0.js.map +0 -1
  111. package/dist/cjs/migrations/v1.d.ts +0 -1
  112. package/dist/cjs/migrations/v1.js +0 -32
  113. package/dist/cjs/migrations/v1.js.map +0 -1
  114. package/dist/cjs/migrations/v2.d.ts +0 -1
  115. package/dist/cjs/migrations/v2.js +0 -29
  116. package/dist/cjs/migrations/v2.js.map +0 -1
  117. package/dist/cjs/migrations/v3.d.ts +0 -1
  118. package/dist/cjs/migrations/v3.js +0 -15
  119. package/dist/cjs/migrations/v3.js.map +0 -1
  120. package/dist/cjs/migrations/v4.d.ts +0 -1
  121. package/dist/cjs/migrations/v4.js +0 -9
  122. package/dist/cjs/migrations/v4.js.map +0 -1
  123. package/dist/cjs/migrations/v5.d.ts +0 -2
  124. package/dist/cjs/migrations/v5.js +0 -37
  125. package/dist/cjs/migrations/v5.js.map +0 -1
  126. package/dist/cjs/migrations/v6.d.ts +0 -1
  127. package/dist/cjs/migrations/v6.js +0 -8
  128. package/dist/cjs/migrations/v6.js.map +0 -1
  129. package/dist/cjs/migrations/v7.d.ts +0 -1
  130. package/dist/cjs/migrations/v7.js +0 -10
  131. package/dist/cjs/migrations/v7.js.map +0 -1
  132. package/dist/cjs/migrations.d.ts +0 -6
  133. package/dist/cjs/migrations.js +0 -72
  134. package/dist/cjs/migrations.js.map +0 -1
  135. package/dist/cjs/migrations.test.js +0 -121
  136. package/dist/cjs/migrations.test.js.map +0 -1
  137. package/dist/cjs/types.d.ts +0 -38
  138. package/dist/cjs/types.js +0 -3
  139. package/dist/cjs/types.js.map +0 -1
  140. package/dist/esm/Baselines.d.ts +0 -16
  141. package/dist/esm/Baselines.js +0 -124
  142. package/dist/esm/Baselines.js.map +0 -1
  143. package/dist/esm/OperationHistory.d.ts +0 -36
  144. package/dist/esm/OperationHistory.js +0 -161
  145. package/dist/esm/OperationHistory.js.map +0 -1
  146. package/dist/esm/Replicas.d.ts +0 -25
  147. package/dist/esm/Replicas.js +0 -192
  148. package/dist/esm/Replicas.js.map +0 -1
  149. package/dist/esm/files/FileMetadata.d.ts +0 -28
  150. package/dist/esm/files/FileMetadata.js +0 -65
  151. package/dist/esm/files/FileMetadata.js.map +0 -1
  152. package/dist/esm/migrations/v0.d.ts +0 -1
  153. package/dist/esm/migrations/v0.js +0 -27
  154. package/dist/esm/migrations/v0.js.map +0 -1
  155. package/dist/esm/migrations/v1.d.ts +0 -1
  156. package/dist/esm/migrations/v1.js +0 -29
  157. package/dist/esm/migrations/v1.js.map +0 -1
  158. package/dist/esm/migrations/v2.d.ts +0 -1
  159. package/dist/esm/migrations/v2.js +0 -26
  160. package/dist/esm/migrations/v2.js.map +0 -1
  161. package/dist/esm/migrations/v3.d.ts +0 -1
  162. package/dist/esm/migrations/v3.js +0 -12
  163. package/dist/esm/migrations/v3.js.map +0 -1
  164. package/dist/esm/migrations/v4.d.ts +0 -1
  165. package/dist/esm/migrations/v4.js +0 -6
  166. package/dist/esm/migrations/v4.js.map +0 -1
  167. package/dist/esm/migrations/v5.d.ts +0 -2
  168. package/dist/esm/migrations/v5.js +0 -33
  169. package/dist/esm/migrations/v5.js.map +0 -1
  170. package/dist/esm/migrations/v6.d.ts +0 -1
  171. package/dist/esm/migrations/v6.js +0 -5
  172. package/dist/esm/migrations/v6.js.map +0 -1
  173. package/dist/esm/migrations/v7.d.ts +0 -1
  174. package/dist/esm/migrations/v7.js +0 -7
  175. package/dist/esm/migrations/v7.js.map +0 -1
  176. package/dist/esm/migrations.d.ts +0 -6
  177. package/dist/esm/migrations.js +0 -45
  178. package/dist/esm/migrations.js.map +0 -1
  179. package/dist/esm/migrations.test.d.ts +0 -1
  180. package/dist/esm/migrations.test.js +0 -116
  181. package/dist/esm/migrations.test.js.map +0 -1
  182. package/dist/tsconfig-cjs.tsbuildinfo +0 -1
  183. package/dist/tsconfig.tsbuildinfo +0 -1
  184. package/src/Baselines.ts +0 -198
  185. package/src/OperationHistory.ts +0 -232
  186. package/src/Replicas.ts +0 -274
  187. package/src/files/FileMetadata.ts +0 -112
  188. package/src/migrations/v0.ts +0 -26
  189. package/src/migrations/v1.ts +0 -28
  190. package/src/migrations/v2.ts +0 -26
  191. package/src/migrations/v3.ts +0 -12
  192. package/src/migrations/v4.ts +0 -6
  193. package/src/migrations/v5.ts +0 -49
  194. package/src/migrations/v6.ts +0 -4
  195. package/src/migrations/v7.ts +0 -6
  196. package/src/migrations.test.ts +0 -133
  197. package/src/migrations.ts +0 -61
  198. /package/dist/{cjs/migrations.test.d.ts → esm/ServerLibrary.test.d.ts} +0 -0
@@ -0,0 +1,447 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { UserProfileLoader } from './Profiles.js';
3
+ import { sqlStorage } from './storage/index.js';
4
+ import { ServerLibrary } from './ServerLibrary.js';
5
+ import {
6
+ ClientMessage,
7
+ DocumentBaseline,
8
+ HybridLogicalClockTimestampProvider,
9
+ Operation,
10
+ ReplicaType,
11
+ ServerMessage,
12
+ } from '@verdant-web/common';
13
+ import { TokenInfo } from './TokenVerifier.js';
14
+ import { MessageSender } from './MessageSender.js';
15
+
16
+ const profiles = new UserProfileLoader<{ id: string; name: string }>({
17
+ get: async (userId: string) => {
18
+ return {
19
+ id: userId,
20
+ name: 'Alice',
21
+ };
22
+ },
23
+ });
24
+
25
+ const baseOptions = () => ({
26
+ profiles,
27
+ storage: sqlStorage({
28
+ databaseFile: ':memory:',
29
+ })({
30
+ fileDeleteExpirationDays: 7,
31
+ replicaTruancyMinutes: 60 * 24,
32
+ }),
33
+ });
34
+
35
+ let time = 0;
36
+ const now = () => `faketime-${time++}`;
37
+
38
+ type TestReplica = {
39
+ key: string;
40
+ replicaId: string;
41
+ messages: ServerMessage[];
42
+ tokenInfo: TokenInfo;
43
+ };
44
+ function createTestReplica(id: string): TestReplica {
45
+ return {
46
+ key: `clientKey-${id}`,
47
+ replicaId: `replica-${id}`,
48
+ messages: [],
49
+ tokenInfo: {
50
+ libraryId: 'library-1',
51
+ syncEndpoint: 'http://localhost:3000/sync',
52
+ token: 'fake',
53
+ type: ReplicaType.Realtime,
54
+ userId: `user-${id}`,
55
+ },
56
+ };
57
+ }
58
+
59
+ // collects all received operations for a replica
60
+ function getAllReceivedReplicaOperations(replica: TestReplica) {
61
+ const operations: Operation[] = [];
62
+ for (const message of replica.messages) {
63
+ switch (message.type) {
64
+ case 'op-re':
65
+ case 'sync-resp':
66
+ operations.push(...message.operations);
67
+ break;
68
+ }
69
+ }
70
+ return operations;
71
+ }
72
+
73
+ describe('ServerLibrary', () => {
74
+ describe('sync handling', () => {
75
+ it('should initialize a new library from a replica', async () => {
76
+ const sender = {
77
+ broadcast: vi.fn(),
78
+ send: vi.fn(),
79
+ };
80
+
81
+ const tokenInfo: TokenInfo = {
82
+ libraryId: 'library-1',
83
+ syncEndpoint: 'http://localhost:3000/sync',
84
+ token: 'fake',
85
+ type: ReplicaType.Realtime,
86
+ userId: 'user-1',
87
+ };
88
+
89
+ const library = new ServerLibrary({
90
+ ...baseOptions(),
91
+ sender,
92
+ });
93
+
94
+ const messages: ClientMessage[] = [
95
+ {
96
+ type: 'sync',
97
+ baselines: [
98
+ {
99
+ oid: 'notes/1',
100
+ timestamp: now(),
101
+ snapshot: {
102
+ title: 'My Note',
103
+ content: 'This is my note.',
104
+ },
105
+ },
106
+ ],
107
+ operations: [
108
+ {
109
+ oid: 'notes/1',
110
+ timestamp: now(),
111
+ data: {
112
+ op: 'set',
113
+ name: 'title',
114
+ value: 'My Note!',
115
+ },
116
+ },
117
+ {
118
+ oid: 'notes/2',
119
+ timestamp: now(),
120
+ data: {
121
+ op: 'initialize',
122
+ value: {
123
+ title: 'Another Note',
124
+ content: 'This is another note.',
125
+ },
126
+ },
127
+ },
128
+ ],
129
+ replicaId: 'replica-1',
130
+ schemaVersion: 1,
131
+ since: null,
132
+ timestamp: now(),
133
+ },
134
+ ];
135
+
136
+ // purposefully do not await in order - these all come at the same time
137
+ await Promise.all(
138
+ messages.map((m) => library.receive(m, 'clientKey-1', tokenInfo)),
139
+ );
140
+
141
+ // expect sender.broadcast for the operations, and a reply with sync response
142
+ expect(sender.broadcast).toHaveBeenCalledWith(
143
+ 'library-1',
144
+ {
145
+ type: 'op-re',
146
+ operations: [
147
+ {
148
+ oid: 'notes/1',
149
+ timestamp: `faketime-1`,
150
+ data: {
151
+ op: 'set',
152
+ name: 'title',
153
+ value: 'My Note!',
154
+ },
155
+ },
156
+ {
157
+ oid: 'notes/2',
158
+ timestamp: `faketime-2`,
159
+ data: {
160
+ op: 'initialize',
161
+ value: {
162
+ title: 'Another Note',
163
+ content: 'This is another note.',
164
+ },
165
+ },
166
+ },
167
+ ],
168
+ replicaId: 'replica-1',
169
+ globalAckTimestamp: undefined,
170
+ },
171
+ ['clientKey-1'],
172
+ );
173
+
174
+ expect(sender.send).toHaveBeenCalledWith('library-1', 'clientKey-1', {
175
+ type: 'sync-resp',
176
+ operations: [],
177
+ baselines: [],
178
+ ackThisNonce: undefined,
179
+ ackedTimestamp: 'faketime-3',
180
+ globalAckTimestamp: undefined,
181
+ overwriteLocalData: false,
182
+ peerPresence: {},
183
+ });
184
+ });
185
+
186
+ it('should sync up to latest data, while also rebroadcasting concurrent new operations', async () => {
187
+ // this test setup is more involved - a fake sender will distribute messages to our
188
+ // fake replicas, and we will verify that the replicas end up with the right data.
189
+ const replicaA = createTestReplica('a');
190
+ const replicaB = createTestReplica('b');
191
+ const replicas = [replicaA, replicaB];
192
+
193
+ const sender: MessageSender = {
194
+ broadcast: (
195
+ libraryId: string,
196
+ message: ServerMessage,
197
+ omitKeys?: string[],
198
+ ) => {
199
+ replicas.forEach((replica) => {
200
+ if (!omitKeys?.includes(replica.key)) {
201
+ replica.messages.push(message);
202
+ }
203
+ });
204
+ },
205
+ send: (
206
+ libraryId: string,
207
+ clientKey: string,
208
+ message: ServerMessage,
209
+ ) => {
210
+ const replica = replicas.find((r) => r.key === clientKey);
211
+ if (replica) {
212
+ replica.messages.push(message);
213
+ }
214
+ },
215
+ };
216
+
217
+ const library = new ServerLibrary({
218
+ ...baseOptions(),
219
+ sender,
220
+ });
221
+
222
+ // replica A sends a sync message to initialize library
223
+ const syncMessageA: ClientMessage = {
224
+ type: 'sync',
225
+ baselines: [
226
+ {
227
+ oid: 'notes/1',
228
+ timestamp: now(),
229
+ snapshot: {
230
+ title: 'My Note',
231
+ content: 'This is my note.',
232
+ },
233
+ },
234
+ ],
235
+ operations: [
236
+ {
237
+ oid: 'notes/1',
238
+ timestamp: now(),
239
+ data: {
240
+ op: 'set',
241
+ name: 'title',
242
+ value: 'My Note!',
243
+ },
244
+ },
245
+ ],
246
+ replicaId: 'replica-a',
247
+ schemaVersion: 1,
248
+ since: null,
249
+ timestamp: now(),
250
+ };
251
+ await library.receive(syncMessageA, replicaA.key, replicaA.tokenInfo);
252
+
253
+ // concurrently, replica B syncs, and A sends more operations
254
+ const syncMessageB: ClientMessage = {
255
+ type: 'sync',
256
+ baselines: [],
257
+ operations: [],
258
+ replicaId: 'replica-b',
259
+ schemaVersion: 1,
260
+ since: null,
261
+ timestamp: now(),
262
+ resyncAll: true,
263
+ };
264
+ const opMessageA: ClientMessage = {
265
+ type: 'op',
266
+ replicaId: 'replica-a',
267
+ operations: [
268
+ {
269
+ oid: 'notes/1',
270
+ timestamp: now(),
271
+ data: {
272
+ op: 'set',
273
+ name: 'content',
274
+ value: 'This is my note!',
275
+ },
276
+ },
277
+ ],
278
+ timestamp: now(),
279
+ };
280
+ await Promise.all([
281
+ library.receive(syncMessageB, replicaB.key, replicaB.tokenInfo),
282
+ library.receive(opMessageA, replicaA.key, replicaA.tokenInfo),
283
+ ]);
284
+
285
+ // there are two valid scenario outcomes here:
286
+ // 1. A's operation is resolved before B's sync reads operations, and so it's
287
+ // included in B's sync response
288
+ // 2. B's sync reads operations before A's operation is resolved, and so it's
289
+ // not included in the sync, but is rebroadcast to B later
290
+ //
291
+ // Either way we should end up with that operation on B.
292
+ const operationsSentToB = getAllReceivedReplicaOperations(replicaB);
293
+ expect(operationsSentToB).toContainEqual({
294
+ oid: 'notes/1',
295
+ timestamp: 'faketime-5',
296
+ data: {
297
+ op: 'set',
298
+ name: 'title',
299
+ value: 'My Note!',
300
+ },
301
+ });
302
+ expect(operationsSentToB).toContainEqual({
303
+ oid: 'notes/1',
304
+ timestamp: 'faketime-8',
305
+ data: {
306
+ op: 'set',
307
+ name: 'content',
308
+ value: 'This is my note!',
309
+ },
310
+ });
311
+ });
312
+
313
+ it('should reject syncs with data from read replicas', async () => {
314
+ const sender = {
315
+ broadcast: vi.fn(),
316
+ send: vi.fn(),
317
+ };
318
+
319
+ const tokenInfo: TokenInfo = {
320
+ libraryId: 'library-1',
321
+ syncEndpoint: 'http://localhost:3000/sync',
322
+ token: 'fake',
323
+ type: ReplicaType.ReadOnlyPull,
324
+ userId: 'user-1',
325
+ };
326
+
327
+ const library = new ServerLibrary({
328
+ ...baseOptions(),
329
+ sender,
330
+ });
331
+
332
+ const messages: ClientMessage[] = [
333
+ {
334
+ type: 'sync',
335
+ baselines: [
336
+ {
337
+ oid: 'notes/1',
338
+ timestamp: now(),
339
+ snapshot: {
340
+ title: 'My Note',
341
+ content: 'This is my note.',
342
+ },
343
+ },
344
+ ],
345
+ operations: [
346
+ {
347
+ oid: 'notes/1',
348
+ timestamp: now(),
349
+ data: {
350
+ op: 'set',
351
+ name: 'title',
352
+ value: 'My Note!',
353
+ },
354
+ },
355
+ {
356
+ oid: 'notes/2',
357
+ timestamp: now(),
358
+ data: {
359
+ op: 'initialize',
360
+ value: {
361
+ title: 'Another Note',
362
+ content: 'This is another note.',
363
+ },
364
+ },
365
+ },
366
+ ],
367
+ replicaId: 'replica-1',
368
+ schemaVersion: 1,
369
+ since: null,
370
+ timestamp: now(),
371
+ },
372
+ ];
373
+
374
+ // purposefully do not await in order - these all come at the same time
375
+ await Promise.all(
376
+ messages.map((m) => library.receive(m, 'clientKey-1', tokenInfo)),
377
+ );
378
+
379
+ expect(sender.send).toHaveBeenCalledOnce();
380
+ expect(sender.send).toHaveBeenCalledWith('library-1', 'clientKey-1', {
381
+ type: 'forbidden',
382
+ });
383
+ });
384
+
385
+ it('should reject messages from a replica claimed by a user other than the one recorded on the server', async () => {
386
+ const sender = {
387
+ broadcast: vi.fn(),
388
+ send: vi.fn(),
389
+ };
390
+
391
+ const tokenInfo: TokenInfo = {
392
+ libraryId: 'library-1',
393
+ syncEndpoint: 'http://localhost:3000/sync',
394
+ token: 'fake',
395
+ type: ReplicaType.Realtime,
396
+ userId: 'user-1',
397
+ };
398
+
399
+ const library = new ServerLibrary({
400
+ ...baseOptions(),
401
+ sender,
402
+ });
403
+
404
+ // purposefully do not await in order - these all come at the same time
405
+ await library.receive(
406
+ {
407
+ type: 'sync',
408
+ baselines: [],
409
+ operations: [],
410
+ replicaId: 'replica-1',
411
+ schemaVersion: 1,
412
+ since: null,
413
+ timestamp: now(),
414
+ },
415
+ 'clientKey-1',
416
+ tokenInfo,
417
+ );
418
+ sender.send.mockReset();
419
+
420
+ // now try to send a message from a different user with the same replica
421
+ const tokenInfo2: TokenInfo = {
422
+ libraryId: 'library-1',
423
+ syncEndpoint: 'http://localhost:3000/sync',
424
+ token: 'fake',
425
+ type: ReplicaType.Realtime,
426
+ userId: 'user-2',
427
+ };
428
+
429
+ const message: ClientMessage = {
430
+ type: 'sync',
431
+ baselines: [],
432
+ operations: [],
433
+ replicaId: 'replica-1',
434
+ schemaVersion: 1,
435
+ since: null,
436
+ timestamp: now(),
437
+ };
438
+
439
+ await library.receive(message, 'clientKey-1', tokenInfo2);
440
+
441
+ expect(sender.send).toHaveBeenCalledOnce();
442
+ expect(sender.send).toHaveBeenCalledWith('library-1', 'clientKey-1', {
443
+ type: 'forbidden',
444
+ });
445
+ });
446
+ });
447
+ });