@rotorsoft/act-sse 1.0.0 → 1.1.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.
@@ -1,35 +1,29 @@
1
- import type { BroadcastMessage, BroadcastState } from "./types.js";
1
+ import type { BroadcastState, PatchMessage } from "./types.js";
2
2
  /**
3
- * Result of applying a broadcast message to cached client state.
3
+ * Result of applying a patch message to cached client state.
4
4
  */
5
5
  export type ApplyResult<S extends BroadcastState = BroadcastState> = {
6
6
  ok: true;
7
7
  state: S;
8
8
  } | {
9
9
  ok: false;
10
- reason: "stale" | "behind" | "patch-failed";
10
+ reason: "stale" | "behind";
11
11
  };
12
12
  /**
13
- * Apply a broadcast message to the client's cached state.
14
- *
15
- * Handles both full state and incremental patches with version validation.
16
- * Returns the new state on success, or a failure reason that the client
17
- * can use to decide whether to resync.
13
+ * Apply a version-keyed patch message to the client's cached state.
18
14
  *
19
15
  * ## Version logic
20
16
  *
21
- * - **Full state**: accepted if `msg._v >= cachedV` (or no cached state)
22
- * - **Patch**:
23
- * - `_baseV < cachedV`"stale" (client ahead, skip — mutation response arrived first)
24
- * - `_baseV > cachedV` → "behind" (client missed a version, must resync)
25
- * - `_baseV === cachedV` → apply patch ops
17
+ * - All patches older than cached "stale" (client already ahead)
18
+ * - Gap between cached version and first patch → "behind" (client missed versions, must resync)
19
+ * - Contiguous from cached version apply in order
26
20
  *
27
21
  * ## Usage (React Query)
28
22
  *
29
23
  * ```typescript
30
24
  * onData: (msg) => {
31
25
  * const cached = utils.getState.getData({ streamId });
32
- * const result = applyBroadcastMessage(msg, cached);
26
+ * const result = applyPatchMessage(msg, cached);
33
27
  * if (result.ok) {
34
28
  * utils.getState.setData({ streamId }, result.state);
35
29
  * } else if (result.reason === "behind") {
@@ -39,5 +33,5 @@ export type ApplyResult<S extends BroadcastState = BroadcastState> = {
39
33
  * }
40
34
  * ```
41
35
  */
42
- export declare function applyBroadcastMessage<S extends BroadcastState>(msg: BroadcastMessage<S>, cached: S | null | undefined): ApplyResult<S>;
36
+ export declare function applyPatchMessage<S extends BroadcastState>(msg: PatchMessage<S>, cached: S | null | undefined): ApplyResult<S>;
43
37
  //# sourceMappingURL=apply-patch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apply-patch.d.ts","sourceRoot":"","sources":["../../src/apply-patch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,IAC7D;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,cAAc,CAAA;CAAE,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,cAAc,EAC5D,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,EACxB,MAAM,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GAC3B,WAAW,CAAC,CAAC,CAAC,CAyBhB"}
1
+ {"version":3,"file":"apply-patch.d.ts","sourceRoot":"","sources":["../../src/apply-patch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/D;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,IAC7D;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,GAAG,QAAQ,CAAA;CAAE,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,cAAc,EACxD,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC,EACpB,MAAM,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GAC3B,WAAW,CAAC,CAAC,CAAC,CAuBhB"}
@@ -1,12 +1,11 @@
1
1
  import { StateCache } from "./state-cache.js";
2
- import type { BroadcastMessage, BroadcastOptions, BroadcastState, Subscriber } from "./types.js";
2
+ import type { BroadcastState, PatchMessage, Subscriber } from "./types.js";
3
3
  /**
4
4
  * Server-side broadcast channel for incremental state sync over SSE.
5
5
  *
6
6
  * Manages per-stream subscriber sets and an LRU state cache. When state
7
- * changes, computes an RFC 6902 JSON Patch against the previous cached
8
- * state and pushes either a patch (small diff) or full state (large diff
9
- * or first broadcast) to all subscribers.
7
+ * changes, forwards domain patches (from event handlers) to all subscribers
8
+ * as version-keyed messages.
10
9
  *
11
10
  * ## Usage
12
11
  *
@@ -14,9 +13,10 @@ import type { BroadcastMessage, BroadcastOptions, BroadcastState, Subscriber } f
14
13
  * const broadcast = new BroadcastChannel<MyState>();
15
14
  *
16
15
  * // After every app.do():
17
- * const snap = await doAction(...);
18
- * const state = deriveState(snap); // app-specific state derivation
19
- * broadcast.publish(streamId, state); // computes patch + pushes to SSE
16
+ * const snaps = await app.do(...);
17
+ * const patches = snaps.map(s => s.patch).filter(Boolean);
18
+ * const state = deriveState(snaps.at(-1));
19
+ * broadcast.publish(streamId, state, patches);
20
20
  *
21
21
  * // In SSE subscription:
22
22
  * const cleanup = broadcast.subscribe(streamId, (msg) => {
@@ -26,7 +26,6 @@ import type { BroadcastMessage, BroadcastOptions, BroadcastState, Subscriber } f
26
26
  *
27
27
  * // Initial state for reconnects:
28
28
  * const cached = broadcast.getState(streamId);
29
- * if (cached) yield { _type: "full", ...cached, serverTime: ... };
30
29
  * ```
31
30
  *
32
31
  * ## Version Contract
@@ -38,25 +37,24 @@ import type { BroadcastMessage, BroadcastOptions, BroadcastState, Subscriber } f
38
37
  export declare class BroadcastChannel<S extends BroadcastState = BroadcastState> {
39
38
  private channels;
40
39
  private stateCache;
41
- private maxPatchOps;
42
- constructor(options?: BroadcastOptions & {
40
+ constructor(options?: {
43
41
  cacheSize?: number;
44
42
  });
45
43
  /**
46
- * Publish new state for a stream. Computes a patch against the previously
47
- * cached state and pushes to all subscribers.
44
+ * Publish domain patches from a commit.
45
+ * patches[i] corresponds to version baseV + i + 1.
48
46
  *
49
47
  * @param streamId - The event store stream ID
50
48
  * @param state - Full state with `_v` set from `snap.event.version`
51
- * @returns The broadcast message that was sent (or undefined if no subscribers and no cache change)
49
+ * @param patches - Array of domain patches, one per emitted event
52
50
  */
53
- publish(streamId: string, state: S): BroadcastMessage<S>;
51
+ publish(streamId: string, state: S, patches?: Partial<S>[]): PatchMessage<S>;
54
52
  /**
55
53
  * Publish a state update that doesn't change the event version
56
54
  * (e.g. presence overlay, computed field refresh).
57
- * Uses the same version as the cached state for _baseV and _v.
55
+ * Uses the same version as the cached state, single entry.
58
56
  */
59
- publishOverlay(streamId: string, state: S): BroadcastMessage<S> | undefined;
57
+ publishOverlay(streamId: string, overlayPatch: Partial<S>): PatchMessage<S> | undefined;
60
58
  /**
61
59
  * Subscribe to broadcast messages for a stream.
62
60
  * Returns a cleanup function that removes the subscription.
@@ -68,6 +66,5 @@ export declare class BroadcastChannel<S extends BroadcastState = BroadcastState>
68
66
  getState(streamId: string): S | undefined;
69
67
  /** Direct access to the state cache (for app-specific reads like presence). */
70
68
  get cache(): StateCache<S>;
71
- private computeMessage;
72
69
  }
