@verdant-web/server 4.1.1 → 4.2.1
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/adapters/node/websocketHandler.js +2 -2
- package/dist/esm/adapters/node/websocketHandler.js.map +1 -1
- package/dist/esm/connections/ClientConnection.d.ts +18 -6
- package/dist/esm/connections/ClientConnection.js +62 -17
- package/dist/esm/connections/ClientConnection.js.map +1 -1
- package/dist/esm/connections/Presence.d.ts +29 -12
- package/dist/esm/connections/Presence.js +54 -44
- package/dist/esm/connections/Presence.js.map +1 -1
- package/dist/esm/internals.d.ts +1 -0
- package/dist/esm/internals.js.map +1 -1
- package/dist/esm/libraries/Library.d.ts +6 -7
- package/dist/esm/libraries/Library.js +22 -13
- package/dist/esm/libraries/Library.js.map +1 -1
- package/dist/esm/libraries/Library.test.js +50 -37
- package/dist/esm/libraries/Library.test.js.map +1 -1
- package/dist/esm/libraries/singleNode.js +1 -2
- package/dist/esm/libraries/singleNode.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/node/websocketHandler.ts +2 -2
- package/src/connections/ClientConnection.ts +75 -18
- package/src/connections/Presence.ts +86 -50
- package/src/internals.ts +1 -0
- package/src/libraries/Library.test.ts +79 -68
- package/src/libraries/Library.ts +26 -29
- package/src/libraries/singleNode.ts +1 -2
|
@@ -7,89 +7,125 @@ import {
|
|
|
7
7
|
import { UserProfiles } from '../Profiles.js';
|
|
8
8
|
|
|
9
9
|
export type PresenceEvents = {
|
|
10
|
-
lost: (
|
|
10
|
+
lost: (userId: string) => void;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
* Stores client presence in-memory for connected
|
|
15
|
-
* clients
|
|
16
|
-
*/
|
|
17
|
-
export class Presence extends EventSubscriber<PresenceEvents> {
|
|
18
|
-
private presences: Record<string, UserInfo<any, any>> = {};
|
|
19
|
-
private connectionToUser: Record<string, string> = {};
|
|
20
|
-
private userToConnection: Record<string, Set<string>> = {};
|
|
21
|
-
private keepalives = new Map<string, NodeJS.Timeout>();
|
|
13
|
+
export type PresenceStorageItem = UserInfo<any, any> & { expiresAt: number };
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
export interface PresenceStorage {
|
|
16
|
+
set: (
|
|
17
|
+
userId: string,
|
|
18
|
+
userInfo: UserInfoUpdate,
|
|
19
|
+
profile: any,
|
|
20
|
+
expiresAt: number,
|
|
21
|
+
) => Promise<UserInfo<any, any>>;
|
|
22
|
+
setExpiresAt: (userId: string, expiresAt: number) => Promise<void>;
|
|
23
|
+
get: (
|
|
24
|
+
userId: string,
|
|
25
|
+
time: number,
|
|
26
|
+
) => Promise<UserInfo<any, any> | undefined>;
|
|
27
|
+
remove: (userId: string) => Promise<void>;
|
|
28
|
+
all: () => Promise<Record<string, UserInfo<any, any>>>;
|
|
29
|
+
clear: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_EXPIRATION_TIME = 60 * 1000;
|
|
33
|
+
|
|
34
|
+
export class PresenceMemoryStorage implements PresenceStorage {
|
|
35
|
+
private presences: Record<string, PresenceStorageItem> = {};
|
|
26
36
|
|
|
27
37
|
set = async (
|
|
28
|
-
connectionKey: string,
|
|
29
38
|
userId: string,
|
|
30
39
|
userInfo: UserInfoUpdate,
|
|
40
|
+
profile: any,
|
|
41
|
+
expiresAt: number,
|
|
31
42
|
) => {
|
|
32
43
|
if (!this.presences[userId]) {
|
|
33
44
|
this.presences[userId] = {
|
|
34
45
|
...userInfo,
|
|
35
|
-
|
|
46
|
+
expiresAt,
|
|
47
|
+
profile,
|
|
36
48
|
internal: userInfo.internal || initialInternalPresence,
|
|
37
49
|
};
|
|
38
50
|
} else {
|
|
39
51
|
Object.assign(this.presences[userId], userInfo);
|
|
40
52
|
}
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
this.userToConnection[userId] || new Set<string>();
|
|
44
|
-
this.userToConnection[userId].add(connectionKey);
|
|
53
|
+
return this.presences[userId]!;
|
|
54
|
+
};
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
setExpiresAt = async (userId: string, expiresAt: number) => {
|
|
57
|
+
const presence = this.presences[userId];
|
|
58
|
+
if (presence) {
|
|
59
|
+
presence.expiresAt = expiresAt;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
47
62
|
|
|
48
|
-
|
|
63
|
+
get = async (userId: string, time: number) => {
|
|
64
|
+
const value = this.presences[userId];
|
|
65
|
+
if (value && value.expiresAt > time) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
49
69
|
};
|
|
50
70
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
71
|
+
remove = async (userId: string) => {
|
|
72
|
+
delete this.presences[userId];
|
|
73
|
+
if (Object.keys(this.presences).length === 0) {
|
|
74
|
+
this.clear();
|
|
55
75
|
}
|
|
76
|
+
};
|
|
56
77
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
setTimeout(() => {
|
|
60
|
-
this.removeConnection(connectionKey);
|
|
61
|
-
this.keepalives.delete(connectionKey);
|
|
62
|
-
}, duration),
|
|
63
|
-
);
|
|
78
|
+
all = async () => {
|
|
79
|
+
return this.presences;
|
|
64
80
|
};
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
clear = async () => {
|
|
83
|
+
this.presences = {};
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Stores client presence for connected
|
|
89
|
+
* clients
|
|
90
|
+
*/
|
|
91
|
+
export class Presence extends EventSubscriber<PresenceEvents> {
|
|
92
|
+
constructor(
|
|
93
|
+
readonly profiles: UserProfiles<any>,
|
|
94
|
+
readonly storage: PresenceStorage = new PresenceMemoryStorage(),
|
|
95
|
+
) {
|
|
96
|
+
super();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
set = async (userId: string, userInfo: UserInfoUpdate) => {
|
|
100
|
+
const value = await this.storage.set(
|
|
101
|
+
userId,
|
|
102
|
+
userInfo,
|
|
103
|
+
await this.profiles.get(userId),
|
|
104
|
+
Date.now() + DEFAULT_EXPIRATION_TIME,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return value;
|
|
68
108
|
};
|
|
69
109
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
110
|
+
keepAlive = (userId: string, duration = DEFAULT_EXPIRATION_TIME) => {
|
|
111
|
+
const time = Date.now() + duration;
|
|
112
|
+
this.storage.setExpiresAt(userId, time);
|
|
113
|
+
};
|
|
73
114
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.emit('lost', connectionKey, userId);
|
|
115
|
+
get = (userId: string) => {
|
|
116
|
+
return this.storage.get(userId, Date.now());
|
|
117
|
+
};
|
|
78
118
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
}
|
|
119
|
+
remove = async (userId: string) => {
|
|
120
|
+
await this.storage.remove(userId);
|
|
121
|
+
this.emit('lost', userId);
|
|
84
122
|
};
|
|
85
123
|
|
|
86
124
|
all = () => {
|
|
87
|
-
return this.
|
|
125
|
+
return this.storage.all();
|
|
88
126
|
};
|
|
89
127
|
|
|
90
128
|
clear = () => {
|
|
91
|
-
this.
|
|
92
|
-
this.connectionToUser = {};
|
|
93
|
-
this.userToConnection = {};
|
|
129
|
+
this.storage.clear();
|
|
94
130
|
};
|
|
95
131
|
}
|
package/src/internals.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { errorHandler } from './adapters/node/errorHandler.js';
|
|
|
2
2
|
export { createHttpRouter as createHonoRouter } from './adapters/node/httpRouter.js';
|
|
3
3
|
export { tokenMiddleware } from './adapters/node/tokenMiddleware.js';
|
|
4
4
|
export { ClientConnectionManager } from './connections/ClientConnection.js';
|
|
5
|
+
export type * from './connections/Presence.js';
|
|
5
6
|
export { FileStorageLibraryDelegate } from './files/FileStorage.js';
|
|
6
7
|
export type * from './libraries/generic.js';
|
|
7
8
|
export { Library, type LibraryEvents } from './libraries/Library.js';
|
|
@@ -21,7 +21,13 @@ const profiles = new UserProfileLoader<{ id: string; name: string }>({
|
|
|
21
21
|
},
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
const baseOptions = async (
|
|
24
|
+
const baseOptions = async ({
|
|
25
|
+
onBroadcast,
|
|
26
|
+
onRespond,
|
|
27
|
+
}: {
|
|
28
|
+
onBroadcast?: (message: ServerMessage, omitKeys?: string[]) => void;
|
|
29
|
+
onRespond?: (clientKey: string, message: ServerMessage) => void;
|
|
30
|
+
} = {}) => ({
|
|
25
31
|
id: 'library-1',
|
|
26
32
|
profiles,
|
|
27
33
|
storage: await sqlShardStorage({
|
|
@@ -29,14 +35,22 @@ const baseOptions = async () => ({
|
|
|
29
35
|
fileDeleteExpirationDays: 7,
|
|
30
36
|
replicaTruancyMinutes: 60 * 24,
|
|
31
37
|
})('library-1'),
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
clientConnections: {
|
|
39
|
+
presence: new Presence({
|
|
40
|
+
get: async (userId: string) => {
|
|
41
|
+
return {
|
|
42
|
+
id: userId,
|
|
43
|
+
name: 'Alice',
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
broadcast: vi.fn((message: ServerMessage, omitKeys?: string[]) => {
|
|
48
|
+
if (onBroadcast) onBroadcast(message, omitKeys);
|
|
49
|
+
}),
|
|
50
|
+
respond: vi.fn((clientKey: string, message: ServerMessage) => {
|
|
51
|
+
if (onRespond) onRespond(clientKey, message);
|
|
52
|
+
}),
|
|
53
|
+
} as any,
|
|
40
54
|
});
|
|
41
55
|
|
|
42
56
|
let time = 0;
|
|
@@ -80,11 +94,6 @@ function getAllReceivedReplicaOperations(replica: TestReplica) {
|
|
|
80
94
|
describe('Library', () => {
|
|
81
95
|
describe('sync handling', () => {
|
|
82
96
|
it('should initialize a new library from a replica', async () => {
|
|
83
|
-
const sender: MessageSender = {
|
|
84
|
-
broadcast: vi.fn(),
|
|
85
|
-
respond: vi.fn((_: string, message: ServerMessage) => {}),
|
|
86
|
-
};
|
|
87
|
-
|
|
88
97
|
const tokenInfo: TokenInfo = {
|
|
89
98
|
libraryId: 'library-1',
|
|
90
99
|
syncEndpoint: 'http://localhost:3000/sync',
|
|
@@ -93,10 +102,9 @@ describe('Library', () => {
|
|
|
93
102
|
userId: 'user-1',
|
|
94
103
|
};
|
|
95
104
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
105
|
+
const options = await baseOptions();
|
|
106
|
+
|
|
107
|
+
const library = new Library(options);
|
|
100
108
|
|
|
101
109
|
const messages: ClientMessage[] = [
|
|
102
110
|
{
|
|
@@ -146,7 +154,7 @@ describe('Library', () => {
|
|
|
146
154
|
);
|
|
147
155
|
|
|
148
156
|
// expect sender.broadcast for the operations, and a reply with sync response
|
|
149
|
-
expect(
|
|
157
|
+
expect(options.clientConnections.broadcast).toHaveBeenCalledWith(
|
|
150
158
|
{
|
|
151
159
|
type: 'op-re',
|
|
152
160
|
baselines: [
|
|
@@ -187,16 +195,19 @@ describe('Library', () => {
|
|
|
187
195
|
['clientKey-1'],
|
|
188
196
|
);
|
|
189
197
|
|
|
190
|
-
expect(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
expect(options.clientConnections.respond).toHaveBeenCalledWith(
|
|
199
|
+
'clientKey-1',
|
|
200
|
+
{
|
|
201
|
+
type: 'sync-resp',
|
|
202
|
+
operations: [],
|
|
203
|
+
baselines: [],
|
|
204
|
+
ackThisNonce: undefined,
|
|
205
|
+
ackedTimestamp: 'faketime-3',
|
|
206
|
+
globalAckTimestamp: undefined,
|
|
207
|
+
overwriteLocalData: false,
|
|
208
|
+
peerPresence: {},
|
|
209
|
+
},
|
|
210
|
+
);
|
|
200
211
|
});
|
|
201
212
|
|
|
202
213
|
it('should sync up to latest data, while also rebroadcasting concurrent new operations', async () => {
|
|
@@ -222,11 +233,24 @@ describe('Library', () => {
|
|
|
222
233
|
},
|
|
223
234
|
};
|
|
224
235
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
236
|
+
const options = await baseOptions({
|
|
237
|
+
onBroadcast: (message: ServerMessage, omitKeys?: string[]) => {
|
|
238
|
+
replicas.forEach((replica) => {
|
|
239
|
+
if (!omitKeys?.includes(replica.key)) {
|
|
240
|
+
replica.messages.push(message);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
onRespond: (clientKey: string, message: ServerMessage) => {
|
|
245
|
+
const replica = replicas.find((r) => r.key === clientKey);
|
|
246
|
+
if (replica) {
|
|
247
|
+
replica.messages.push(message);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
228
250
|
});
|
|
229
251
|
|
|
252
|
+
const library = new Library(options);
|
|
253
|
+
|
|
230
254
|
// replica A sends a sync message to initialize library
|
|
231
255
|
const syncMessageA: ClientMessage = {
|
|
232
256
|
type: 'sync',
|
|
@@ -323,11 +347,6 @@ describe('Library', () => {
|
|
|
323
347
|
});
|
|
324
348
|
|
|
325
349
|
it('should reject syncs with data from read replicas', async () => {
|
|
326
|
-
const sender = {
|
|
327
|
-
broadcast: vi.fn(),
|
|
328
|
-
respond: vi.fn(),
|
|
329
|
-
};
|
|
330
|
-
|
|
331
350
|
const tokenInfo: TokenInfo = {
|
|
332
351
|
libraryId: 'library-1',
|
|
333
352
|
syncEndpoint: 'http://localhost:3000/sync',
|
|
@@ -336,10 +355,9 @@ describe('Library', () => {
|
|
|
336
355
|
userId: 'user-1',
|
|
337
356
|
};
|
|
338
357
|
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
});
|
|
358
|
+
const options = await baseOptions();
|
|
359
|
+
|
|
360
|
+
const library = new Library(options);
|
|
343
361
|
|
|
344
362
|
const messages: ClientMessage[] = [
|
|
345
363
|
{
|
|
@@ -388,18 +406,16 @@ describe('Library', () => {
|
|
|
388
406
|
messages.map((m) => library.handleMessage(m, 'clientKey-1', tokenInfo)),
|
|
389
407
|
);
|
|
390
408
|
|
|
391
|
-
expect(
|
|
392
|
-
expect(
|
|
393
|
-
|
|
394
|
-
|
|
409
|
+
expect(options.clientConnections.respond).toHaveBeenCalledOnce();
|
|
410
|
+
expect(options.clientConnections.respond).toHaveBeenCalledWith(
|
|
411
|
+
'clientKey-1',
|
|
412
|
+
{
|
|
413
|
+
type: 'forbidden',
|
|
414
|
+
},
|
|
415
|
+
);
|
|
395
416
|
});
|
|
396
417
|
|
|
397
418
|
it('should reject messages from a replica claimed by a user other than the one recorded on the server', async () => {
|
|
398
|
-
const sender = {
|
|
399
|
-
broadcast: vi.fn(),
|
|
400
|
-
respond: vi.fn((_: string, message: ServerMessage) => {}),
|
|
401
|
-
};
|
|
402
|
-
|
|
403
419
|
const tokenInfo: TokenInfo = {
|
|
404
420
|
libraryId: 'library-1',
|
|
405
421
|
syncEndpoint: 'http://localhost:3000/sync',
|
|
@@ -408,10 +424,9 @@ describe('Library', () => {
|
|
|
408
424
|
userId: 'user-1',
|
|
409
425
|
};
|
|
410
426
|
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
});
|
|
427
|
+
const options = await baseOptions();
|
|
428
|
+
|
|
429
|
+
const library = new Library(options);
|
|
415
430
|
|
|
416
431
|
// purposefully do not await in order - these all come at the same time
|
|
417
432
|
await library.handleMessage(
|
|
@@ -427,7 +442,7 @@ describe('Library', () => {
|
|
|
427
442
|
'clientKey-1',
|
|
428
443
|
tokenInfo,
|
|
429
444
|
);
|
|
430
|
-
|
|
445
|
+
options.clientConnections.respond.mockReset();
|
|
431
446
|
|
|
432
447
|
// now try to send a message from a different user with the same replica
|
|
433
448
|
const tokenInfo2: TokenInfo = {
|
|
@@ -450,23 +465,19 @@ describe('Library', () => {
|
|
|
450
465
|
|
|
451
466
|
await library.handleMessage(message, 'clientKey-1', tokenInfo2);
|
|
452
467
|
|
|
453
|
-
expect(
|
|
454
|
-
expect(
|
|
455
|
-
|
|
456
|
-
|
|
468
|
+
expect(options.clientConnections.respond).toHaveBeenCalledOnce();
|
|
469
|
+
expect(options.clientConnections.respond).toHaveBeenCalledWith(
|
|
470
|
+
'clientKey-1',
|
|
471
|
+
{
|
|
472
|
+
type: 'forbidden',
|
|
473
|
+
},
|
|
474
|
+
);
|
|
457
475
|
});
|
|
458
476
|
});
|
|
459
477
|
|
|
460
478
|
it('should fully destroy itself', async () => {
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
respond: vi.fn(),
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
const library = new Library({
|
|
467
|
-
...(await baseOptions()),
|
|
468
|
-
sender,
|
|
469
|
-
});
|
|
479
|
+
const options = await baseOptions();
|
|
480
|
+
const library = new Library(options);
|
|
470
481
|
|
|
471
482
|
const opMessage: ClientMessage = {
|
|
472
483
|
type: 'op',
|
package/src/libraries/Library.ts
CHANGED
|
@@ -16,9 +16,8 @@ import {
|
|
|
16
16
|
rewriteAuthzOriginator,
|
|
17
17
|
SyncMessage,
|
|
18
18
|
} from '@verdant-web/common';
|
|
19
|
-
import { MessageSender } from '../connections/MessageSender.js';
|
|
20
|
-
import { Presence } from '../connections/Presence.js';
|
|
21
19
|
import { FileInfo, FileStorageLibraryDelegate } from '../files/FileStorage.js';
|
|
20
|
+
import { ClientConnectionManager } from '../internals.js';
|
|
22
21
|
import { Logger } from '../logger.js';
|
|
23
22
|
import { Storage } from '../storage/Storage.js';
|
|
24
23
|
import { TokenInfo } from '../TokenVerifier.js';
|
|
@@ -47,42 +46,44 @@ export interface LibraryFileInfo {
|
|
|
47
46
|
|
|
48
47
|
export class Library {
|
|
49
48
|
private storage;
|
|
50
|
-
private sender;
|
|
51
49
|
private disableRebasing: boolean;
|
|
52
50
|
private fileStorage;
|
|
53
51
|
private events;
|
|
54
52
|
readonly id: string;
|
|
55
|
-
private
|
|
53
|
+
private clientConnections: ClientConnectionManager;
|
|
54
|
+
private get presence() {
|
|
55
|
+
return this.clientConnections.presence;
|
|
56
|
+
}
|
|
57
|
+
private get sender() {
|
|
58
|
+
return this.clientConnections;
|
|
59
|
+
}
|
|
56
60
|
|
|
57
61
|
private log: Logger;
|
|
58
62
|
|
|
59
63
|
constructor({
|
|
60
64
|
storage,
|
|
61
|
-
sender,
|
|
62
65
|
log = () => {},
|
|
63
66
|
disableRebasing,
|
|
64
67
|
fileStorage,
|
|
65
68
|
events,
|
|
66
69
|
id,
|
|
67
|
-
|
|
70
|
+
clientConnections,
|
|
68
71
|
}: {
|
|
69
72
|
storage: Storage;
|
|
70
|
-
sender: MessageSender;
|
|
71
73
|
log?: Logger;
|
|
72
74
|
disableRebasing?: boolean;
|
|
73
75
|
fileStorage?: FileStorageLibraryDelegate;
|
|
74
76
|
events?: EventSubscriber<LibraryEvents>;
|
|
75
77
|
id: string;
|
|
76
|
-
|
|
78
|
+
clientConnections: ClientConnectionManager;
|
|
77
79
|
}) {
|
|
78
80
|
this.id = id;
|
|
79
81
|
this.storage = storage;
|
|
80
|
-
this.sender = sender;
|
|
81
82
|
this.log = log;
|
|
82
83
|
this.disableRebasing = !!disableRebasing;
|
|
83
84
|
this.fileStorage = fileStorage;
|
|
84
85
|
this.events = events;
|
|
85
|
-
this.
|
|
86
|
+
this.clientConnections = clientConnections;
|
|
86
87
|
this.presence.subscribe('lost', this.onPresenceLost);
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -490,13 +491,16 @@ export class Library {
|
|
|
490
491
|
|
|
491
492
|
// respond to client
|
|
492
493
|
|
|
494
|
+
const peerPresence = await this.presence.all();
|
|
495
|
+
|
|
493
496
|
this.log(
|
|
494
497
|
'info',
|
|
495
498
|
'Sending sync response with',
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
499
|
+
{
|
|
500
|
+
ops: ops.length,
|
|
501
|
+
baselines: baselines.length,
|
|
502
|
+
peers: Object.keys(peerPresence),
|
|
503
|
+
},
|
|
500
504
|
'to client key',
|
|
501
505
|
clientKey,
|
|
502
506
|
);
|
|
@@ -511,7 +515,7 @@ export class Library {
|
|
|
511
515
|
baselines: this.removeBaselineExtras(baselines),
|
|
512
516
|
globalAckTimestamp:
|
|
513
517
|
(await this.storage.replicas.getGlobalAck()) ?? undefined,
|
|
514
|
-
peerPresence
|
|
518
|
+
peerPresence,
|
|
515
519
|
// only request the client to overwrite local data if a reset is requested
|
|
516
520
|
// and there is data to overwrite it. otherwise the client may still
|
|
517
521
|
// send its own history to us.
|
|
@@ -578,7 +582,7 @@ export class Library {
|
|
|
578
582
|
clientKey: string,
|
|
579
583
|
_info: TokenInfo,
|
|
580
584
|
) => {
|
|
581
|
-
this.
|
|
585
|
+
await this.clientConnections.remove(clientKey);
|
|
582
586
|
};
|
|
583
587
|
|
|
584
588
|
private updateHighwater = async (
|
|
@@ -633,7 +637,7 @@ export class Library {
|
|
|
633
637
|
// for global ack, to determine consensus, also allow
|
|
634
638
|
// for all actively connected replicas to ack regardless of their
|
|
635
639
|
// type.
|
|
636
|
-
const activeReplicaIds = Object.values(this.presence.all()).map(
|
|
640
|
+
const activeReplicaIds = Object.values(await this.presence.all()).map(
|
|
637
641
|
(p) => p.replicaId,
|
|
638
642
|
);
|
|
639
643
|
const globalAck =
|
|
@@ -711,7 +715,7 @@ export class Library {
|
|
|
711
715
|
clientKey: string,
|
|
712
716
|
info: TokenInfo,
|
|
713
717
|
) => {
|
|
714
|
-
const updated = await this.presence.set(
|
|
718
|
+
const updated = await this.presence.set(info.userId, {
|
|
715
719
|
presence: message.presence,
|
|
716
720
|
internal: message.internal,
|
|
717
721
|
replicaId: message.replicaId,
|
|
@@ -728,20 +732,13 @@ export class Library {
|
|
|
728
732
|
);
|
|
729
733
|
};
|
|
730
734
|
|
|
731
|
-
private onPresenceLost = async (
|
|
732
|
-
this.log(
|
|
733
|
-
'info',
|
|
734
|
-
'User disconnected from all replicas:',
|
|
735
|
-
userId,
|
|
736
|
-
'replica:',
|
|
737
|
-
replicaId,
|
|
738
|
-
);
|
|
735
|
+
private onPresenceLost = async (userId: string) => {
|
|
736
|
+
this.log('info', 'User disconnected from all replicas:', userId);
|
|
739
737
|
this.sender.broadcast({
|
|
740
738
|
type: 'presence-offline',
|
|
741
|
-
replicaId,
|
|
742
739
|
userId,
|
|
743
740
|
});
|
|
744
|
-
if (Object.keys(this.presence.all()).length === 0) {
|
|
741
|
+
if (Object.keys(await this.presence.all()).length === 0) {
|
|
745
742
|
this.log('info', `All users have disconnected`);
|
|
746
743
|
// could happen - if the server is shutting down manually
|
|
747
744
|
if (!this.storage.open) {
|
|
@@ -809,7 +806,7 @@ export class Library {
|
|
|
809
806
|
};
|
|
810
807
|
|
|
811
808
|
getPresence = () => {
|
|
812
|
-
return
|
|
809
|
+
return this.presence.all();
|
|
813
810
|
};
|
|
814
811
|
|
|
815
812
|
getInfo = async (): Promise<LibraryInfo | null> => {
|
|
@@ -75,8 +75,7 @@ export class SingleNodeLibraryManager {
|
|
|
75
75
|
storage,
|
|
76
76
|
id: libraryId,
|
|
77
77
|
events: this.events,
|
|
78
|
-
|
|
79
|
-
presence: clientConnections.presence,
|
|
78
|
+
clientConnections,
|
|
80
79
|
log: (level, ...args) =>
|
|
81
80
|
this.ctx.log?.(level, `[Library ${libraryId}]`, ...args),
|
|
82
81
|
fileStorage: this.ctx.fileStorage
|