@rotorsoft/act-sse 1.0.1 → 1.1.1
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 +64 -705
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +62 -710
- 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"}
|