@sadhaka/loom-engine 0.10.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/LICENSE +21 -0
- package/README.md +344 -0
- package/dist/animation/animation-clip.d.ts +12 -0
- package/dist/animation/animation-clip.d.ts.map +1 -0
- package/dist/animation/animation-clip.js +85 -0
- package/dist/animation/animation-clip.js.map +1 -0
- package/dist/animation/animation-state-pool.d.ts +25 -0
- package/dist/animation/animation-state-pool.d.ts.map +1 -0
- package/dist/animation/animation-state-pool.js +113 -0
- package/dist/animation/animation-state-pool.js.map +1 -0
- package/dist/asset/sprite-sheet-loader.d.ts +41 -0
- package/dist/asset/sprite-sheet-loader.d.ts.map +1 -0
- package/dist/asset/sprite-sheet-loader.js +313 -0
- package/dist/asset/sprite-sheet-loader.js.map +1 -0
- package/dist/audio/audio-bus.d.ts +43 -0
- package/dist/audio/audio-bus.d.ts.map +1 -0
- package/dist/audio/audio-bus.js +258 -0
- package/dist/audio/audio-bus.js.map +1 -0
- package/dist/combat/mob-catalog.d.ts +29 -0
- package/dist/combat/mob-catalog.d.ts.map +1 -0
- package/dist/combat/mob-catalog.js +104 -0
- package/dist/combat/mob-catalog.js.map +1 -0
- package/dist/components/health.d.ts +28 -0
- package/dist/components/health.d.ts.map +1 -0
- package/dist/components/health.js +150 -0
- package/dist/components/health.js.map +1 -0
- package/dist/components/interactable.d.ts +30 -0
- package/dist/components/interactable.d.ts.map +1 -0
- package/dist/components/interactable.js +94 -0
- package/dist/components/interactable.js.map +1 -0
- package/dist/components/particle-emitter.d.ts +62 -0
- package/dist/components/particle-emitter.d.ts.map +1 -0
- package/dist/components/particle-emitter.js +193 -0
- package/dist/components/particle-emitter.js.map +1 -0
- package/dist/components/pursue.d.ts +23 -0
- package/dist/components/pursue.d.ts.map +1 -0
- package/dist/components/pursue.js +96 -0
- package/dist/components/pursue.js.map +1 -0
- package/dist/components/ranged-attack.d.ts +44 -0
- package/dist/components/ranged-attack.d.ts.map +1 -0
- package/dist/components/ranged-attack.js +120 -0
- package/dist/components/ranged-attack.js.map +1 -0
- package/dist/components/sprite.d.ts +27 -0
- package/dist/components/sprite.d.ts.map +1 -0
- package/dist/components/sprite.js +122 -0
- package/dist/components/sprite.js.map +1 -0
- package/dist/components/transform.d.ts +30 -0
- package/dist/components/transform.d.ts.map +1 -0
- package/dist/components/transform.js +150 -0
- package/dist/components/transform.js.map +1 -0
- package/dist/director/director-bridge.d.ts +22 -0
- package/dist/director/director-bridge.d.ts.map +1 -0
- package/dist/director/director-bridge.js +23 -0
- package/dist/director/director-bridge.js.map +1 -0
- package/dist/director/director-encounter-system.d.ts +21 -0
- package/dist/director/director-encounter-system.d.ts.map +1 -0
- package/dist/director/director-encounter-system.js +128 -0
- package/dist/director/director-encounter-system.js.map +1 -0
- package/dist/director/director-system.d.ts +19 -0
- package/dist/director/director-system.d.ts.map +1 -0
- package/dist/director/director-system.js +179 -0
- package/dist/director/director-system.js.map +1 -0
- package/dist/director/event-envelope.d.ts +144 -0
- package/dist/director/event-envelope.d.ts.map +1 -0
- package/dist/director/event-envelope.js +108 -0
- package/dist/director/event-envelope.js.map +1 -0
- package/dist/director/knot-context-resource.d.ts +25 -0
- package/dist/director/knot-context-resource.d.ts.map +1 -0
- package/dist/director/knot-context-resource.js +152 -0
- package/dist/director/knot-context-resource.js.map +1 -0
- package/dist/director/mock-director-bridge.d.ts +18 -0
- package/dist/director/mock-director-bridge.d.ts.map +1 -0
- package/dist/director/mock-director-bridge.js +75 -0
- package/dist/director/mock-director-bridge.js.map +1 -0
- package/dist/director/snapshot-recovery.d.ts +37 -0
- package/dist/director/snapshot-recovery.d.ts.map +1 -0
- package/dist/director/snapshot-recovery.js +180 -0
- package/dist/director/snapshot-recovery.js.map +1 -0
- package/dist/director/sse-director-bridge.d.ts +42 -0
- package/dist/director/sse-director-bridge.d.ts.map +1 -0
- package/dist/director/sse-director-bridge.js +280 -0
- package/dist/director/sse-director-bridge.js.map +1 -0
- package/dist/engine.d.ts +25 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +166 -0
- package/dist/engine.js.map +1 -0
- package/dist/entity.d.ts +17 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +77 -0
- package/dist/entity.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/input/input-manager.d.ts +91 -0
- package/dist/input/input-manager.d.ts.map +1 -0
- package/dist/input/input-manager.js +349 -0
- package/dist/input/input-manager.js.map +1 -0
- package/dist/input/tap-to-walk.d.ts +27 -0
- package/dist/input/tap-to-walk.d.ts.map +1 -0
- package/dist/input/tap-to-walk.js +118 -0
- package/dist/input/tap-to-walk.js.map +1 -0
- package/dist/input/virtual-dpad.d.ts +34 -0
- package/dist/input/virtual-dpad.d.ts.map +1 -0
- package/dist/input/virtual-dpad.js +267 -0
- package/dist/input/virtual-dpad.js.map +1 -0
- package/dist/renderer/camera.d.ts +26 -0
- package/dist/renderer/camera.d.ts.map +1 -0
- package/dist/renderer/camera.js +39 -0
- package/dist/renderer/camera.js.map +1 -0
- package/dist/renderer/canvas2d-device.d.ts +26 -0
- package/dist/renderer/canvas2d-device.d.ts.map +1 -0
- package/dist/renderer/canvas2d-device.js +252 -0
- package/dist/renderer/canvas2d-device.js.map +1 -0
- package/dist/renderer/graphics-device.d.ts +36 -0
- package/dist/renderer/graphics-device.d.ts.map +1 -0
- package/dist/renderer/graphics-device.js +12 -0
- package/dist/renderer/graphics-device.js.map +1 -0
- package/dist/renderer/iso-projection.d.ts +11 -0
- package/dist/renderer/iso-projection.d.ts.map +1 -0
- package/dist/renderer/iso-projection.js +59 -0
- package/dist/renderer/iso-projection.js.map +1 -0
- package/dist/resources.d.ts +28 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +53 -0
- package/dist/resources.js.map +1 -0
- package/dist/system.d.ts +14 -0
- package/dist/system.d.ts.map +1 -0
- package/dist/system.js +25 -0
- package/dist/system.js.map +1 -0
- package/dist/systems/animation-system.d.ts +8 -0
- package/dist/systems/animation-system.d.ts.map +1 -0
- package/dist/systems/animation-system.js +77 -0
- package/dist/systems/animation-system.js.map +1 -0
- package/dist/systems/attack-system.d.ts +17 -0
- package/dist/systems/attack-system.d.ts.map +1 -0
- package/dist/systems/attack-system.js +94 -0
- package/dist/systems/attack-system.js.map +1 -0
- package/dist/systems/damage-system.d.ts +18 -0
- package/dist/systems/damage-system.d.ts.map +1 -0
- package/dist/systems/damage-system.js +77 -0
- package/dist/systems/damage-system.js.map +1 -0
- package/dist/systems/input-system.d.ts +7 -0
- package/dist/systems/input-system.d.ts.map +1 -0
- package/dist/systems/input-system.js +27 -0
- package/dist/systems/input-system.js.map +1 -0
- package/dist/systems/interaction-system.d.ts +23 -0
- package/dist/systems/interaction-system.d.ts.map +1 -0
- package/dist/systems/interaction-system.js +120 -0
- package/dist/systems/interaction-system.js.map +1 -0
- package/dist/systems/particle-emitter-system.d.ts +8 -0
- package/dist/systems/particle-emitter-system.d.ts.map +1 -0
- package/dist/systems/particle-emitter-system.js +161 -0
- package/dist/systems/particle-emitter-system.js.map +1 -0
- package/dist/systems/particle-render-system.d.ts +7 -0
- package/dist/systems/particle-render-system.d.ts.map +1 -0
- package/dist/systems/particle-render-system.js +53 -0
- package/dist/systems/particle-render-system.js.map +1 -0
- package/dist/systems/particle-simulation-system.d.ts +8 -0
- package/dist/systems/particle-simulation-system.d.ts.map +1 -0
- package/dist/systems/particle-simulation-system.js +45 -0
- package/dist/systems/particle-simulation-system.js.map +1 -0
- package/dist/systems/projectile-render-system.d.ts +7 -0
- package/dist/systems/projectile-render-system.d.ts.map +1 -0
- package/dist/systems/projectile-render-system.js +35 -0
- package/dist/systems/projectile-render-system.js.map +1 -0
- package/dist/systems/projectile-system.d.ts +7 -0
- package/dist/systems/projectile-system.d.ts.map +1 -0
- package/dist/systems/projectile-system.js +114 -0
- package/dist/systems/projectile-system.js.map +1 -0
- package/dist/systems/pursue-system.d.ts +7 -0
- package/dist/systems/pursue-system.d.ts.map +1 -0
- package/dist/systems/pursue-system.js +83 -0
- package/dist/systems/pursue-system.js.map +1 -0
- package/dist/systems/ranged-attack-system.d.ts +7 -0
- package/dist/systems/ranged-attack-system.d.ts.map +1 -0
- package/dist/systems/ranged-attack-system.js +98 -0
- package/dist/systems/ranged-attack-system.js.map +1 -0
- package/dist/systems/sprite-render-system.d.ts +9 -0
- package/dist/systems/sprite-render-system.d.ts.map +1 -0
- package/dist/systems/sprite-render-system.js +108 -0
- package/dist/systems/sprite-render-system.js.map +1 -0
- package/dist/systems/veil-budget-system.d.ts +7 -0
- package/dist/systems/veil-budget-system.d.ts.map +1 -0
- package/dist/systems/veil-budget-system.js +37 -0
- package/dist/systems/veil-budget-system.js.map +1 -0
- package/dist/util/color.d.ts +19 -0
- package/dist/util/color.d.ts.map +1 -0
- package/dist/util/color.js +45 -0
- package/dist/util/color.js.map +1 -0
- package/dist/util/math.d.ts +26 -0
- package/dist/util/math.d.ts.map +1 -0
- package/dist/util/math.js +47 -0
- package/dist/util/math.js.map +1 -0
- package/dist/util/typed-arrays.d.ts +7 -0
- package/dist/util/typed-arrays.d.ts.map +1 -0
- package/dist/util/typed-arrays.js +42 -0
- package/dist/util/typed-arrays.js.map +1 -0
- package/dist/vfx/particle-pool.d.ts +61 -0
- package/dist/vfx/particle-pool.d.ts.map +1 -0
- package/dist/vfx/particle-pool.js +204 -0
- package/dist/vfx/particle-pool.js.map +1 -0
- package/dist/vfx/projectile-pool.d.ts +56 -0
- package/dist/vfx/projectile-pool.d.ts.map +1 -0
- package/dist/vfx/projectile-pool.js +157 -0
- package/dist/vfx/projectile-pool.js.map +1 -0
- package/dist/world.d.ts +23 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +101 -0
- package/dist/world.js.map +1 -0
- package/dist/zone/zone-catalog.d.ts +17 -0
- package/dist/zone/zone-catalog.d.ts.map +1 -0
- package/dist/zone/zone-catalog.js +116 -0
- package/dist/zone/zone-catalog.js.map +1 -0
- package/dist/zone/zone-state.d.ts +18 -0
- package/dist/zone/zone-state.d.ts.map +1 -0
- package/dist/zone/zone-state.js +52 -0
- package/dist/zone/zone-state.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DirectorEvent } from './event-envelope.js';
|
|
2
|
+
import { type IDirectorBridge, type DirectorBridgeStatus, type DirectorBridgeStats } from './director-bridge.js';
|
|
3
|
+
export declare class MockDirectorBridge implements IDirectorBridge {
|
|
4
|
+
private queue;
|
|
5
|
+
private statusValue;
|
|
6
|
+
private statsValue;
|
|
7
|
+
start(): void;
|
|
8
|
+
stop(): void;
|
|
9
|
+
status(): DirectorBridgeStatus;
|
|
10
|
+
isConnected(): boolean;
|
|
11
|
+
getLastEventId(): number;
|
|
12
|
+
pollEvents(): DirectorEvent[];
|
|
13
|
+
stats(): Readonly<DirectorBridgeStats>;
|
|
14
|
+
enqueue(event: DirectorEvent): void;
|
|
15
|
+
enqueueAll(events: ReadonlyArray<DirectorEvent>): void;
|
|
16
|
+
pending(): number;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=mock-director-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-director-bridge.d.ts","sourceRoot":"","sources":["../../src/director/mock-director-bridge.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAE9B,qBAAa,kBAAmB,YAAW,eAAe;IACxD,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,UAAU,CAOhB;IAEF,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI;IAIZ,MAAM,IAAI,oBAAoB;IAI9B,WAAW,IAAI,OAAO;IAItB,cAAc,IAAI,MAAM;IAIxB,UAAU,IAAI,aAAa,EAAE;IAO7B,KAAK,IAAI,QAAQ,CAAC,mBAAmB,CAAC;IAStC,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAWnC,UAAU,CAAC,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,IAAI;IAQtD,OAAO,IAAI,MAAM;CAGlB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// MockDirectorBridge - in-memory event source for tests + offline demos.
|
|
2
|
+
//
|
|
3
|
+
// Tests inject envelopes via enqueue() and let DirectorSystem drain
|
|
4
|
+
// them. The demo can also use this when running without a backend
|
|
5
|
+
// connection (e.g. open file:// or no auth). pollEvents() drains the
|
|
6
|
+
// queue in FIFO order.
|
|
7
|
+
//
|
|
8
|
+
// Tracks lastEventId so the bridge contract is satisfied (gap detection
|
|
9
|
+
// in DirectorSystem reads lastEventId after each poll). Does not
|
|
10
|
+
// reconnect, does not validate envelopes - that's parseEnvelope's job
|
|
11
|
+
// upstream of enqueue().
|
|
12
|
+
export class MockDirectorBridge {
|
|
13
|
+
queue = [];
|
|
14
|
+
statusValue = 'idle';
|
|
15
|
+
statsValue = {
|
|
16
|
+
eventsReceived: 0,
|
|
17
|
+
reconnects: 0,
|
|
18
|
+
lastEventId: 0,
|
|
19
|
+
outOfOrderEvents: 0,
|
|
20
|
+
serverDropsP1: 0,
|
|
21
|
+
serverDropsP2: 0,
|
|
22
|
+
};
|
|
23
|
+
start() {
|
|
24
|
+
this.statusValue = 'connected';
|
|
25
|
+
}
|
|
26
|
+
stop() {
|
|
27
|
+
this.statusValue = 'closed';
|
|
28
|
+
}
|
|
29
|
+
status() {
|
|
30
|
+
return this.statusValue;
|
|
31
|
+
}
|
|
32
|
+
isConnected() {
|
|
33
|
+
return this.statusValue === 'connected';
|
|
34
|
+
}
|
|
35
|
+
getLastEventId() {
|
|
36
|
+
return this.statsValue.lastEventId;
|
|
37
|
+
}
|
|
38
|
+
pollEvents() {
|
|
39
|
+
if (this.queue.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
const out = this.queue;
|
|
42
|
+
this.queue = [];
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
stats() {
|
|
46
|
+
return this.statsValue;
|
|
47
|
+
}
|
|
48
|
+
// ----- Mock-only injection helpers -----
|
|
49
|
+
// Enqueue an envelope as if it came from the server. Out-of-order
|
|
50
|
+
// injection is allowed; the consumer's gap detection logic should
|
|
51
|
+
// handle it.
|
|
52
|
+
enqueue(event) {
|
|
53
|
+
this.queue.push(event);
|
|
54
|
+
this.statsValue.eventsReceived++;
|
|
55
|
+
if (event.id > this.statsValue.lastEventId) {
|
|
56
|
+
this.statsValue.lastEventId = event.id;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.statsValue.outOfOrderEvents++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Convenience: enqueue a batch in order.
|
|
63
|
+
enqueueAll(events) {
|
|
64
|
+
for (let i = 0; i < events.length; i++) {
|
|
65
|
+
const e = events[i];
|
|
66
|
+
if (e)
|
|
67
|
+
this.enqueue(e);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Inspect-only: how many events are buffered waiting for a poll.
|
|
71
|
+
pending() {
|
|
72
|
+
return this.queue.length;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=mock-director-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-director-bridge.js","sourceRoot":"","sources":["../../src/director/mock-director-bridge.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,EAAE;AACF,oEAAoE;AACpE,kEAAkE;AAClE,qEAAqE;AACrE,uBAAuB;AACvB,EAAE;AACF,wEAAwE;AACxE,iEAAiE;AACjE,sEAAsE;AACtE,yBAAyB;AASzB,MAAM,OAAO,kBAAkB;IACrB,KAAK,GAAoB,EAAE,CAAC;IAC5B,WAAW,GAAyB,MAAM,CAAC;IAC3C,UAAU,GAAwB;QACxC,cAAc,EAAE,CAAC;QACjB,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,CAAC;QACd,gBAAgB,EAAE,CAAC;QACnB,aAAa,EAAE,CAAC;QAChB,aAAa,EAAE,CAAC;KACjB,CAAC;IAEF,KAAK;QACH,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IAED,IAAI;QACF,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAC;IAC9B,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,WAAW,KAAK,WAAW,CAAC;IAC1C,CAAC;IAED,cAAc;QACZ,OAAO,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;IACrC,CAAC;IAED,UAAU;QACR,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,KAAK;QACH,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,0CAA0C;IAE1C,kEAAkE;IAClE,kEAAkE;IAClE,aAAa;IACb,OAAO,CAAC,KAAoB;QAC1B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvB,IAAI,CAAC,UAAU,CAAC,cAAc,EAAE,CAAC;QACjC,IAAI,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;YAC3C,IAAI,CAAC,UAAU,CAAC,WAAW,GAAG,KAAK,CAAC,EAAE,CAAC;QACzC,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC;QACrC,CAAC;IACH,CAAC;IAED,yCAAyC;IACzC,UAAU,CAAC,MAAoC;QAC7C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACpB,IAAI,CAAC;gBAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,iEAAiE;IACjE,OAAO;QACL,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IAC3B,CAAC;CACF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { World } from '../world.js';
|
|
2
|
+
import type { EventEnvelope } from './event-envelope.js';
|
|
3
|
+
export interface SnapshotResponse {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
character_id: string;
|
|
6
|
+
tail_id: number;
|
|
7
|
+
snapshot: {
|
|
8
|
+
knot_context: EventEnvelope<'knot.context'> | null;
|
|
9
|
+
ve_budget: EventEnvelope<'ve.budget.update'> | null;
|
|
10
|
+
scene: EventEnvelope<'scene.transition'> | null;
|
|
11
|
+
active_encounter: EventEnvelope<'encounter.spawn'> | null;
|
|
12
|
+
};
|
|
13
|
+
ts: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class SnapshotFetchError extends Error {
|
|
16
|
+
readonly kind: 'network' | 'http' | 'parse' | 'invalid';
|
|
17
|
+
readonly status: number;
|
|
18
|
+
readonly url: string;
|
|
19
|
+
constructor(kind: SnapshotFetchError['kind'], url: string, message: string, status?: number);
|
|
20
|
+
}
|
|
21
|
+
export interface SnapshotRecoveryOptions {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
characterId: string;
|
|
24
|
+
fetchImpl?: typeof fetch;
|
|
25
|
+
}
|
|
26
|
+
export declare class SnapshotRecoveryHelper {
|
|
27
|
+
private readonly baseUrl;
|
|
28
|
+
private readonly characterId;
|
|
29
|
+
private readonly fetchImpl;
|
|
30
|
+
constructor(opts: SnapshotRecoveryOptions);
|
|
31
|
+
fetchSnapshot(): Promise<SnapshotResponse>;
|
|
32
|
+
applySnapshot(world: World, snapshot: SnapshotResponse): void;
|
|
33
|
+
recover(world: World): Promise<number>;
|
|
34
|
+
private buildUrl;
|
|
35
|
+
private validateResponse;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=snapshot-recovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-recovery.d.ts","sourceRoot":"","sources":["../../src/director/snapshot-recovery.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAiBzC,OAAO,KAAK,EACV,aAAa,EAKd,MAAM,qBAAqB,CAAC;AAI7B,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE;QACR,YAAY,EAAE,aAAa,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC;QACnD,SAAS,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAAC;QACpD,KAAK,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAAC;QAChD,gBAAgB,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC;KAC3D,CAAC;IACF,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBACT,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAU;CAO/F;AAED,MAAM,WAAW,uBAAuB;IAGtC,OAAO,EAAE,MAAM,CAAC;IAEhB,WAAW,EAAE,MAAM,CAAC;IAEpB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAaD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,IAAI,EAAE,uBAAuB;IAenC,aAAa,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAsBhD,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IA0DvD,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ5C,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,gBAAgB;CAsBzB"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// SnapshotRecoveryHelper - the renderer-side counterpart to backend
|
|
2
|
+
// Phase 6.5's GET /api/v1/loom/director/state endpoint.
|
|
3
|
+
//
|
|
4
|
+
// When the SSEDirectorBridge surfaces a `system.snapshot.required`
|
|
5
|
+
// event (gap exceeds REPLAY_MAX_EVENTS or oldest retained id older
|
|
6
|
+
// than client's last_known_id), the application:
|
|
7
|
+
//
|
|
8
|
+
// 1. Stops the current bridge.
|
|
9
|
+
// 2. Calls SnapshotRecoveryHelper.recover(world).
|
|
10
|
+
// 3. Receives the snapshot's tail_id back.
|
|
11
|
+
// 4. Constructs a new SSEDirectorBridge with
|
|
12
|
+
// initialLastEventId = tail_id so any replayed events <=
|
|
13
|
+
// tail_id are silently deduped.
|
|
14
|
+
// 5. Starts the new bridge; live events resume from > tail_id.
|
|
15
|
+
//
|
|
16
|
+
// The helper itself is stateless. fetchSnapshot is async (network
|
|
17
|
+
// IO); applySnapshot is sync (pure resource mutation). recover is
|
|
18
|
+
// the convenience that chains both.
|
|
19
|
+
//
|
|
20
|
+
// Per LOOM-DIRECTOR-PROTOCOL.md §3.11 + §13 LOCKED invariants, the
|
|
21
|
+
// renderer never decides palette / VE tier / zone / encounter -
|
|
22
|
+
// the snapshot endpoint is authoritative; the helper simply applies
|
|
23
|
+
// what the server returned.
|
|
24
|
+
import { RESOURCE_KNOT_CONTEXT } from './director-bridge.js';
|
|
25
|
+
import { RESOURCE_VEIL_BUDGET, } from '../resources.js';
|
|
26
|
+
import { RESOURCE_DIRECTOR_LOG, } from './director-system.js';
|
|
27
|
+
import { RESOURCE_ZONE_STATE, beginTransition, } from '../zone/zone-state.js';
|
|
28
|
+
export class SnapshotFetchError extends Error {
|
|
29
|
+
kind;
|
|
30
|
+
status;
|
|
31
|
+
url;
|
|
32
|
+
constructor(kind, url, message, status = 0) {
|
|
33
|
+
super('SnapshotFetchError[' + kind + '] ' + url + ': ' + message);
|
|
34
|
+
this.name = 'SnapshotFetchError';
|
|
35
|
+
this.kind = kind;
|
|
36
|
+
this.url = url;
|
|
37
|
+
this.status = status;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Tier-to-scalar table mirrors DirectorSystem so applying a
|
|
41
|
+
// snapshot's ve.budget produces the same engine state as a live
|
|
42
|
+
// ve.budget.update event would.
|
|
43
|
+
const PARTICLE_BUDGET_BASE = 4096;
|
|
44
|
+
const SHADER_BUDGET_BASE = 8;
|
|
45
|
+
const TIER_SCALARS = {
|
|
46
|
+
green: { particle: 1.0, audio: 1.0, shader: 1.0 },
|
|
47
|
+
amber: { particle: 0.5, audio: 0.7, shader: 0.5 },
|
|
48
|
+
red: { particle: 0.05, audio: 0.4, shader: 0.0 },
|
|
49
|
+
};
|
|
50
|
+
export class SnapshotRecoveryHelper {
|
|
51
|
+
baseUrl;
|
|
52
|
+
characterId;
|
|
53
|
+
fetchImpl;
|
|
54
|
+
constructor(opts) {
|
|
55
|
+
this.baseUrl = opts.baseUrl;
|
|
56
|
+
this.characterId = opts.characterId;
|
|
57
|
+
if (opts.fetchImpl) {
|
|
58
|
+
this.fetchImpl = opts.fetchImpl;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (typeof fetch === 'undefined') {
|
|
62
|
+
throw new Error('SnapshotRecoveryHelper: fetch is unavailable. Pass options.fetchImpl in non-DOM contexts.');
|
|
63
|
+
}
|
|
64
|
+
this.fetchImpl = fetch;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Fetch + parse the snapshot. Throws SnapshotFetchError on any
|
|
68
|
+
// failure (network, non-2xx HTTP, JSON parse, shape validation).
|
|
69
|
+
async fetchSnapshot() {
|
|
70
|
+
const url = this.buildUrl();
|
|
71
|
+
let resp;
|
|
72
|
+
try {
|
|
73
|
+
resp = await this.fetchImpl(url, { credentials: 'include' });
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
throw new SnapshotFetchError('network', url, err instanceof Error ? err.message : String(err));
|
|
77
|
+
}
|
|
78
|
+
if (!resp.ok) {
|
|
79
|
+
throw new SnapshotFetchError('http', url, 'HTTP ' + resp.status + ' ' + resp.statusText, resp.status);
|
|
80
|
+
}
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = await resp.json();
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw new SnapshotFetchError('parse', url, err instanceof Error ? err.message : String(err));
|
|
87
|
+
}
|
|
88
|
+
return this.validateResponse(raw, url);
|
|
89
|
+
}
|
|
90
|
+
// Apply the snapshot to the world's engine resources. Pure (no
|
|
91
|
+
// network I/O). Idempotent - reapplying the same snapshot is safe.
|
|
92
|
+
applySnapshot(world, snapshot) {
|
|
93
|
+
const knotCtx = world.resources.get(RESOURCE_KNOT_CONTEXT);
|
|
94
|
+
const budget = world.resources.get(RESOURCE_VEIL_BUDGET);
|
|
95
|
+
const log = world.resources.get(RESOURCE_DIRECTOR_LOG);
|
|
96
|
+
const zone = world.resources.get(RESOURCE_ZONE_STATE);
|
|
97
|
+
const nowMs = typeof performance !== 'undefined' ? performance.now() : 0;
|
|
98
|
+
// knot_context: apply palette + mood with a short fade so the
|
|
99
|
+
// visual snap-in isn't jarring.
|
|
100
|
+
if (knotCtx && snapshot.snapshot.knot_context) {
|
|
101
|
+
const d = snapshot.snapshot.knot_context.data;
|
|
102
|
+
knotCtx.knot = d.knot;
|
|
103
|
+
knotCtx.mood = d.mood;
|
|
104
|
+
// Use a short fade (200ms) regardless of the original event's
|
|
105
|
+
// fade_ms because we're snapping, not transitioning.
|
|
106
|
+
knotCtx.beginFade(d.palette, 200, nowMs);
|
|
107
|
+
if (log)
|
|
108
|
+
log.lastKnot = d.knot;
|
|
109
|
+
}
|
|
110
|
+
// ve_budget: apply tier + scalar like DirectorSystem does.
|
|
111
|
+
if (budget && snapshot.snapshot.ve_budget) {
|
|
112
|
+
const d = snapshot.snapshot.ve_budget.data;
|
|
113
|
+
const scalars = TIER_SCALARS[d.tier];
|
|
114
|
+
budget.particleBudget = Math.round(PARTICLE_BUDGET_BASE * scalars.particle);
|
|
115
|
+
budget.audioBudget = scalars.audio;
|
|
116
|
+
budget.shaderBudget = Math.round(SHADER_BUDGET_BASE * scalars.shader);
|
|
117
|
+
budget.eventBudget = d.encounter_budget_ve;
|
|
118
|
+
if (log)
|
|
119
|
+
log.lastTier = d.tier;
|
|
120
|
+
}
|
|
121
|
+
// scene: apply zone transition. If the snapshot's scene says
|
|
122
|
+
// we're in zone X but our state says Y, snap to X without a
|
|
123
|
+
// long fade.
|
|
124
|
+
if (zone && snapshot.snapshot.scene) {
|
|
125
|
+
const d = snapshot.snapshot.scene.data;
|
|
126
|
+
const target = d.to_zone;
|
|
127
|
+
// 1ms fade = effectively instant; engine clamps to 1 frame.
|
|
128
|
+
beginTransition(zone, target, 'instant', 1, nowMs);
|
|
129
|
+
}
|
|
130
|
+
// active_encounter: set encounter id + narrator line if present.
|
|
131
|
+
if (log && snapshot.snapshot.active_encounter) {
|
|
132
|
+
const d = snapshot.snapshot.active_encounter.data;
|
|
133
|
+
log.activeEncounterId = d.encounter_id;
|
|
134
|
+
if (d.narrator_line) {
|
|
135
|
+
log.lastNarratorLine = d.narrator_line;
|
|
136
|
+
log.lastNarratorTtlMs = 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (log) {
|
|
140
|
+
// No active encounter in the snapshot - clear if we had one.
|
|
141
|
+
log.activeEncounterId = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Convenience: fetch + apply + return the tail_id. Application
|
|
145
|
+
// uses the tail_id when constructing the replacement bridge so
|
|
146
|
+
// duplicate replayed events are silently deduped.
|
|
147
|
+
async recover(world) {
|
|
148
|
+
const snap = await this.fetchSnapshot();
|
|
149
|
+
this.applySnapshot(world, snap);
|
|
150
|
+
return snap.tail_id;
|
|
151
|
+
}
|
|
152
|
+
// ----- Private helpers -----
|
|
153
|
+
buildUrl() {
|
|
154
|
+
const sep = this.baseUrl.includes('?') ? '&' : '?';
|
|
155
|
+
return this.baseUrl + sep + 'character_id=' + encodeURIComponent(this.characterId);
|
|
156
|
+
}
|
|
157
|
+
validateResponse(raw, url) {
|
|
158
|
+
if (!raw || typeof raw !== 'object') {
|
|
159
|
+
throw new SnapshotFetchError('invalid', url, 'response is not an object');
|
|
160
|
+
}
|
|
161
|
+
const r = raw;
|
|
162
|
+
if (r['ok'] !== true) {
|
|
163
|
+
throw new SnapshotFetchError('invalid', url, 'response.ok is not true');
|
|
164
|
+
}
|
|
165
|
+
if (typeof r['character_id'] !== 'string') {
|
|
166
|
+
throw new SnapshotFetchError('invalid', url, 'character_id missing or not a string');
|
|
167
|
+
}
|
|
168
|
+
if (typeof r['tail_id'] !== 'number' || r['tail_id'] < 0) {
|
|
169
|
+
throw new SnapshotFetchError('invalid', url, 'tail_id missing or negative');
|
|
170
|
+
}
|
|
171
|
+
if (!r['snapshot'] || typeof r['snapshot'] !== 'object') {
|
|
172
|
+
throw new SnapshotFetchError('invalid', url, 'snapshot field missing');
|
|
173
|
+
}
|
|
174
|
+
// The snapshot.* envelopes are validated lazily on apply; full
|
|
175
|
+
// shape validation here would duplicate parseEnvelope. Trust
|
|
176
|
+
// the server for v1.
|
|
177
|
+
return r;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=snapshot-recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-recovery.js","sourceRoot":"","sources":["../../src/director/snapshot-recovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DirectorEvent } from './event-envelope.js';
|
|
2
|
+
import { type IDirectorBridge, type DirectorBridgeStatus, type DirectorBridgeStats } from './director-bridge.js';
|
|
3
|
+
export interface SSEDirectorBridgeOptions {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
characterId: string;
|
|
6
|
+
fps?: number;
|
|
7
|
+
dropP2?: boolean;
|
|
8
|
+
eventSourceFactory?: (url: string) => EventSource;
|
|
9
|
+
initialLastEventId?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class SSEDirectorBridge implements IDirectorBridge {
|
|
12
|
+
private readonly baseUrl;
|
|
13
|
+
private readonly characterId;
|
|
14
|
+
private readonly fps;
|
|
15
|
+
private readonly dropP2;
|
|
16
|
+
private readonly eventSourceFactory;
|
|
17
|
+
private es;
|
|
18
|
+
private queue;
|
|
19
|
+
private statusValue;
|
|
20
|
+
private statsValue;
|
|
21
|
+
private reorderBuffer;
|
|
22
|
+
private reorderTimeoutHandle;
|
|
23
|
+
private static readonly REORDER_BUFFER_MAX;
|
|
24
|
+
private static readonly REORDER_TIMEOUT_MS;
|
|
25
|
+
constructor(opts: SSEDirectorBridgeOptions);
|
|
26
|
+
start(): void;
|
|
27
|
+
stop(): void;
|
|
28
|
+
status(): DirectorBridgeStatus;
|
|
29
|
+
isConnected(): boolean;
|
|
30
|
+
getLastEventId(): number;
|
|
31
|
+
pollEvents(): DirectorEvent[];
|
|
32
|
+
stats(): Readonly<DirectorBridgeStats>;
|
|
33
|
+
private buildUrl;
|
|
34
|
+
private openConnection;
|
|
35
|
+
private closeConnection;
|
|
36
|
+
private handleRaw;
|
|
37
|
+
private armReorderTimeout;
|
|
38
|
+
private drainReorderBuffer;
|
|
39
|
+
private flushReorderBufferAsIs;
|
|
40
|
+
private clearReorderBuffer;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=sse-director-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-director-bridge.d.ts","sourceRoot":"","sources":["../../src/director/sse-director-bridge.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,wBAAwB;IAKvC,OAAO,EAAE,MAAM,CAAC;IAEhB,WAAW,EAAE,MAAM,CAAC;IAEpB,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,MAAM,CAAC,EAAE,OAAO,CAAC;IAGjB,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,WAAW,CAAC;IAOlD,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,qBAAa,iBAAkB,YAAW,eAAe;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA+B;IAElE,OAAO,CAAC,EAAE,CAA4B;IACtC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,UAAU,CAOhB;IAIF,OAAO,CAAC,aAAa,CAAyC;IAC9D,OAAO,CAAC,oBAAoB,CAA8C;IAC1E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAM;IAChD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAO;gBAErC,IAAI,EAAE,wBAAwB;IAmB1C,KAAK,IAAI,IAAI;IAMb,IAAI,IAAI,IAAI;IAMZ,MAAM,IAAI,oBAAoB;IAI9B,WAAW,IAAI,OAAO;IAItB,cAAc,IAAI,MAAM;IAIxB,UAAU,IAAI,aAAa,EAAE;IAO7B,KAAK,IAAI,QAAQ,CAAC,mBAAmB,CAAC;IAMtC,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,cAAc;IA4DtB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,SAAS;IAyDjB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,kBAAkB;IAe1B,OAAO,CAAC,sBAAsB;IAgB9B,OAAO,CAAC,kBAAkB;CAO3B"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// SSEDirectorBridge - real EventSource subscription to the backend's
|
|
2
|
+
// /api/v1/loom/director/events endpoint.
|
|
3
|
+
//
|
|
4
|
+
// Per LOOM-DIRECTOR-PROTOCOL.md Section 2: SSE primary, native
|
|
5
|
+
// EventSource auto-reconnect with Last-Event-ID, JSON payload per
|
|
6
|
+
// frame. The renderer never decides palette or VE tier (Section 5.1,
|
|
7
|
+
// 6.5); it just receives events and feeds them to DirectorSystem.
|
|
8
|
+
//
|
|
9
|
+
// Reconnect strategy (Section 4):
|
|
10
|
+
// - EventSource handles transport-layer reconnect on its own with
|
|
11
|
+
// exponential backoff. We just observe via onerror/onopen.
|
|
12
|
+
// - On a clean reconnect with Last-Event-ID set automatically by
|
|
13
|
+
// the browser, server replays the gap and emits
|
|
14
|
+
// system.replay.complete.
|
|
15
|
+
// - On a hard gap (system.snapshot.required), bridge stops; the
|
|
16
|
+
// consumer is responsible for fetching /state and constructing
|
|
17
|
+
// a fresh bridge with the new tail id.
|
|
18
|
+
//
|
|
19
|
+
// Browser-only. Constructor throws if EventSource is undefined (Node
|
|
20
|
+
// test environment). Tests use MockDirectorBridge instead.
|
|
21
|
+
import { parseEnvelopeJson } from './event-envelope.js';
|
|
22
|
+
export class SSEDirectorBridge {
|
|
23
|
+
baseUrl;
|
|
24
|
+
characterId;
|
|
25
|
+
fps;
|
|
26
|
+
dropP2;
|
|
27
|
+
eventSourceFactory;
|
|
28
|
+
es = null;
|
|
29
|
+
queue = [];
|
|
30
|
+
statusValue = 'idle';
|
|
31
|
+
statsValue = {
|
|
32
|
+
eventsReceived: 0,
|
|
33
|
+
reconnects: 0,
|
|
34
|
+
lastEventId: 0,
|
|
35
|
+
outOfOrderEvents: 0,
|
|
36
|
+
serverDropsP1: 0,
|
|
37
|
+
serverDropsP2: 0,
|
|
38
|
+
};
|
|
39
|
+
// Reorder buffer (Section 4.2). Max 32 entries; drained when the
|
|
40
|
+
// missing id arrives or after a 500ms timeout that triggers reconnect.
|
|
41
|
+
reorderBuffer = new Map();
|
|
42
|
+
reorderTimeoutHandle = null;
|
|
43
|
+
static REORDER_BUFFER_MAX = 32;
|
|
44
|
+
static REORDER_TIMEOUT_MS = 500;
|
|
45
|
+
constructor(opts) {
|
|
46
|
+
this.baseUrl = opts.baseUrl;
|
|
47
|
+
this.characterId = opts.characterId;
|
|
48
|
+
this.fps = opts.fps ?? 60;
|
|
49
|
+
this.dropP2 = opts.dropP2 ?? true;
|
|
50
|
+
if (typeof opts.initialLastEventId === 'number' && opts.initialLastEventId > 0) {
|
|
51
|
+
this.statsValue.lastEventId = opts.initialLastEventId;
|
|
52
|
+
}
|
|
53
|
+
if (opts.eventSourceFactory) {
|
|
54
|
+
this.eventSourceFactory = opts.eventSourceFactory;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
if (typeof EventSource === 'undefined') {
|
|
58
|
+
throw new Error('SSEDirectorBridge: EventSource is not available in this environment. Use MockDirectorBridge for tests.');
|
|
59
|
+
}
|
|
60
|
+
const ESCtor = EventSource;
|
|
61
|
+
this.eventSourceFactory = (u) => new ESCtor(u, { withCredentials: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
start() {
|
|
65
|
+
if (this.es)
|
|
66
|
+
return;
|
|
67
|
+
this.statusValue = 'connecting';
|
|
68
|
+
this.openConnection();
|
|
69
|
+
}
|
|
70
|
+
stop() {
|
|
71
|
+
this.statusValue = 'closed';
|
|
72
|
+
this.closeConnection();
|
|
73
|
+
this.clearReorderBuffer();
|
|
74
|
+
}
|
|
75
|
+
status() {
|
|
76
|
+
return this.statusValue;
|
|
77
|
+
}
|
|
78
|
+
isConnected() {
|
|
79
|
+
return this.statusValue === 'connected';
|
|
80
|
+
}
|
|
81
|
+
getLastEventId() {
|
|
82
|
+
return this.statsValue.lastEventId;
|
|
83
|
+
}
|
|
84
|
+
pollEvents() {
|
|
85
|
+
if (this.queue.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const out = this.queue;
|
|
88
|
+
this.queue = [];
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
stats() {
|
|
92
|
+
return this.statsValue;
|
|
93
|
+
}
|
|
94
|
+
// ----- Internal -----
|
|
95
|
+
buildUrl() {
|
|
96
|
+
const sep = this.baseUrl.includes('?') ? '&' : '?';
|
|
97
|
+
return (this.baseUrl +
|
|
98
|
+
sep +
|
|
99
|
+
'character_id=' + encodeURIComponent(this.characterId) +
|
|
100
|
+
'&fps=' + this.fps +
|
|
101
|
+
'&drop_p2=' + (this.dropP2 ? 'true' : 'false'));
|
|
102
|
+
}
|
|
103
|
+
openConnection() {
|
|
104
|
+
const url = this.buildUrl();
|
|
105
|
+
let es;
|
|
106
|
+
try {
|
|
107
|
+
es = this.eventSourceFactory(url);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
this.statusValue = 'closed';
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
this.es = es;
|
|
114
|
+
es.onopen = () => {
|
|
115
|
+
this.statusValue = 'connected';
|
|
116
|
+
};
|
|
117
|
+
es.onerror = () => {
|
|
118
|
+
// EventSource handles its own retry. We just track the state
|
|
119
|
+
// change so consumers can render a 'reconnecting' indicator.
|
|
120
|
+
// If readyState is CLOSED, we're done; otherwise reconnect is
|
|
121
|
+
// in progress.
|
|
122
|
+
const closed = es.readyState === 2; // EventSource.CLOSED
|
|
123
|
+
if (closed) {
|
|
124
|
+
this.statusValue = 'closed';
|
|
125
|
+
this.closeConnection();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.statusValue = 'reconnecting';
|
|
129
|
+
this.statsValue.reconnects++;
|
|
130
|
+
};
|
|
131
|
+
// The default 'message' event captures frames with no event: line.
|
|
132
|
+
// The backend uses event: <type> for everything per spec, so we
|
|
133
|
+
// route on the 'event' name. Subscribing to a few key types
|
|
134
|
+
// explicitly keeps EventSource from coalescing them under 'message'.
|
|
135
|
+
es.onmessage = (e) => {
|
|
136
|
+
this.handleRaw(e);
|
|
137
|
+
};
|
|
138
|
+
// Subscribe to all known event types so EventSource fires the
|
|
139
|
+
// type-specific listener, not just onmessage. This makes the SSE
|
|
140
|
+
// 'event:' line meaningful.
|
|
141
|
+
const knownTypes = [
|
|
142
|
+
'encounter.spawn',
|
|
143
|
+
'encounter.tick',
|
|
144
|
+
'encounter.end',
|
|
145
|
+
'encounter.loot',
|
|
146
|
+
'knot.context',
|
|
147
|
+
've.budget.update',
|
|
148
|
+
'scene.transition',
|
|
149
|
+
'narrator.line',
|
|
150
|
+
'system.heartbeat',
|
|
151
|
+
'system.replay.complete',
|
|
152
|
+
'system.snapshot.required',
|
|
153
|
+
];
|
|
154
|
+
for (let i = 0; i < knownTypes.length; i++) {
|
|
155
|
+
const t = knownTypes[i];
|
|
156
|
+
if (!t)
|
|
157
|
+
continue;
|
|
158
|
+
es.addEventListener(t, (e) => {
|
|
159
|
+
this.handleRaw(e);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
closeConnection() {
|
|
164
|
+
if (!this.es)
|
|
165
|
+
return;
|
|
166
|
+
try {
|
|
167
|
+
this.es.close();
|
|
168
|
+
}
|
|
169
|
+
catch { /* ignore */ }
|
|
170
|
+
this.es = null;
|
|
171
|
+
}
|
|
172
|
+
handleRaw(e) {
|
|
173
|
+
const dataStr = typeof e.data === 'string' ? e.data : '';
|
|
174
|
+
const ev = parseEnvelopeJson(dataStr);
|
|
175
|
+
if (!ev) {
|
|
176
|
+
// Malformed envelope - log + drop. EventSource will keep flowing.
|
|
177
|
+
// In v1 we don't kill the stream on a single bad payload.
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.statsValue.eventsReceived++;
|
|
181
|
+
// System events drive bridge behaviour even if the consumer
|
|
182
|
+
// hasn't drained yet.
|
|
183
|
+
if (ev.type === 'system.heartbeat') {
|
|
184
|
+
this.statsValue.serverDropsP1 = ev.data.drops_p1;
|
|
185
|
+
this.statsValue.serverDropsP2 = ev.data.drops_p2;
|
|
186
|
+
// Heartbeats are still surfaced to the consumer (it may want to
|
|
187
|
+
// log / display drop counters) but they don't gate the gap-
|
|
188
|
+
// detection logic - just stash and pass through.
|
|
189
|
+
}
|
|
190
|
+
if (ev.type === 'system.snapshot.required') {
|
|
191
|
+
this.statusValue = 'snapshot-required';
|
|
192
|
+
// Surface to the consumer + close. The application layer must
|
|
193
|
+
// fetch /state and build a fresh bridge.
|
|
194
|
+
this.queue.push(ev);
|
|
195
|
+
this.closeConnection();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const expected = this.statsValue.lastEventId + 1;
|
|
199
|
+
if (ev.id === expected) {
|
|
200
|
+
// In-order. Push + drain any reorder buffer entries that now
|
|
201
|
+
// contiguously follow.
|
|
202
|
+
this.statsValue.lastEventId = ev.id;
|
|
203
|
+
this.queue.push(ev);
|
|
204
|
+
this.drainReorderBuffer();
|
|
205
|
+
}
|
|
206
|
+
else if (ev.id > expected) {
|
|
207
|
+
// Future event - hold in reorder buffer up to REORDER_BUFFER_MAX.
|
|
208
|
+
// Reset the timeout so the buffer fires eventually.
|
|
209
|
+
this.statsValue.outOfOrderEvents++;
|
|
210
|
+
if (this.reorderBuffer.size < SSEDirectorBridge.REORDER_BUFFER_MAX) {
|
|
211
|
+
this.reorderBuffer.set(ev.id, ev);
|
|
212
|
+
this.armReorderTimeout();
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// Buffer full - hard gap. Force-flush the buffer in id order
|
|
216
|
+
// and accept the gap. Consumer's gap detection should treat
|
|
217
|
+
// this like a missed range.
|
|
218
|
+
this.flushReorderBufferAsIs();
|
|
219
|
+
this.statsValue.lastEventId = ev.id;
|
|
220
|
+
this.queue.push(ev);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// Past event (lastEventId already advanced beyond this). Likely
|
|
225
|
+
// a duplicate from EventSource's at-least-once delivery during
|
|
226
|
+
// reconnect; drop silently.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
armReorderTimeout() {
|
|
230
|
+
if (this.reorderTimeoutHandle !== null)
|
|
231
|
+
return;
|
|
232
|
+
this.reorderTimeoutHandle = setTimeout(() => {
|
|
233
|
+
this.reorderTimeoutHandle = null;
|
|
234
|
+
// Timeout: flush whatever's in the buffer in id order. The
|
|
235
|
+
// missing ids are skipped; consumer's lastEventId advances.
|
|
236
|
+
this.flushReorderBufferAsIs();
|
|
237
|
+
}, SSEDirectorBridge.REORDER_TIMEOUT_MS);
|
|
238
|
+
}
|
|
239
|
+
drainReorderBuffer() {
|
|
240
|
+
while (true) {
|
|
241
|
+
const next = this.statsValue.lastEventId + 1;
|
|
242
|
+
const ev = this.reorderBuffer.get(next);
|
|
243
|
+
if (!ev)
|
|
244
|
+
break;
|
|
245
|
+
this.reorderBuffer.delete(next);
|
|
246
|
+
this.statsValue.lastEventId = next;
|
|
247
|
+
this.queue.push(ev);
|
|
248
|
+
}
|
|
249
|
+
if (this.reorderBuffer.size === 0 && this.reorderTimeoutHandle !== null) {
|
|
250
|
+
clearTimeout(this.reorderTimeoutHandle);
|
|
251
|
+
this.reorderTimeoutHandle = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
flushReorderBufferAsIs() {
|
|
255
|
+
if (this.reorderBuffer.size === 0)
|
|
256
|
+
return;
|
|
257
|
+
const ids = Array.from(this.reorderBuffer.keys()).sort((a, b) => a - b);
|
|
258
|
+
for (const id of ids) {
|
|
259
|
+
const ev = this.reorderBuffer.get(id);
|
|
260
|
+
if (!ev)
|
|
261
|
+
continue;
|
|
262
|
+
this.queue.push(ev);
|
|
263
|
+
if (id > this.statsValue.lastEventId)
|
|
264
|
+
this.statsValue.lastEventId = id;
|
|
265
|
+
}
|
|
266
|
+
this.reorderBuffer.clear();
|
|
267
|
+
if (this.reorderTimeoutHandle !== null) {
|
|
268
|
+
clearTimeout(this.reorderTimeoutHandle);
|
|
269
|
+
this.reorderTimeoutHandle = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
clearReorderBuffer() {
|
|
273
|
+
this.reorderBuffer.clear();
|
|
274
|
+
if (this.reorderTimeoutHandle !== null) {
|
|
275
|
+
clearTimeout(this.reorderTimeoutHandle);
|
|
276
|
+
this.reorderTimeoutHandle = null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=sse-director-bridge.js.map
|