wenay-common2 1.0.74 → 1.0.75

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.
@@ -0,0 +1,10 @@
1
+ # 1.0.75
2
+
3
+ - Stable RPC path identity: `func`, `space`, and `strict` cache child proxies per client instance, semantic surface, and collision-safe path; `all === func`, dotted keys stay distinct, and the same client keeps identity through schema refresh, reauth, and transient reconnect.
4
+ - Additive hub lifecycle listeners: `connectListen(cb)` / `disconnectListen(cb)` return individual off functions while legacy `onConnect(fn | null)` / `onDisconnect(fn | null)` retain their single-slot behavior. Internal recovery is independent from every public callback slot.
5
+ - Server-side HELLO barrier serializes concurrent STRICT/CALL/PIPE behind async auth resolution, so reconnect cannot resubscribe against a pre-auth facade; compatibility with older servers and clients is unchanged.
6
+ - Transient Socket.IO reconnect through `createRpcClientHub` now preserves logical deduped RPC Listen subscriptions and their local consumers, retires the dead physical generation, and creates exactly one replacement wire subscription after the new handshake. Unsubscribing offline prevents resurrection.
7
+ - Replay subscribers on RPC remotes recover automatically from their own last delivered `seq`: live is restored first, racing envelopes queue, then frame/since catch-up is ordered and deduplicated. Queue policy keeps an ungated live line; producer-declared mini-frames/keyframes may cover a raw seq jump without a logical data gap.
8
+ - Sacred queues remain exact: a retained journal replays every missing envelope, while eviction/no-keyframe is a terminal `onError`; queued events after the hole are never emitted as a false continuation.
9
+ - Hard boundaries stay hard: `close()` / `dispose()` and hub `connect()` / `setToken()` terminate the old generation. Pending or attempted ordinary RPC/pipe calls fail on disconnect and are never retried, avoiding duplicated side effects.
10
+ - Add real Socket.IO reconnect coverage for stable proxy identity, deduped multi-consumer recovery, repeated reconnect generations, sacred eviction, offline dispose, token rotation, and multicast lifecycle listeners.
@@ -150,16 +150,27 @@ createRpcServerAuto(opts) // canonical: nested object
150
150
  // path stays byte-for-byte (Pkt.MAP only grows additively), plus line/frameLine/since/keyframe/frame — so
151
151
  // upgrading listen -> replayListen is a declaration-site-only change; replaySubscribe(client.func.key) works as is.
152
152
  // opts.replayOpts {pending?, highWater?, lowWater?, pollMs?} arms the per-connection lag gate on frameLine
153
- // (consumer picks policy 'queue'|'frame' at subscribe time); the replay `line` is never throttled (no seq holes).
153
+ // (consumer picks policy 'queue'|'frame' at subscribe time); the replay `line` stays ungated for connected queue-policy live delivery.
154
154
  createRpcServer(opts) // lower-level core
155
155
  createRpcServerAutoDetect(opts) // + legacy/v2 protocol auto-detection (createRpcServerAutoWithProtocolDetection)
156
156
  createRpcServerInProc(...) // in-process fast path (no socket)
157
157
  // clients
158
- createRpcClientHub(opts) + rpc // multiplexing client hub: connect(token)/reauth(token)/onConnect
159
- // alias: hub.setToken->connect
158
+ createRpcClientHub(opts) + rpc // multiplexing client hub: connect(token)/reauth(token)/onConnect/onDisconnect + connectListen/disconnectListen
159
+ // alias: hub.setToken->connect. onConnect/onDisconnect are legacy single-slot setters; the additive
160
+ // *Listen registries return per-listener off functions and cannot overwrite each other or internal recovery.
161
+ // A transient disconnect/reconnect of the SAME Socket.IO object keeps this client generation: after
162
+ // the new route/auth handshake, each active deduped logical Listen gets exactly one new physical attempt.
163
+ // Local consumers and their off handles survive; a consumer removed offline is not resurrected.
164
+ // connect()/setToken() hard-rotates the socket/client generation; close()/dispose() is terminal.
160
165
  client members: func (proxy) · strict (schema-safe) · schema() · auth() · reauth() · onDisconnect()
161
166
  close(reason?, {socketAlive?}) · ready() · init(obj?) · api.subscriptions()
162
167
  // alias: dispose->close · readyStrict->ready · initStrict->init
