@kubun/plugin-p2p 0.10.1 → 0.12.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 +1646 -1
- package/lib/context/hub.js +151 -1
- package/lib/context/join.js +115 -1
- package/lib/context/peer.d.ts +92 -0
- package/lib/context/peer.js +1216 -0
- package/lib/context/require-admin.d.ts +57 -0
- package/lib/context/require-admin.js +57 -0
- package/lib/context/sync.d.ts +12 -0
- package/lib/context/sync.js +252 -1
- package/lib/context/types.d.ts +58 -22
- package/lib/context/types.js +48 -1
- package/lib/groups/access-default-token.d.ts +44 -0
- package/lib/groups/access-default-token.js +103 -0
- package/lib/groups/admin-roster.d.ts +55 -0
- package/lib/groups/admin-roster.js +69 -0
- package/lib/groups/anchor-store.d.ts +15 -0
- package/lib/groups/anchor-store.js +37 -0
- package/lib/groups/app-cursor-store.d.ts +17 -0
- package/lib/groups/app-cursor-store.js +34 -0
- package/lib/groups/broadcast-codec.js +6 -1
- package/lib/groups/broadcast.d.ts +238 -106
- package/lib/groups/broadcast.js +702 -1
- package/lib/groups/catalog-token.d.ts +50 -0
- package/lib/groups/catalog-token.js +96 -0
- package/lib/groups/circle-projection.d.ts +90 -0
- package/lib/groups/circle-projection.js +202 -0
- package/lib/groups/circle-reducers.d.ts +139 -0
- package/lib/groups/circle-reducers.js +128 -0
- package/lib/groups/commit-adoption.d.ts +77 -0
- package/lib/groups/commit-adoption.js +108 -0
- package/lib/groups/commit-journal.d.ts +14 -0
- package/lib/groups/commit-journal.js +65 -0
- package/lib/groups/control-request.d.ts +66 -0
- package/lib/groups/control-request.js +101 -0
- package/lib/groups/events.d.ts +19 -2
- package/lib/groups/events.js +10 -1
- package/lib/groups/group-anchor.d.ts +24 -0
- package/lib/groups/group-anchor.js +22 -0
- package/lib/groups/group-crypto.d.ts +42 -0
- package/lib/groups/group-crypto.js +159 -0
- package/lib/groups/group-handle-registry.d.ts +138 -9
- package/lib/groups/group-handle-registry.js +533 -1
- package/lib/groups/group-handlers.d.ts +30 -0
- package/lib/groups/group-handlers.js +218 -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 +66 -0
- package/lib/groups/group-mls.js +696 -0
- package/lib/groups/group-peer-manager.d.ts +301 -0
- package/lib/groups/group-peer-manager.js +1336 -0
- package/lib/groups/group-protocols.d.ts +754 -0
- package/lib/groups/group-protocols.js +448 -0
- package/lib/groups/invite-payload.d.ts +49 -1
- package/lib/groups/invite-payload.js +26 -1
- package/lib/groups/join-request-identity.d.ts +21 -0
- package/lib/groups/join-request-identity.js +43 -0
- package/lib/groups/join-utils.d.ts +61 -0
- package/lib/groups/join-utils.js +366 -1
- package/lib/groups/ledger-affected-events.d.ts +20 -0
- package/lib/groups/ledger-affected-events.js +85 -0
- package/lib/groups/ledger-commit-fold.d.ts +107 -0
- package/lib/groups/ledger-commit-fold.js +167 -0
- package/lib/groups/ledger-ingest.d.ts +124 -0
- package/lib/groups/ledger-ingest.js +212 -0
- package/lib/groups/ledger.d.ts +44 -0
- package/lib/groups/ledger.js +30 -0
- package/lib/groups/manager.d.ts +217 -85
- package/lib/groups/manager.js +798 -1
- package/lib/groups/mls-codec.d.ts +28 -7
- package/lib/groups/mls-codec.js +33 -1
- package/lib/groups/mls-encryptor.d.ts +2 -2
- package/lib/groups/mls-encryptor.js +33 -1
- package/lib/groups/mls-group-handle.d.ts +37 -2
- package/lib/groups/mls-group-handle.js +18 -1
- package/lib/groups/mls-json.js +17 -1
- package/lib/groups/mls-receive-errors.d.ts +27 -0
- package/lib/groups/mls-receive-errors.js +39 -0
- package/lib/groups/mls-state.d.ts +6 -4
- package/lib/groups/mls-state.js +79 -1
- package/lib/groups/peer-presence.d.ts +90 -0
- package/lib/groups/peer-presence.js +295 -0
- package/lib/groups/peer-selection.d.ts +37 -0
- package/lib/groups/peer-selection.js +72 -0
- package/lib/groups/roster-projection.d.ts +53 -0
- package/lib/groups/roster-projection.js +65 -0
- package/lib/groups/store-received-grant.d.ts +7 -3
- package/lib/groups/store-received-grant.js +161 -1
- package/lib/groups/store-received-revocation.d.ts +19 -16
- package/lib/groups/store-received-revocation.js +83 -1
- package/lib/hub/ack-backoff.d.ts +22 -0
- package/lib/hub/ack-backoff.js +88 -0
- package/lib/hub/http-client.d.ts +18 -10
- package/lib/hub/http-client.js +15 -1
- package/lib/hub/hub-like.d.ts +75 -0
- package/lib/hub/hub-like.js +1022 -0
- package/lib/hub/loopback-log-hub.d.ts +20 -0
- package/lib/hub/loopback-log-hub.js +178 -0
- package/lib/hub/manager.d.ts +25 -40
- package/lib/hub/manager.js +155 -1
- package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
- package/lib/hub/peer-scoped-hub-view.js +92 -0
- package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
- package/lib/hub/sender-scoped-hub-view.js +59 -0
- package/lib/hub/server-did.d.ts +67 -0
- package/lib/hub/server-did.js +131 -0
- package/lib/hub/wiring.d.ts +77 -51
- package/lib/hub/wiring.js +189 -1
- package/lib/index.d.ts +58 -22
- package/lib/index.js +563 -1
- package/lib/peer/connection-registry.d.ts +45 -0
- package/lib/peer/connection-registry.js +52 -0
- package/lib/peer/handlers.d.ts +69 -0
- package/lib/peer/handlers.js +110 -0
- package/lib/protocol.d.ts +214 -1
- package/lib/protocol.js +519 -1
- package/lib/schema.d.ts +3 -1
- package/lib/schema.js +1150 -39
- package/lib/sync/access-default-sender.d.ts +23 -0
- package/lib/sync/access-default-sender.js +80 -0
- package/lib/sync/authorize.d.ts +58 -4
- package/lib/sync/authorize.js +114 -1
- package/lib/sync/broadcast-queue.d.ts +3 -4
- package/lib/sync/broadcast-queue.js +182 -1
- package/lib/sync/broadcast-sender.js +179 -1
- package/lib/sync/catalog-scope.d.ts +8 -7
- package/lib/sync/catalog-scope.js +84 -1
- package/lib/sync/errors.d.ts +12 -2
- package/lib/sync/errors.js +29 -1
- package/lib/sync/forwarder.d.ts +13 -19
- package/lib/sync/forwarder.js +271 -1
- package/lib/sync/handlers.js +200 -1
- package/lib/sync/hub-tunnel-sync-listener.d.ts +19 -5
- package/lib/sync/hub-tunnel-sync-listener.js +261 -1
- package/lib/sync/hub-tunnel-sync-provider.d.ts +34 -14
- package/lib/sync/hub-tunnel-sync-provider.js +113 -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 +11 -3
- package/lib/sync/merkle-channel.js +0 -0
- package/lib/sync/merkle-tree.js +120 -1
- package/lib/sync/peer-registry.d.ts +15 -11
- package/lib/sync/peer-registry.js +82 -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 +14 -4
- package/lib/sync/sync-client.js +254 -1
- package/lib/sync/sync-manager.d.ts +24 -6
- package/lib/sync/sync-manager.js +301 -1
- package/lib/sync/tunnel-listeners.d.ts +52 -0
- package/lib/sync/tunnel-listeners.js +175 -0
- package/lib/sync/tunnel-observability.d.ts +25 -0
- package/lib/sync/tunnel-observability.js +63 -0
- package/lib/sync/tunnel-topics.d.ts +20 -0
- package/lib/sync/tunnel-topics.js +28 -0
- package/lib/types.d.ts +704 -22
- package/lib/types.js +1 -1
- package/lib/util/handler-error.d.ts +16 -0
- package/lib/util/handler-error.js +64 -0
- package/lib/util/mutex.d.ts +14 -1
- package/lib/util/mutex.js +44 -1
- package/lib/utils.js +21 -1
- package/package.json +58 -47
- 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/did-observing-transport.d.ts +0 -64
- package/lib/hub/did-observing-transport.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/errors.d.ts +0 -30
- package/lib/hub/errors.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,58 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@kubun/logger';
|
|
2
|
-
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
3
|
-
/** The reply payload a current member sends back, correlated by `requestID`. */
|
|
4
|
-
type CatchupReply = Extract<GroupBroadcastMessage, {
|
|
5
|
-
type: 'circle-catchup:reply';
|
|
6
|
-
}>;
|
|
7
|
-
export type CircleCatchupRequesterParams = {
|
|
8
|
-
groupID: string;
|
|
9
|
-
/** Fresh correlation id per request (incl. retries). */
|
|
10
|
-
genRequestID: () => string;
|
|
11
|
-
/** Broadcast a `circle-catchup:request` on the group channel. */
|
|
12
|
-
sendRequest: (requestID: string) => Promise<void>;
|
|
13
|
-
/**
|
|
14
|
-
* Apply a received reply batch row-by-row (idempotent, order-independent).
|
|
15
|
-
* Mirrors `applyCatchupReply` bound to the local store/engine context.
|
|
16
|
-
*/
|
|
17
|
-
applyReply: (reply: CatchupReply) => Promise<void>;
|
|
18
|
-
requestTimeoutMs?: number;
|
|
19
|
-
maxAttempts?: number;
|
|
20
|
-
backoffMs?: (attempt: number) => number;
|
|
21
|
-
logger?: Logger;
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Drives a rejoining device's circle/member catch-up for one group: broadcast a
|
|
25
|
-
* `circle-catchup:request`, await the first matching `circle-catchup:reply`
|
|
26
|
-
* (correlated by `requestID`) within a timeout, apply it, done. On timeout,
|
|
27
|
-
* retry with a capped full-jitter backoff up to `maxAttempts`.
|
|
28
|
-
*
|
|
29
|
-
* Mirrors the rejoin manager's request/await/retry shape, minus any MLS
|
|
30
|
-
* join/confirm — there is no Commit to broadcast and no head to confirm against,
|
|
31
|
-
* because `applyReply` is idempotent and order-independent: one good reply
|
|
32
|
-
* lands the snapshot, and a duplicate or late reply is a harmless no-op.
|
|
33
|
-
*
|
|
34
|
-
* Exhaustion is NON-FATAL: a device that never hears back simply keeps the
|
|
35
|
-
* state it has; it resolves without throwing.
|
|
36
|
-
*/
|
|
37
|
-
export declare class CircleCatchupRequester {
|
|
38
|
-
#private;
|
|
39
|
-
constructor(params: CircleCatchupRequesterParams);
|
|
40
|
-
get inFlight(): boolean;
|
|
41
|
-
/**
|
|
42
|
-
* Route a received `circle-catchup:reply`: resolve the matching in-flight
|
|
43
|
-
* waiter. Unknown `requestID`s (peers' requests, stale or duplicate replies
|
|
44
|
-
* for an already-resolved attempt) are ignored — harmless.
|
|
45
|
-
*/
|
|
46
|
-
onCatchupReply(requestID: string, reply: CatchupReply): void;
|
|
47
|
-
/**
|
|
48
|
-
* Run the catch-up flow. Idempotent: a concurrent call while one is in flight
|
|
49
|
-
* shares the existing run's promise rather than starting a second.
|
|
50
|
-
*/
|
|
51
|
-
run(): Promise<void>;
|
|
52
|
-
/**
|
|
53
|
-
* Stop a running catch-up and release timers. Idempotent; the loop checks
|
|
54
|
-
* `#disposed` between attempts, pending waiters resolve `null`. On channel close.
|
|
55
|
-
*/
|
|
56
|
-
dispose(): void;
|
|
57
|
-
}
|
|
58
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{sleep as e,unref as t}from"../utils.js";export class CircleCatchupRequester{#e;#t;#s;#i;#r;#l;#a;#u;#n=new Map;#o=null;#h=!1;constructor(e){this.#e=e.groupID,this.#t=e.genRequestID,this.#s=e.sendRequest,this.#i=e.applyReply,this.#r=e.requestTimeoutMs??5e3,this.#l=e.maxAttempts??8,this.#a=e.backoffMs??s,this.#u=e.logger}get inFlight(){return null!=this.#o}onCatchupReply(e,t){let s=this.#n.get(e);null!=s&&(this.#n.delete(e),s.resolve(t))}run(){if(null!=this.#o)return this.#o;let e=this.#p().finally(()=>{this.#o=null});return this.#o=e,e}async #p(){for(let t=0;t<this.#l&&!this.#h;t++){if(t>0&&(await e(this.#a(t)),this.#h))return;let s=await this.#c(this.#r);if(null!=s)try{await this.#i(s);return}catch(e){this.#u?.warn("circle catch-up: applying reply failed",{groupID:this.#e,error:e})}}this.#h||this.#u?.warn("circle catch-up: no usable reply, giving up",{groupID:this.#e,attempts:this.#l})}async #c(e){let t=this.#t(),s=this.#g(t,e);try{await this.#s(t)}catch(e){return this.#m(t),this.#u?.warn("circle catch-up: request send failed",{groupID:this.#e,error:e}),null}return s}#g(e,s){return new Promise(i=>{let r=setTimeout(()=>{this.#n.delete(e),i(null)},s);t(r),this.#n.set(e,{resolve:e=>{clearTimeout(r),i(e)},cancel:()=>{clearTimeout(r),i(null)}})})}#m(e){let t=this.#n.get(e);null!=t&&(this.#n.delete(e),t.cancel())}dispose(){for(let e of(this.#h=!0,this.#n.values()))e.cancel();this.#n.clear()}}function s(e){return Math.floor(Math.random()*Math.min(500*2**(e-1),5e3))}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@kubun/logger';
|
|
2
|
-
import type { Circle, CircleMember } from '@kubun/store-p2p';
|
|
3
|
-
import type { CircleMemberSnapshot, CircleSnapshot } from '../groups/broadcast.js';
|
|
4
|
-
/**
|
|
5
|
-
* Reads the group's circles + circle members from the store. Mirrors the
|
|
6
|
-
* `listCirclesByGroup` / `listCircleMembers` store API, both returning
|
|
7
|
-
* tombstoned rows when `includeRemoved` is set so the snapshot can carry
|
|
8
|
-
* removed entries.
|
|
9
|
-
*/
|
|
10
|
-
export type CircleCatchupStoreReader = {
|
|
11
|
-
listCirclesByGroup: (groupID: string, options?: {
|
|
12
|
-
includeRemoved?: boolean;
|
|
13
|
-
}) => Promise<Array<Circle>>;
|
|
14
|
-
listCircleMembers: (circleID: string, options?: {
|
|
15
|
-
includeRemoved?: boolean;
|
|
16
|
-
}) => Promise<Array<CircleMember>>;
|
|
17
|
-
};
|
|
18
|
-
export type CircleCatchupResponderParams = {
|
|
19
|
-
groupID: string;
|
|
20
|
-
/** Reads the group's circle + circle-member rows (including tombstoned). */
|
|
21
|
-
store: CircleCatchupStoreReader;
|
|
22
|
-
/** Broadcast a `circle-catchup:reply` to the group channel. */
|
|
23
|
-
sendCatchupReply: (requestID: string, circles: Array<CircleSnapshot>, circleMembers: Array<CircleMemberSnapshot>) => Promise<void>;
|
|
24
|
-
/** Jitter delay (ms) before replying. Defaults to a random 0..250ms. */
|
|
25
|
-
nextJitterMs?: () => number;
|
|
26
|
-
/** How long a `requestID` stays suppressed after being answered. */
|
|
27
|
-
suppressTTLMs?: number;
|
|
28
|
-
logger?: Logger;
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Answers `circle-catchup:request`s with one batched snapshot of the group's
|
|
32
|
-
* circle + circle-member rows (including tombstoned, each carrying its authored
|
|
33
|
-
* HLC). Every current member could reply, so jitter (random pre-reply delay) +
|
|
34
|
-
* suppression (cancel on seeing a peer's reply for the same `requestID`)
|
|
35
|
-
* collapse the storm to ~1 reply. Keyed on `requestID` (fresh per attempt);
|
|
36
|
-
* suppression entries TTL out to bound memory.
|
|
37
|
-
*
|
|
38
|
-
* Mirrors the rejoin responder's storm-collapse shape; suppression collapses
|
|
39
|
-
* only duplicates of one `requestID`, so distinct `requestID`s still draw one
|
|
40
|
-
* reply each.
|
|
41
|
-
*/
|
|
42
|
-
export declare class CircleCatchupResponder {
|
|
43
|
-
#private;
|
|
44
|
-
constructor(params: CircleCatchupResponderParams);
|
|
45
|
-
/** Handle an inbound `circle-catchup:request`: schedule a jittered, suppressible reply. */
|
|
46
|
-
onCatchupRequest(requestID: string): void;
|
|
47
|
-
/** Handle an observed `circle-catchup:reply` (own or peer's): suppress this request. */
|
|
48
|
-
onCatchupReplySeen(requestID: string): void;
|
|
49
|
-
dispose(): void;
|
|
50
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{unref as e}from"../utils.js";function s(e){return{id:e.id,groupID:e.group_id,name:e.name,description:e.description,catalogIDs:e.catalog_ids,hlc:e.hlc,removedAtHLC:e.removed_at_hlc??null}}export class CircleCatchupResponder{#e;#s;#t;#r;#i;#p;#o=new Map;#l=new Map;constructor(e){this.#e=e.groupID,this.#s=e.store,this.#t=e.sendCatchupReply,this.#r=e.nextJitterMs??(()=>Math.floor(250*Math.random())),this.#i=e.suppressTTLMs??5e3,this.#p=e.logger}onCatchupRequest(s){if(this.#l.has(s)||this.#o.has(s))return;let t=setTimeout(()=>{this.#o.delete(s),this.#h(s)},this.#r());e(t),this.#o.set(s,t)}onCatchupReplySeen(e){let s=this.#o.get(e);null!=s&&(clearTimeout(s),this.#o.delete(e)),this.#n(e)}dispose(){for(let e of this.#o.values())clearTimeout(e);for(let e of(this.#o.clear(),this.#l.values()))clearTimeout(e);this.#l.clear()}async #h(e){if(!this.#l.has(e)){this.#n(e);try{let{circles:s,circleMembers:t}=await this.#u();await this.#t(e,s,t)}catch(s){this.#p?.warn("circle catch-up responder failed to reply",{groupID:this.#e,requestID:e,error:s})}}}async #u(){let e=await this.#s.listCirclesByGroup(this.#e,{includeRemoved:!0}),t=e.map(s),r=[];for(let s of e)for(let e of(await this.#s.listCircleMembers(s.id,{includeRemoved:!0})))r.push({circleID:e.circle_id,memberDID:e.member_did,role:e.role,hlc:e.hlc,removedAtHLC:e.removed_at_hlc??null});return{circles:t,circleMembers:r}}#n(s){let t=this.#l.get(s);null!=t&&clearTimeout(t);let r=setTimeout(()=>{this.#l.delete(s)},this.#i);e(r),this.#l.set(s,r)}}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { Client } from '@enkaku/client';
|
|
2
|
-
import type { HubProtocol } from '@enkaku/hub-protocol';
|
|
3
|
-
import type { Logger } from '@kubun/logger';
|
|
4
|
-
import type { CreateHubClient } from './relay-manager.js';
|
|
5
|
-
export type HubClientRef = {
|
|
6
|
-
client: Client<HubProtocol>;
|
|
7
|
-
refID: string;
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* Fired once per freshly-spawned hub client when its first signed response
|
|
11
|
-
* arrives. The pool binds the hub URL into the call site; consumers (hub
|
|
12
|
-
* manager `captureServerDID`) use the URL to look up the local hub row and
|
|
13
|
-
* apply the conflict matrix. Errors are caught and logged — capture never
|
|
14
|
-
* blocks message forwarding, even on `HubServerDIDConflictError`.
|
|
15
|
-
*/
|
|
16
|
-
export type OnServerDIDObserved = (params: {
|
|
17
|
-
hubURL: string;
|
|
18
|
-
serverDID: string;
|
|
19
|
-
}) => void | Promise<void>;
|
|
20
|
-
/**
|
|
21
|
-
* Resolves the locally-pinned `server_did` for a hub URL at spawn time. When
|
|
22
|
-
* the hub row for this URL exists and has `server_did` set, the pool passes
|
|
23
|
-
* it as `expectedServerDID` to the client factory so the wrapper enforces
|
|
24
|
-
* the pin. Returning `null` falls back to TOFU.
|
|
25
|
-
*
|
|
26
|
-
* Resolution happens fresh on every spawn so `updateHub({ serverDID: null })`
|
|
27
|
-
* re-arms TOFU automatically: the next disposed/evicted client triggers a
|
|
28
|
-
* fresh resolve.
|
|
29
|
-
*/
|
|
30
|
-
export type ResolvePinnedServerDID = (hubURL: string) => Promise<string | null>;
|
|
31
|
-
export type HubConnectionPoolParams = {
|
|
32
|
-
createHubClient: CreateHubClient;
|
|
33
|
-
logger?: Logger;
|
|
34
|
-
onServerDIDObserved?: OnServerDIDObserved;
|
|
35
|
-
resolvePinnedDID?: ResolvePinnedServerDID;
|
|
36
|
-
};
|
|
37
|
-
export declare class HubConnectionPool {
|
|
38
|
-
#private;
|
|
39
|
-
constructor(params: HubConnectionPoolParams);
|
|
40
|
-
acquire(hubURL: string): Promise<HubClientRef>;
|
|
41
|
-
release(hubURL: string, refID: string): Promise<void>;
|
|
42
|
-
disposeAll(): Promise<void>;
|
|
43
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export class HubConnectionPool{#e;#t;#i;#r;#s=new Map;#n=0;constructor(e){this.#e=e.createHubClient,this.#t=e.logger,this.#i=e.onServerDIDObserved,this.#r=e.resolvePinnedDID}async acquire(e){let t=this.#s.get(e);if(null!=t&&null!=t.disposing&&(await t.disposing,this.#s.get(e)===t&&this.#s.delete(e),t=void 0),null==t){let i,r=this.#i;if(null!=this.#r)try{let t=await this.#r(e);null!=t&&(i=t)}catch(t){this.#t?.warn("failed to resolve pinned server DID, falling back to TOFU",{hubURL:e,error:t})}let s=await this.#e(e,{onServerDID:null==r?void 0:async t=>{try{await r({hubURL:e,serverDID:t})}catch(i){this.#t?.warn("hub server DID capture failed",{hubURL:e,serverDID:t,error:i})}},expectedServerDID:i,onMismatch:null==i?void 0:e=>{this.#t?.warn("hub server DID mismatch — pinning enforcement triggered",{hubURL:e.hubURL,expectedServerDID:e.expectedServerDID,observedServerDID:e.observedServerDID})}}),n={client:s,refs:new Set};t=n,this.#s.set(e,n),s.disposed.then(()=>{this.#s.get(e)===n&&(this.#t?.debug("hub client disposed externally, evicting pool entry",{hubURL:e,outstandingRefs:[...n.refs]}),this.#s.delete(e))}).catch(()=>{})}let i=String(++this.#n);return t.refs.add(i),{client:t.client,refID:i}}async release(e,t){let i=this.#s.get(e);if(null==i)return void this.#t?.debug("HubConnectionPool.release: unknown hubURL",{hubURL:e,refID:t});if(!i.refs.has(t))return void this.#t?.debug("HubConnectionPool.release: unknown refID",{hubURL:e,refID:t});if(i.refs.delete(t),i.refs.size>0)return;let r=i.client.dispose();i.disposing=r;try{await r}finally{this.#s.get(e)===i&&this.#s.delete(e)}}async disposeAll(){let e=Array.from(this.#s.entries());await Promise.all(e.map(async([e,t])=>{t.refs.size>0&&this.#t?.warn("Disposing hub client with outstanding refs",{hubURL:e,refs:Array.from(t.refs)}),await t.client.dispose()})),this.#s.clear()}}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Disposer } from '@enkaku/async';
|
|
2
|
-
import type { EventEmitter } from '@enkaku/event';
|
|
3
|
-
import type { AnyClientMessageOf, AnyServerMessageOf, ClientTransportOf, ProtocolDefinition } from '@enkaku/protocol';
|
|
4
|
-
import type { TransportEvents } from '@enkaku/transport';
|
|
5
|
-
export type ServerDIDObserver = (serverDID: string) => void | Promise<void>;
|
|
6
|
-
export type ServerDIDMismatchObserver = (params: {
|
|
7
|
-
hubURL: string;
|
|
8
|
-
expectedServerDID: string;
|
|
9
|
-
observedServerDID: string;
|
|
10
|
-
}) => void;
|
|
11
|
-
export type DIDObservingTransportParams<Protocol extends ProtocolDefinition> = {
|
|
12
|
-
inner: ClientTransportOf<Protocol>;
|
|
13
|
-
/**
|
|
14
|
-
* Fired on the first message bearing `payload.iss` when no `expectedServerDID`
|
|
15
|
-
* is set (TOFU capture). When `expectedServerDID` is set and matches, the
|
|
16
|
-
* callback is NOT fired — the pin is already authoritative.
|
|
17
|
-
*/
|
|
18
|
-
onServerDID: ServerDIDObserver;
|
|
19
|
-
/**
|
|
20
|
-
* Pinned hub server DID. When set, the wrapper enforces it: a non-matching
|
|
21
|
-
* first observed `iss` triggers a permanent failed state — `read()` rejects
|
|
22
|
-
* with `HubServerDIDMismatchError`, the offending message is NOT delivered
|
|
23
|
-
* to the inner Client, and every subsequent `read()` re-raises the same
|
|
24
|
-
* error. When unset, the wrapper falls back to TOFU capture.
|
|
25
|
-
*/
|
|
26
|
-
expectedServerDID?: string;
|
|
27
|
-
/** Hub URL — only used to populate `HubServerDIDMismatchError` for callers. */
|
|
28
|
-
hubURL?: string;
|
|
29
|
-
/**
|
|
30
|
-
* Optional side-channel notification fired on mismatch. Lets the pool log /
|
|
31
|
-
* evict without having to inspect the rejected `read()` cause. Inner
|
|
32
|
-
* transport disposal is the pool's job, not the wrapper's.
|
|
33
|
-
*/
|
|
34
|
-
onMismatch?: ServerDIDMismatchObserver;
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Wraps a `ClientTransportOf<Protocol>` to enforce hub-server-DID
|
|
38
|
-
* TOFU + pinned-mismatch semantics on the receive side.
|
|
39
|
-
*
|
|
40
|
-
* - `expectedServerDID == null`: TOFU mode. First message bearing
|
|
41
|
-
* `payload.iss` fires `onServerDID(iss)` exactly once; subsequent messages
|
|
42
|
-
* never re-fire, even with a differing `iss`. Every message is forwarded
|
|
43
|
-
* to the inner Client untouched.
|
|
44
|
-
* - `expectedServerDID != null` and the first observed `iss` matches: the
|
|
45
|
-
* pin is already authoritative; `onServerDID` is NOT fired and forwarding
|
|
46
|
-
* continues normally.
|
|
47
|
-
* - `expectedServerDID != null` and the first observed `iss` differs: the
|
|
48
|
-
* wrapper enters a permanent failed state (`HubServerDIDMismatchError`).
|
|
49
|
-
* The current `read()` call AND every subsequent `read()` reject with the
|
|
50
|
-
* error; the offending message is NOT delivered. Inner-transport lifecycle
|
|
51
|
-
* stays the pool's responsibility — the wrapper does not dispose its inner.
|
|
52
|
-
*
|
|
53
|
-
* Extends `Disposer` and matches the `ClientTransportOf<Protocol>` shape
|
|
54
|
-
* structurally so it can be passed straight to `Client<Protocol>`.
|
|
55
|
-
*/
|
|
56
|
-
export declare class DIDObservingTransport<Protocol extends ProtocolDefinition> extends Disposer {
|
|
57
|
-
#private;
|
|
58
|
-
constructor(params: DIDObservingTransportParams<Protocol>);
|
|
59
|
-
get events(): EventEmitter<TransportEvents>;
|
|
60
|
-
getWritable(): WritableStream<AnyClientMessageOf<Protocol>>;
|
|
61
|
-
read(): Promise<ReadableStreamReadResult<AnyServerMessageOf<Protocol>>>;
|
|
62
|
-
write(value: AnyClientMessageOf<Protocol>): Promise<void>;
|
|
63
|
-
[Symbol.asyncIterator](): AsyncIterator<AnyServerMessageOf<Protocol>, AnyServerMessageOf<Protocol> | null>;
|
|
64
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Disposer as e}from"@enkaku/async";import{HubServerDIDMismatchError as r}from"./errors.js";export class DIDObservingTransport extends e{#e;#r;#t;#n;#i;#s=!1;#l=null;constructor(e){super({signal:e.inner.signal,dispose:async r=>{await e.inner.dispose(r)}}),this.#e=e.inner,this.#r=e.onServerDID,this.#t=e.expectedServerDID,this.#n=e.hubURL,this.#i=e.onMismatch}get events(){return this.#e.events}getWritable(){return this.#e.getWritable()}async read(){if(null!=this.#l)throw this.#l;let e=await this.#e.read(),r=this.#a(e);if(null!=r)throw r;return e}async write(e){await this.#e.write(e)}[Symbol.asyncIterator](){return{next:async()=>{let e=await this.read();return e.done?{done:!0,value:e.value??null}:{done:!1,value:e.value}}}}#a(e){if(this.#s||e.done||null==e.value)return null;let t=e.value.payload,n=t?.iss;if("string"!=typeof n||0===n.length)return null;if(this.#s=!0,null!=this.#t){if(n===this.#t)return null;let e=new r({hubURL:this.#n??"",expectedServerDID:this.#t,observedServerDID:n});if(this.#l=e,null!=this.#i)try{this.#i({hubURL:this.#n??"",expectedServerDID:this.#t,observedServerDID:n})}catch{}return e}return Promise.resolve().then(()=>this.#r(n)).catch(()=>{}),null}}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { GroupHealthMonitor } from '../groups/group-health-monitor.js';
|
|
2
|
-
import type { ReceiveOutcome } from './receive-handler.js';
|
|
3
|
-
export type EpochStaleDetectorParams = {
|
|
4
|
-
groupID: string;
|
|
5
|
-
monitor: GroupHealthMonitor;
|
|
6
|
-
};
|
|
7
|
-
/**
|
|
8
|
-
* Maps a group's receive outcomes to `epoch-stale` signals (one per group):
|
|
9
|
-
* `applied` → reset; `decrypt-failed` → signal; `commit-out-of-order` → signal
|
|
10
|
-
* only on a REPEAT `sequenceID` (first sighting is normal replay; a repeat means
|
|
11
|
-
* the gap-filling Commit was GC'd at the hub and never arrives); else no signal.
|
|
12
|
-
* The seen-set grows only while stuck and clears on the next `applied`.
|
|
13
|
-
*/
|
|
14
|
-
export declare class EpochStaleDetector {
|
|
15
|
-
#private;
|
|
16
|
-
constructor(params: EpochStaleDetectorParams);
|
|
17
|
-
onReceiveOutcome(outcome: ReceiveOutcome, sequenceID: string): void;
|
|
18
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export class EpochStaleDetector{#e;#t;#r=new Set;constructor(e){this.#e=e.groupID,this.#t=e.monitor}onReceiveOutcome(e,t){switch(e){case"applied":this.#r.clear(),this.#t.reset(this.#e,"epoch-stale");return;case"decrypt-failed":this.#t.signal(this.#e,"epoch-stale");return;case"commit-out-of-order":this.#r.has(t)?this.#t.signal(this.#e,"epoch-stale"):this.#r.add(t);return;default:return}}}
|
package/lib/hub/errors.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export declare class ReconnectingError extends Error {
|
|
2
|
-
#private;
|
|
3
|
-
constructor(hubURL: string, attempt: number);
|
|
4
|
-
get hubURL(): string;
|
|
5
|
-
get attempt(): number;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Thrown by `DIDObservingTransport.read()` when the first observed `payload.iss`
|
|
9
|
-
* does not match the pinned `expectedServerDID`. The transport enters a
|
|
10
|
-
* permanent failed state: no message bearing the offending `iss` is delivered
|
|
11
|
-
* to the inner Client, and every subsequent `read()` rejects with the same
|
|
12
|
-
* error. In-flight Enkaku RPCs see this error as the `cause` of the Client's
|
|
13
|
-
* "Transport read failed" abort.
|
|
14
|
-
*
|
|
15
|
-
* Distinct from `HubServerDIDConflictError` (raised by manager-level
|
|
16
|
-
* `captureServerDID` when the pin row already differs from the observed DID
|
|
17
|
-
* on the TOFU capture path); this class is raised client-side, before any
|
|
18
|
-
* message is delivered, when a row was already pinned at connect time.
|
|
19
|
-
*/
|
|
20
|
-
export declare class HubServerDIDMismatchError extends Error {
|
|
21
|
-
#private;
|
|
22
|
-
constructor(params: {
|
|
23
|
-
hubURL: string;
|
|
24
|
-
expectedServerDID: string;
|
|
25
|
-
observedServerDID: string;
|
|
26
|
-
});
|
|
27
|
-
get hubURL(): string;
|
|
28
|
-
get expectedServerDID(): string;
|
|
29
|
-
get observedServerDID(): string;
|
|
30
|
-
}
|
package/lib/hub/errors.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export class ReconnectingError extends Error{#e;#r;constructor(e,r){super(`broadcast wait timed out: hub ${e} reconnecting (attempt ${r})`),this.name="ReconnectingError",this.#e=e,this.#r=r}get hubURL(){return this.#e}get attempt(){return this.#r}}export class HubServerDIDMismatchError extends Error{#e;#t;#s;constructor(e){super(`hub at ${e.hubURL} signed responses with ${e.observedServerDID}; pinned to ${e.expectedServerDID}`),this.name="HubServerDIDMismatchError",this.#e=e.hubURL,this.#t=e.expectedServerDID,this.#s=e.observedServerDID}get hubURL(){return this.#e}get expectedServerDID(){return this.#t}get observedServerDID(){return this.#s}}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@kubun/logger';
|
|
2
|
-
import type { BroadcastEvent } from '../groups/broadcast-service.js';
|
|
3
|
-
import type { P2PEventEmitter } from '../groups/events.js';
|
|
4
|
-
/**
|
|
5
|
-
* Map a `BroadcastEvent` from `BroadcastService` into the matching
|
|
6
|
-
* `P2PEventEmitter` event.
|
|
7
|
-
*
|
|
8
|
-
* - Events with `applied === false` are ignored (no state change).
|
|
9
|
-
* - `catalog:*` messages are currently a no-op. Catalog send-side broadcasts
|
|
10
|
-
* and event propagation are deferred to a future task (see design spec).
|
|
11
|
-
* - Payloads are built from `event.affected.row`, which `processBroadcast`
|
|
12
|
-
* captures at the appropriate moment (after-state for create / update,
|
|
13
|
-
* before-state for delete / remove).
|
|
14
|
-
*/
|
|
15
|
-
export declare function forwardRemoteBroadcast(emitter: P2PEventEmitter, event: BroadcastEvent, logger?: Logger): Promise<void>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{toISO as e}from"../context/types.js";export async function forwardRemoteBroadcast(r,t,a){if(!t.applied)return;let{message:i,affected:c}=t;if("delegation:share"!==i.type){if(null==c)return void a?.warn("remote broadcast applied but affected row missing",{type:i.type});switch(i.type){case"circle:create":{if("circle"!==c.kind)return;let{row:t}=c;await r.emit("circleCreated",{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at)});return}case"circle:update":{if("circle"!==c.kind)return;let{row:t}=c,a=null!=i.update.catalogIDs?"circleCatalogsChanged":"circleDataChanged";await r.emit(a,{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at),circleID:t.id});return}case"circle:delete":{if("circle"!==c.kind)return;let{row:t}=c;await r.emit("circleDeleted",{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at)});return}case"member:add":{if("member"!==c.kind)return;let{row:t}=c;await r.emit("circleMemberAdded",{circleID:t.circle_id,memberDID:t.member_did,role:t.role,createdAt:e(t.created_at)});return}case"member:remove":{if("member"!==c.kind)return;let{row:t}=c;await r.emit("circleMemberRemoved",{circleID:t.circle_id,memberDID:t.member_did,role:t.role,createdAt:e(t.created_at)});return}case"group:update":{if("group"!==c.kind)return;let{row:t}=c;await r.emit("groupDataChanged",{id:t.id,name:t.name,description:t.description,createdBy:t.created_by,createdAt:e(t.created_at),groupID:t.id});return}case"catalog:create":case"catalog:update":case"catalog:delete":return void a?.debug("catalog broadcast ignored (not yet wired)",{type:i.type})}}}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { Disposer } from '@enkaku/async';
|
|
2
|
-
import type { OwnIdentity } from '@enkaku/token';
|
|
3
|
-
import type { Logger } from '@kubun/logger';
|
|
4
|
-
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
5
|
-
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
6
|
-
import type { BroadcastService } from '../groups/broadcast-service.js';
|
|
7
|
-
import type { GroupHealthMonitor } from '../groups/group-health-monitor.js';
|
|
8
|
-
import type { RejoinResult } from '../types.js';
|
|
9
|
-
import type { HubConnection } from './hub-connection.js';
|
|
10
|
-
import { RejoinManager } from './rejoin-manager.js';
|
|
11
|
-
export type AckBatchParams = {
|
|
12
|
-
flushMs: number;
|
|
13
|
-
flushMax: number;
|
|
14
|
-
send: (ids: Array<string>) => Promise<void>;
|
|
15
|
-
logger?: Logger;
|
|
16
|
-
};
|
|
17
|
-
export declare class AckBatch {
|
|
18
|
-
#private;
|
|
19
|
-
constructor(params: AckBatchParams);
|
|
20
|
-
add(id: string): void;
|
|
21
|
-
/**
|
|
22
|
-
* Drain the pending id set and send it via the configured sender. Never
|
|
23
|
-
* rejects: send errors are logged and the ids are dropped silently (the hub
|
|
24
|
-
* will redeliver unacknowledged messages, so the next receive will produce a
|
|
25
|
-
* fresh ack).
|
|
26
|
-
*/
|
|
27
|
-
flush(): Promise<void>;
|
|
28
|
-
dispose(): void;
|
|
29
|
-
}
|
|
30
|
-
export declare function computeBackoff(attempt: number, base: number, max: number, jitter: number): number;
|
|
31
|
-
export declare function abortableSleep(ms: number, signal: AbortSignal): Promise<void>;
|
|
32
|
-
export type GroupChannelParams = {
|
|
33
|
-
groupID: string;
|
|
34
|
-
deviceID: string;
|
|
35
|
-
hubConnection: HubConnection;
|
|
36
|
-
broadcastService: BroadcastService;
|
|
37
|
-
p2pStore: P2PStoreAPI;
|
|
38
|
-
logger?: Logger;
|
|
39
|
-
/** When provided, receive outcomes feed epoch-stale health detection and the
|
|
40
|
-
* rejoin flow becomes available (both need the shared health monitor). */
|
|
41
|
-
monitor?: GroupHealthMonitor;
|
|
42
|
-
/** Device identity — required by `joinGroupExternal` in the rejoin flow. */
|
|
43
|
-
identity?: OwnIdentity;
|
|
44
|
-
/** Fresh correlation ids for rejoin requests. */
|
|
45
|
-
getRandomID?: () => string;
|
|
46
|
-
/**
|
|
47
|
-
* Pre-built rejoin manager, overriding the one this channel would construct
|
|
48
|
-
* from `monitor`/`identity`/`getRandomID`. Lets the rejoin orchestration be
|
|
49
|
-
* driven without standing up real MLS; production callers leave it unset.
|
|
50
|
-
*/
|
|
51
|
-
rejoinManager?: RejoinManager;
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* Per-group handle: thin wrapper around a shared `HubConnection`.
|
|
55
|
-
*
|
|
56
|
-
* `open()` subscribes to the connection (issuing `hub/group/join` once),
|
|
57
|
-
* `close()` unsubscribes (drains the per-group mutex then issues
|
|
58
|
-
* `hub/group/leave`), and `broadcast()` runs the MLS encrypt+send pipeline
|
|
59
|
-
* inside `HubConnection.runInGroup()` so the per-group mutex serializes send
|
|
60
|
-
* and receive on the same MLS state.
|
|
61
|
-
*
|
|
62
|
-
* All transport state (refID, ack batching, receive loop, reconnect/backoff)
|
|
63
|
-
* lives on `HubConnection` and is shared across every channel for that
|
|
64
|
-
* `hubURL`. A peer with N groups uses ONE `hub/receive` stream per
|
|
65
|
-
* `(deviceDID, hubURL)` rather than N — this preserves the hub-server's
|
|
66
|
-
* single-writer-per-DID invariant for bridge peers.
|
|
67
|
-
*/
|
|
68
|
-
export declare class GroupChannel extends Disposer {
|
|
69
|
-
#private;
|
|
70
|
-
constructor(params: GroupChannelParams);
|
|
71
|
-
open(): Promise<void>;
|
|
72
|
-
/**
|
|
73
|
-
* Run stale-device recovery for this group. Available only when constructed
|
|
74
|
-
* with a health monitor + identity; returns `recovery-failed` otherwise.
|
|
75
|
-
*/
|
|
76
|
-
rejoinGroup(): Promise<RejoinResult>;
|
|
77
|
-
close(): Promise<void>;
|
|
78
|
-
broadcast(message: GroupBroadcastMessage, opts?: {
|
|
79
|
-
timeoutMs?: number;
|
|
80
|
-
}): Promise<void>;
|
|
81
|
-
/**
|
|
82
|
-
* Fan out a pre-produced MLS handshake Commit to the group's existing
|
|
83
|
-
* members. Mirrors {@link broadcast} but routes through `handleSendCommit`
|
|
84
|
-
* (no application encryption — the Commit is already self-protected).
|
|
85
|
-
*/
|
|
86
|
-
sendCommit(commitBytes: Uint8Array, opts?: {
|
|
87
|
-
timeoutMs?: number;
|
|
88
|
-
}): Promise<void>;
|
|
89
|
-
}
|
package/lib/hub/group-channel.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Disposer as e}from"@enkaku/async";import{toB64 as r}from"@enkaku/codec";import{CircleCatchupRequester as t}from"./circle-catchup-requester.js";import{CircleCatchupResponder as o}from"./circle-catchup-responder.js";import{EpochStaleDetector as s}from"./epoch-stale-detector.js";import{handleReceivedMessage as i}from"./receive-handler.js";import{RejoinManager as n}from"./rejoin-manager.js";import{RejoinResponder as a}from"./rejoin-responder.js";import{handleSendBroadcast as u,handleSendCommit as h}from"./send-handler.js";export class AckBatch{#e;#r;#t;#o;#s=new Set;#i=null;constructor(e){this.#e=e.flushMs,this.#r=e.flushMax,this.#t=e.send,this.#o=e.logger}add(e){let r=0===this.#s.size;if(this.#s.add(e),this.#s.size>=this.#r){null!==this.#i&&(clearTimeout(this.#i),this.#i=null),this.flush();return}r&&null===this.#i&&(this.#i=setTimeout(()=>{this.#i=null,this.flush()},this.#e))}async flush(){if(null!==this.#i&&(clearTimeout(this.#i),this.#i=null),0===this.#s.size)return;let e=Array.from(this.#s);this.#s.clear();try{await this.#t(e)}catch(r){this.#o?.error("ack batch send failed",{error:r,count:e.length})}}dispose(){null!==this.#i&&(clearTimeout(this.#i),this.#i=null),this.#s.clear()}}export function computeBackoff(e,r,t,o){let s=Math.min(r*2**e*(1+o*(2*Math.random()-1)),t);return s<0?0:s}export function abortableSleep(e,r){return new Promise((t,o)=>{if(r.aborted)return void o(r.reason??Error("aborted"));let s=setTimeout(()=>{r.removeEventListener("abort",i),t()},e),i=()=>{clearTimeout(s),r.removeEventListener("abort",i),o(r.reason??Error("aborted"))};r.addEventListener("abort",i)})}export class GroupChannel extends e{#n;#a;#u;#h;#p;#o;#c;#d;#l;#g;#m;#I=!1;constructor(e){super({dispose:async()=>{await this.#f()}}),this.#n=e.groupID,this.#a=e.deviceID,this.#u=e.hubConnection,this.#h=e.broadcastService,this.#p=e.p2pStore,this.#o=e.logger,null!=e.monitor&&(this.#c=new s({groupID:e.groupID,monitor:e.monitor})),this.#d=new a({groupID:e.groupID,exportGroupInfo:()=>this.#h.exportGroupInfo(this.#n),sendGroupInfo:(e,r)=>this.#D(e,r),logger:e.logger}),null!=e.rejoinManager?this.#l=e.rejoinManager:null!=e.monitor&&null!=e.identity&&null!=e.getRandomID&&(this.#l=new n({groupID:e.groupID,identity:e.identity,readCredential:()=>this.#h.readCredential(this.#n),genRequestID:e.getRandomID,sendRequest:e=>this.#b(e),sendCommit:e=>this.sendCommit(e),replaceHandle:e=>this.#h.replaceHandle(this.#n,e),monitor:e.monitor,logger:e.logger})),this.#g=new o({groupID:e.groupID,store:this.#p,sendCatchupReply:(e,r,t)=>this.#y(e,r,t),logger:e.logger}),null!=e.getRandomID&&(this.#m=new t({groupID:e.groupID,genRequestID:e.getRandomID,sendRequest:e=>this.#R(e),applyReply:async e=>{await this.#u.runInGroup(this.#n,()=>this.#h.applyCatchupReply(this.#n,e))},logger:e.logger}))}async open(){if(this.#I)throw Error("GroupChannel already opened");let e=await this.#p.getMLSState(this.#n,this.#a);if(null==e)throw Error(`MLS state missing for group ${this.#n}; cannot join hub group`);await this.#u.subscribe(this.#n,e.credential,e=>this.#j(e)),this.#I=!0}async #j(e){let r=await i({groupID:this.#n,message:e,broadcastService:this.#h,logger:this.#o});this.#c?.onReceiveOutcome(r.outcome,r.sequenceID);let t=r.result;if(t?.kind==="rejoin-request")this.#d.onRejoinRequest(t.requestID);else if(t?.kind==="rejoin-groupinfo")this.#d.onRejoinGroupInfoSeen(t.requestID),this.#l?.onGroupInfo(t.requestID,t.groupInfo);else if(t?.kind==="app"){let e=t.message;"circle-catchup:request"===e.type?this.#g.onCatchupRequest(e.requestID):"circle-catchup:reply"===e.type&&(this.#g.onCatchupReplySeen(e.requestID),this.#m?.onCatchupReply(e.requestID,e))}return{ack:r.ack}}async rejoinGroup(){if(null==this.#l)return{status:"recovery-failed"};let e=await this.#l.rejoin();return"recovered"===e.status&&null!=this.#m&&this.#m.run().catch(e=>{this.#o?.warn("circle catch-up after rejoin failed",{groupID:this.#n,error:e})}),e}#b(e){return this.#I?this.#h.prepareSendRejoinRequest({requestID:e,send:e=>this.#u.broadcast(this.#n,r(e))}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for rejoin request`))}#D(e,t){return this.#I?this.#h.prepareSendRejoinGroupInfo({requestID:e,groupInfo:t,send:e=>this.#u.broadcast(this.#n,r(e))}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for rejoin reply`))}#R(e){return this.#I?this.#h.prepareSend({groupID:this.#n,message:{type:"circle-catchup:request",requestID:e},send:e=>this.#u.broadcast(this.#n,r(e))}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for catch-up request`))}#y(e,t,o){return this.#I?this.#h.prepareSend({groupID:this.#n,message:{type:"circle-catchup:reply",requestID:e,circles:t,circleMembers:o},send:e=>this.#u.broadcast(this.#n,r(e))}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for catch-up reply`))}async close(){this.#I&&await this.dispose()}async #f(){if(this.#d.dispose(),this.#l?.dispose(),this.#g.dispose(),this.#m?.dispose(),this.#I){try{await this.#u.unsubscribe(this.#n)}catch(e){this.#o?.warn("hub-connection unsubscribe failed during close",{groupID:this.#n,error:e})}this.#I=!1}}broadcast(e,r){return this.#I?this.#u.runInGroup(this.#n,async()=>{if(!this.#I)throw Error(`GroupChannel for ${this.#n} is not ready for broadcast`);await u({groupID:this.#n,message:e,broadcastService:this.#h,send:(e,t)=>this.#u.broadcast(e,t,{timeoutMs:r?.timeoutMs}),logger:this.#o})},{timeoutMs:r?.timeoutMs}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for broadcast`))}sendCommit(e,r){return this.#I?this.#u.runInGroup(this.#n,async()=>{if(!this.#I)throw Error(`GroupChannel for ${this.#n} is not ready for sendCommit`);await h({groupID:this.#n,commitBytes:e,broadcastService:this.#h,send:(e,t)=>this.#u.broadcast(e,t,{timeoutMs:r?.timeoutMs}),logger:this.#o})},{timeoutMs:r?.timeoutMs}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for sendCommit`))}}
|
|
@@ -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}}
|