@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.
- package/README.md +67 -62
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/apply-patch.d.ts +9 -15
- package/dist/@types/apply-patch.d.ts.map +1 -1
- package/dist/@types/broadcast.d.ts +14 -17
- package/dist/@types/broadcast.d.ts.map +1 -1
- package/dist/@types/index.d.ts +12 -14
- package/dist/@types/index.d.ts.map +1 -1
- package/dist/@types/patch.d.ts +12 -0
- package/dist/@types/patch.d.ts.map +1 -0
- package/dist/@types/types.d.ts +10 -30
- package/dist/@types/types.d.ts.map +1 -1
- package/dist/index.cjs +66 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +64 -55
- package/dist/index.js.map +1 -1
- package/package.json +2 -4
|
@@ -1,35 +1,29 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BroadcastState, PatchMessage } from "./types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Result of applying a
|
|
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"
|
|
10
|
+
reason: "stale" | "behind";
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
|
-
* Apply a
|
|
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
|
-
* -
|
|
22
|
-
* -
|
|
23
|
-
*
|
|
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 =
|
|
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
|
|
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,
|
|
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 {
|
|
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,
|
|
8
|
-
*
|
|
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
|
|
18
|
-
* const
|
|
19
|
-
*
|
|
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
|
-
|
|
42
|
-
constructor(options?: BroadcastOptions & {
|
|
40
|
+
constructor(options?: {
|
|
43
41
|
cacheSize?: number;
|
|
44
42
|
});
|
|
45
43
|
/**
|
|
46
|
-
* Publish
|
|
47
|
-
*
|
|
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
|
-
* @
|
|
49
|
+
* @param patches - Array of domain patches, one per emitted event
|
|
52
50
|
*/
|
|
53
|
-
publish(streamId: string, state: 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
|
|
55
|
+
* Uses the same version as the cached state, single entry.
|
|
58
56
|
*/
|
|
59
|
-
publishOverlay(streamId: string,
|
|
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,
|
|
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"}
|
package/dist/@types/index.d.ts
CHANGED
|
@@ -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
|
|
8
|
-
*
|
|
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() →
|
|
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
|
-
* ├──
|
|
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:
|
|
27
|
+
* Client: applyPatchMessage(msg, cached)
|
|
30
28
|
* │
|
|
31
|
-
* ├──
|
|
32
|
-
* ├──
|
|
33
|
-
*
|
|
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 {
|
|
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 {
|
|
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
|
|
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"}
|
package/dist/@types/types.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
10
|
+
* Recursive deep partial — mirrors act core's Patch<T>.
|
|
12
11
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
|
22
|
+
* Subscriber callback — receives version-keyed patch messages.
|
|
37
23
|
*/
|
|
38
|
-
export type Subscriber<S extends BroadcastState = BroadcastState> = (msg:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
function applyBroadcastMessage(msg, cached) {
|
|
68
|
+
function applyPatchMessage(msg, cached) {
|
|
33
69
|
const cachedV = cached?._v ?? 0;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
109
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
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
|
|
157
|
+
* Uses the same version as the cached state, single entry.
|
|
129
158
|
*/
|
|
130
|
-
publishOverlay(streamId,
|
|
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 =
|
|
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
|
-
|
|
232
|
+
applyPatchMessage,
|
|
233
|
+
patch
|
|
224
234
|
});
|
|
225
235
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":"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","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":[]}
|