168
+ // func/space/strict child proxies are cached per createRpcClient instance + surface + lossless RPC path:
169
+ // c.func.a.b === c.func.a.b and c.all === c.func; dotted keys remain distinct from segmented paths.
170
+ // Identity survives strict-schema refresh, reauth and transient reconnect. Hard token rotation creates
171
+ // a fresh client/cache; incompatible wait/surface semantics never share a proxy.
172
+ // Only Listen subscriptions are transport-resumed. Pending/attempted ordinary RPC and pipe calls reject
173
+ // on disconnect and are NEVER replayed: retrying an arbitrary command could repeat side effects.
163
174
  noStrict(obj) / isNoStrict(obj) // dynamic (no-schema) subtree
164
175
  endCallback(fn) // alias: rpcEndCallback
165
176
  // subscription primitives (rare/manual; createRpcServerAuto/createRpcClientHub are the normal path)
@@ -458,28 +469,36 @@ withReplayListen(base, {current?, frame?, history?, getSince?, onJournal?, now?,
458
469
  // sugar `current: 'last'` — single-entity lines: keyframe = last journaled envelope, no hand-kept state.
459
470
  // Source 2 (mini-frame): `frame` lambda — gets the raw tail, returns a state-equivalent compact
460
471
  // (last-per-entity, gap aggregate...); cost ~ touched entities, wins over keyframe while the journal covers.
461
- // Line classes FOLLOW from declared lambdas (no mode flags): current+frame = condensable; current only =
462
- // snapshot-recoverable; neither = sacred queue — full tail only, evicted -> frame() THROWS (loud, never silent loss).
472
+ // Line classes FOLLOW from declared lambdas (no mode flags): current+frame = condensable with keyframe fallback;
473
+ // current only = exact retained tail + keyframe fallback; frame without current condenses only while the tail
474
+ // is retained and still fails on eviction; neither = sacred exact queue — eviction THROWS terminally.
463
475
  // Triggers: reconnect (`since`), client pull (own timer — replaces any server-side interval mode), server gate drain.
464
476
  // The transport sees ONLY seq; entity keys/skip rules live in producer lambdas (hint = opaque per-subscriber pass-through).
465
477
  exposeReplay(replay) <-> replaySubscribe(remote, cb, {since?, onSeq?, staleMs?, onStale?, skewMs?, now?, policy?, hint?}) -> off // wire pair over the EXISTING rpc: line = plain Listen, since/keyframe/frame = plain methods
466
478
  // NORMAL PATH: createRpcServerAuto exposes replay listens automatically (see rpc section) — exposeReplay stays
467
479
  // as the manual/custom-transport path. replaySubscribe prefers `frame` when the server has it (one round trip,
468
480
  // server picks tail/mini-frame/keyframe; sacred throw -> onError), falls back to since/keyframe on old servers.
469
- // policy: 'queue' (default — socket buffers everything, nothing skipped) | 'frame' (subscribes frameLine when
470
- // present: on lag the server drops and recovers via frame(lastSent) — mini-frame, no backlog). A non-envelope
481
+ // policy: 'queue' (default — ungated connected live line) | 'frame' (subscribes frameLine when present: on lag
482
+ // the server may drop and recover via a state-equivalent frame(lastSent)). Catch-up is shared by both policies
483
+ // and may use a producer mini-frame/keyframe, so a covered raw seq jump is not a logical data gap. A non-envelope
471
484
  // on the line (RPC_STOP — e.g. the gate's loud sacred failure) surfaces via onError + off, never silence.
472
485
  // hint reaches the frame lambda on catch-up and on every explicit frame(seq, hint) call (pull); the push-gate's
473
486
  // drain recovery uses the line's DEFAULT condensation — client-specific rules/pace = the pull path.
474
- // off.ready (catch-up done) · off.seq() (reconnect point) · off.isStale()/off.lastTs(); reconnect = call again with {since: prev.seq()}
487
+ // off.ready (first handover done, or terminal error/teardown) · off.seq() (last honestly delivered coordinate) · off.isStale()/off.lastTs().
488
+ // Hub-managed RPC remotes resume automatically after transient reconnect from off.seq(): live is restored first,
489
+ // racing envelopes queue, then frame/since catch-up is sorted+deduped. Transport-agnostic remotes without RPC
490
+ // lifecycle metadata still reconnect by creating a new subscriber with {since: prev.seq()}.
475
491
  replayRouteSubscribe(remote, cb, {label?, since?, onSeq?, onError?, onRoute?}) -> off & {ready, switch(nextRemote, {label?, since?, reset?, policy?, hint?}), seq(), label(), active()}
476
492
  // transport hand-off helper: old route remains live, replacement subscribes+catches up from seq, then old closes; overlap is seq-deduped. Use for relay -> direct and direct -> relay over any ordered ReplayRemote.
477
493
  // DELIVERY CONTRACT (guaranteed, not best-effort): the subscriber's cb sees ONE uniform stream —
478
494
  // first delivery = the snapshot (keyframe as an event of the SAME type; store: root patch),
479
- // then only strictly-newer events, seq-ascending, no gaps, no dups. Live events racing ahead of the
495
+ // then only strictly-newer events, seq-ascending and deduped. A raw seq jump is valid only when a
496
+ // producer-declared mini-frame/keyframe is state-equivalent to the omitted range; sacred retained tails
497
+ // are exact. Live events racing ahead of the
480
498
  // keyframe over the wire are queued during catch-up and seq-deduped — they can NEVER arrive first.
481
499
  // With {since: K}: same fold, journal tail after K instead of a keyframe (evicted -> keyframe fallback,
482
- // visible to the client as a seq jump > +1). Requires an ORDERED transport (socket.io / TCP / in-proc).
500
+ // visible as a covered seq jump > +1). A sacred eviction instead calls onError, closes the subscriber and
501
+ // never drains post-gap live events as a false continuation. Requires an ORDERED transport (socket.io / TCP / in-proc).
483
502
  // Net effect: one client fold `state = apply(state, event)` handles cold start, reconnect,
484
503
  // conflation recovery and archive playback identically — snapshot is not a special case.
485
504
  // FRESHNESS (staleMs/onStale — an option, not consumer boilerplate): delivery is consistent but silent
@@ -496,7 +515,8 @@ createRouteCoordinator({connect, policy?, shadow?, catchUpTimeoutMs?}) -> coordi
496
515
  // LAYERING: a CONNECTOR is a pure transport (no route decisions) — {info: {label, kind: 'relay'|'direct',
497
516
  // binary?, ordered?, reliable?}, open() -> ReplayRemote, close(), state(), metrics?() -> {rtt?, pending?},
498
517
  // onFail?}; the COORDINATOR alone owns promotion/fallback (state machine + policy); data continuity is
499
- // replayRouteSubscribe underneath, so ANY route change is gap-free and dup-free by the seq contract.
518
+ // replayRouteSubscribe underneath, so ANY route change is deduped and has no uncovered logical gap;
519
+ // a producer-authorized frame/keyframe may still jump raw seq.
500
520
  // WebRTC/NAT is deliberately absent here: a future datachannel is just another RouteConnector.
501
521
  // connect(ref, kind) -> RouteConnector — transport factory per pair and kind (called per activation).
502
522
  // policy hooks run BEFORE any transport action; absent hook = allowed, present hook must return true:
@@ -630,7 +650,7 @@ openHistory(storage, live?) -> {at({seq?|ts?}?), subscribe(cb, {since?|ts?, onSe
630
650
  // seamless rewind->live: create the line with getSince reading the same storage («memory outside»); else the gap closes with a keyframe jump (still consistent)
631
651
  storeReplayAt(storage, {seq?|ts?}?) -> snapshot | undefined // store time machine: bit-exact state at any archived moment (same applyStorePatch mechanism)
632
652
  ```
633
- > Killer property: a lagging/late/stalled consumer never gets a queue backlog evicted seq / full outgoing buffer -> fresh keyframe + live from it.
653
+ > Killer property for state/frame lines: a lagging/late/stalled consumer can replace backlog with a state-equivalent frame/keyframe. A sacred queue deliberately does not: its retained tail is exact, and eviction is a terminal error rather than silent loss.
634
654
  > Files: `src/Common/events/replay-{listen,wire,conflate,history,index}.ts` + `src/Common/Observe/store-{replay,offline}.ts`;
635
655
  > everything is additive (the canonical Listen surface gained only `registerListenOn`/`ListenOnBrand`; exposeStore/mirror untouched).
636
656
  > Oracles: `npx ts-node replay/<f>.ts` — replay-listen / store-replay / offline-store / socket-replay / offline-store-socket / conflate / conflate-socket / coalesce / history / staleness / canvas-socket (raw bytes) / video-socket.demo;
@@ -682,4 +702,4 @@ const facade = {
682
702
  // createRpcServerAuto({ object: facade, ... }) exposes accountChanged/accountChangedPaths/btcChanged
683
703
  // as normal RPC Listen subscriptions. This is a notification stream, not a full
684
704
  // automatic snapshot mirror; send/read snapshots explicitly via facade methods.
685
- ```
705
+ ```