@kubun/plugin-p2p 0.10.1 → 0.11.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/context/delegation.js +130 -1
- package/lib/context/group.js +1319 -1
- package/lib/context/hub.js +150 -1
- package/lib/context/join.js +80 -1
- package/lib/context/sync.js +19 -1
- package/lib/context/types.d.ts +3 -3
- package/lib/context/types.js +28 -1
- package/lib/groups/admin-roster.d.ts +130 -0
- package/lib/groups/admin-roster.js +184 -0
- package/lib/groups/broadcast-codec.js +6 -1
- package/lib/groups/broadcast.d.ts +105 -91
- package/lib/groups/broadcast.js +627 -1
- package/lib/groups/circle-projection.d.ts +122 -0
- package/lib/groups/circle-projection.js +288 -0
- package/lib/groups/circle-reducers.d.ts +178 -0
- package/lib/groups/circle-reducers.js +198 -0
- package/lib/groups/events.d.ts +5 -1
- package/lib/groups/events.js +10 -1
- package/lib/groups/group-anchor.d.ts +63 -0
- package/lib/groups/group-anchor.js +107 -0
- package/lib/groups/group-crypto.d.ts +19 -0
- package/lib/groups/group-crypto.js +85 -0
- package/lib/groups/group-handle-registry.d.ts +61 -2
- package/lib/groups/group-handle-registry.js +243 -1
- package/lib/groups/group-handlers.d.ts +29 -0
- package/lib/groups/group-handlers.js +92 -0
- package/lib/groups/group-health-monitor.d.ts +11 -0
- package/lib/groups/group-health-monitor.js +139 -1
- package/lib/groups/group-mls.d.ts +35 -0
- package/lib/groups/group-mls.js +164 -0
- package/lib/groups/group-peer-manager.d.ts +108 -0
- package/lib/groups/group-peer-manager.js +344 -0
- package/lib/groups/group-protocols.d.ts +297 -0
- package/lib/groups/group-protocols.js +174 -0
- package/lib/groups/invite-payload.d.ts +1 -1
- package/lib/groups/invite-payload.js +26 -1
- package/lib/groups/join-utils.d.ts +1 -0
- package/lib/groups/join-utils.js +104 -1
- package/lib/groups/ledger-affected-events.d.ts +15 -0
- package/lib/groups/ledger-affected-events.js +96 -0
- package/lib/groups/ledger-entry.d.ts +37 -0
- package/lib/groups/ledger-entry.js +56 -0
- package/lib/groups/ledger-fold.d.ts +56 -0
- package/lib/groups/ledger-fold.js +53 -0
- package/lib/groups/manager.d.ts +20 -71
- package/lib/groups/manager.js +281 -1
- package/lib/groups/mls-codec.d.ts +1 -1
- package/lib/groups/mls-codec.js +18 -1
- package/lib/groups/mls-encryptor.d.ts +2 -2
- package/lib/groups/mls-encryptor.js +30 -1
- package/lib/groups/mls-group-handle.d.ts +1 -1
- package/lib/groups/mls-group-handle.js +10 -1
- package/lib/groups/mls-json.js +17 -1
- package/lib/groups/mls-receive-errors.d.ts +29 -0
- package/lib/groups/mls-receive-errors.js +42 -0
- package/lib/groups/mls-state.d.ts +1 -1
- package/lib/groups/mls-state.js +56 -1
- package/lib/groups/store-received-grant.d.ts +4 -1
- package/lib/groups/store-received-grant.js +116 -1
- package/lib/groups/store-received-revocation.d.ts +3 -3
- package/lib/groups/store-received-revocation.js +82 -1
- package/lib/hub/ack-backoff.d.ts +22 -0
- package/lib/hub/ack-backoff.js +88 -0
- package/lib/hub/did-observing-transport.d.ts +13 -2
- package/lib/hub/did-observing-transport.js +126 -1
- package/lib/hub/errors.d.ts +0 -6
- package/lib/hub/errors.js +33 -1
- package/lib/hub/http-client.d.ts +28 -3
- package/lib/hub/http-client.js +32 -1
- package/lib/hub/hub-like.d.ts +24 -0
- package/lib/hub/hub-like.js +535 -0
- package/lib/hub/manager.js +198 -1
- package/lib/hub/wiring.d.ts +17 -48
- package/lib/hub/wiring.js +132 -1
- package/lib/index.d.ts +17 -16
- package/lib/index.js +392 -1
- package/lib/protocol.d.ts +1 -1
- package/lib/protocol.js +251 -1
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +439 -2
- package/lib/sync/authorize.d.ts +1 -1
- package/lib/sync/authorize.js +73 -1
- package/lib/sync/broadcast-queue.js +183 -1
- package/lib/sync/broadcast-sender.js +179 -1
- package/lib/sync/catalog-scope.js +68 -1
- package/lib/sync/errors.d.ts +3 -2
- package/lib/sync/errors.js +19 -1
- package/lib/sync/forwarder.js +277 -1
- package/lib/sync/handlers.js +160 -1
- package/lib/sync/hub-tunnel-sync-listener.d.ts +8 -4
- package/lib/sync/hub-tunnel-sync-listener.js +172 -1
- package/lib/sync/hub-tunnel-sync-provider.d.ts +14 -14
- package/lib/sync/hub-tunnel-sync-provider.js +64 -1
- package/lib/sync/merkle-apply.d.ts +5 -0
- package/lib/sync/merkle-apply.js +194 -1
- package/lib/sync/merkle-channel.d.ts +2 -2
- package/lib/sync/merkle-channel.js +182 -1
- package/lib/sync/merkle-tree.js +120 -1
- package/lib/sync/peer-registry.d.ts +1 -1
- package/lib/sync/peer-registry.js +107 -1
- package/lib/sync/receive-access-gate.js +24 -1
- package/lib/sync/scope-resolver.js +41 -1
- package/lib/sync/sync-client.d.ts +3 -3
- package/lib/sync/sync-client.js +192 -1
- package/lib/sync/sync-manager.d.ts +2 -2
- package/lib/sync/sync-manager.js +272 -1
- package/lib/types.d.ts +11 -0
- package/lib/types.js +2 -1
- package/lib/util/mutex.js +10 -1
- package/lib/utils.js +21 -1
- package/package.json +47 -40
- package/lib/groups/broadcast-service.d.ts +0 -263
- package/lib/groups/broadcast-service.js +0 -1
- package/lib/groups/rejoin-codec.d.ts +0 -14
- package/lib/groups/rejoin-codec.js +0 -1
- package/lib/groups/wire-frame.d.ts +0 -34
- package/lib/groups/wire-frame.js +0 -1
- package/lib/hub/circle-catchup-requester.d.ts +0 -58
- package/lib/hub/circle-catchup-requester.js +0 -1
- package/lib/hub/circle-catchup-responder.d.ts +0 -50
- package/lib/hub/circle-catchup-responder.js +0 -1
- package/lib/hub/connection-pool.d.ts +0 -43
- package/lib/hub/connection-pool.js +0 -1
- package/lib/hub/epoch-stale-detector.d.ts +0 -18
- package/lib/hub/epoch-stale-detector.js +0 -1
- package/lib/hub/forward-remote-broadcast.d.ts +0 -15
- package/lib/hub/forward-remote-broadcast.js +0 -1
- package/lib/hub/group-channel.d.ts +0 -89
- package/lib/hub/group-channel.js +0 -1
- package/lib/hub/hub-connection.d.ts +0 -96
- package/lib/hub/hub-connection.js +0 -1
- package/lib/hub/receive-handler.d.ts +0 -51
- package/lib/hub/receive-handler.js +0 -1
- package/lib/hub/rejoin-manager.d.ts +0 -78
- package/lib/hub/rejoin-manager.js +0 -1
- package/lib/hub/rejoin-responder.d.ts +0 -32
- package/lib/hub/rejoin-responder.js +0 -1
- package/lib/hub/relay-manager.d.ts +0 -142
- package/lib/hub/relay-manager.js +0 -1
- package/lib/hub/send-handler.d.ts +0 -40
- package/lib/hub/send-handler.js +0 -1
- package/lib/hub/tunnel-inbox.d.ts +0 -20
- package/lib/hub/tunnel-inbox.js +0 -1
- package/lib/hub/wait-for-gate.d.ts +0 -14
- package/lib/hub/wait-for-gate.js +0 -1
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { Disposer } from '@enkaku/async';
|
|
2
|
-
import { EventEmitter } from '@enkaku/event';
|
|
3
|
-
import type { StoredMessage } from '@enkaku/hub-protocol';
|
|
4
|
-
import type { Logger } from '@kubun/logger';
|
|
5
|
-
import type { HubConnectionPool } from './connection-pool.js';
|
|
6
|
-
import type { ReceivedHubMessage } from './receive-handler.js';
|
|
7
|
-
export type TunnelMessageStream = AsyncIterable<StoredMessage> & {
|
|
8
|
-
return(): void;
|
|
9
|
-
};
|
|
10
|
-
export type TunnelMessageStreamParams = {
|
|
11
|
-
peerDID: string;
|
|
12
|
-
};
|
|
13
|
-
export type SubscriptionHandler = (message: ReceivedHubMessage) => Promise<{
|
|
14
|
-
ack: boolean;
|
|
15
|
-
}>;
|
|
16
|
-
export type ConnectionLifecycleEvent = {
|
|
17
|
-
type: 'connected';
|
|
18
|
-
} | {
|
|
19
|
-
type: 'disconnected';
|
|
20
|
-
reason?: string;
|
|
21
|
-
} | {
|
|
22
|
-
type: 'error';
|
|
23
|
-
error: unknown;
|
|
24
|
-
} | {
|
|
25
|
-
type: 'reconnecting';
|
|
26
|
-
attempt: number;
|
|
27
|
-
delayMs: number;
|
|
28
|
-
};
|
|
29
|
-
type HubConnectionEvents = {
|
|
30
|
-
lifecycle: ConnectionLifecycleEvent;
|
|
31
|
-
};
|
|
32
|
-
export type HubConnectionParams = {
|
|
33
|
-
hubURL: string;
|
|
34
|
-
deviceID: string;
|
|
35
|
-
pool: HubConnectionPool;
|
|
36
|
-
logger?: Logger;
|
|
37
|
-
ackFlushMs?: number;
|
|
38
|
-
ackFlushMax?: number;
|
|
39
|
-
backoffBaseMs?: number;
|
|
40
|
-
backoffMaxMs?: number;
|
|
41
|
-
backoffJitter?: number;
|
|
42
|
-
};
|
|
43
|
-
export declare class HubConnection extends Disposer {
|
|
44
|
-
#private;
|
|
45
|
-
constructor(params: HubConnectionParams);
|
|
46
|
-
get events(): EventEmitter<HubConnectionEvents>;
|
|
47
|
-
open(): Promise<void>;
|
|
48
|
-
subscribe(groupID: string, credential: string, handler: SubscriptionHandler): Promise<void>;
|
|
49
|
-
unsubscribe(groupID: string): Promise<void>;
|
|
50
|
-
broadcast(groupID: string, encryptedPayload: string, opts?: {
|
|
51
|
-
timeoutMs?: number;
|
|
52
|
-
}): Promise<void>;
|
|
53
|
-
/**
|
|
54
|
-
* Send an opaque tunnel-style message to explicit recipients via
|
|
55
|
-
* `hub/send`. No `groupID` is attached at the hub level — recipients
|
|
56
|
-
* receive the message on the tunnel-routing path (see
|
|
57
|
-
* {@link tunnelMessageStream}). Used by hub-mediated document sync.
|
|
58
|
-
*/
|
|
59
|
-
send(params: {
|
|
60
|
-
recipients: Array<string>;
|
|
61
|
-
payload: Uint8Array;
|
|
62
|
-
timeoutMs?: number;
|
|
63
|
-
}): Promise<{
|
|
64
|
-
sequenceID: string;
|
|
65
|
-
}>;
|
|
66
|
-
/**
|
|
67
|
-
* Run `fn` inside the same per-group mutex used by receive `#dispatch`. This
|
|
68
|
-
* lets callers (e.g. `GroupChannel.broadcast`) serialize MLS-state-mutating
|
|
69
|
-
* pipelines with incoming-message handling on the same group, avoiding races
|
|
70
|
-
* between concurrent send-encrypt and receive-decrypt against shared MLS
|
|
71
|
-
* group state.
|
|
72
|
-
*
|
|
73
|
-
* The gate-await ensures the subscription map is settled before lookup —
|
|
74
|
-
* i.e. mid-reconnect callers wait for the subscription to be (re)registered
|
|
75
|
-
* rather than throwing.
|
|
76
|
-
*/
|
|
77
|
-
runInGroup<T>(groupID: string, fn: () => Promise<T>, opts?: {
|
|
78
|
-
timeoutMs?: number;
|
|
79
|
-
}): Promise<T>;
|
|
80
|
-
/**
|
|
81
|
-
* Register a per-peer tunnel inbox and return a disposable async iterable
|
|
82
|
-
* of {@link StoredMessage} values received from that peer over the tunnel
|
|
83
|
-
* (i.e. direct hub messages with no `groupID`).
|
|
84
|
-
*
|
|
85
|
-
* Used by the hub-mediated document sync server adapter (see Q3 of the
|
|
86
|
-
* hub-mediated document sync plan). The hub's receive-side dispatcher
|
|
87
|
-
* keys tunnel routing on `senderDID` because hub-level messages cannot
|
|
88
|
-
* reveal the in-frame `sessionID` (the body is encrypted). P2P pairs
|
|
89
|
-
* `peerDID` and `sessionID` at session creation, so peer-keying is
|
|
90
|
-
* sufficient. Concurrent tunnel sessions to the same peer are not
|
|
91
|
-
* supported and are the caller's responsibility to avoid.
|
|
92
|
-
*/
|
|
93
|
-
tunnelMessageStream({ peerDID }: TunnelMessageStreamParams): TunnelMessageStream;
|
|
94
|
-
close(): Promise<void>;
|
|
95
|
-
}
|
|
96
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Disposer as e}from"@enkaku/async";import{fromB64 as t,toB64 as s}from"@enkaku/codec";import{EventEmitter as n}from"@enkaku/event";import{createMutex as i}from"../util/mutex.js";import{ReconnectingError as r}from"./errors.js";import{AckBatch as o,abortableSleep as h,computeBackoff as a}from"./group-channel.js";import{TunnelInbox as c}from"./tunnel-inbox.js";import{waitForGate as l}from"./wait-for-gate.js";let u=Symbol("disposed");export class HubConnection extends e{#e;#t;#s;#n;#i;#r;#o;#h;#a;#c=null;#l=null;#u=null;#b=null;#p=null;#d=new Map;#f=!1;#g=0;#m=null;#y=new Map;#w;constructor(e){super({dispose:async()=>{await this.#L()}}),this.#e=e.hubURL,this.#t=e.deviceID,this.#s=e.pool,this.#n=e.logger,this.#i=e.ackFlushMs??500,this.#r=e.ackFlushMax??10,this.#o=e.backoffBaseMs??1e3,this.#h=e.backoffMaxMs??6e4,this.#a=e.backoffJitter??.25,this.#w=new n}get events(){return this.#w}async #I(e){try{await this.#w.emit("lifecycle",e)}catch(t){this.#n?.warn("hub-connection lifecycle subscriber threw",{hubURL:this.#e,type:e.type,error:t})}}async open(){if(this.#f)throw Error("HubConnection already opened");try{await this.#k(),this.#f=!0,await this.#I({type:"connected"})}catch(e){if(null!=this.#l){try{await this.#s.release(this.#e,this.#l)}catch(e){this.#n?.debug("hub-connection pool release after open failure failed",{hubURL:this.#e,error:e})}this.#l=null}throw this.#c=null,e}}async #k(){if(null!=this.#l){let e=this.#l;this.#l=null,this.#c=null;try{await this.#s.release(this.#e,e)}catch(e){this.#n?.debug("hub-connection pool release before reconnect failed",{hubURL:this.#e,error:e})}}let{client:e,refID:t}=await this.#s.acquire(this.#e);this.#c=e,this.#l=t;let s=e.createChannel("hub/receive",{param:{},signal:this.signal});this.#u=s,s.catch(()=>{}),this.#b=new o({flushMs:this.#i,flushMax:this.#r,send:async e=>{await s.send({ack:e})},logger:this.#n}),this.#p=this.#R(e).catch(e=>{this.#n?.debug("hub-connection receive task tail error",{hubURL:this.#e,error:e})});let n=[...this.#d.entries()];if(n.length>0){let t=await Promise.allSettled(n.map(async([t,s])=>{await e.request("hub/group/join",{param:{groupID:t,credential:s.credential}})}));for(let e=0;e<t.length;e++){let s=t[e];if("rejected"===s.status){let t=n[e][0];this.#n?.warn("hub-connection rejoin failed",{hubURL:this.#e,groupID:t,error:s.reason})}}}}async subscribe(e,t,s){if(await l(this.#m,void 0),!this.#f||null==this.#c)throw Error("HubConnection not open");if(this.#d.has(e))throw Error(`already subscribed to group ${e}`);await this.#c.request("hub/group/join",{param:{groupID:e,credential:t}}),this.#d.set(e,{credential:t,handler:s,mutex:i()})}async unsubscribe(e){await l(this.#m,void 0);let t=this.#c;if(!this.#f||null==t)throw Error("HubConnection not open");let s=this.#d.get(e);if(null==s)throw Error(`not subscribed to group ${e}`);await s.mutex.run(async()=>{}),await t.request("hub/group/leave",{param:{groupID:e}}),this.#d.delete(e)}async broadcast(e,t,s){if(await l(this.#m,s?.timeoutMs,()=>new r(this.#e,this.#g)),!this.#f||null==this.#c)throw Error("HubConnection not open");await this.#c.request("hub/group/send",{param:{groupID:e,payload:t}})}async send(e){if(await l(this.#m,e.timeoutMs,()=>new r(this.#e,this.#g)),!this.#f||null==this.#c)throw Error("HubConnection not open");return{sequenceID:(await this.#c.request("hub/send",{param:{recipients:e.recipients,payload:s(e.payload)}})).sequenceID}}async runInGroup(e,t,s){if(await l(this.#m,s?.timeoutMs,()=>new r(this.#e,this.#g)),!this.#f)throw Error("HubConnection not open");let n=this.#d.get(e);if(null==n)throw Error(`not subscribed to group ${e}`);return n.mutex.run(t)}tunnelMessageStream({peerDID:e}){if(this.#y.has(e))throw Error("tunnelMessageStream: peerDID already registered");let t=new c,s={inbox:t};this.#y.set(e,s);let n=!1;return{[Symbol.asyncIterator]:()=>t.iterator(),return:()=>{n||(n=!0,this.#y.get(e)===s&&this.#y.delete(e),t.close())}}}async #R(e){let t=this.#u;if(null==t)return;let s=t.readable.getReader(),n=null;try{for(;!this.signal.aborted;){let t=s.read(),n=e.disposed.then(()=>u),i=await Promise.race([t,n]);if("symbol"==typeof i){this.#n?.debug("hub-connection receive loop observed hub client disposal",{hubURL:this.#e});break}let{done:r,value:o}=i;if(r)break;this.#D(o),this.#g=0}}catch(e){n=e,this.#n?.warn("hub-connection receive loop error",{hubURL:this.#e,error:e})}finally{try{s.releaseLock()}catch{}}if(!this.signal.aborted){let e=n instanceof Error&&n.message?n.message:"receive loop ended";await this.#I({type:"disconnected",reason:e}),this.#U()}}#U(){this.#p=this.#M().catch(e=>{this.#n?.debug("hub-connection reconnect task tail error",{hubURL:this.#e,error:e})})}async #M(){let e=()=>{};this.#m=new Promise(t=>{e=t});try{for(;!this.signal.aborted;){let e=a(this.#g++,this.#o,this.#h,this.#a);this.#n?.debug("hub-connection reconnect scheduled",{hubURL:this.#e,delay:e,attempt:this.#g});try{await h(e,this.signal)}catch{return}await this.#I({type:"reconnecting",attempt:this.#g,delayMs:e});try{this.#b?.dispose(),this.#b=null,this.#u=null,await this.#k(),this.#g=0,await this.#I({type:"connected"});return}catch(e){this.#n?.warn("hub-connection reconnect attempt failed",{hubURL:this.#e,attempt:this.#g,error:e}),await this.#I({type:"error",error:e})}}}finally{this.#m=null,e()}}#D(e){if(null==e.groupID){let s=this.#y.get(e.senderDID);if(null!=s){let n={sequenceID:e.sequenceID,senderDID:e.senderDID,payload:t(e.payload)};s.inbox.push(n),this.#b?.add(e.sequenceID);return}this.#n?.debug("direct message received, no handler",{hubURL:this.#e,sequenceID:e.sequenceID}),this.#b?.add(e.sequenceID);return}let s=this.#d.get(e.groupID);null==s?this.#n?.debug("message for unsubscribed group",{hubURL:this.#e,groupID:e.groupID,sequenceID:e.sequenceID}):s.mutex.run(()=>s.handler(e)).then(t=>{t.ack&&this.#b?.add(e.sequenceID)}).catch(t=>{this.#n?.warn("hub-connection subscription handler error",{hubURL:this.#e,groupID:e.groupID,sequenceID:e.sequenceID,error:t})})}async close(){this.#f&&await this.dispose()}async #L(){if(!this.#f)return;let e=this.#b;if(this.#b=null,null!=e){try{await e.flush()}catch(e){this.#n?.debug("hub-connection ack flush during close failed",{hubURL:this.#e,error:e})}e.dispose()}try{this.#u?.close()}catch{}try{await this.#p}catch{}let t=[...this.#d.values()].map(e=>e.mutex.run(async()=>{}).catch(()=>{}));if(await Promise.allSettled(t),null!=this.#l){try{await this.#s.release(this.#e,this.#l)}catch(e){this.#n?.debug("hub-connection pool release during close failed",{hubURL:this.#e,error:e})}this.#l=null}for(let e of(this.#c=null,this.#u=null,this.#p=null,this.#d.clear(),this.#y.values()))e.inbox.close();this.#y.clear(),this.#f=!1}}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@kubun/logger';
|
|
2
|
-
import { type BroadcastService, type ReceiveBroadcastResult } from '../groups/broadcast-service.js';
|
|
3
|
-
/**
|
|
4
|
-
* Shape of a message pushed by `hub/receive`. Matches `@enkaku/hub-protocol`
|
|
5
|
-
* server-to-client push payload.
|
|
6
|
-
*/
|
|
7
|
-
export type ReceivedHubMessage = {
|
|
8
|
-
sequenceID: string;
|
|
9
|
-
senderDID: string;
|
|
10
|
-
groupID?: string;
|
|
11
|
-
/** base64-encoded encrypted MLS application message */
|
|
12
|
-
payload: string;
|
|
13
|
-
};
|
|
14
|
-
export type HandleReceivedMessageParams = {
|
|
15
|
-
groupID: string;
|
|
16
|
-
message: ReceivedHubMessage;
|
|
17
|
-
broadcastService: BroadcastService;
|
|
18
|
-
logger?: Logger;
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Classification of a single received message's processing result. Drives both
|
|
22
|
-
* the ack decision and the epoch-stale health signal (see `EpochStaleDetector`):
|
|
23
|
-
* - `applied` — decrypted/applied cleanly; the device is at the current epoch.
|
|
24
|
-
* - `decrypt-failed` — could not derive the key for the current epoch.
|
|
25
|
-
* - `commit-out-of-order` — a Commit ahead of the local epoch (gap in stream).
|
|
26
|
-
* - `apply-failed` — decrypted but failed to apply to the local DB (transient).
|
|
27
|
-
* - `error` — unexpected failure.
|
|
28
|
-
* - `skipped` — not for this group; no processing attempted.
|
|
29
|
-
* - `control` — a rejoin control frame (request/groupinfo); processed cleanly
|
|
30
|
-
* but NOT a health indicator (must not reset epoch-stale tracking).
|
|
31
|
-
*/
|
|
32
|
-
export type ReceiveOutcome = 'applied' | 'decrypt-failed' | 'commit-out-of-order' | 'apply-failed' | 'error' | 'skipped' | 'control';
|
|
33
|
-
export type HandleReceivedMessageResult = {
|
|
34
|
-
ack: boolean;
|
|
35
|
-
outcome: ReceiveOutcome;
|
|
36
|
-
sequenceID: string;
|
|
37
|
-
/** The processed result, present on success — lets the channel route rejoin
|
|
38
|
-
* control frames to the responder/requester. */
|
|
39
|
-
result?: ReceiveBroadcastResult;
|
|
40
|
-
};
|
|
41
|
-
/**
|
|
42
|
-
* Apply a single hub-received message for a group.
|
|
43
|
-
*
|
|
44
|
-
* Returns `{ ack: true }` when the message has been processed to a state where
|
|
45
|
-
* redelivery serves no purpose (applied successfully, or decrypt failed with
|
|
46
|
-
* no recovery path). Returns `{ ack: false }` when the hub should redeliver
|
|
47
|
-
* (transient apply failure or unexpected error). Skips with `{ ack: false }`
|
|
48
|
-
* on groupID mismatch so the message stays queued for whichever recipient it
|
|
49
|
-
* was intended for. The `outcome` classifies the result for health tracking.
|
|
50
|
-
*/
|
|
51
|
-
export declare function handleReceivedMessage(params: HandleReceivedMessageParams): Promise<HandleReceivedMessageResult>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{fromB64 as e}from"@enkaku/codec";import{ApplyError as r,CommitOutOfOrderError as o,DecryptError as a,MalformedControlError as c}from"../groups/broadcast-service.js";export async function handleReceivedMessage(i){let t,{groupID:n,message:s,broadcastService:p,logger:u}=i,d=s.sequenceID;if(s.groupID!==n)return u?.debug("hub message groupID mismatch, skipping",{groupID:n,messageGroupID:s.groupID,sequenceID:d}),{ack:!1,outcome:"skipped",sequenceID:d};let l=e(s.payload);try{t=await p.processReceived({groupID:n,encrypted:l})}catch(e){if(e instanceof c)return u?.debug("malformed control frame, acking and skipping",{groupID:n,sequenceID:d,error:e}),{ack:!0,outcome:"control",sequenceID:d};if(e instanceof a)return u?.debug("decrypt failed, acking and skipping",{groupID:n,sequenceID:d,error:e}),{ack:!0,outcome:"decrypt-failed",sequenceID:d};if(e instanceof o)return u?.debug("mls commit out of order, will redeliver after gap fills",{groupID:n,sequenceID:d,messageEpoch:e.messageEpoch.toString(),handleEpoch:e.handleEpoch.toString()}),{ack:!1,outcome:"commit-out-of-order",sequenceID:d};if(e instanceof r)return u?.error("broadcast apply failed, will redeliver",{groupID:n,sequenceID:d,error:e}),{ack:!1,outcome:"apply-failed",sequenceID:d};return u?.error("unexpected error processing hub message",{groupID:n,sequenceID:d,error:e}),{ack:!1,outcome:"error",sequenceID:d}}return{ack:!0,outcome:"rejoin-request"===t.kind||"rejoin-groupinfo"===t.kind?"control":"applied",sequenceID:d,result:t}}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { type GroupHandle, type MemberCredential } from '@enkaku/group';
|
|
2
|
-
import type { OwnIdentity } from '@enkaku/token';
|
|
3
|
-
import type { Logger } from '@kubun/logger';
|
|
4
|
-
import type { GroupHealthMonitor } from '../groups/group-health-monitor.js';
|
|
5
|
-
import type { RejoinResult } from '../types.js';
|
|
6
|
-
export type JoinExternalResult = {
|
|
7
|
-
/** Framed MLSMessage(Commit) bytes to broadcast to existing members. */
|
|
8
|
-
commitMessage: Uint8Array;
|
|
9
|
-
/** Optimistic post-commit handle (NOT yet persisted). */
|
|
10
|
-
pendingHandle: GroupHandle;
|
|
11
|
-
};
|
|
12
|
-
export type RejoinManagerParams = {
|
|
13
|
-
groupID: string;
|
|
14
|
-
identity: OwnIdentity;
|
|
15
|
-
/** Read the stale handle's cached MemberCredential (reused for the rejoin). */
|
|
16
|
-
readCredential: () => Promise<MemberCredential>;
|
|
17
|
-
/** Fresh correlation id per request (incl. retries). */
|
|
18
|
-
genRequestID: () => string;
|
|
19
|
-
/** Broadcast a `rejoin-request` on the group channel. */
|
|
20
|
-
sendRequest: (requestID: string) => Promise<void>;
|
|
21
|
-
/** Broadcast the resync Commit (`mls` frame) to existing members. */
|
|
22
|
-
sendCommit: (commitBytes: Uint8Array) => Promise<void>;
|
|
23
|
-
/** Atomically swap the cached handle for the confirmed one (registry). */
|
|
24
|
-
replaceHandle: (handle: GroupHandle) => Promise<void>;
|
|
25
|
-
monitor: GroupHealthMonitor;
|
|
26
|
-
/**
|
|
27
|
-
* Test seam: defaults to `@enkaku/group` `joinGroupExternal({resync:true})`
|
|
28
|
-
* built from `identity` + `readCredential`. Override to skip real MLS.
|
|
29
|
-
*/
|
|
30
|
-
joinExternal?: (groupInfo: Uint8Array) => Promise<JoinExternalResult>;
|
|
31
|
-
/** Test seam: defaults to `@enkaku/group` `inspectGroupInfo`. */
|
|
32
|
-
inspectGroupInfo?: (groupInfo: Uint8Array) => {
|
|
33
|
-
epoch: bigint;
|
|
34
|
-
treeHash: Uint8Array;
|
|
35
|
-
};
|
|
36
|
-
requestTimeoutMs?: number;
|
|
37
|
-
confirmTimeoutMs?: number;
|
|
38
|
-
/** Delay between the candidate-head probe and its re-probe (settle window). */
|
|
39
|
-
confirmSettleMs?: number;
|
|
40
|
-
maxAttempts?: number;
|
|
41
|
-
backoffMs?: (attempt: number) => number;
|
|
42
|
-
logger?: Logger;
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Drives a stale device's MLS epoch recovery for one group: per attempt,
|
|
46
|
-
* request GroupInfo → `joinGroupExternal({resync:true})` → broadcast the resync
|
|
47
|
-
* Commit → confirm → `replaceHandle`.
|
|
48
|
-
*
|
|
49
|
-
* Confirm is a tree-hash compare against the current head, because the hub never
|
|
50
|
-
* echoes the sender's own Commit. Three guards keep it reliable under concurrent
|
|
51
|
-
* rejoins (a resync Commit is canonical only for an instant):
|
|
52
|
-
* - behind-responder filter: a probe reply below our epoch can't confirm us.
|
|
53
|
-
* - chase-the-head: a reply above our epoch means we lost the race; re-join the
|
|
54
|
-
* newer GroupInfo next attempt (`{resync:true}` rebuilds the tree — no replay).
|
|
55
|
-
* - settle re-probe: re-probe after a delay and require the head still ours
|
|
56
|
-
* before swapping. Shrinks (not closes) the transient-head window — a Commit
|
|
57
|
-
* landing in the gap leaves us one epoch behind, which detect→rejoin catches.
|
|
58
|
-
* A fall-behind, never a silent fork; a wrong `recovered` is unreachable.
|
|
59
|
-
*
|
|
60
|
-
* Retries capped; exhaustion → `recovery-failed`.
|
|
61
|
-
*/
|
|
62
|
-
export declare class RejoinManager {
|
|
63
|
-
#private;
|
|
64
|
-
constructor(params: RejoinManagerParams);
|
|
65
|
-
get inFlight(): boolean;
|
|
66
|
-
/**
|
|
67
|
-
* Route a received `rejoin-groupinfo`: resolve the matching in-flight waiter.
|
|
68
|
-
* Unknown `requestID`s (peers' requests, stale responses) are ignored.
|
|
69
|
-
*/
|
|
70
|
-
onGroupInfo(requestID: string, groupInfo: Uint8Array): void;
|
|
71
|
-
/** Run the recovery flow. Idempotent: a concurrent call returns the loser early. */
|
|
72
|
-
rejoin(): Promise<RejoinResult>;
|
|
73
|
-
/**
|
|
74
|
-
* Stop a running rejoin and release timers. Idempotent; the loop checks
|
|
75
|
-
* `#disposed` between attempts, pending waiters resolve `null`. On channel close.
|
|
76
|
-
*/
|
|
77
|
-
dispose(): void;
|
|
78
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{inspectGroupInfo as e,joinGroupExternal as t}from"@enkaku/group";import{bytesEqual as r,sleep as i,unref as s}from"../utils.js";export class RejoinManager{#e;#t;#r;#i;#s;#o;#n;#a;#l;#u;#h;#c;#p;#m;#d=new Map;#f=!1;#g=!1;constructor(r){this.#e=r.groupID,this.#t=r.genRequestID,this.#r=r.sendRequest,this.#i=r.sendCommit,this.#s=r.replaceHandle,this.#o=r.monitor,this.#n=r.joinExternal??(async e=>{let i=await r.readCredential(),{commitMessage:s,group:o}=await t({identity:r.identity,groupInfo:e,credential:i,resync:!0});return{commitMessage:s,pendingHandle:o}}),this.#a=r.inspectGroupInfo??e,this.#l=r.requestTimeoutMs??5e3,this.#u=r.confirmTimeoutMs??5e3,this.#h=r.confirmSettleMs??300,this.#c=r.maxAttempts??8,this.#p=r.backoffMs??o,this.#m=r.logger}get inFlight(){return this.#f}onGroupInfo(e,t){let r=this.#d.get(e);null!=r&&(this.#d.delete(e),r.resolve(t))}async rejoin(){if(this.#f)return{status:"recovery-failed"};this.#f=!0,this.#o.markRecovering(this.#e);try{for(let e=0;e<this.#c&&!this.#g&&(await i(this.#p(e)),!this.#g);e++){let e=await this.#w();if("recovered"===e)return this.#o.markRecovered(this.#e),{status:"recovered"}}return this.#o.markRecoveryFailed(this.#e),{status:"recovery-failed"}}finally{this.#f=!1}}async #w(){let e,t=await this.#I(this.#l);if(null==t)return"retry";try{e=await this.#n(t)}catch(e){return this.#m?.warn("rejoin: joinGroupExternal failed",{groupID:this.#e,error:e}),"retry"}let{commitMessage:s,pendingHandle:o}=e,n=o.epoch,a=o.treeHash;try{await this.#i(s)}catch(e){return this.#m?.warn("rejoin: resync Commit send failed",{groupID:this.#e,error:e}),"retry"}let l=await this.#y(n);if(null==l||!(l.epoch===n&&r(l.treeHash,a)))return"retry";await i(this.#h);let u=await this.#y(n);return null==u?"retry":u.epoch===n&&r(u.treeHash,a)?(await this.#s(o),"recovered"):"retry"}async #y(e){let t,r=await this.#I(this.#u);if(null==r)return null;try{t=this.#a(r)}catch(e){return this.#m?.warn("rejoin: confirm GroupInfo inspect failed",{groupID:this.#e,error:e}),null}return t.epoch<e?null:t}async #I(e){let t=this.#t(),r=this.#M(t,e);try{await this.#r(t)}catch(e){return this.#D(t),this.#m?.warn("rejoin: request send failed",{groupID:this.#e,error:e}),null}return r}#M(e,t){return new Promise(r=>{let i=setTimeout(()=>{this.#d.delete(e),r(null)},t);s(i),this.#d.set(e,{resolve:e=>{clearTimeout(i),r(e)},cancel:()=>{clearTimeout(i),r(null)}})})}#D(e){this.#d.delete(e)}dispose(){for(let e of(this.#g=!0,this.#d.values()))e.cancel();this.#d.clear()}}function o(e){return Math.floor(Math.random()*Math.min(500*2**(e-1),5e3))}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@kubun/logger';
|
|
2
|
-
export type RejoinResponderParams = {
|
|
3
|
-
groupID: string;
|
|
4
|
-
/** Produce a fresh GroupInfo for this group (current MLS handle). */
|
|
5
|
-
exportGroupInfo: () => Promise<Uint8Array>;
|
|
6
|
-
/** Broadcast a `rejoin-groupinfo` reply to the group channel. */
|
|
7
|
-
sendGroupInfo: (requestID: string, groupInfo: Uint8Array) => Promise<void>;
|
|
8
|
-
/** Jitter delay (ms) before replying. Defaults to a random 0..250ms. */
|
|
9
|
-
nextJitterMs?: () => number;
|
|
10
|
-
/** How long a `requestID` stays suppressed after being answered. */
|
|
11
|
-
suppressTTLMs?: number;
|
|
12
|
-
logger?: Logger;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Answers `rejoin-request`s with a fresh GroupInfo. Every member could reply, so
|
|
16
|
-
* jitter (random pre-reply delay) + suppression (cancel on seeing a peer's reply
|
|
17
|
-
* for the same `requestID`) collapse the storm to ~1 reply. Keyed on `requestID`
|
|
18
|
-
* (fresh per attempt); suppression entries TTL out to bound memory.
|
|
19
|
-
*
|
|
20
|
-
* Suppression collapses only duplicates of one `requestID`, so distinct
|
|
21
|
-
* `requestID`s still draw one reply each — an amplification. A per-window reply
|
|
22
|
-
* cap is tracked in `docs/agents/plans/backlog/rejoin-frame-authentication.md`.
|
|
23
|
-
*/
|
|
24
|
-
export declare class RejoinResponder {
|
|
25
|
-
#private;
|
|
26
|
-
constructor(params: RejoinResponderParams);
|
|
27
|
-
/** Handle an inbound `rejoin-request`: schedule a jittered, suppressible reply. */
|
|
28
|
-
onRejoinRequest(requestID: string): void;
|
|
29
|
-
/** Handle an observed `rejoin-groupinfo` (own or peer's): suppress this request. */
|
|
30
|
-
onRejoinGroupInfoSeen(requestID: string): void;
|
|
31
|
-
dispose(): void;
|
|
32
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{unref as e}from"../utils.js";export class RejoinResponder{#e;#s;#t;#r;#p;#o;#i=new Map;#n=new Map;constructor(e){this.#e=e.groupID,this.#s=e.exportGroupInfo,this.#t=e.sendGroupInfo,this.#r=e.nextJitterMs??(()=>Math.floor(250*Math.random())),this.#p=e.suppressTTLMs??5e3,this.#o=e.logger}onRejoinRequest(s){if(this.#n.has(s)||this.#i.has(s))return;let t=setTimeout(()=>{this.#i.delete(s),this.#u(s)},this.#r());e(t),this.#i.set(s,t)}onRejoinGroupInfoSeen(e){let s=this.#i.get(e);null!=s&&(clearTimeout(s),this.#i.delete(e)),this.#h(e)}dispose(){for(let e of this.#i.values())clearTimeout(e);for(let e of(this.#i.clear(),this.#n.values()))clearTimeout(e);this.#n.clear()}async #u(e){if(!this.#n.has(e)){this.#h(e);try{let s=await this.#s();await this.#t(e,s)}catch(s){this.#o?.warn("rejoin responder failed to reply",{groupID:this.#e,requestID:e,error:s})}}}#h(s){let t=this.#n.get(s);null!=t&&clearTimeout(t);let r=setTimeout(()=>{this.#n.delete(s)},this.#p);e(r),this.#n.set(s,r)}}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import type { Client } from '@enkaku/client';
|
|
2
|
-
import { EventEmitter } from '@enkaku/event';
|
|
3
|
-
import type { HubProtocol } from '@enkaku/hub-protocol';
|
|
4
|
-
import type { OwnIdentity } from '@enkaku/token';
|
|
5
|
-
import type { Logger } from '@kubun/logger';
|
|
6
|
-
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
7
|
-
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
8
|
-
import type { BroadcastService } from '../groups/broadcast-service.js';
|
|
9
|
-
import type { GroupHealthMonitor } from '../groups/group-health-monitor.js';
|
|
10
|
-
import type { RejoinResult } from '../types.js';
|
|
11
|
-
import { type OnServerDIDObserved } from './connection-pool.js';
|
|
12
|
-
import type { ServerDIDMismatchObserver, ServerDIDObserver } from './did-observing-transport.js';
|
|
13
|
-
import { GroupChannel, type GroupChannelParams } from './group-channel.js';
|
|
14
|
-
import { HubConnection, type HubConnectionParams } from './hub-connection.js';
|
|
15
|
-
export type CreateHubClientOptions = {
|
|
16
|
-
/**
|
|
17
|
-
* Per-call observer fired once when the first signed response arrives. Set
|
|
18
|
-
* by `HubConnectionPool` so the pool can route the observed server DID
|
|
19
|
-
* through its `onServerDIDObserved` handler with the hub URL bound. Factories
|
|
20
|
-
* that don't honour this opt simply skip TOFU capture for pool-spawned
|
|
21
|
-
* clients.
|
|
22
|
-
*/
|
|
23
|
-
onServerDID?: ServerDIDObserver;
|
|
24
|
-
/**
|
|
25
|
-
* Pinned `server_did` resolved by the pool from the local hub row at spawn
|
|
26
|
-
* time. When set, the underlying `DIDObservingTransport` enforces it: a
|
|
27
|
-
* non-matching first response raises `HubServerDIDMismatchError` and no
|
|
28
|
-
* message is delivered to the inner Client (hard-fail path). When unset,
|
|
29
|
-
* the wrapper falls back to TOFU capture via `onServerDID`.
|
|
30
|
-
*/
|
|
31
|
-
expectedServerDID?: string;
|
|
32
|
-
/**
|
|
33
|
-
* Side-channel notifier fired when a pinned-DID mismatch is detected. Lets
|
|
34
|
-
* the pool log + evict on top of the typed error that propagates through
|
|
35
|
-
* the read loop.
|
|
36
|
-
*/
|
|
37
|
-
onMismatch?: ServerDIDMismatchObserver;
|
|
38
|
-
};
|
|
39
|
-
export type CreateHubClient = (hubURL: string, opts?: CreateHubClientOptions) => Promise<Client<HubProtocol>>;
|
|
40
|
-
export type CreateGroupChannel = (params: GroupChannelParams) => GroupChannel;
|
|
41
|
-
export type CreateHubConnection = (params: HubConnectionParams) => HubConnection;
|
|
42
|
-
export type RelayEvent = {
|
|
43
|
-
type: 'connected';
|
|
44
|
-
hubURL: string;
|
|
45
|
-
groupIDs: Array<string>;
|
|
46
|
-
} | {
|
|
47
|
-
type: 'disconnected';
|
|
48
|
-
hubURL: string;
|
|
49
|
-
groupIDs: Array<string>;
|
|
50
|
-
reason?: string;
|
|
51
|
-
} | {
|
|
52
|
-
type: 'error';
|
|
53
|
-
hubURL: string;
|
|
54
|
-
groupIDs: Array<string>;
|
|
55
|
-
error: unknown;
|
|
56
|
-
} | {
|
|
57
|
-
type: 'reconnecting';
|
|
58
|
-
hubURL: string;
|
|
59
|
-
groupIDs: Array<string>;
|
|
60
|
-
attempt: number;
|
|
61
|
-
delayMs: number;
|
|
62
|
-
};
|
|
63
|
-
type RelayManagerEvents = {
|
|
64
|
-
relay: RelayEvent;
|
|
65
|
-
};
|
|
66
|
-
export type HubRelayManagerParams = {
|
|
67
|
-
p2pStore: P2PStoreAPI;
|
|
68
|
-
broadcastService: BroadcastService;
|
|
69
|
-
deviceID: string;
|
|
70
|
-
createHubClient: CreateHubClient;
|
|
71
|
-
logger?: Logger;
|
|
72
|
-
ackFlushMs?: number;
|
|
73
|
-
ackFlushMax?: number;
|
|
74
|
-
backoffBaseMs?: number;
|
|
75
|
-
backoffMaxMs?: number;
|
|
76
|
-
backoffJitter?: number;
|
|
77
|
-
broadcastTimeoutMs?: number;
|
|
78
|
-
/** When provided, receive outcomes drive epoch-stale health detection. */
|
|
79
|
-
monitor?: GroupHealthMonitor;
|
|
80
|
-
/** Device identity — enables the rejoin flow (needs `joinGroupExternal`). */
|
|
81
|
-
identity?: OwnIdentity;
|
|
82
|
-
/** Fresh correlation ids for rejoin requests. */
|
|
83
|
-
getRandomID?: () => string;
|
|
84
|
-
createGroupChannel?: CreateGroupChannel;
|
|
85
|
-
createHubConnection?: CreateHubConnection;
|
|
86
|
-
/**
|
|
87
|
-
* Forwarded to the underlying `HubConnectionPool`. Fired with the observed
|
|
88
|
-
* `serverDID` after the first signed response from a freshly-spawned client.
|
|
89
|
-
* Used by the hub manager to apply the TOFU capture conflict matrix (the
|
|
90
|
-
* `null → DID-A` upgrade). Errors are caught and logged by the pool;
|
|
91
|
-
* pinned-mismatch enforcement is a separate path inside the wrapper.
|
|
92
|
-
*/
|
|
93
|
-
onServerDIDObserved?: OnServerDIDObserved;
|
|
94
|
-
};
|
|
95
|
-
export declare class HubRelayManager {
|
|
96
|
-
#private;
|
|
97
|
-
get relayEvents(): EventEmitter<RelayManagerEvents>;
|
|
98
|
-
constructor(params: HubRelayManagerParams);
|
|
99
|
-
/**
|
|
100
|
-
* Open channels for each group in parallel. Per-group failures are logged
|
|
101
|
-
* and swallowed: this method never rejects, so a single bad group does not
|
|
102
|
-
* block the others from starting. Callers who need per-group readiness
|
|
103
|
-
* signals must check `addGroup` / `broadcast` results individually.
|
|
104
|
-
*/
|
|
105
|
-
start(groupIDs: Array<string>): Promise<void>;
|
|
106
|
-
addGroup(groupID: string): Promise<void>;
|
|
107
|
-
/**
|
|
108
|
-
* Post-join binding-add hook. Single-hub regime — when the group already
|
|
109
|
-
* has an active channel:
|
|
110
|
-
* - same hubURL → debug log, no-op (idempotent re-bind);
|
|
111
|
-
* - different hubURL → warn, no-op (extra binding deferred until
|
|
112
|
-
* multi-hub support lands).
|
|
113
|
-
* When the group has no active channel yet (e.g. binding was created
|
|
114
|
-
* before relay setup ran), open one for the supplied hub.
|
|
115
|
-
*/
|
|
116
|
-
addBinding(groupID: string, hubURL: string): Promise<void>;
|
|
117
|
-
/**
|
|
118
|
-
* Post-join binding-remove hook. No-op when the unbound hub is not the
|
|
119
|
-
* group's active hub (single-hub regime: unbinding a non-active hub leaves
|
|
120
|
-
* the channel intact). When it matches, behaves like `removeGroup`.
|
|
121
|
-
*/
|
|
122
|
-
removeBinding(groupID: string, hubURL: string): Promise<void>;
|
|
123
|
-
removeGroup(groupID: string): Promise<void>;
|
|
124
|
-
/**
|
|
125
|
-
* Run stale-device MLS epoch recovery for an active group via its channel.
|
|
126
|
-
* Throws if the group is not active; returns `recovery-failed` if the rejoin
|
|
127
|
-
* flow is unavailable (no monitor/identity wired).
|
|
128
|
-
*/
|
|
129
|
-
rejoinGroup(groupID: string): Promise<RejoinResult>;
|
|
130
|
-
broadcast(groupID: string, message: GroupBroadcastMessage, opts?: {
|
|
131
|
-
timeoutMs?: number;
|
|
132
|
-
}): Promise<void>;
|
|
133
|
-
/**
|
|
134
|
-
* Fan out a pre-produced MLS handshake Commit to a group's existing members
|
|
135
|
-
* via its active channel. Mirrors {@link broadcast}.
|
|
136
|
-
*/
|
|
137
|
-
sendCommit(groupID: string, commitBytes: Uint8Array, opts?: {
|
|
138
|
-
timeoutMs?: number;
|
|
139
|
-
}): Promise<void>;
|
|
140
|
-
stop(): Promise<void>;
|
|
141
|
-
}
|
|
142
|
-
export {};
|
package/lib/hub/relay-manager.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{EventEmitter as e}from"@enkaku/event";import{HubConnectionPool as t}from"./connection-pool.js";import{GroupChannel as s}from"./group-channel.js";import{HubConnection as o}from"./hub-connection.js";export class HubRelayManager{#e;#t;#s;#o;#r;#i;#a;#n;#l;#c;#u;#h;#g;#p;#d;#b;#f=new Map;#m=new Map;#M=new Map;#v=new Map;#w;get relayEvents(){return this.#w}constructor(r){this.#e=r.p2pStore,this.#t=r.broadcastService,this.#s=r.deviceID,this.#r=r.logger,this.#i=r.ackFlushMs??500,this.#a=r.ackFlushMax??10,this.#n=r.backoffBaseMs??1e3,this.#l=r.backoffMaxMs??6e4,this.#c=r.backoffJitter??.25,this.#u=r.broadcastTimeoutMs,this.#h=r.monitor,this.#g=r.identity,this.#p=r.getRandomID;let i=r.p2pStore;this.#o=new t({createHubClient:r.createHubClient,logger:r.logger,onServerDIDObserved:r.onServerDIDObserved,resolvePinnedDID:async e=>{let t=await i.getHubByURL(e);return t?.server_did??null}}),this.#d=r.createGroupChannel??(e=>new s(e)),this.#b=r.createHubConnection??(e=>new o(e)),this.#w=new e}async #y(e){let t=this.#f.get(e);if(null!=t)return t;let s=this.#b({hubURL:e,deviceID:this.#s,pool:this.#o,logger:this.#r,ackFlushMs:this.#i,ackFlushMax:this.#a,backoffBaseMs:this.#n,backoffMaxMs:this.#l,backoffJitter:this.#c});try{await s.open()}catch(e){try{await s.dispose()}catch{}throw e}let o=s.events.on("lifecycle",async t=>{let s=Object.freeze([...this.#M.entries()].filter(([,t])=>t===e).map(([e])=>e));await this.#w.emit("relay",{...t,hubURL:e,groupIDs:s})});return this.#v.set(e,o),this.#f.set(e,s),s}async start(e){(await Promise.allSettled(e.map(e=>this.addGroup(e)))).forEach((t,s)=>{"rejected"===t.status&&this.#r?.error("addGroup failed during start",{groupID:e[s],error:t.reason})})}async addGroup(e){if(this.#m.has(e))return void this.#r?.debug("group already added",{groupID:e});if(null==await this.#e.getGroup(e))throw Error(`Group ${e} not found`);let t=await this.#e.listHubsByGroupID(e);0===t.length?this.#r?.warn("group has no hub bindings, skipping",{groupID:e}):(t.length>1&&this.#r?.warn("multi-hub not yet supported, using first",{groupID:e,hubURLs:t.map(e=>e.url)}),await this.#k(e,t[0].url))}async #k(e,t){let s=await this.#y(t),o=this.#d({groupID:e,deviceID:this.#s,hubConnection:s,broadcastService:this.#t,p2pStore:this.#e,logger:this.#r,monitor:this.#h,identity:this.#g,getRandomID:this.#p});await o.open(),this.#m.set(e,o),this.#M.set(e,t)}async addBinding(e,t){let s=this.#M.get(e);null!=s?s===t?this.#r?.debug("binding already active",{groupID:e,hubURL:t}):this.#r?.warn("multi-hub not yet supported, ignoring extra binding",{groupID:e,activeHubURL:s,newHubURL:t}):await this.#k(e,t)}async removeBinding(e,t){this.#M.get(e)!==t?this.#r?.debug("binding not active, ignoring removal",{groupID:e,hubURL:t}):await this.removeGroup(e)}async removeGroup(e){let t=this.#m.get(e);null!=t&&(await t.close(),this.#m.delete(e),this.#M.delete(e))}async rejoinGroup(e){let t=this.#m.get(e);if(null==t)throw Error(`Group ${e} is not active in HubRelayManager`);return await t.rejoinGroup()}async broadcast(e,t,s){let o=this.#m.get(e);if(null==o)throw Error(`Group ${e} is not active in HubRelayManager`);let r=s?.timeoutMs??this.#u;await o.broadcast(t,{timeoutMs:r})}async sendCommit(e,t,s){let o=this.#m.get(e);if(null==o)throw Error(`Group ${e} is not active in HubRelayManager`);let r=s?.timeoutMs??this.#u;await o.sendCommit(t,{timeoutMs:r})}async stop(){for(let e of this.#v.values())e();this.#v.clear();let e=[...this.#m.values()];this.#m.clear(),this.#M.clear(),(await Promise.allSettled(e.map(e=>e.close()))).forEach(e=>{"rejected"===e.status&&this.#r?.warn("GroupChannel close failed",{error:e.reason})});let t=[...this.#f.values()];this.#f.clear(),(await Promise.allSettled(t.map(e=>e.close()))).forEach(e=>{"rejected"===e.status&&this.#r?.warn("HubConnection close failed",{error:e.reason})}),await this.#o.disposeAll()}}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@kubun/logger';
|
|
2
|
-
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
3
|
-
import type { BroadcastService } from '../groups/broadcast-service.js';
|
|
4
|
-
export type HandleSendBroadcastParams = {
|
|
5
|
-
groupID: string;
|
|
6
|
-
message: GroupBroadcastMessage;
|
|
7
|
-
broadcastService: BroadcastService;
|
|
8
|
-
/**
|
|
9
|
-
* Wire-send callback. Receives the groupID and the base64-encoded encrypted
|
|
10
|
-
* MLS application message, and is expected to fan it out via the underlying
|
|
11
|
-
* hub transport (e.g. `HubConnection.broadcast`). Decoupling from the hub
|
|
12
|
-
* client lets callers serialize the send through a per-group mutex shared
|
|
13
|
-
* with the receive path.
|
|
14
|
-
*/
|
|
15
|
-
send: (groupID: string, payloadB64: string) => Promise<void>;
|
|
16
|
-
logger?: Logger;
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Encrypt + send a broadcast for a group. MLS state persistence is handled
|
|
20
|
-
* by the registry inside `BroadcastService.prepareSend`. Wedge protection
|
|
21
|
-
* (failed send → no advance of persisted state) is preserved because send
|
|
22
|
-
* runs INSIDE the registry's per-group lock and a throw skips persist.
|
|
23
|
-
*/
|
|
24
|
-
export declare function handleSendBroadcast(params: HandleSendBroadcastParams): Promise<void>;
|
|
25
|
-
export type HandleSendCommitParams = {
|
|
26
|
-
groupID: string;
|
|
27
|
-
/** Framed MLSMessage(Commit) bytes from the committer's `GroupManager` call. */
|
|
28
|
-
commitBytes: Uint8Array;
|
|
29
|
-
broadcastService: BroadcastService;
|
|
30
|
-
/** Same wire-send callback as {@link HandleSendBroadcastParams.send}. */
|
|
31
|
-
send: (groupID: string, payloadB64: string) => Promise<void>;
|
|
32
|
-
logger?: Logger;
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Fan out a pre-produced MLS handshake Commit to a group's existing members.
|
|
36
|
-
* Unlike {@link handleSendBroadcast} this does not encrypt — the Commit is
|
|
37
|
-
* already a self-protected `PrivateMessage`; `prepareSendCommit` just tags it
|
|
38
|
-
* `mls` on the wire.
|
|
39
|
-
*/
|
|
40
|
-
export declare function handleSendCommit(params: HandleSendCommitParams): Promise<void>;
|
package/lib/hub/send-handler.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{toB64 as r}from"@enkaku/codec";export async function handleSendBroadcast(e){let{groupID:o,message:t,broadcastService:a,send:n,logger:d}=e;try{await a.prepareSend({groupID:o,message:t,send:e=>n(o,r(e))})}catch(r){throw d?.error("hub/group/send failed",{groupID:o,error:r}),r}}export async function handleSendCommit(e){let{groupID:o,commitBytes:t,broadcastService:a,send:n,logger:d}=e;try{await a.prepareSendCommit({groupID:o,commitBytes:t,send:e=>n(o,r(e))})}catch(r){throw d?.error("hub/group/send (mls commit) failed",{groupID:o,error:r}),r}}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { StoredMessage } from '@enkaku/hub-protocol';
|
|
2
|
-
/**
|
|
3
|
-
* FIFO queue used by `HubConnection.tunnelMessageStream` to deliver synthetic /
|
|
4
|
-
* forwarded `StoredMessage` values to tunnel transports. Callers `push` messages;
|
|
5
|
-
* each consumer drains via an `AsyncIterator` returned from
|
|
6
|
-
* {@link TunnelInbox.iterator}. Disposal is idempotent: once `close` runs,
|
|
7
|
-
* subsequent pushes are dropped and pending iteration resolves to `done`.
|
|
8
|
-
*
|
|
9
|
-
* Multiple sequential iterators are supported (one consumer at a time, but
|
|
10
|
-
* iterators may be created and returned across the inbox's lifetime). Each
|
|
11
|
-
* iterator tracks its own pending waiter so `iterator.return()` drains only
|
|
12
|
-
* that iterator's waiter — leaving queued messages and other consumers'
|
|
13
|
-
* waiters intact for future iterators.
|
|
14
|
-
*/
|
|
15
|
-
export declare class TunnelInbox {
|
|
16
|
-
#private;
|
|
17
|
-
push(message: StoredMessage): void;
|
|
18
|
-
close(): void;
|
|
19
|
-
iterator(onReturn?: () => void): AsyncIterator<StoredMessage>;
|
|
20
|
-
}
|
package/lib/hub/tunnel-inbox.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export class TunnelInbox{#e=[];#s=[];#t=!1;push(e){if(this.#t)return;let s=this.#s.shift();null!=s?s.resolve({value:e,done:!1}):this.#e.push(e)}close(){if(!this.#t)for(this.#t=!0;this.#s.length>0;){let e=this.#s.shift();e?.resolve({value:void 0,done:!0})}}iterator(e){let s=!1,t=new Set,i=()=>{if(!s){for(let e of(s=!0,t)){let s=this.#s.indexOf(e);s>=0&&this.#s.splice(s,1),e.resolve({value:void 0,done:!0})}t.clear(),e?.()}};return{next:()=>{let e;return s?Promise.resolve({value:void 0,done:!0}):this.#e.length>0?Promise.resolve({value:this.#e.shift(),done:!1}):this.#t?(i(),Promise.resolve({value:void 0,done:!0})):new Promise(s=>{e={resolve:s},t.add(e),this.#s.push(e)}).then(s=>(null!=e&&t.delete(e),!0===s.done&&i(),s))},return:()=>(i(),Promise.resolve({value:void 0,done:!0}))}}}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wait for a reconnect gate (or similar boolean-style gate) to clear, with an
|
|
3
|
-
* optional timeout. If `gate` is null the call returns immediately without
|
|
4
|
-
* allocating any timer or signal — this is the hot path while the connection
|
|
5
|
-
* is healthy. If `timeoutMs` is undefined we simply await the gate without
|
|
6
|
-
* arming a timer (callers with no timeout can also omit `onTimeout` to skip
|
|
7
|
-
* the closure allocation). Both set: race the gate against
|
|
8
|
-
* `AbortSignal.timeout`; the first to settle wins.
|
|
9
|
-
*
|
|
10
|
-
* `onTimeout` is invoked lazily — only when the timeout actually fires — so
|
|
11
|
-
* callers can capture mutable state (e.g. current reconnect attempt) at the
|
|
12
|
-
* moment of failure rather than at call time.
|
|
13
|
-
*/
|
|
14
|
-
export declare function waitForGate(gate: Promise<void> | null, timeoutMs: number | undefined, onTimeout?: () => Error): Promise<void>;
|
package/lib/hub/wait-for-gate.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export async function waitForGate(e,t,r){if(null==e)return;if(null==t)return void await e;if(null==r)throw Error("waitForGate: onTimeout required when timeoutMs is set");let i=AbortSignal.timeout(t),n=null,o=new Promise((e,t)=>{i.aborted?t(r()):(n=()=>{t(r())},i.addEventListener("abort",n,{once:!0}))});try{await Promise.race([e,o])}finally{null!=n&&i.removeEventListener("abort",n)}}
|