73
70
  //# sourceMappingURL=broadcast.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../src/broadcast.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,UAAU,EACX,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,gBAAgB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc;IACrE,OAAO,CAAC,QAAQ,CAAyC;IACzD,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,CAAC,EAAE,gBAAgB,GAAG;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAK/D;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC;IAYxD;;;;OAIG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,GAAG,SAAS;IAc3E;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAW1D,kDAAkD;IAClD,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAI5C,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAIzC,+EAA+E;IAC/E,IAAI,KAAK,IAAI,UAAU,CAAC,CAAC,CAAC,CAEzB;IAID,OAAO,CAAC,cAAc;CAwBvB"}
1
+ {"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../src/broadcast.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,gBAAgB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc;IACrE,OAAO,CAAC,QAAQ,CAAyC;IACzD,OAAO,CAAC,UAAU,CAAgB;gBAEtB,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAI5C;;;;;;;OAOG;IACH,OAAO,CACL,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,EACR,OAAO,GAAE,OAAO,CAAC,CAAC,CAAC,EAAO,GACzB,YAAY,CAAC,CAAC,CAAC;IAgBlB;;;;OAIG;IACH,cAAc,CACZ,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC,GACvB,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS;IAe9B;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAW1D,kDAAkD;IAClD,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAI5C,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAIzC,+EAA+E;IAC/E,IAAI,KAAK,IAAI,UAAU,CAAC,CAAC,CAAC,CAEzB;CACF"}
@@ -4,34 +4,31 @@
4
4
  *
5
5
  * Incremental state broadcast over SSE for act event-sourced apps.
6
6
  *
7
- * Provides server-side broadcast with automatic RFC 6902 JSON Patch
8
- * computation, an LRU state cache, presence tracking, and a client-side
7
+ * Provides server-side broadcast with domain patch forwarding,
8
+ * an LRU state cache, presence tracking, and a client-side
9
9
  * patch applicator with version validation and resync detection.
10
10
  *
11
11
  * ## Architecture
12
12
  *
13
13
  * ```
14
- * app.do() → snap
14
+ * app.do() → snapshots (each carries its domain patch)
15
15
  * │
16
16
  * ▼
17
17
  * deriveState(snap) ← app-specific (overlay presence, deadlines, etc.)
18
18
  * state._v = snap.event.version
19
19
  * │
20
20
  * ▼
21
- * broadcast.publish(streamId, state)
21
+ * broadcast.publish(streamId, state, patches)
22
22
  * │
23
- * ├── compare(prev, state) RFC 6902 ops
24
- * ├── if ops ≤ threshold → PatchMessage { _baseV, _v, _patch }
25
- * ├── if ops > threshold → FullStateMessage { _v, ...state }
23
+ * ├── version-key each patch: { [baseV+1]: patch1, [baseV+2]: patch2 }
26
24
  * └── push to all SSE subscribers
27
25
  * │
28
26
  * ▼
29
- * Client: applyBroadcastMessage(msg, cached)
27
+ * Client: applyPatchMessage(msg, cached)
30
28
  * │
31
- * ├── full accept if _v cachedV
32
- * ├── patch apply if _baseV === cachedV
33
- * ├── stale skip (_baseV < cachedV, mutation response arrived first)
34
- * └── behind → resync (_baseV > cachedV, client missed a version)
29
+ * ├── contiguous deep-merge patches in version order
30
+ * ├── stale skip (client already ahead)
31
+ * └── behind resync (client missed versions)
35
32
  * ```
36
33
  *
37
34
  * ## Version Contract
@@ -39,10 +36,11 @@
39
36
  * `_v` is always the event store stream version (`snap.event.version`).
40
37
  * No separate version counters. The event store is the single source of truth.
41
38
  */
42
- export { applyBroadcastMessage } from "./apply-patch.js";
39
+ export { applyPatchMessage } from "./apply-patch.js";
43
40
  export type { ApplyResult } from "./apply-patch.js";
44
41
  export { BroadcastChannel } from "./broadcast.js";
42
+ export { patch } from "./patch.js";
45
43
  export { PresenceTracker } from "./presence.js";
46
44
  export { StateCache } from "./state-cache.js";
47
- export type { BroadcastMessage, BroadcastOptions, BroadcastState, FullStateMessage, PatchMessage, Subscriber, } from "./types.js";
45
+ export type { BroadcastState, PatchMessage, Subscriber } from "./types.js";
48
46
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,UAAU,GACX,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Browser-safe deep merge utility — identical semantics to @rotorsoft/act's patch().
3
+ * Inlined here so act-sse has zero Node dependencies.
4
+ */
5
+ type Schema = Record<string, any>;
6
+ type Patch<T> = {
7
+ [K in keyof T]?: T[K] extends Schema ? Patch<T[K]> : T[K];
8
+ };
9
+ /** Immutably deep-merge `patches` into `original`. */
10
+ export declare const patch: <S extends Schema>(original: Readonly<S>, patches: Readonly<Patch<S>>) => Readonly<S>;
11
+ export {};
12
+ //# sourceMappingURL=patch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../../src/patch.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAClC,KAAK,KAAK,CAAC,CAAC,IAAI;KACb,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAC1D,CAAC;AA8BF,sDAAsD;AACtD,eAAO,MAAM,KAAK,GAAI,CAAC,SAAS,MAAM,EACpC,UAAU,QAAQ,CAAC,CAAC,CAAC,EACrB,SAAS,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAC1B,QAAQ,CAAC,CAAC,CAgBZ,CAAC"}
@@ -1,4 +1,3 @@
1
- import type { Operation } from "fast-json-patch";
2
1
  /**
3
2
  * Base constraint for state objects managed by the broadcast system.
4
3
  * Apps extend this with their own domain state shape.
@@ -8,39 +7,20 @@ export type BroadcastState = Record<string, unknown> & {
8
7
  _v: number;
9
8
  };
10
9
  /**
11
- * Full state messagesent on initial connect, resync, or when patch is too large.
10
+ * Recursive deep partialmirrors act core's Patch<T>.
12
11
  */
