@kumiai/hub-server 0.12.1 → 0.13.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/lib/handlers.js +40 -4
- package/lib/hub.d.ts +2 -2
- package/lib/hub.js +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/registry.d.ts +11 -2
- package/lib/registry.js +2 -2
- package/lib/wake.js +9 -6
- package/package.json +11 -10
package/lib/handlers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HandlerError } from '@enkaku/server';
|
|
2
2
|
import { HUB_ERROR_CODES, hubErrorCodeOf, InvalidPayloadError, KeyPackageFetchLimitError, keyPackageDigest, wakeRecipientKeyProblem } from '@kumiai/hub-protocol';
|
|
3
3
|
import { fromB64, toB64 } from '@sozai/codec';
|
|
4
|
-
import { getReporter } from '@sozai/log';
|
|
4
|
+
import { getLogger, getReporter } from '@sozai/log';
|
|
5
5
|
import { createRateLimiter } from './rateLimit.js';
|
|
6
6
|
function normalizeAuthorizeDecision(decision) {
|
|
7
7
|
return typeof decision === 'boolean' ? {
|
|
@@ -29,6 +29,11 @@ const reportStoreError = getReporter([
|
|
|
29
29
|
'kumiai',
|
|
30
30
|
'hub-server'
|
|
31
31
|
], '@kumiai/hub-server');
|
|
32
|
+
const receiveLogger = getLogger([
|
|
33
|
+
'kumiai',
|
|
34
|
+
'hub-server',
|
|
35
|
+
'receive'
|
|
36
|
+
]);
|
|
32
37
|
/** What the failed operation was about, for the default log line: a DID for the per-recipient
|
|
33
38
|
* methods, a topic for fan-out, nothing for a store-wide purge. */ function subjectOf(event) {
|
|
34
39
|
switch(event.method){
|
|
@@ -550,11 +555,22 @@ export function createHandlers(params) {
|
|
|
550
555
|
// backpressure stalls `writer.write` until `receiveBufferLimit` tears the channel down.
|
|
551
556
|
function waitForRoom(bytes) {
|
|
552
557
|
if (tornDown || hasRoom(bytes)) return Promise.resolve();
|
|
558
|
+
receiveLogger.debug('receive window full for {recipientDID}: paused with {frames} frames ({bytes} bytes) unacked', {
|
|
559
|
+
recipientDID: clientDID,
|
|
560
|
+
frames: inFlight.size,
|
|
561
|
+
bytes: inFlightBytes
|
|
562
|
+
});
|
|
553
563
|
return new Promise((resolve)=>{
|
|
554
564
|
let timer;
|
|
555
565
|
const arm = ()=>{
|
|
556
566
|
clearTimeout(timer);
|
|
557
567
|
timer = setTimeout(()=>{
|
|
568
|
+
receiveLogger.warn('receive ack timeout for {recipientDID}: released {frames} frames ({bytes} bytes) after {timeoutMs} ms', {
|
|
569
|
+
recipientDID: clientDID,
|
|
570
|
+
frames: inFlight.size,
|
|
571
|
+
bytes: inFlightBytes,
|
|
572
|
+
timeoutMs: receiveAckTimeout
|
|
573
|
+
});
|
|
558
574
|
releaseAll();
|
|
559
575
|
notifyWindow();
|
|
560
576
|
}, receiveAckTimeout);
|
|
@@ -593,13 +609,25 @@ export function createHandlers(params) {
|
|
|
593
609
|
const done = new Promise((resolve)=>{
|
|
594
610
|
resolveDone = resolve;
|
|
595
611
|
});
|
|
612
|
+
function logUnackedFrames() {
|
|
613
|
+
if (inFlight.size === 0) return;
|
|
614
|
+
receiveLogger.debug('receive stream ended for {recipientDID} with {frames} frames ({bytes} bytes) unacked', {
|
|
615
|
+
recipientDID: clientDID,
|
|
616
|
+
frames: inFlight.size,
|
|
617
|
+
bytes: inFlightBytes
|
|
618
|
+
});
|
|
619
|
+
}
|
|
596
620
|
// Idempotent teardown, shared by every failure and end path. Frames stay pending in the store
|
|
597
621
|
// and redeliver on the next connect (store-and-forward).
|
|
598
622
|
function finish() {
|
|
599
623
|
if (tornDown) return;
|
|
624
|
+
logUnackedFrames();
|
|
600
625
|
tornDown = true;
|
|
601
626
|
notifyWindow();
|
|
602
|
-
if (receiveToken != null) registry.releaseReceiveWriter(
|
|
627
|
+
if (receiveToken != null) registry.releaseReceiveWriter({
|
|
628
|
+
did: clientDID,
|
|
629
|
+
token: receiveToken
|
|
630
|
+
});
|
|
603
631
|
registry.unregisterIfIdle(clientDID);
|
|
604
632
|
reader.cancel().catch(()=>{});
|
|
605
633
|
writer.abort(new Error('receive channel torn down')).catch(()=>{});
|
|
@@ -693,7 +721,11 @@ export function createHandlers(params) {
|
|
|
693
721
|
// Binding evicts whatever held the lane: a reconnect means the old connection broke and the
|
|
694
722
|
// server learns last, so the stale writer must give way to the live one. See
|
|
695
723
|
// `HubClientRegistry.bindReceiveWriter`.
|
|
696
|
-
const { token, evicted } = registry.bindReceiveWriter(
|
|
724
|
+
const { token, evicted } = registry.bindReceiveWriter({
|
|
725
|
+
did: clientDID,
|
|
726
|
+
writer: onLive,
|
|
727
|
+
endReceive: ()=>endEvicted?.()
|
|
728
|
+
});
|
|
697
729
|
receiveToken = token;
|
|
698
730
|
// After the bind, so the lane is never unheld between the two.
|
|
699
731
|
evicted?.();
|
|
@@ -742,9 +774,13 @@ export function createHandlers(params) {
|
|
|
742
774
|
}
|
|
743
775
|
} catch (error) {
|
|
744
776
|
// A synchronous drain error (e.g. store.fetch threw): clean up and reject the handler.
|
|
777
|
+
if (!tornDown) logUnackedFrames();
|
|
745
778
|
tornDown = true;
|
|
746
779
|
notifyWindow();
|
|
747
|
-
if (receiveToken != null) registry.releaseReceiveWriter(
|
|
780
|
+
if (receiveToken != null) registry.releaseReceiveWriter({
|
|
781
|
+
did: clientDID,
|
|
782
|
+
token: receiveToken
|
|
783
|
+
});
|
|
748
784
|
registry.unregisterIfIdle(clientDID);
|
|
749
785
|
reader.cancel().catch(()=>{});
|
|
750
786
|
writer.abort(error).catch(()=>{});
|
package/lib/hub.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ServerTransportOf } from '@enkaku/protocol';
|
|
2
2
|
import type { AccessRules, ReplayOptions, ResourceLimits, Server } from '@enkaku/server';
|
|
3
3
|
import type { VerifyTokenHook } from '@kokuin/capability';
|
|
4
|
-
import type { Identity } from '@kokuin/token';
|
|
4
|
+
import type { CryptoOptions, Identity } from '@kokuin/token';
|
|
5
5
|
import type { HubProtocol, HubStore, WakeRegistry, WakeSender } from '@kumiai/hub-protocol';
|
|
6
6
|
import { type AuthorizeHook, type HubRateLimits, type HubStoreErrorHook, type KeyPackageFetchLimits } from './handlers.js';
|
|
7
7
|
import { HubClientRegistry } from './registry.js';
|
|
@@ -28,7 +28,7 @@ export type HubPurgeOptions = {
|
|
|
28
28
|
};
|
|
29
29
|
/** Replay settings a host may tune. The hub always enables replay checks and rejects stale tokens. */
|
|
30
30
|
export type HubReplayOptions = Omit<ReplayOptions, 'enabled' | 'rejectStale'>;
|
|
31
|
-
export type CreateHubParams = {
|
|
31
|
+
export type CreateHubParams = CryptoOptions & {
|
|
32
32
|
transport: ServerTransportOf<HubProtocol>;
|
|
33
33
|
store: HubStore;
|
|
34
34
|
/**
|
package/lib/hub.js
CHANGED
|
@@ -55,6 +55,7 @@ export function createHub(params) {
|
|
|
55
55
|
protocol: hubProtocol,
|
|
56
56
|
transport: params.transport,
|
|
57
57
|
identity: params.identity,
|
|
58
|
+
crypto: params.crypto,
|
|
58
59
|
accessRules: params.accessRules ?? DEFAULT_HUB_ACCESS_RULES,
|
|
59
60
|
verifyToken: params.verifyToken,
|
|
60
61
|
replay: {
|
package/lib/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export type { CreateHubParams, HubInstance, HubPurgeOptions, HubReplayOptions }
|
|
|
4
4
|
export { createHub, DEFAULT_HUB_ACCESS_RULES } from './hub.js';
|
|
5
5
|
export { createMemoryStore, type MemoryStoreOptions } from './memoryStore.js';
|
|
6
6
|
export { createRateLimiter, type RateLimitConfig, type RateLimiter } from './rateLimit.js';
|
|
7
|
-
export type { ClientEntry } from './registry.js';
|
|
7
|
+
export type { ClientEntry, HubClientRegistryBindReceiveWriterParams, HubClientRegistryReleaseReceiveWriterParams, } from './registry.js';
|
|
8
8
|
export { HubClientRegistry } from './registry.js';
|
|
9
9
|
export { createWakeDispatcher, type WakeDispatcher, type WakeDispatcherParams, type WakeNotifyParams, } from './wake.js';
|
package/lib/registry.d.ts
CHANGED
|
@@ -7,6 +7,15 @@ export type ClientEntry = {
|
|
|
7
7
|
/** Identifies the binding, so a channel can only ever release its OWN. */
|
|
8
8
|
token: symbol | null;
|
|
9
9
|
};
|
|
10
|
+
export type HubClientRegistryBindReceiveWriterParams = {
|
|
11
|
+
did: string;
|
|
12
|
+
writer: (message: StoredMessage) => void;
|
|
13
|
+
endReceive: () => void;
|
|
14
|
+
};
|
|
15
|
+
export type HubClientRegistryReleaseReceiveWriterParams = {
|
|
16
|
+
did: string;
|
|
17
|
+
token: symbol;
|
|
18
|
+
};
|
|
10
19
|
/**
|
|
11
20
|
* Tracks online clients and their live receive-channel writers. Subscription
|
|
12
21
|
* state is durable in the store, not here — the registry only routes live
|
|
@@ -39,12 +48,12 @@ export declare class HubClientRegistry {
|
|
|
39
48
|
* it. So a release names the binding it is releasing, and one that is no longer current is a
|
|
40
49
|
* no-op.
|
|
41
50
|
*/
|
|
42
|
-
bindReceiveWriter(did
|
|
51
|
+
bindReceiveWriter({ did, writer, endReceive }: HubClientRegistryBindReceiveWriterParams): {
|
|
43
52
|
token: symbol;
|
|
44
53
|
evicted: (() => void) | null;
|
|
45
54
|
};
|
|
46
55
|
/** Clear the binding this token names. A token that is no longer current is a no-op. */
|
|
47
|
-
releaseReceiveWriter(did
|
|
56
|
+
releaseReceiveWriter({ did, token }: HubClientRegistryReleaseReceiveWriterParams): void;
|
|
48
57
|
getClient(did: string): ClientEntry | undefined;
|
|
49
58
|
isOnline(did: string): boolean;
|
|
50
59
|
isWriterBound(did: string): boolean;
|
package/lib/registry.js
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* null the writer the new channel has just bound, leaving the DID online with nothing behind
|
|
48
48
|
* it. So a release names the binding it is releasing, and one that is no longer current is a
|
|
49
49
|
* no-op.
|
|
50
|
-
*/ bindReceiveWriter(did, writer, endReceive) {
|
|
50
|
+
*/ bindReceiveWriter({ did, writer, endReceive }) {
|
|
51
51
|
const entry = this.#clients.get(did);
|
|
52
52
|
if (entry == null) return {
|
|
53
53
|
token: Symbol('unbound'),
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
evicted
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
-
/** Clear the binding this token names. A token that is no longer current is a no-op. */ releaseReceiveWriter(did, token) {
|
|
66
|
+
/** Clear the binding this token names. A token that is no longer current is a no-op. */ releaseReceiveWriter({ did, token }) {
|
|
67
67
|
const entry = this.#clients.get(did);
|
|
68
68
|
if (entry == null || entry.token !== token) return;
|
|
69
69
|
entry.sendMessage = null;
|
package/lib/wake.js
CHANGED
|
@@ -31,12 +31,15 @@ const DEFAULT_DEBOUNCE_MS = 10_000;
|
|
|
31
31
|
const registration = await params.registry.get(did);
|
|
32
32
|
if (registration == null) return;
|
|
33
33
|
const body = sealWakeHint({
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
hint: {
|
|
35
|
+
topicID,
|
|
36
|
+
sequenceID,
|
|
37
|
+
count
|
|
38
|
+
},
|
|
39
|
+
recipient: {
|
|
40
|
+
publicKey: fromB64U(registration.publicKey),
|
|
41
|
+
authSecret: fromB64U(registration.authSecret)
|
|
42
|
+
}
|
|
40
43
|
});
|
|
41
44
|
const verdict = await params.sender.send({
|
|
42
45
|
registration,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kumiai/hub-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Hub server for Enkaku E2EE group message routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hub",
|
|
@@ -25,20 +25,21 @@
|
|
|
25
25
|
"lib/*"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@enkaku/protocol": "^0.
|
|
29
|
-
"@enkaku/server": "^0.
|
|
30
|
-
"@kokuin/capability": "^0.
|
|
31
|
-
"@kokuin/token": "^0.
|
|
32
|
-
"@kumiai/hub-protocol": "^0.
|
|
28
|
+
"@enkaku/protocol": "^0.23.0",
|
|
29
|
+
"@enkaku/server": "^0.23.0",
|
|
30
|
+
"@kokuin/capability": "^0.7.0",
|
|
31
|
+
"@kokuin/token": "^0.7.0",
|
|
32
|
+
"@kumiai/hub-protocol": "^0.13.0",
|
|
33
33
|
"@sozai/codec": "^0.4.4",
|
|
34
34
|
"@sozai/event": "^0.1.6",
|
|
35
35
|
"@sozai/log": "^0.4.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@enkaku/client": "^0.
|
|
39
|
-
"@enkaku/transport": "^0.
|
|
40
|
-
"@
|
|
41
|
-
"@kumiai/hub-
|
|
38
|
+
"@enkaku/client": "^0.23.0",
|
|
39
|
+
"@enkaku/transport": "^0.23.0",
|
|
40
|
+
"@kokuin/crypto": "^0.7.0",
|
|
41
|
+
"@kumiai/hub-conformance": "^0.13.0",
|
|
42
|
+
"@kumiai/hub-wake": "^0.13.0"
|
|
42
43
|
},
|
|
43
44
|
"publishConfig": {
|
|
44
45
|
"access": "public"
|