@syncular/server-hono 0.0.6-171 → 0.0.6-177
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/console/gateway.d.ts +5 -0
- package/dist/console/gateway.d.ts.map +1 -1
- package/dist/console/gateway.js +1 -16
- package/dist/console/gateway.js.map +1 -1
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +15 -30
- package/dist/console/routes.js.map +1 -1
- package/dist/console/types.d.ts +1 -1
- package/dist/proxy/routes.d.ts +3 -1
- package/dist/proxy/routes.d.ts.map +1 -1
- package/dist/proxy/routes.js +1 -16
- package/dist/proxy/routes.js.map +1 -1
- package/dist/routes.d.ts +3 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +133 -77
- package/dist/routes.js.map +1 -1
- package/dist/websocket-origin.d.ts +8 -0
- package/dist/websocket-origin.d.ts.map +1 -0
- package/dist/websocket-origin.js +43 -0
- package/dist/websocket-origin.js.map +1 -0
- package/dist/ws.d.ts +24 -8
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +54 -44
- package/dist/ws.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/console-gateway-live-routes.test.ts +13 -0
- package/src/__tests__/create-server.test.ts +187 -2
- package/src/__tests__/pull-chunk-storage.test.ts +114 -1
- package/src/__tests__/websocket-origin.test.ts +55 -0
- package/src/__tests__/ws-connection-manager.test.ts +22 -7
- package/src/console/gateway.ts +6 -18
- package/src/console/routes.ts +22 -39
- package/src/console/types.ts +1 -1
- package/src/proxy/routes.ts +4 -19
- package/src/routes.ts +192 -109
- package/src/websocket-origin.ts +54 -0
- package/src/ws.ts +86 -45
package/src/ws.ts
CHANGED
|
@@ -18,6 +18,14 @@ export interface PresenceEntry {
|
|
|
18
18
|
metadata?: Record<string, unknown>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function createWebSocketConnectionOwnerKey(args: {
|
|
22
|
+
partitionId: string;
|
|
23
|
+
actorId: string;
|
|
24
|
+
clientId: string;
|
|
25
|
+
}): string {
|
|
26
|
+
return JSON.stringify([args.partitionId, args.actorId, args.clientId]);
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
/**
|
|
22
30
|
* Push response data sent back to the client over WS
|
|
23
31
|
*/
|
|
@@ -49,6 +57,7 @@ export interface SyncWebSocketEvent {
|
|
|
49
57
|
presence?: {
|
|
50
58
|
action: 'join' | 'leave' | 'update' | 'snapshot';
|
|
51
59
|
scopeKey: string;
|
|
60
|
+
ownerKey?: string;
|
|
52
61
|
clientId?: string;
|
|
53
62
|
actorId?: string;
|
|
54
63
|
metadata?: Record<string, unknown>;
|
|
@@ -81,6 +90,7 @@ export interface WebSocketConnection {
|
|
|
81
90
|
sendPresence(data: {
|
|
82
91
|
action: 'join' | 'leave' | 'update' | 'snapshot';
|
|
83
92
|
scopeKey: string;
|
|
93
|
+
ownerKey?: string;
|
|
84
94
|
clientId?: string;
|
|
85
95
|
actorId?: string;
|
|
86
96
|
metadata?: Record<string, unknown>;
|
|
@@ -98,6 +108,8 @@ export interface WebSocketConnection {
|
|
|
98
108
|
actorId: string;
|
|
99
109
|
/** Client/device identifier for this connection */
|
|
100
110
|
clientId: string;
|
|
111
|
+
/** Stable owner identity for this connection */
|
|
112
|
+
ownerKey: string;
|
|
101
113
|
/** Transport path used by this connection. */
|
|
102
114
|
transportPath: 'direct' | 'relay';
|
|
103
115
|
}
|
|
@@ -113,7 +125,12 @@ function safeSend(ws: WSContext, message: string): boolean {
|
|
|
113
125
|
|
|
114
126
|
export function createWebSocketConnection(
|
|
115
127
|
ws: WSContext,
|
|
116
|
-
args: {
|
|
128
|
+
args: {
|
|
129
|
+
actorId: string;
|
|
130
|
+
clientId: string;
|
|
131
|
+
ownerKey: string;
|
|
132
|
+
transportPath: 'direct' | 'relay';
|
|
133
|
+
}
|
|
117
134
|
): WebSocketConnection {
|
|
118
135
|
let closed = false;
|
|
119
136
|
|
|
@@ -124,6 +141,7 @@ export function createWebSocketConnection(
|
|
|
124
141
|
},
|
|
125
142
|
actorId: args.actorId,
|
|
126
143
|
clientId: args.clientId,
|
|
144
|
+
ownerKey: args.ownerKey,
|
|
127
145
|
transportPath: args.transportPath,
|
|
128
146
|
sendSync(
|
|
129
147
|
cursor: number,
|
|
@@ -158,6 +176,7 @@ export function createWebSocketConnection(
|
|
|
158
176
|
sendPresence(data: {
|
|
159
177
|
action: 'join' | 'leave' | 'update' | 'snapshot';
|
|
160
178
|
scopeKey: string;
|
|
179
|
+
ownerKey?: string;
|
|
161
180
|
clientId?: string;
|
|
162
181
|
actorId?: string;
|
|
163
182
|
metadata?: Record<string, unknown>;
|
|
@@ -219,7 +238,7 @@ export class WebSocketConnectionManager {
|
|
|
219
238
|
|
|
220
239
|
/**
|
|
221
240
|
* In-memory presence tracking by scope key.
|
|
222
|
-
* Map<scopeKey, Map<
|
|
241
|
+
* Map<scopeKey, Map<ownerKey, PresenceEntry>>
|
|
223
242
|
*/
|
|
224
243
|
private presenceByScopeKey = new Map<string, Map<string, PresenceEntry>>();
|
|
225
244
|
|
|
@@ -229,6 +248,7 @@ export class WebSocketConnectionManager {
|
|
|
229
248
|
onPresenceChange?: (event: {
|
|
230
249
|
action: 'join' | 'leave' | 'update';
|
|
231
250
|
scopeKey: string;
|
|
251
|
+
ownerKey: string;
|
|
232
252
|
clientId: string;
|
|
233
253
|
actorId: string;
|
|
234
254
|
metadata?: Record<string, unknown>;
|
|
@@ -241,8 +261,8 @@ export class WebSocketConnectionManager {
|
|
|
241
261
|
this.onPresenceChange = options?.onPresenceChange;
|
|
242
262
|
this.registry = new RealtimeConnectionRegistry({
|
|
243
263
|
heartbeatIntervalMs: options?.heartbeatIntervalMs,
|
|
244
|
-
|
|
245
|
-
this.
|
|
264
|
+
onOwnerDisconnected: (ownerKey) => {
|
|
265
|
+
this.cleanupOwnerPresence(ownerKey);
|
|
246
266
|
},
|
|
247
267
|
});
|
|
248
268
|
}
|
|
@@ -262,15 +282,18 @@ export class WebSocketConnectionManager {
|
|
|
262
282
|
* Update the effective scopes for an already-connected client.
|
|
263
283
|
* If the client has no active connections, this is a no-op.
|
|
264
284
|
*/
|
|
265
|
-
|
|
266
|
-
this.registry.
|
|
285
|
+
updateConnectionScopeKeys(ownerKey: string, scopeKeys: string[]): void {
|
|
286
|
+
this.registry.updateOwnerScopeKeys(ownerKey, scopeKeys);
|
|
267
287
|
}
|
|
268
288
|
|
|
269
289
|
/**
|
|
270
290
|
* Check whether a client is currently authorized/subscribed for a scope key.
|
|
271
291
|
*/
|
|
272
|
-
|
|
273
|
-
|
|
292
|
+
isConnectionSubscribedToScopeKey(
|
|
293
|
+
ownerKey: string,
|
|
294
|
+
scopeKey: string
|
|
295
|
+
): boolean {
|
|
296
|
+
return this.registry.isOwnerSubscribedToScopeKey(ownerKey, scopeKey);
|
|
274
297
|
}
|
|
275
298
|
|
|
276
299
|
// =========================================================================
|
|
@@ -282,20 +305,19 @@ export class WebSocketConnectionManager {
|
|
|
282
305
|
* Called when a client wants to be visible to others in a scope.
|
|
283
306
|
*/
|
|
284
307
|
joinPresence(
|
|
285
|
-
|
|
308
|
+
ownerKey: string,
|
|
286
309
|
scopeKey: string,
|
|
287
310
|
metadata?: Record<string, unknown>
|
|
288
311
|
): boolean {
|
|
289
|
-
const conns = this.registry.
|
|
312
|
+
const conns = this.registry.getConnectionsForOwner(ownerKey);
|
|
290
313
|
if (!conns || conns.size === 0) return false;
|
|
291
|
-
if (!this.
|
|
314
|
+
if (!this.isConnectionSubscribedToScopeKey(ownerKey, scopeKey))
|
|
315
|
+
return false;
|
|
292
316
|
|
|
293
|
-
// Get actorId from first connection
|
|
294
317
|
const conn = conns.values().next().value;
|
|
295
318
|
if (!conn) return false;
|
|
296
|
-
const actorId = conn
|
|
319
|
+
const { actorId, clientId } = conn;
|
|
297
320
|
|
|
298
|
-
// Add to presence map
|
|
299
321
|
let scopePresence = this.presenceByScopeKey.get(scopeKey);
|
|
300
322
|
if (!scopePresence) {
|
|
301
323
|
scopePresence = new Map();
|
|
@@ -308,21 +330,21 @@ export class WebSocketConnectionManager {
|
|
|
308
330
|
joinedAt: Date.now(),
|
|
309
331
|
metadata,
|
|
310
332
|
};
|
|
311
|
-
scopePresence.set(
|
|
333
|
+
scopePresence.set(ownerKey, entry);
|
|
312
334
|
|
|
313
|
-
// Notify other clients in this scope
|
|
314
335
|
this.broadcastPresenceEvent(scopeKey, {
|
|
315
336
|
action: 'join',
|
|
316
337
|
scopeKey,
|
|
338
|
+
ownerKey,
|
|
317
339
|
clientId,
|
|
318
340
|
actorId,
|
|
319
341
|
metadata,
|
|
320
342
|
});
|
|
321
343
|
|
|
322
|
-
// Callback for cross-instance broadcasting
|
|
323
344
|
this.onPresenceChange?.({
|
|
324
345
|
action: 'join',
|
|
325
346
|
scopeKey,
|
|
347
|
+
ownerKey,
|
|
326
348
|
clientId,
|
|
327
349
|
actorId,
|
|
328
350
|
metadata,
|
|
@@ -335,31 +357,31 @@ export class WebSocketConnectionManager {
|
|
|
335
357
|
* Leave presence for a scope key.
|
|
336
358
|
* Called when a client no longer wants to be visible in a scope.
|
|
337
359
|
*/
|
|
338
|
-
leavePresence(
|
|
360
|
+
leavePresence(ownerKey: string, scopeKey: string): boolean {
|
|
339
361
|
const scopePresence = this.presenceByScopeKey.get(scopeKey);
|
|
340
362
|
if (!scopePresence) return false;
|
|
341
363
|
|
|
342
|
-
const entry = scopePresence.get(
|
|
364
|
+
const entry = scopePresence.get(ownerKey);
|
|
343
365
|
if (!entry) return false;
|
|
344
366
|
|
|
345
|
-
scopePresence.delete(
|
|
367
|
+
scopePresence.delete(ownerKey);
|
|
346
368
|
if (scopePresence.size === 0) {
|
|
347
369
|
this.presenceByScopeKey.delete(scopeKey);
|
|
348
370
|
}
|
|
349
371
|
|
|
350
|
-
// Notify other clients in this scope
|
|
351
372
|
this.broadcastPresenceEvent(scopeKey, {
|
|
352
373
|
action: 'leave',
|
|
353
374
|
scopeKey,
|
|
354
|
-
|
|
375
|
+
ownerKey,
|
|
376
|
+
clientId: entry.clientId,
|
|
355
377
|
actorId: entry.actorId,
|
|
356
378
|
});
|
|
357
379
|
|
|
358
|
-
// Callback for cross-instance broadcasting
|
|
359
380
|
this.onPresenceChange?.({
|
|
360
381
|
action: 'leave',
|
|
361
382
|
scopeKey,
|
|
362
|
-
|
|
383
|
+
ownerKey,
|
|
384
|
+
clientId: entry.clientId,
|
|
363
385
|
actorId: entry.actorId,
|
|
364
386
|
});
|
|
365
387
|
|
|
@@ -371,33 +393,34 @@ export class WebSocketConnectionManager {
|
|
|
371
393
|
* Used to update what entity a user is viewing/editing.
|
|
372
394
|
*/
|
|
373
395
|
updatePresenceMetadata(
|
|
374
|
-
|
|
396
|
+
ownerKey: string,
|
|
375
397
|
scopeKey: string,
|
|
376
398
|
metadata: Record<string, unknown>
|
|
377
399
|
): boolean {
|
|
378
|
-
if (!this.
|
|
400
|
+
if (!this.isConnectionSubscribedToScopeKey(ownerKey, scopeKey))
|
|
401
|
+
return false;
|
|
379
402
|
const scopePresence = this.presenceByScopeKey.get(scopeKey);
|
|
380
403
|
if (!scopePresence) return false;
|
|
381
404
|
|
|
382
|
-
const entry = scopePresence.get(
|
|
405
|
+
const entry = scopePresence.get(ownerKey);
|
|
383
406
|
if (!entry) return false;
|
|
384
407
|
|
|
385
408
|
entry.metadata = metadata;
|
|
386
409
|
|
|
387
|
-
// Notify other clients in this scope
|
|
388
410
|
this.broadcastPresenceEvent(scopeKey, {
|
|
389
411
|
action: 'update',
|
|
390
412
|
scopeKey,
|
|
391
|
-
|
|
413
|
+
ownerKey,
|
|
414
|
+
clientId: entry.clientId,
|
|
392
415
|
actorId: entry.actorId,
|
|
393
416
|
metadata,
|
|
394
417
|
});
|
|
395
418
|
|
|
396
|
-
// Callback for cross-instance broadcasting
|
|
397
419
|
this.onPresenceChange?.({
|
|
398
420
|
action: 'update',
|
|
399
421
|
scopeKey,
|
|
400
|
-
|
|
422
|
+
ownerKey,
|
|
423
|
+
clientId: entry.clientId,
|
|
401
424
|
actorId: entry.actorId,
|
|
402
425
|
metadata,
|
|
403
426
|
});
|
|
@@ -448,13 +471,24 @@ export class WebSocketConnectionManager {
|
|
|
448
471
|
handleRemotePresenceEvent(event: {
|
|
449
472
|
action: 'join' | 'leave' | 'update';
|
|
450
473
|
scopeKey: string;
|
|
474
|
+
ownerKey?: string;
|
|
451
475
|
clientId: string;
|
|
452
476
|
actorId: string;
|
|
453
477
|
metadata?: Record<string, unknown>;
|
|
454
478
|
}): void {
|
|
455
|
-
const {
|
|
479
|
+
const {
|
|
480
|
+
action,
|
|
481
|
+
scopeKey,
|
|
482
|
+
clientId,
|
|
483
|
+
actorId,
|
|
484
|
+
metadata,
|
|
485
|
+
ownerKey = createWebSocketConnectionOwnerKey({
|
|
486
|
+
partitionId: scopeKey.split(':', 1)[0] ?? 'default',
|
|
487
|
+
actorId,
|
|
488
|
+
clientId,
|
|
489
|
+
}),
|
|
490
|
+
} = event;
|
|
456
491
|
|
|
457
|
-
// Update local presence state
|
|
458
492
|
let scopePresence = this.presenceByScopeKey.get(scopeKey);
|
|
459
493
|
|
|
460
494
|
switch (action) {
|
|
@@ -463,7 +497,7 @@ export class WebSocketConnectionManager {
|
|
|
463
497
|
scopePresence = new Map();
|
|
464
498
|
this.presenceByScopeKey.set(scopeKey, scopePresence);
|
|
465
499
|
}
|
|
466
|
-
scopePresence.set(
|
|
500
|
+
scopePresence.set(ownerKey, {
|
|
467
501
|
clientId,
|
|
468
502
|
actorId,
|
|
469
503
|
joinedAt: Date.now(),
|
|
@@ -473,7 +507,7 @@ export class WebSocketConnectionManager {
|
|
|
473
507
|
}
|
|
474
508
|
case 'leave': {
|
|
475
509
|
if (scopePresence) {
|
|
476
|
-
scopePresence.delete(
|
|
510
|
+
scopePresence.delete(ownerKey);
|
|
477
511
|
if (scopePresence.size === 0) {
|
|
478
512
|
this.presenceByScopeKey.delete(scopeKey);
|
|
479
513
|
}
|
|
@@ -482,7 +516,7 @@ export class WebSocketConnectionManager {
|
|
|
482
516
|
}
|
|
483
517
|
case 'update': {
|
|
484
518
|
if (scopePresence) {
|
|
485
|
-
const entry = scopePresence.get(
|
|
519
|
+
const entry = scopePresence.get(ownerKey);
|
|
486
520
|
if (entry) {
|
|
487
521
|
entry.metadata = metadata;
|
|
488
522
|
}
|
|
@@ -503,6 +537,7 @@ export class WebSocketConnectionManager {
|
|
|
503
537
|
event: {
|
|
504
538
|
action: 'join' | 'leave' | 'update';
|
|
505
539
|
scopeKey: string;
|
|
540
|
+
ownerKey?: string;
|
|
506
541
|
clientId?: string;
|
|
507
542
|
actorId?: string;
|
|
508
543
|
metadata?: Record<string, unknown>;
|
|
@@ -520,32 +555,31 @@ export class WebSocketConnectionManager {
|
|
|
520
555
|
}
|
|
521
556
|
|
|
522
557
|
/**
|
|
523
|
-
* Clean up presence when
|
|
558
|
+
* Clean up presence when an owner fully disconnects (all connections closed).
|
|
524
559
|
*/
|
|
525
|
-
private
|
|
526
|
-
// Find all scopes this client has presence in
|
|
560
|
+
private cleanupOwnerPresence(ownerKey: string): void {
|
|
527
561
|
for (const [scopeKey, scopePresence] of this.presenceByScopeKey) {
|
|
528
|
-
const entry = scopePresence.get(
|
|
562
|
+
const entry = scopePresence.get(ownerKey);
|
|
529
563
|
if (!entry) continue;
|
|
530
564
|
|
|
531
|
-
scopePresence.delete(
|
|
565
|
+
scopePresence.delete(ownerKey);
|
|
532
566
|
if (scopePresence.size === 0) {
|
|
533
567
|
this.presenceByScopeKey.delete(scopeKey);
|
|
534
568
|
}
|
|
535
569
|
|
|
536
|
-
// Notify other clients
|
|
537
570
|
this.broadcastPresenceEvent(scopeKey, {
|
|
538
571
|
action: 'leave',
|
|
539
572
|
scopeKey,
|
|
540
|
-
|
|
573
|
+
ownerKey,
|
|
574
|
+
clientId: entry.clientId,
|
|
541
575
|
actorId: entry.actorId,
|
|
542
576
|
});
|
|
543
577
|
|
|
544
|
-
// Callback for cross-instance broadcasting
|
|
545
578
|
this.onPresenceChange?.({
|
|
546
579
|
action: 'leave',
|
|
547
580
|
scopeKey,
|
|
548
|
-
|
|
581
|
+
ownerKey,
|
|
582
|
+
clientId: entry.clientId,
|
|
549
583
|
actorId: entry.actorId,
|
|
550
584
|
});
|
|
551
585
|
}
|
|
@@ -617,6 +651,13 @@ export class WebSocketConnectionManager {
|
|
|
617
651
|
return this.registry.getConnectionCount(clientId);
|
|
618
652
|
}
|
|
619
653
|
|
|
654
|
+
/**
|
|
655
|
+
* Get the number of active connections for one owner identity.
|
|
656
|
+
*/
|
|
657
|
+
getScopedConnectionCount(ownerKey: string): number {
|
|
658
|
+
return this.registry.getScopedConnectionCount(ownerKey);
|
|
659
|
+
}
|
|
660
|
+
|
|
620
661
|
/**
|
|
621
662
|
* Get the current transport path for a client if connected.
|
|
622
663
|
*/
|