13
- export type FullStateMessage<S extends BroadcastState = BroadcastState> = S & {
14
- _type: "full";
15
- serverTime: string;
12
+ type DeepPartial<T> = {
13
+ [K in keyof T]?: T[K] extends Record<string, any> ? DeepPartial<T[K]> : T[K];
16
14
  };
17
15
  /**
18
- * Incremental patch message sent when the diff is small enough.
19
- * Client applies RFC 6902 operations to its cached state at _baseV to reach _v.
16
+ * SSE message: version-keyed domain patches.
17
+ * Keys are stringified version numbers, values are domain patches (deep partials).
18
+ * Multi-event commits produce multiple version-keyed entries.
20
19
  */
21
- export type PatchMessage = {
22
- _type: "patch";
23
- /** Target version after applying the patch */
24
- _v: number;
25
- /** Version the patch applies to (client must have this version cached) */
26
- _baseV: number;
27
- /** RFC 6902 JSON Patch operations */
28
- _patch: Operation[];
29
- serverTime: string;
30
- };
31
- /**
32
- * Discriminated union sent over SSE — client switches on `_type`.
33
- */
34
- export type BroadcastMessage<S extends BroadcastState = BroadcastState> = FullStateMessage<S> | PatchMessage;
20
+ export type PatchMessage<S extends BroadcastState = BroadcastState> = Record<number, DeepPartial<S>>;
35
21
  /**
36
- * Subscriber callback — receives either a patch or full state message.
22
+ * Subscriber callback — receives version-keyed patch messages.
37
23
  */
38
- export type Subscriber<S extends BroadcastState = BroadcastState> = (msg: BroadcastMessage<S>) => void;
39
- /**
40
- * Options for creating a broadcast channel.
41
- */
42
- export type BroadcastOptions = {
43
- /** Max RFC 6902 operations before falling back to full state (default: 50) */
44
- maxPatchOps?: number;
45
- };
24
+ export type Subscriber<S extends BroadcastState = BroadcastState> = (msg: PatchMessage<S>) => void;
25
+ export {};
46
26
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IACrD,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,IAAI,CAAC,GAAG;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,8CAA8C;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,IAClE,gBAAgB,CAAC,CAAC,CAAC,GACnB,YAAY,CAAC;AAEjB;;GAEG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,IAAI,CAClE,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,KACrB,IAAI,CAAC;AAEV;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IACrD,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF;;GAEG;AAEH,KAAK,WAAW,CAAC,CAAC,IAAI;KACnB,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAC7E,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,IAAI,MAAM,CAC1E,MAAM,EACN,WAAW,CAAC,CAAC,CAAC,CACf,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,IAAI,CAClE,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC,KACjB,IAAI,CAAC"}
package/dist/index.cjs CHANGED
@@ -23,35 +23,64 @@ __export(index_exports, {
23
23
  BroadcastChannel: () => BroadcastChannel,
24
24
  PresenceTracker: () => PresenceTracker,
25
25
  StateCache: () => StateCache,
26
- applyBroadcastMessage: () => applyBroadcastMessage
26
+ applyPatchMessage: () => applyPatchMessage,
27
+ patch: () => patch
27
28
  });
28
29
  module.exports = __toCommonJS(index_exports);
29
30
 
31
+ // src/patch.ts
32
+ var UNMERGEABLES = [
33
+ RegExp,
34
+ Date,
35
+ Array,
36
+ Map,
37
+ Set,
38
+ WeakMap,
39
+ WeakSet,
40
+ ArrayBuffer,
41
+ SharedArrayBuffer,
42
+ DataView,
43
+ Int8Array,
44
+ Uint8Array,
45
+ Uint8ClampedArray,
46
+ Int16Array,
47
+ Uint16Array,
48
+ Int32Array,
49
+ Uint32Array,
50
+ Float32Array,
51
+ Float64Array
52
+ ];
53
+ var is_mergeable = (value) => !!value && typeof value === "object" && !UNMERGEABLES.some((t) => value instanceof t);
54
+ var patch = (original, patches) => {
55
+ const copy = {};
56
+ Object.keys({ ...original, ...patches }).forEach((key) => {
57
+ const patched_value = patches[key];
58
+ const original_value = original[key];
59
+ const patched = patches && key in patches;
60
+ const deleted = patched && (typeof patched_value === "undefined" || patched_value === null);
61
+ const value = patched && !deleted ? patched_value : original_value;
62
+ !deleted && (copy[key] = is_mergeable(value) ? patch(original_value || {}, patched_value || {}) : value);
63
+ });
64
+ return copy;
65
+ };
66
+
30
67
  // src/apply-patch.ts
31
- var import_fast_json_patch = require("fast-json-patch");
32
- function applyBroadcastMessage(msg, cached) {
68
+ function applyPatchMessage(msg, cached) {
33
69
  const cachedV = cached?._v ?? 0;
34
- if (msg._type === "full") {
35
- if (msg._v < cachedV) return { ok: false, reason: "stale" };
36
- const { _type, ...state } = msg;
37
- return { ok: true, state };
38
- }
39
- if (msg._baseV < cachedV) return { ok: false, reason: "stale" };
40
- if (msg._baseV > cachedV) return { ok: false, reason: "behind" };
41
- if (!cached) return { ok: false, reason: "behind" };
42
- try {
43
- const clone = structuredClone(cached);
44
- (0, import_fast_json_patch.applyPatch)(clone, msg._patch, true);
45
- clone._v = msg._v;
46
- return { ok: true, state: clone };
47
- } catch {
48
- return { ok: false, reason: "patch-failed" };
49
- }
70
+ const versions = Object.keys(msg).map(Number).sort((a, b) => a - b);
71
+ if (!versions.length) return { ok: false, reason: "stale" };
72
+ const minV = versions[0];
73
+ const maxV = versions[versions.length - 1];
74
+ if (maxV <= cachedV) return { ok: false, reason: "stale" };
75
+ if (!cached || minV > cachedV + 1) return { ok: false, reason: "behind" };
76
+ let state = cached;
77
+ for (const v of versions) {
78
+ if (v <= cachedV) continue;
79
+ state = { ...patch(state, msg[v]), _v: v };
80
+ }
81
+ return { ok: true, state };
50
82
  }
51
83
 
52
- // src/broadcast.ts
53
- var import_fast_json_patch2 = require("fast-json-patch");
54
-
55
84
  // src/state-cache.ts
56
85
  var StateCache = class {
57
86
  cache = /* @__PURE__ */ new Map();
@@ -95,27 +124,27 @@ var StateCache = class {
95
124
  };
96
125
 
97
126
  // src/broadcast.ts
98
- var DEFAULT_MAX_PATCH_OPS = 50;
99
127
  var BroadcastChannel = class {
100
128
  channels = /* @__PURE__ */ new Map();
101
129
  stateCache;
102
- maxPatchOps;
103
130
  constructor(options) {
104
131
  this.stateCache = new StateCache(options?.cacheSize ?? 50);
105
- this.maxPatchOps = options?.maxPatchOps ?? DEFAULT_MAX_PATCH_OPS;
106
132
  }
107
133
  /**
108
- * Publish new state for a stream. Computes a patch against the previously
109
- * cached state and pushes to all subscribers.
134
+ * Publish domain patches from a commit.
135
+ * patches[i] corresponds to version baseV + i + 1.
110
136
  *
111
137
  * @param streamId - The event store stream ID
112
138
  * @param state - Full state with `_v` set from `snap.event.version`
113
- * @returns The broadcast message that was sent (or undefined if no subscribers and no cache change)
139
+ * @param patches - Array of domain patches, one per emitted event
114
140
  */
115
- publish(streamId, state) {
116
- const prev = this.stateCache.get(streamId);
141
+ publish(streamId, state, patches = []) {
117
142
  this.stateCache.set(streamId, state);
118
- const msg = this.computeMessage(prev, state);
143
+ const baseV = state._v - patches.length;
144
+ const msg = {};
145
+ patches.forEach((p, i) => {
146
+ msg[baseV + i + 1] = p;
147
+ });
119
148
  const subs = this.channels.get(streamId);
120
149
  if (subs?.size) {
121
150
  for (const cb of subs) cb(msg);
@@ -125,13 +154,14 @@ var BroadcastChannel = class {
125
154
  /**
126
155
  * Publish a state update that doesn't change the event version
127
156
  * (e.g. presence overlay, computed field refresh).
128
- * Uses the same version as the cached state for _baseV and _v.
157
+ * Uses the same version as the cached state, single entry.
129
158
  */
130
- publishOverlay(streamId, state) {
159
+ publishOverlay(streamId, overlayPatch) {
131
160
  const prev = this.stateCache.get(streamId);
132
161
  if (!prev) return void 0;
162
+ const state = patch(prev, overlayPatch);
133
163
  this.stateCache.set(streamId, state);
134
- const msg = this.computeMessage(prev, state);
164
+ const msg = { [state._v]: overlayPatch };
135
165
  const subs = this.channels.get(streamId);
136
166
  if (subs?.size) {
137
167
  for (const cb of subs) cb(msg);
@@ -164,27 +194,6 @@ var BroadcastChannel = class {
164
194
  get cache() {
165
195
  return this.stateCache;
166
196
  }
167
- // --- internals ---
168
- computeMessage(prev, next) {
169
- const serverTime = (/* @__PURE__ */ new Date()).toISOString();
170
- if (!prev) {
171
- return { _type: "full", ...next, serverTime };
172
- }
173
- const ops = (0, import_fast_json_patch2.compare)(prev, next);
174
- if (ops.length === 0) {
175
- return { _type: "full", ...next, serverTime };
176
- }
177
- if (ops.length > this.maxPatchOps) {
178
- return { _type: "full", ...next, serverTime };
179
- }
180
- return {
181
- _type: "patch",
182
- _v: next._v,
183
- _baseV: prev._v,
184
- _patch: ops,
185
- serverTime
186
- };
187
- }
188
197
  };
189
198
 
190
199
  // src/presence.ts
@@ -220,6 +229,7 @@ var PresenceTracker = class {
220
229
  BroadcastChannel,
221
230
  PresenceTracker,
222
231
  StateCache,
223
- applyBroadcastMessage
232
+ applyPatchMessage,
233
+ patch
224
234
  });
225
235
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/apply-patch.ts","../src/broadcast.ts","../src/state-cache.ts","../src/presence.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-sse\n *\n * Incremental state broadcast over SSE for act event-sourced apps.\n *\n * Provides server-side broadcast with automatic RFC 6902 JSON Patch\n * computation, an LRU state cache, presence tracking, and a client-side\n * patch applicator with version validation and resync detection.\n *\n * ## Architecture\n *\n * ```\n * app.do() → snap\n * │\n * ▼\n * deriveState(snap) ← app-specific (overlay presence, deadlines, etc.)\n * state._v = snap.event.version\n * │\n * ▼\n * broadcast.publish(streamId, state)\n * │\n * ├── compare(prev, state) → RFC 6902 ops\n * ├── if ops ≤ threshold → PatchMessage { _baseV, _v, _patch }\n * ├── if ops > threshold → FullStateMessage { _v, ...state }\n * └── push to all SSE subscribers\n * │\n * ▼\n * Client: applyBroadcastMessage(msg, cached)\n * │\n * ├── full → accept if _v ≥ cachedV\n * ├── patch → apply if _baseV === cachedV\n * ├── stale → skip (_baseV < cachedV, mutation response arrived first)\n * └── behind → resync (_baseV > cachedV, client missed a version)\n * ```\n *\n * ## Version Contract\n *\n * `_v` is always the event store stream version (`snap.event.version`).\n * No separate version counters. The event store is the single source of truth.\n */\n\nexport { applyBroadcastMessage } from \"./apply-patch.js\";\nexport type { ApplyResult } from \"./apply-patch.js\";\nexport { BroadcastChannel } from \"./broadcast.js\";\nexport { PresenceTracker } from \"./presence.js\";\nexport { StateCache } from \"./state-cache.js\";\nexport type {\n BroadcastMessage,\n BroadcastOptions,\n BroadcastState,\n FullStateMessage,\n PatchMessage,\n Subscriber,\n} from \"./types.js\";\n","import { applyPatch } from \"fast-json-patch\";\nimport type { BroadcastMessage, BroadcastState } from \"./types.js\";\n\n/**\n * Result of applying a broadcast message to cached client state.\n */\nexport type ApplyResult<S extends BroadcastState = BroadcastState> =\n | { ok: true; state: S }\n | { ok: false; reason: \"stale\" | \"behind\" | \"patch-failed\" };\n\n/**\n * Apply a broadcast message to the client's cached state.\n *\n * Handles both full state and incremental patches with version validation.\n * Returns the new state on success, or a failure reason that the client\n * can use to decide whether to resync.\n *\n * ## Version logic\n *\n * - **Full state**: accepted if `msg._v >= cachedV` (or no cached state)\n * - **Patch**:\n * - `_baseV < cachedV` → \"stale\" (client ahead, skip — mutation response arrived first)\n * - `_baseV > cachedV` → \"behind\" (client missed a version, must resync)\n * - `_baseV === cachedV` → apply patch ops\n *\n * ## Usage (React Query)\n *\n * ```typescript\n * onData: (msg) => {\n * const cached = utils.getState.getData({ streamId });\n * const result = applyBroadcastMessage(msg, cached);\n * if (result.ok) {\n * utils.getState.setData({ streamId }, result.state);\n * } else if (result.reason === \"behind\") {\n * utils.getState.invalidate({ streamId }); // trigger full refetch\n * }\n * // \"stale\" → no-op, client already has newer state\n * }\n * ```\n */\nexport function applyBroadcastMessage<S extends BroadcastState>(\n msg: BroadcastMessage<S>,\n cached: S | null | undefined\n): ApplyResult<S> {\n const cachedV = cached?._v ?? 0;\n\n if (msg._type === \"full\") {\n if (msg._v < cachedV) return { ok: false, reason: \"stale\" };\n // Strip _type from the state stored in cache\n const { _type, ...state } = msg;\n return { ok: true, state: state as S };\n }\n\n // Patch message\n if (msg._baseV < cachedV) return { ok: false, reason: \"stale\" };\n if (msg._baseV > cachedV) return { ok: false, reason: \"behind\" };\n\n // _baseV === cachedV — apply\n if (!cached) return { ok: false, reason: \"behind\" };\n\n try {\n const clone = structuredClone(cached);\n applyPatch(clone, msg._patch, true); // mutates clone in-place, throws on validation failure\n clone._v = msg._v;\n return { ok: true, state: clone };\n } catch {\n return { ok: false, reason: \"patch-failed\" };\n }\n}\n","import { compare } from \"fast-json-patch\";\nimport { StateCache } from \"./state-cache.js\";\nimport type {\n BroadcastMessage,\n BroadcastOptions,\n BroadcastState,\n Subscriber,\n} from \"./types.js\";\n\nconst DEFAULT_MAX_PATCH_OPS = 50;\n\n/**\n * Server-side broadcast channel for incremental state sync over SSE.\n *\n * Manages per-stream subscriber sets and an LRU state cache. When state\n * changes, computes an RFC 6902 JSON Patch against the previous cached\n * state and pushes either a patch (small diff) or full state (large diff\n * or first broadcast) to all subscribers.\n *\n * ## Usage\n *\n * ```typescript\n * const broadcast = new BroadcastChannel<MyState>();\n *\n * // After every app.do():\n * const snap = await doAction(...);\n * const state = deriveState(snap); // app-specific state derivation\n * broadcast.publish(streamId, state); // computes patch + pushes to SSE\n *\n * // In SSE subscription:\n * const cleanup = broadcast.subscribe(streamId, (msg) => {\n * pending = msg;\n * resolve?.();\n * });\n *\n * // Initial state for reconnects:\n * const cached = broadcast.getState(streamId);\n * if (cached) yield { _type: \"full\", ...cached, serverTime: ... };\n * ```\n *\n * ## Version Contract\n *\n * The `_v` field on state MUST be set from `snap.event.version` (the event\n * store's monotonic stream version) BEFORE calling `publish()`. This is the\n * single source of truth for ordering — no separate version counters.\n */\nexport class BroadcastChannel<S extends BroadcastState = BroadcastState> {\n private channels = new Map<string, Set<Subscriber<S>>>();\n private stateCache: StateCache<S>;\n private maxPatchOps: number;\n\n constructor(options?: BroadcastOptions & { cacheSize?: number }) {\n this.stateCache = new StateCache<S>(options?.cacheSize ?? 50);\n this.maxPatchOps = options?.maxPatchOps ?? DEFAULT_MAX_PATCH_OPS;\n }\n\n /**\n * Publish new state for a stream. Computes a patch against the previously\n * cached state and pushes to all subscribers.\n *\n * @param streamId - The event store stream ID\n * @param state - Full state with `_v` set from `snap.event.version`\n * @returns The broadcast message that was sent (or undefined if no subscribers and no cache change)\n */\n publish(streamId: string, state: S): BroadcastMessage<S> {\n const prev = this.stateCache.get(streamId);\n this.stateCache.set(streamId, state);\n\n const msg = this.computeMessage(prev, state);\n const subs = this.channels.get(streamId);\n if (subs?.size) {\n for (const cb of subs) cb(msg);\n }\n return msg;\n }\n\n /**\n * Publish a state update that doesn't change the event version\n * (e.g. presence overlay, computed field refresh).\n * Uses the same version as the cached state for _baseV and _v.\n */\n publishOverlay(streamId: string, state: S): BroadcastMessage<S> | undefined {\n const prev = this.stateCache.get(streamId);\n if (!prev) return undefined;\n\n this.stateCache.set(streamId, state);\n\n const msg = this.computeMessage(prev, state);\n const subs = this.channels.get(streamId);\n if (subs?.size) {\n for (const cb of subs) cb(msg);\n }\n return msg;\n }\n\n /**\n * Subscribe to broadcast messages for a stream.\n * Returns a cleanup function that removes the subscription.\n */\n subscribe(streamId: string, cb: Subscriber<S>): () => void {\n if (!this.channels.has(streamId)) this.channels.set(streamId, new Set());\n this.channels.get(streamId)!.add(cb);\n return () => {\n this.channels.get(streamId)?.delete(cb);\n if (this.channels.get(streamId)?.size === 0) {\n this.channels.delete(streamId);\n }\n };\n }\n\n /** Get the number of subscribers for a stream. */\n getSubscriberCount(streamId: string): number {\n return this.channels.get(streamId)?.size ?? 0;\n }\n\n /** Get the cached state for a stream (for reconnects / initial SSE yield). */\n getState(streamId: string): S | undefined {\n return this.stateCache.get(streamId);\n }\n\n /** Direct access to the state cache (for app-specific reads like presence). */\n get cache(): StateCache<S> {\n return this.stateCache;\n }\n\n // --- internals ---\n\n private computeMessage(prev: S | undefined, next: S): BroadcastMessage<S> {\n const serverTime = new Date().toISOString();\n\n if (!prev) {\n return { _type: \"full\", ...next, serverTime };\n }\n\n const ops = compare(prev, next);\n if (ops.length === 0) {\n // No actual diff — still send full state so subscribers get serverTime refresh\n return { _type: \"full\", ...next, serverTime };\n }\n if (ops.length > this.maxPatchOps) {\n return { _type: \"full\", ...next, serverTime };\n }\n\n return {\n _type: \"patch\",\n _v: next._v,\n _baseV: prev._v,\n _patch: ops,\n serverTime,\n };\n }\n}\n","import type { BroadcastState } from \"./types.js\";\n\n/**\n * Generic LRU cache for aggregate state objects.\n *\n * Keyed by stream ID. Each entry stores the full state (with `_v` from the\n * event store). Used as the \"previous state\" baseline for computing patches,\n * and as the fast path for reconnects.\n *\n * The cache is shared between the broadcast hot path and read queries.\n * Projections should maintain their own cache to avoid double-apply bugs.\n */\nexport class StateCache<S extends BroadcastState = BroadcastState> {\n private cache = new Map<string, S>();\n private maxSize: number;\n\n constructor(maxSize = 50) {\n this.maxSize = maxSize;\n }\n\n /** Get a cached state, promoting it to MRU position. */\n get(key: string): S | undefined {\n const s = this.cache.get(key);\n if (s) {\n // Move to end (MRU)\n this.cache.delete(key);\n this.cache.set(key, s);\n }\n return s;\n }\n\n /** Set a cached state, evicting the LRU entry if at capacity. */\n set(key: string, state: S): void {\n this.cache.delete(key);\n this.cache.set(key, state);\n if (this.cache.size > this.maxSize) {\n // Size > max guarantees at least one entry exists\n this.cache.delete(this.cache.keys().next().value!);\n }\n }\n\n /** Remove a cached entry. */\n delete(key: string): void {\n this.cache.delete(key);\n }\n\n /** Check if a key exists in the cache. */\n has(key: string): boolean {\n return this.cache.has(key);\n }\n\n /** Current number of cached entries. */\n get size(): number {\n return this.cache.size;\n }\n\n /** Direct access to the underlying map (for iteration). */\n entries(): IterableIterator<[string, S]> {\n return this.cache.entries();\n }\n}\n","/**\n * Generic presence tracker — ref-counted online status per stream per identity.\n *\n * Supports multi-tab: each subscribe increments the ref count, each\n * unsubscribe decrements it. An identity is considered online when\n * ref count > 0.\n *\n * ## Usage\n *\n * ```typescript\n * const presence = new PresenceTracker();\n *\n * // On SSE connect:\n * presence.add(gameId, playerId);\n *\n * // On SSE disconnect:\n * presence.remove(gameId, playerId);\n *\n * // Query:\n * presence.getOnline(gameId); // Set<string>\n * ```\n */\nexport class PresenceTracker {\n private streams = new Map<string, Map<string, number>>();\n\n /** Increment ref count for an identity on a stream. */\n add(streamId: string, identityId: string): void {\n if (!this.streams.has(streamId)) this.streams.set(streamId, new Map());\n const counts = this.streams.get(streamId)!;\n counts.set(identityId, (counts.get(identityId) ?? 0) + 1);\n }\n\n /** Decrement ref count. Removes the identity when count reaches 0. */\n remove(streamId: string, identityId: string): void {\n const counts = this.streams.get(streamId);\n if (!counts) return;\n const n = (counts.get(identityId) ?? 1) - 1;\n if (n <= 0) counts.delete(identityId);\n else counts.set(identityId, n);\n if (counts.size === 0) this.streams.delete(streamId);\n }\n\n /** Get the set of online identity IDs for a stream. */\n getOnline(streamId: string): Set<string> {\n const counts = this.streams.get(streamId);\n return counts ? new Set(counts.keys()) : new Set();\n }\n\n /** Check if a specific identity is online for a stream. */\n isOnline(streamId: string, identityId: string): boolean {\n return (this.streams.get(streamId)?.get(identityId) ?? 0) > 0;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,6BAA2B;AAwCpB,SAAS,sBACd,KACA,QACgB;AAChB,QAAM,UAAU,QAAQ,MAAM;AAE9B,MAAI,IAAI,UAAU,QAAQ;AACxB,QAAI,IAAI,KAAK,QAAS,QAAO,EAAE,IAAI,OAAO,QAAQ,QAAQ;AAE1D,UAAM,EAAE,OAAO,GAAG,MAAM,IAAI;AAC5B,WAAO,EAAE,IAAI,MAAM,MAAkB;AAAA,EACvC;AAGA,MAAI,IAAI,SAAS,QAAS,QAAO,EAAE,IAAI,OAAO,QAAQ,QAAQ;AAC9D,MAAI,IAAI,SAAS,QAAS,QAAO,EAAE,IAAI,OAAO,QAAQ,SAAS;AAG/D,MAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,OAAO,QAAQ,SAAS;AAElD,MAAI;AACF,UAAM,QAAQ,gBAAgB,MAAM;AACpC,2CAAW,OAAO,IAAI,QAAQ,IAAI;AAClC,UAAM,KAAK,IAAI;AACf,WAAO,EAAE,IAAI,MAAM,OAAO,MAAM;AAAA,EAClC,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,QAAQ,eAAe;AAAA,EAC7C;AACF;;;ACpEA,IAAAA,0BAAwB;;;ACYjB,IAAM,aAAN,MAA4D;AAAA,EACzD,QAAQ,oBAAI,IAAe;AAAA,EAC3B;AAAA,EAER,YAAY,UAAU,IAAI;AACxB,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,KAA4B;AAC9B,UAAM,IAAI,KAAK,MAAM,IAAI,GAAG;AAC5B,QAAI,GAAG;AAEL,WAAK,MAAM,OAAO,GAAG;AACrB,WAAK,MAAM,IAAI,KAAK,CAAC;AAAA,IACvB;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,IAAI,KAAa,OAAgB;AAC/B,SAAK,MAAM,OAAO,GAAG;AACrB,SAAK,MAAM,IAAI,KAAK,KAAK;AACzB,QAAI,KAAK,MAAM,OAAO,KAAK,SAAS;AAElC,WAAK,MAAM,OAAO,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE,KAAM;AAAA,IACnD;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,KAAmB;AACxB,SAAK,MAAM,OAAO,GAAG;AAAA,EACvB;AAAA;AAAA,EAGA,IAAI,KAAsB;AACxB,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA;AAAA,EAGA,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA,EAGA,UAAyC;AACvC,WAAO,KAAK,MAAM,QAAQ;AAAA,EAC5B;AACF;;;ADnDA,IAAM,wBAAwB;AAqCvB,IAAM,mBAAN,MAAkE;AAAA,EAC/D,WAAW,oBAAI,IAAgC;AAAA,EAC/C;AAAA,EACA;AAAA,EAER,YAAY,SAAqD;AAC/D,SAAK,aAAa,IAAI,WAAc,SAAS,aAAa,EAAE;AAC5D,SAAK,cAAc,SAAS,eAAe;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,QAAQ,UAAkB,OAA+B;AACvD,UAAM,OAAO,KAAK,WAAW,IAAI,QAAQ;AACzC,SAAK,WAAW,IAAI,UAAU,KAAK;AAEnC,UAAM,MAAM,KAAK,eAAe,MAAM,KAAK;AAC3C,UAAM,OAAO,KAAK,SAAS,IAAI,QAAQ;AACvC,QAAI,MAAM,MAAM;AACd,iBAAW,MAAM,KAAM,IAAG,GAAG;AAAA,IAC/B;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,eAAe,UAAkB,OAA2C;AAC1E,UAAM,OAAO,KAAK,WAAW,IAAI,QAAQ;AACzC,QAAI,CAAC,KAAM,QAAO;AAElB,SAAK,WAAW,IAAI,UAAU,KAAK;AAEnC,UAAM,MAAM,KAAK,eAAe,MAAM,KAAK;AAC3C,UAAM,OAAO,KAAK,SAAS,IAAI,QAAQ;AACvC,QAAI,MAAM,MAAM;AACd,iBAAW,MAAM,KAAM,IAAG,GAAG;AAAA,IAC/B;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,UAAkB,IAA+B;AACzD,QAAI,CAAC,KAAK,SAAS,IAAI,QAAQ,EAAG,MAAK,SAAS,IAAI,UAAU,oBAAI,IAAI,CAAC;AACvE,SAAK,SAAS,IAAI,QAAQ,EAAG,IAAI,EAAE;AACnC,WAAO,MAAM;AACX,WAAK,SAAS,IAAI,QAAQ,GAAG,OAAO,EAAE;AACtC,UAAI,KAAK,SAAS,IAAI,QAAQ,GAAG,SAAS,GAAG;AAC3C,aAAK,SAAS,OAAO,QAAQ;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,mBAAmB,UAA0B;AAC3C,WAAO,KAAK,SAAS,IAAI,QAAQ,GAAG,QAAQ;AAAA,EAC9C;AAAA;AAAA,EAGA,SAAS,UAAiC;AACxC,WAAO,KAAK,WAAW,IAAI,QAAQ;AAAA,EACrC;AAAA;AAAA,EAGA,IAAI,QAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAIQ,eAAe,MAAqB,MAA8B;AACxE,UAAM,cAAa,oBAAI,KAAK,GAAE,YAAY;AAE1C,QAAI,CAAC,MAAM;AACT,aAAO,EAAE,OAAO,QAAQ,GAAG,MAAM,WAAW;AAAA,IAC9C;AAEA,UAAM,UAAM,iCAAQ,MAAM,IAAI;AAC9B,QAAI,IAAI,WAAW,GAAG;AAEpB,aAAO,EAAE,OAAO,QAAQ,GAAG,MAAM,WAAW;AAAA,IAC9C;AACA,QAAI,IAAI,SAAS,KAAK,aAAa;AACjC,aAAO,EAAE,OAAO,QAAQ,GAAG,MAAM,WAAW;AAAA,IAC9C;AAEA,WAAO;AAAA,MACL,OAAO;AAAA,MACP,IAAI,KAAK;AAAA,MACT,QAAQ,KAAK;AAAA,MACb,QAAQ;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;;;AEjIO,IAAM,kBAAN,MAAsB;AAAA,EACnB,UAAU,oBAAI,IAAiC;AAAA;AAAA,EAGvD,IAAI,UAAkB,YAA0B;AAC9C,QAAI,CAAC,KAAK,QAAQ,IAAI,QAAQ,EAAG,MAAK,QAAQ,IAAI,UAAU,oBAAI,IAAI,CAAC;AACrE,UAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ;AACxC,WAAO,IAAI,aAAa,OAAO,IAAI,UAAU,KAAK,KAAK,CAAC;AAAA,EAC1D;AAAA;AAAA,EAGA,OAAO,UAAkB,YAA0B;AACjD,UAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ;AACxC,QAAI,CAAC,OAAQ;AACb,UAAM,KAAK,OAAO,IAAI,UAAU,KAAK,KAAK;AAC1C,QAAI,KAAK,EAAG,QAAO,OAAO,UAAU;AAAA,QAC/B,QAAO,IAAI,YAAY,CAAC;AAC7B,QAAI,OAAO,SAAS,EAAG,MAAK,QAAQ,OAAO,QAAQ;AAAA,EACrD;AAAA;AAAA,EAGA,UAAU,UAA+B;AACvC,UAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ;AACxC,WAAO,SAAS,IAAI,IAAI,OAAO,KAAK,CAAC,IAAI,oBAAI,IAAI;AAAA,EACnD;AAAA;AAAA,EAGA,SAAS,UAAkB,YAA6B;AACtD,YAAQ,KAAK,QAAQ,IAAI,QAAQ,GAAG,IAAI,UAAU,KAAK,KAAK;AAAA,EAC9D;AACF;","names":["import_fast_json_patch"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/patch.ts","../src/apply-patch.ts","../src/state-cache.ts","../src/broadcast.ts","../src/presence.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-sse\n *\n * Incremental state broadcast over SSE for act event-sourced apps.\n *\n * Provides server-side broadcast with domain patch forwarding,\n * an LRU state cache, presence tracking, and a client-side\n * patch applicator with version validation and resync detection.\n *\n * ## Architecture\n *\n * ```\n * app.do() → snapshots (each carries its domain patch)\n * │\n * ▼\n * deriveState(snap) ← app-specific (overlay presence, deadlines, etc.)\n * state._v = snap.event.version\n * │\n * ▼\n * broadcast.publish(streamId, state, patches)\n * │\n * ├── version-key each patch: { [baseV+1]: patch1, [baseV+2]: patch2 }\n * └── push to all SSE subscribers\n * │\n * ▼\n * Client: applyPatchMessage(msg, cached)\n * │\n * ├── contiguous → deep-merge patches in version order\n * ├── stale → skip (client already ahead)\n * └── behind → resync (client missed versions)\n * ```\n *\n * ## Version Contract\n *\n * `_v` is always the event store stream version (`snap.event.version`).\n * No separate version counters. The event store is the single source of truth.\n */\n\nexport { applyPatchMessage } from \"./apply-patch.js\";\nexport type { ApplyResult } from \"./apply-patch.js\";\nexport { BroadcastChannel } from \"./broadcast.js\";\nexport { patch } from \"./patch.js\";\nexport { PresenceTracker } from \"./presence.js\";\nexport { StateCache } from \"./state-cache.js\";\nexport type { BroadcastState, PatchMessage, Subscriber } from \"./types.js\";\n","/**\n * Browser-safe deep merge utility — identical semantics to @rotorsoft/act's patch().\n * Inlined here so act-sse has zero Node dependencies.\n */\n\ntype Schema = Record<string, any>;\ntype Patch<T> = {\n [K in keyof T]?: T[K] extends Schema ? Patch<T[K]> : T[K];\n};\n\n/** These objects are copied instead of deep merged */\nconst UNMERGEABLES = [\n RegExp,\n Date,\n Array,\n Map,\n Set,\n WeakMap,\n WeakSet,\n ArrayBuffer,\n SharedArrayBuffer,\n DataView,\n Int8Array,\n Uint8Array,\n Uint8ClampedArray,\n Int16Array,\n Uint16Array,\n Int32Array,\n Uint32Array,\n Float32Array,\n Float64Array,\n];\n\nconst is_mergeable = (value: any): boolean =>\n !!value &&\n typeof value === \"object\" &&\n !UNMERGEABLES.some((t) => value instanceof t);\n\n/** Immutably deep-merge `patches` into `original`. */\nexport const patch = <S extends Schema>(\n original: Readonly<S>,\n patches: Readonly<Patch<S>>\n): Readonly<S> => {\n const copy = {} as Record<string, any>;\n Object.keys({ ...original, ...patches }).forEach((key) => {\n const patched_value = patches[key as keyof typeof patches];\n const original_value = original[key as keyof typeof original];\n const patched = patches && key in patches;\n const deleted =\n patched &&\n (typeof patched_value === \"undefined\" || patched_value === null);\n const value = patched && !deleted ? patched_value : original_value;\n !deleted &&\n (copy[key] = is_mergeable(value)\n ? patch(original_value || {}, patched_value || {})\n : value);\n });\n return copy as S;\n};\n","import { patch as deepMerge } from \"./patch.js\";\nimport type { BroadcastState, PatchMessage } from \"./types.js\";\n\n/**\n * Result of applying a patch message to cached client state.\n */\nexport type ApplyResult<S extends BroadcastState = BroadcastState> =\n | { ok: true; state: S }\n | { ok: false; reason: \"stale\" | \"behind\" };\n\n/**\n * Apply a version-keyed patch message to the client's cached state.\n *\n * ## Version logic\n *\n * - All patches older than cached → \"stale\" (client already ahead)\n * - Gap between cached version and first patch → \"behind\" (client missed versions, must resync)\n * - Contiguous from cached version → apply in order\n *\n * ## Usage (React Query)\n *\n * ```typescript\n * onData: (msg) => {\n * const cached = utils.getState.getData({ streamId });\n * const result = applyPatchMessage(msg, cached);\n * if (result.ok) {\n * utils.getState.setData({ streamId }, result.state);\n * } else if (result.reason === \"behind\") {\n * utils.getState.invalidate({ streamId }); // trigger full refetch\n * }\n * // \"stale\" → no-op, client already has newer state\n * }\n * ```\n */\nexport function applyPatchMessage<S extends BroadcastState>(\n msg: PatchMessage<S>,\n cached: S | null | undefined\n): ApplyResult<S> {\n const cachedV = cached?._v ?? 0;\n const versions = Object.keys(msg)\n .map(Number)\n .sort((a, b) => a - b);\n\n if (!versions.length) return { ok: false, reason: \"stale\" };\n\n const minV = versions[0];\n const maxV = versions[versions.length - 1];\n\n // All patches are older than what we have\n if (maxV <= cachedV) return { ok: false, reason: \"stale\" };\n // Gap — we missed versions\n if (!cached || minV > cachedV + 1) return { ok: false, reason: \"behind\" };\n\n // Apply patches in version order, skipping any we already have\n let state = cached;\n for (const v of versions) {\n if (v <= cachedV) continue; // already applied\n state = { ...deepMerge(state, msg[v]), _v: v } as S;\n }\n return { ok: true, state };\n}\n","import type { BroadcastState } from \"./types.js\";\n\n/**\n * Generic LRU cache for aggregate state objects.\n *\n * Keyed by stream ID. Each entry stores the full state (with `_v` from the\n * event store). Used as the \"previous state\" baseline for computing patches,\n * and as the fast path for reconnects.\n *\n * The cache is shared between the broadcast hot path and read queries.\n * Projections should maintain their own cache to avoid double-apply bugs.\n */\nexport class StateCache<S extends BroadcastState = BroadcastState> {\n private cache = new Map<string, S>();\n private maxSize: number;\n\n constructor(maxSize = 50) {\n this.maxSize = maxSize;\n }\n\n /** Get a cached state, promoting it to MRU position. */\n get(key: string): S | undefined {\n const s = this.cache.get(key);\n if (s) {\n // Move to end (MRU)\n this.cache.delete(key);\n this.cache.set(key, s);\n }\n return s;\n }\n\n /** Set a cached state, evicting the LRU entry if at capacity. */\n set(key: string, state: S): void {\n this.cache.delete(key);\n this.cache.set(key, state);\n if (this.cache.size > this.maxSize) {\n // Size > max guarantees at least one entry exists\n this.cache.delete(this.cache.keys().next().value!);\n }\n }\n\n /** Remove a cached entry. */\n delete(key: string): void {\n this.cache.delete(key);\n }\n\n /** Check if a key exists in the cache. */\n has(key: string): boolean {\n return this.cache.has(key);\n }\n\n /** Current number of cached entries. */\n get size(): number {\n return this.cache.size;\n }\n\n /** Direct access to the underlying map (for iteration). */\n entries(): IterableIterator<[string, S]> {\n return this.cache.entries();\n }\n}\n","import { patch as applyPatch } from \"./patch.js\";\nimport { StateCache } from \"./state-cache.js\";\nimport type { BroadcastState, PatchMessage, Subscriber } from \"./types.js\";\n\n/**\n * Server-side broadcast channel for incremental state sync over SSE.\n *\n * Manages per-stream subscriber sets and an LRU state cache. When state\n * changes, forwards domain patches (from event handlers) to all subscribers\n * as version-keyed messages.\n *\n * ## Usage\n *\n * ```typescript\n * const broadcast = new BroadcastChannel<MyState>();\n *\n * // After every app.do():\n * const snaps = await app.do(...);\n * const patches = snaps.map(s => s.patch).filter(Boolean);\n * const state = deriveState(snaps.at(-1));\n * broadcast.publish(streamId, state, patches);\n *\n * // In SSE subscription:\n * const cleanup = broadcast.subscribe(streamId, (msg) => {\n * pending = msg;\n * resolve?.();\n * });\n *\n * // Initial state for reconnects:\n * const cached = broadcast.getState(streamId);\n * ```\n *\n * ## Version Contract\n *\n * The `_v` field on state MUST be set from `snap.event.version` (the event\n * store's monotonic stream version) BEFORE calling `publish()`. This is the\n * single source of truth for ordering — no separate version counters.\n */\nexport class BroadcastChannel<S extends BroadcastState = BroadcastState> {\n private channels = new Map<string, Set<Subscriber<S>>>();\n private stateCache: StateCache<S>;\n\n constructor(options?: { cacheSize?: number }) {\n this.stateCache = new StateCache<S>(options?.cacheSize ?? 50);\n }\n\n /**\n * Publish domain patches from a commit.\n * patches[i] corresponds to version baseV + i + 1.\n *\n * @param streamId - The event store stream ID\n * @param state - Full state with `_v` set from `snap.event.version`\n * @param patches - Array of domain patches, one per emitted event\n */\n publish(\n streamId: string,\n state: S,\n patches: Partial<S>[] = []\n ): PatchMessage<S> {\n this.stateCache.set(streamId, state);\n\n const baseV = state._v - patches.length;\n const msg: PatchMessage<S> = {};\n patches.forEach((p, i) => {\n msg[baseV + i + 1] = p;\n });\n\n const subs = this.channels.get(streamId);\n if (subs?.size) {\n for (const cb of subs) cb(msg);\n }\n return msg;\n }\n\n /**\n * Publish a state update that doesn't change the event version\n * (e.g. presence overlay, computed field refresh).\n * Uses the same version as the cached state, single entry.\n */\n publishOverlay(\n streamId: string,\n overlayPatch: Partial<S>\n ): PatchMessage<S> | undefined {\n const prev = this.stateCache.get(streamId);\n if (!prev) return undefined;\n\n const state = applyPatch(prev, overlayPatch) as S;\n this.stateCache.set(streamId, state);\n\n const msg: PatchMessage<S> = { [state._v]: overlayPatch };\n const subs = this.channels.get(streamId);\n if (subs?.size) {\n for (const cb of subs) cb(msg);\n }\n return msg;\n }\n\n /**\n * Subscribe to broadcast messages for a stream.\n * Returns a cleanup function that removes the subscription.\n */\n subscribe(streamId: string, cb: Subscriber<S>): () => void {\n if (!this.channels.has(streamId)) this.channels.set(streamId, new Set());\n this.channels.get(streamId)!.add(cb);\n return () => {\n this.channels.get(streamId)?.delete(cb);\n if (this.channels.get(streamId)?.size === 0) {\n this.channels.delete(streamId);\n }\n };\n }\n\n /** Get the number of subscribers for a stream. */\n getSubscriberCount(streamId: string): number {\n return this.channels.get(streamId)?.size ?? 0;\n }\n\n /** Get the cached state for a stream (for reconnects / initial SSE yield). */\n getState(streamId: string): S | undefined {\n return this.stateCache.get(streamId);\n }\n\n /** Direct access to the state cache (for app-specific reads like presence). */\n get cache(): StateCache<S> {\n return this.stateCache;\n }\n}\n","/**\n * Generic presence tracker — ref-counted online status per stream per identity.\n *\n * Supports multi-tab: each subscribe increments the ref count, each\n * unsubscribe decrements it. An identity is considered online when\n * ref count > 0.\n *\n * ## Usage\n *\n * ```typescript\n * const presence = new PresenceTracker();\n *\n * // On SSE connect:\n * presence.add(gameId, playerId);\n *\n * // On SSE disconnect:\n * presence.remove(gameId, playerId);\n *\n * // Query:\n * presence.getOnline(gameId); // Set<string>\n * ```\n */\nexport class PresenceTracker {\n private streams = new Map<string, Map<string, number>>();\n\n /** Increment ref count for an identity on a stream. */\n add(streamId: string, identityId: string): void {\n if (!this.streams.has(streamId)) this.streams.set(streamId, new Map());\n const counts = this.streams.get(streamId)!;\n counts.set(identityId, (counts.get(identityId) ?? 0) + 1);\n }\n\n /** Decrement ref count. Removes the identity when count reaches 0. */\n remove(streamId: string, identityId: string): void {\n const counts = this.streams.get(streamId);\n if (!counts) return;\n const n = (counts.get(identityId) ?? 1) - 1;\n if (n <= 0) counts.delete(identityId);\n else counts.set(identityId, n);\n if (counts.size === 0) this.streams.delete(streamId);\n }\n\n /** Get the set of online identity IDs for a stream. */\n getOnline(streamId: string): Set<string> {\n const counts = this.streams.get(streamId);\n return counts ? new Set(counts.keys()) : new Set();\n }\n\n /** Check if a specific identity is online for a stream. */\n isOnline(streamId: string, identityId: string): boolean {\n return (this.streams.get(streamId)?.get(identityId) ?? 0) > 0;\n }\n}\n"],"mappings":"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","names":[]}