@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.
- package/dist/esm/ClientConnection.d.ts +1 -0
- package/dist/esm/ClientConnection.js.map +1 -1
- package/dist/esm/Presence.js.map +1 -1
- package/dist/esm/Profiles.js.map +1 -1
- package/dist/esm/ReplicaKeepaliveTimers.js.map +1 -1
- package/dist/esm/Server.d.ts +15 -11
- package/dist/esm/Server.js +29 -30
- package/dist/esm/Server.js.map +1 -1
- package/dist/esm/ServerLibrary.d.ts +10 -17
- package/dist/esm/ServerLibrary.js +151 -127
- package/dist/esm/ServerLibrary.js.map +1 -1
- package/dist/esm/ServerLibrary.test.js +369 -0
- package/dist/esm/ServerLibrary.test.js.map +1 -0
- package/dist/esm/TokenVerifier.js.map +1 -1
- package/dist/esm/files/FileStorage.js.map +1 -1
- package/dist/esm/storage/Storage.d.ts +67 -0
- package/dist/esm/storage/Storage.js +2 -0
- package/dist/esm/storage/Storage.js.map +1 -0
- package/dist/esm/storage/index.d.ts +2 -0
- package/dist/esm/storage/index.js +3 -0
- package/dist/esm/storage/index.js.map +1 -0
- package/dist/esm/storage/sql/SqlBaselines.d.ts +22 -0
- package/dist/esm/storage/sql/SqlBaselines.js +101 -0
- package/dist/esm/storage/sql/SqlBaselines.js.map +1 -0
- package/dist/esm/storage/sql/SqlFileMetadata.d.ts +18 -0
- package/dist/esm/storage/sql/SqlFileMetadata.js +69 -0
- package/dist/esm/storage/sql/SqlFileMetadata.js.map +1 -0
- package/dist/esm/storage/sql/SqlOperations.d.ts +19 -0
- package/dist/esm/storage/sql/SqlOperations.js +104 -0
- package/dist/esm/storage/sql/SqlOperations.js.map +1 -0
- package/dist/esm/storage/sql/SqlReplicas.d.ts +32 -0
- package/dist/esm/storage/sql/SqlReplicas.js +163 -0
- package/dist/esm/storage/sql/SqlReplicas.js.map +1 -0
- package/dist/esm/storage/sql/migrations/v1.d.ts +3 -0
- package/dist/esm/storage/sql/migrations/v1.js +59 -0
- package/dist/esm/storage/sql/migrations/v1.js.map +1 -0
- package/dist/esm/storage/sql/migrations.d.ts +5 -0
- package/dist/esm/storage/sql/migrations.js +3 -0
- package/dist/esm/storage/sql/migrations.js.map +1 -0
- package/dist/esm/storage/sql/sqlStorage.d.ts +5 -0
- package/dist/esm/storage/sql/sqlStorage.js +43 -0
- package/dist/esm/storage/sql/sqlStorage.js.map +1 -0
- package/dist/esm/storage/sql/tables.d.ts +40 -0
- package/dist/esm/storage/sql/tables.js +2 -0
- package/dist/esm/storage/sql/tables.js.map +1 -0
- package/dist/esm/types.d.ts +14 -5
- package/package.json +20 -14
- package/src/Server.ts +37 -43
- package/src/ServerLibrary.test.ts +447 -0
- package/src/ServerLibrary.ts +239 -163
- package/src/storage/Storage.ts +118 -0
- package/src/storage/index.ts +2 -0
- package/src/storage/sql/SqlBaselines.ts +141 -0
- package/src/storage/sql/SqlFileMetadata.ts +94 -0
- package/src/storage/sql/SqlOperations.ts +149 -0
- package/src/storage/sql/SqlReplicas.ts +234 -0
- package/src/storage/sql/migrations/v1.ts +64 -0
- package/src/storage/sql/migrations.ts +3 -0
- package/src/storage/sql/sqlStorage.ts +59 -0
- package/src/storage/sql/tables.ts +45 -0
- package/src/types.ts +15 -4
- package/bin/server.mjs +0 -115
- package/dist/cjs/Baselines.d.ts +0 -16
- package/dist/cjs/Baselines.js +0 -128
- package/dist/cjs/Baselines.js.map +0 -1
- package/dist/cjs/ClientConnection.d.ts +0 -20
- package/dist/cjs/ClientConnection.js +0 -139
- package/dist/cjs/ClientConnection.js.map +0 -1
- package/dist/cjs/MessageSender.d.ts +0 -5
- package/dist/cjs/MessageSender.js +0 -3
- package/dist/cjs/MessageSender.js.map +0 -1
- package/dist/cjs/OperationHistory.d.ts +0 -36
- package/dist/cjs/OperationHistory.js +0 -165
- package/dist/cjs/OperationHistory.js.map +0 -1
- package/dist/cjs/Presence.d.ts +0 -15
- package/dist/cjs/Presence.js +0 -60
- package/dist/cjs/Presence.js.map +0 -1
- package/dist/cjs/Profiles.d.ts +0 -10
- package/dist/cjs/Profiles.js +0 -20
- package/dist/cjs/Profiles.js.map +0 -1
- package/dist/cjs/ReplicaKeepaliveTimers.d.ts +0 -17
- package/dist/cjs/ReplicaKeepaliveTimers.js +0 -31
- package/dist/cjs/ReplicaKeepaliveTimers.js.map +0 -1
- package/dist/cjs/Replicas.d.ts +0 -25
- package/dist/cjs/Replicas.js +0 -196
- package/dist/cjs/Replicas.js.map +0 -1
- package/dist/cjs/Server.d.ts +0 -167
- package/dist/cjs/Server.js +0 -552
- package/dist/cjs/Server.js.map +0 -1
- package/dist/cjs/ServerLibrary.d.ts +0 -65
- package/dist/cjs/ServerLibrary.js +0 -505
- package/dist/cjs/ServerLibrary.js.map +0 -1
- package/dist/cjs/TokenProvider.d.ts +0 -19
- package/dist/cjs/TokenProvider.js +0 -27
- package/dist/cjs/TokenProvider.js.map +0 -1
- package/dist/cjs/TokenVerifier.d.ts +0 -16
- package/dist/cjs/TokenVerifier.js +0 -40
- package/dist/cjs/TokenVerifier.js.map +0 -1
- package/dist/cjs/files/FileMetadata.d.ts +0 -28
- package/dist/cjs/files/FileMetadata.js +0 -69
- package/dist/cjs/files/FileMetadata.js.map +0 -1
- package/dist/cjs/files/FileStorage.d.ts +0 -35
- package/dist/cjs/files/FileStorage.js +0 -72
- package/dist/cjs/files/FileStorage.js.map +0 -1
- package/dist/cjs/index.d.ts +0 -10
- package/dist/cjs/index.js +0 -12
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/migrations/v0.d.ts +0 -1
- package/dist/cjs/migrations/v0.js +0 -30
- package/dist/cjs/migrations/v0.js.map +0 -1
- package/dist/cjs/migrations/v1.d.ts +0 -1
- package/dist/cjs/migrations/v1.js +0 -32
- package/dist/cjs/migrations/v1.js.map +0 -1
- package/dist/cjs/migrations/v2.d.ts +0 -1
- package/dist/cjs/migrations/v2.js +0 -29
- package/dist/cjs/migrations/v2.js.map +0 -1
- package/dist/cjs/migrations/v3.d.ts +0 -1
- package/dist/cjs/migrations/v3.js +0 -15
- package/dist/cjs/migrations/v3.js.map +0 -1
- package/dist/cjs/migrations/v4.d.ts +0 -1
- package/dist/cjs/migrations/v4.js +0 -9
- package/dist/cjs/migrations/v4.js.map +0 -1
- package/dist/cjs/migrations/v5.d.ts +0 -2
- package/dist/cjs/migrations/v5.js +0 -37
- package/dist/cjs/migrations/v5.js.map +0 -1
- package/dist/cjs/migrations/v6.d.ts +0 -1
- package/dist/cjs/migrations/v6.js +0 -8
- package/dist/cjs/migrations/v6.js.map +0 -1
- package/dist/cjs/migrations/v7.d.ts +0 -1
- package/dist/cjs/migrations/v7.js +0 -10
- package/dist/cjs/migrations/v7.js.map +0 -1
- package/dist/cjs/migrations.d.ts +0 -6
- package/dist/cjs/migrations.js +0 -72
- package/dist/cjs/migrations.js.map +0 -1
- package/dist/cjs/migrations.test.js +0 -121
- package/dist/cjs/migrations.test.js.map +0 -1
- package/dist/cjs/types.d.ts +0 -38
- package/dist/cjs/types.js +0 -3
- package/dist/cjs/types.js.map +0 -1
- package/dist/esm/Baselines.d.ts +0 -16
- package/dist/esm/Baselines.js +0 -124
- package/dist/esm/Baselines.js.map +0 -1
- package/dist/esm/OperationHistory.d.ts +0 -36
- package/dist/esm/OperationHistory.js +0 -161
- package/dist/esm/OperationHistory.js.map +0 -1
- package/dist/esm/Replicas.d.ts +0 -25
- package/dist/esm/Replicas.js +0 -192
- package/dist/esm/Replicas.js.map +0 -1
- package/dist/esm/files/FileMetadata.d.ts +0 -28
- package/dist/esm/files/FileMetadata.js +0 -65
- package/dist/esm/files/FileMetadata.js.map +0 -1
- package/dist/esm/migrations/v0.d.ts +0 -1
- package/dist/esm/migrations/v0.js +0 -27
- package/dist/esm/migrations/v0.js.map +0 -1
- package/dist/esm/migrations/v1.d.ts +0 -1
- package/dist/esm/migrations/v1.js +0 -29
- package/dist/esm/migrations/v1.js.map +0 -1
- package/dist/esm/migrations/v2.d.ts +0 -1
- package/dist/esm/migrations/v2.js +0 -26
- package/dist/esm/migrations/v2.js.map +0 -1
- package/dist/esm/migrations/v3.d.ts +0 -1
- package/dist/esm/migrations/v3.js +0 -12
- package/dist/esm/migrations/v3.js.map +0 -1
- package/dist/esm/migrations/v4.d.ts +0 -1
- package/dist/esm/migrations/v4.js +0 -6
- package/dist/esm/migrations/v4.js.map +0 -1
- package/dist/esm/migrations/v5.d.ts +0 -2
- package/dist/esm/migrations/v5.js +0 -33
- package/dist/esm/migrations/v5.js.map +0 -1
- package/dist/esm/migrations/v6.d.ts +0 -1
- package/dist/esm/migrations/v6.js +0 -5
- package/dist/esm/migrations/v6.js.map +0 -1
- package/dist/esm/migrations/v7.d.ts +0 -1
- package/dist/esm/migrations/v7.js +0 -7
- package/dist/esm/migrations/v7.js.map +0 -1
- package/dist/esm/migrations.d.ts +0 -6
- package/dist/esm/migrations.js +0 -45
- package/dist/esm/migrations.js.map +0 -1
- package/dist/esm/migrations.test.d.ts +0 -1
- package/dist/esm/migrations.test.js +0 -116
- package/dist/esm/migrations.test.js.map +0 -1
- package/dist/tsconfig-cjs.tsbuildinfo +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/Baselines.ts +0 -198
- package/src/OperationHistory.ts +0 -232
- package/src/Replicas.ts +0 -274
- package/src/files/FileMetadata.ts +0 -112
- package/src/migrations/v0.ts +0 -26
- package/src/migrations/v1.ts +0 -28
- package/src/migrations/v2.ts +0 -26
- package/src/migrations/v3.ts +0 -12
- package/src/migrations/v4.ts +0 -6
- package/src/migrations/v5.ts +0 -49
- package/src/migrations/v6.ts +0 -4
- package/src/migrations/v7.ts +0 -6
- package/src/migrations.test.ts +0 -133
- package/src/migrations.ts +0 -61
- /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
|
+
});
|