@mindot/will 0.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/LICENSE +202 -0
- package/README.md +777 -0
- package/dist/index.d.ts +8144 -0
- package/dist/index.js +26231 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/cognition/agency/access.grants.ts +59 -0
- package/src/cognition/agency/competence.codec.ts +89 -0
- package/src/cognition/agency/engines/action.selector.ts +462 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
- package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
- package/src/cognition/agency/engines/instruction.intake.ts +54 -0
- package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
- package/src/cognition/agency/engines/reafference.engine.ts +241 -0
- package/src/cognition/agency/execution.primitives.ts +118 -0
- package/src/cognition/agency/index.ts +58 -0
- package/src/cognition/agency/proactive.communicator.ts +274 -0
- package/src/cognition/agency/reconcile.learning.ts +82 -0
- package/src/cognition/agency/schemas/external.ts +59 -0
- package/src/cognition/agency/schemas/innate.ts +111 -0
- package/src/cognition/agency/schemas/repertoire.ts +246 -0
- package/src/cognition/agency/selection.scoring.ts +191 -0
- package/src/cognition/agency/types.ts +165 -0
- package/src/cognition/bus.ts +391 -0
- package/src/cognition/completion.inbox.ts +86 -0
- package/src/cognition/config.mirror.entities.ts +475 -0
- package/src/cognition/conversation.memory.ts +81 -0
- package/src/cognition/event.log.ts +116 -0
- package/src/cognition/event.schemas.ts +568 -0
- package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
- package/src/cognition/faculties/affective.blender.ts +562 -0
- package/src/cognition/faculties/attachment.evaluator.ts +432 -0
- package/src/cognition/faculties/attention.allocator.ts +422 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
- package/src/cognition/faculties/bias.detector.ts +363 -0
- package/src/cognition/faculties/circadian.oscillator.ts +294 -0
- package/src/cognition/faculties/confidence.calibrator.ts +324 -0
- package/src/cognition/faculties/dream.simulator.ts +251 -0
- package/src/cognition/faculties/empathy.simulator.ts +253 -0
- package/src/cognition/faculties/energy.regulator.ts +320 -0
- package/src/cognition/faculties/episodic.consolidator.ts +728 -0
- package/src/cognition/faculties/executive.engine/commands.ts +459 -0
- package/src/cognition/faculties/executive.engine/config.ts +46 -0
- package/src/cognition/faculties/executive.engine/context.ts +605 -0
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
- package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
- package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
- package/src/cognition/faculties/executive.engine/facet.ts +628 -0
- package/src/cognition/faculties/executive.engine/gating.ts +222 -0
- package/src/cognition/faculties/executive.engine/index.ts +6 -0
- package/src/cognition/faculties/executive.engine/messages.ts +102 -0
- package/src/cognition/faculties/executive.engine/parser.ts +380 -0
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
- package/src/cognition/faculties/executive.engine/types.ts +323 -0
- package/src/cognition/faculties/exteroception.ts +338 -0
- package/src/cognition/faculties/forgetting.curve.ts +202 -0
- package/src/cognition/faculties/frustration.evaluator.ts +280 -0
- package/src/cognition/faculties/goal.manager.ts +1008 -0
- package/src/cognition/faculties/inhibition.controller.ts +351 -0
- package/src/cognition/faculties/interoception.ts +417 -0
- package/src/cognition/faculties/introspection.engine.ts +246 -0
- package/src/cognition/faculties/known.entity.tracker.ts +439 -0
- package/src/cognition/faculties/loss.evaluator.ts +224 -0
- package/src/cognition/faculties/moral.evaluator.ts +437 -0
- package/src/cognition/faculties/novelty.detector.ts +254 -0
- package/src/cognition/faculties/persona.consolidator.ts +845 -0
- package/src/cognition/faculties/planning.engine/engine.ts +859 -0
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
- package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
- package/src/cognition/faculties/planning.engine/types.ts +143 -0
- package/src/cognition/faculties/reputation.tracker.ts +294 -0
- package/src/cognition/faculties/reward.evaluator.ts +304 -0
- package/src/cognition/faculties/self.model.updater.ts +677 -0
- package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
- package/src/cognition/faculties/semantic.engine/index.ts +6 -0
- package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
- package/src/cognition/faculties/semantic.engine/types.ts +53 -0
- package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
- package/src/cognition/faculties/social.perception.ts +333 -0
- package/src/cognition/faculties/spaced.repetition.ts +605 -0
- package/src/cognition/faculties/stress.regulator.ts +375 -0
- package/src/cognition/faculties/task.switcher.ts +239 -0
- package/src/cognition/faculties/theory.of.mind.ts +333 -0
- package/src/cognition/faculties/threat.evaluator.ts +312 -0
- package/src/cognition/faculties/working.memory.ts +406 -0
- package/src/cognition/generative.model.ts +344 -0
- package/src/cognition/heartbeat.ts +70 -0
- package/src/cognition/index.ts +291 -0
- package/src/cognition/instruction.handler.ts +146 -0
- package/src/cognition/memory/index.ts +35 -0
- package/src/cognition/memory/vector.adapter.ts +316 -0
- package/src/cognition/memory/vector.content.ts +47 -0
- package/src/cognition/memory/vector.embedder.ts +212 -0
- package/src/cognition/memory/vector.index.ts +459 -0
- package/src/cognition/memory/vector.types.ts +65 -0
- package/src/cognition/orchestrator.ts +142 -0
- package/src/cognition/persona.prior.ts +265 -0
- package/src/cognition/schema.registry.ts +145 -0
- package/src/cognition/senses/audition.engine/engine.ts +976 -0
- package/src/cognition/senses/audition.engine/salience.ts +59 -0
- package/src/cognition/senses/base.sense.engine.ts +117 -0
- package/src/cognition/senses/gustation.engine.ts +24 -0
- package/src/cognition/senses/index.ts +172 -0
- package/src/cognition/senses/olfaction.engine.ts +24 -0
- package/src/cognition/senses/somatosensation.engine.ts +23 -0
- package/src/cognition/senses/vision.engine.ts +23 -0
- package/src/cognition/types.ts +63 -0
- package/src/cognition/utilities/token.tracker.ts +492 -0
- package/src/core/abstracts.ts +60 -0
- package/src/core/async.engine.ts +539 -0
- package/src/core/clock.ts +196 -0
- package/src/core/completion.recorder.ts +122 -0
- package/src/core/conflict.detector.ts +140 -0
- package/src/core/distributed.ts +626 -0
- package/src/core/event.bus.ts +292 -0
- package/src/core/inbound.recorder.ts +100 -0
- package/src/core/index.ts +167 -0
- package/src/core/logger.ts +85 -0
- package/src/core/metrics.ts +107 -0
- package/src/core/orchestrator.ts +625 -0
- package/src/core/replay.ts +589 -0
- package/src/core/scenario.ts +120 -0
- package/src/core/serialization.ts +448 -0
- package/src/core/simulation.ts +197 -0
- package/src/core/snapshot.manager.ts +293 -0
- package/src/core/state.manager.ts +283 -0
- package/src/core/types.ts +274 -0
- package/src/core/utils.ts +82 -0
- package/src/core/wall.clock.ts +23 -0
- package/src/extensions/livestream.ext.ts +570 -0
- package/src/extensions/time.ext.ts +339 -0
- package/src/index.ts +91 -0
- package/src/llm/gate.ts +140 -0
- package/src/llm/index.ts +716 -0
- package/src/llm/summarizer.ts +170 -0
- package/src/llm/wire.contracts.ts +67 -0
- package/src/pma/eval.ts +651 -0
- package/src/pma/index.ts +1082 -0
- package/src/profiles/built-in.ts +7 -0
- package/src/profiles/companion.ts +28 -0
- package/src/profiles/company-brain.ts +54 -0
- package/src/profiles/customer-service.ts +30 -0
- package/src/profiles/game-npc.ts +20 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/smart-home.ts +34 -0
- package/src/runners/coherence.runner.ts +49 -0
- package/src/runners/outreach.runner.ts +158 -0
- package/src/runners/social.runner.ts +182 -0
- package/src/runners/thin-shim.runner.ts +195 -0
- package/src/sdk/will.ts +334 -0
- package/src/stem/assembly.audit.ts +88 -0
- package/src/stem/distribution.ts +203 -0
- package/src/stem/guards/identity.coherence.ts +152 -0
- package/src/stem/guards/identity.guard.ts +227 -0
- package/src/stem/index.ts +1145 -0
- package/src/stem/mind.ts +1090 -0
- package/src/stem/tracts/ack.reconciler.ts +55 -0
- package/src/stem/tracts/biography.writer.ts +190 -0
- package/src/stem/tracts/effector.controller.ts +143 -0
- package/src/stem/tracts/health.reporter.ts +113 -0
- package/src/stem/tracts/inbound.queue.ts +54 -0
- package/src/stem/tracts/outbox.controller.ts +167 -0
- package/src/stem/tracts/outbox.writer.ts +133 -0
- package/src/stem/tracts/pma.controller.ts +109 -0
- package/src/stem/tracts/replay.controller.ts +177 -0
- package/src/stem/tracts/sensory.controller.ts +168 -0
- package/src/stem/tracts/session.logger.ts +157 -0
- package/src/stem/tracts/transport/index.ts +38 -0
- package/src/stem/tracts/transport/loopback.transport.ts +86 -0
- package/src/stem/tracts/transport/socketio.transport.ts +200 -0
- package/src/stem/tracts/transport/stream.transport.ts +152 -0
- package/src/stem/tracts/transport/types.ts +193 -0
- package/src/stem/tracts/transport.controller.ts +380 -0
- package/src/types.ts +116 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/extensions/live.ext.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Live streaming extension for dashboards, monitoring, and observability.
|
|
7
|
+
* Layers on top of core — not part of the framework itself.
|
|
8
|
+
*
|
|
9
|
+
* Provides WebSocket (full-duplex) and Server-Sent Events (unidirectional)
|
|
10
|
+
* transports for real-time simulation state, metrics, and event streaming.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { logger } from '#core/logger'
|
|
14
|
+
import type { SimulationClock } from '#core/clock'
|
|
15
|
+
import type { EventBus } from '#core/event.bus'
|
|
16
|
+
import type { MetricCollector } from '#core/metrics'
|
|
17
|
+
import type { Orchestrator } from '#core/orchestrator'
|
|
18
|
+
import type { StateManager } from '#core/state.manager'
|
|
19
|
+
import type {
|
|
20
|
+
SimulationContext,
|
|
21
|
+
SimulationEvent,
|
|
22
|
+
ReadonlySimulationState,
|
|
23
|
+
Tick,
|
|
24
|
+
Timestamp,
|
|
25
|
+
} from '../core/types'
|
|
26
|
+
|
|
27
|
+
// ── Transport-agnostic stream message ──────────────────────
|
|
28
|
+
|
|
29
|
+
export interface StreamMessage {
|
|
30
|
+
type: 'snapshot' | 'tick-summary' | 'event' | 'metric-point' | 'log'
|
|
31
|
+
timestamp: Timestamp
|
|
32
|
+
tick: Tick
|
|
33
|
+
payload: unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TickSummary {
|
|
37
|
+
tick: Tick
|
|
38
|
+
simTime: Timestamp
|
|
39
|
+
wallDuration: number // ms this tick took
|
|
40
|
+
entityCount: number
|
|
41
|
+
metricCount: number
|
|
42
|
+
pendingEvents: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Stream transports ─────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Pluggable transport for live data.
|
|
49
|
+
* Implementations: WebSocketStream, SSEStream, ConsoleStream, NoopStream
|
|
50
|
+
*/
|
|
51
|
+
export interface LiveStreamTransport {
|
|
52
|
+
/** Push a single message to connected clients. */
|
|
53
|
+
send( message: StreamMessage ): void
|
|
54
|
+
|
|
55
|
+
/** Close all connections gracefully. */
|
|
56
|
+
close(): void
|
|
57
|
+
|
|
58
|
+
/** Called when the transport is registered with the extension. */
|
|
59
|
+
onRegister?(): void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Configuration ─────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface LiveStreamConfig {
|
|
65
|
+
/** Emit full state snapshot every N ticks (0 = never) */
|
|
66
|
+
snapshotIntervalTicks?: number
|
|
67
|
+
/** Emit tick summary (entity count, latency) every N ticks (1 = every tick) */
|
|
68
|
+
tickSummaryIntervalTicks?: number
|
|
69
|
+
/** Forward all events from EventBus */
|
|
70
|
+
streamEvents?: boolean
|
|
71
|
+
/** Forward metric points */
|
|
72
|
+
streamMetrics?: boolean
|
|
73
|
+
/** Event types to exclude from streaming (e.g., verbose debug events) */
|
|
74
|
+
excludeEventTypes?: string[]
|
|
75
|
+
/** Maximum message queue depth before dropping (backpressure) */
|
|
76
|
+
maxQueueDepth?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Extension implementation ──────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* FIX 1: Instead of implementing TickMiddleware (which has a call-signature
|
|
83
|
+
* constraint), we expose the handler as a property that matches the signature.
|
|
84
|
+
* The class no longer `implements TickMiddleware` — it provides an `onTick`
|
|
85
|
+
* arrow function that is structurally compatible and can be assigned to
|
|
86
|
+
* orchestratorConfig.onAfterTick.
|
|
87
|
+
*/
|
|
88
|
+
export class LiveStreamExtension {
|
|
89
|
+
private _transport: LiveStreamTransport
|
|
90
|
+
private _config: LiveStreamConfig
|
|
91
|
+
private _eventBus: EventBus
|
|
92
|
+
private _stateManager: StateManager
|
|
93
|
+
private _metricCollector?: MetricCollector
|
|
94
|
+
private _clock: SimulationClock
|
|
95
|
+
/** FIX 2: Use the concrete DefaultOrchestrator type which has tickLatencies. */
|
|
96
|
+
private _orchestrator: Orchestrator
|
|
97
|
+
private _getTickLatency: () => number
|
|
98
|
+
|
|
99
|
+
private _unsubscribeEventBus?: () => void
|
|
100
|
+
private _messageQueue: StreamMessage[] = []
|
|
101
|
+
private _maxQueueDepth: number
|
|
102
|
+
private _flushInterval: ReturnType<typeof setInterval> | null = null
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
transport: LiveStreamTransport,
|
|
106
|
+
eventBus: EventBus,
|
|
107
|
+
stateManager: StateManager,
|
|
108
|
+
orchestrator: Orchestrator,
|
|
109
|
+
clock: SimulationClock,
|
|
110
|
+
config: LiveStreamConfig = {},
|
|
111
|
+
metricCollector?: MetricCollector
|
|
112
|
+
){
|
|
113
|
+
this._transport = transport
|
|
114
|
+
this._eventBus = eventBus
|
|
115
|
+
this._stateManager = stateManager
|
|
116
|
+
this._orchestrator = orchestrator
|
|
117
|
+
this._clock = clock
|
|
118
|
+
this._metricCollector = metricCollector
|
|
119
|
+
this._config = {
|
|
120
|
+
snapshotIntervalTicks: config.snapshotIntervalTicks ?? 0,
|
|
121
|
+
tickSummaryIntervalTicks: config.tickSummaryIntervalTicks ?? 1,
|
|
122
|
+
streamEvents: config.streamEvents ?? true,
|
|
123
|
+
streamMetrics: config.streamMetrics ?? false,
|
|
124
|
+
excludeEventTypes: config.excludeEventTypes ?? [],
|
|
125
|
+
maxQueueDepth: config.maxQueueDepth ?? 1000,
|
|
126
|
+
}
|
|
127
|
+
this._maxQueueDepth = this._config.maxQueueDepth!
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* FIX 2: tickLatencies is only available on DefaultOrchestrator, not the
|
|
131
|
+
* Orchestrator interface. We probe for it and provide a fallback so the
|
|
132
|
+
* extension works with any orchestrator implementation.
|
|
133
|
+
*/
|
|
134
|
+
const orchestratorAny = orchestrator as any
|
|
135
|
+
if( Array.isArray( orchestratorAny.tickLatencies ) ){
|
|
136
|
+
this._getTickLatency = () => {
|
|
137
|
+
const latencies = orchestratorAny.tickLatencies as number[]
|
|
138
|
+
return latencies.length > 0 ? ( latencies.at( -1 ) ?? 0 ) : 0
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this._getTickLatency = () => 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Wire transport lifecycle
|
|
146
|
+
transport.onRegister?.()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Tick handler (assignable to OrchestratorConfig.onAfterTick) ──
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Arrow function property — structurally matches the TickMiddleware
|
|
153
|
+
* call signature `(tick, time, state, context) => void | Promise<void>`.
|
|
154
|
+
* Assign this to orchestratorConfig.onAfterTick.
|
|
155
|
+
*/
|
|
156
|
+
readonly onTick = (
|
|
157
|
+
tick: Tick,
|
|
158
|
+
_time: Timestamp,
|
|
159
|
+
state: ReadonlySimulationState,
|
|
160
|
+
_context: SimulationContext
|
|
161
|
+
): void => {
|
|
162
|
+
// Tick summary
|
|
163
|
+
if(
|
|
164
|
+
this._config.tickSummaryIntervalTicks! > 0
|
|
165
|
+
&& tick % this._config.tickSummaryIntervalTicks! === 0
|
|
166
|
+
){
|
|
167
|
+
this._enqueue({
|
|
168
|
+
type: 'tick-summary',
|
|
169
|
+
timestamp: Date.now(),
|
|
170
|
+
tick,
|
|
171
|
+
payload: {
|
|
172
|
+
tick,
|
|
173
|
+
simTime: this._clock.now,
|
|
174
|
+
wallDuration: this._getTickLatency(),
|
|
175
|
+
entityCount: state.entities.size,
|
|
176
|
+
metricCount: state.metrics.size,
|
|
177
|
+
pendingEvents: this._eventBus.getPendingCount(),
|
|
178
|
+
} satisfies TickSummary,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Full snapshot
|
|
183
|
+
if(
|
|
184
|
+
this._config.snapshotIntervalTicks! > 0
|
|
185
|
+
&& tick % this._config.snapshotIntervalTicks! === 0
|
|
186
|
+
){
|
|
187
|
+
this._enqueue({
|
|
188
|
+
type: 'snapshot',
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
tick,
|
|
191
|
+
payload: this._stateManager.snapshot(),
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Lifecycle ───────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
/** Start streaming. Must be called after construction. */
|
|
199
|
+
start(): void {
|
|
200
|
+
// Subscribe to event bus
|
|
201
|
+
if( this._config.streamEvents ){
|
|
202
|
+
this._unsubscribeEventBus = this._eventBus.subscribeAll( ( event, _ctx ) => {
|
|
203
|
+
if( this._config.excludeEventTypes?.includes( event.type ) ) return
|
|
204
|
+
this._enqueue({
|
|
205
|
+
type: 'event',
|
|
206
|
+
timestamp: Date.now(),
|
|
207
|
+
tick: event.tick,
|
|
208
|
+
payload: event,
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Periodic flush (handles metric streaming + drain)
|
|
214
|
+
this._flushInterval = setInterval( () => {
|
|
215
|
+
// Metric streaming can be wired here if MetricCollector exposes a
|
|
216
|
+
// per-point callback. Currently metrics flow through tick summaries.
|
|
217
|
+
this._drainQueue()
|
|
218
|
+
}, 50 ) // 20 fps flush rate
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Stop streaming and close transport. */
|
|
222
|
+
stop(): void {
|
|
223
|
+
this._unsubscribeEventBus?.()
|
|
224
|
+
if( this._flushInterval ) clearInterval( this._flushInterval )
|
|
225
|
+
this._drainQueue()
|
|
226
|
+
this._transport.close()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Backpressure handling ───────────────────────────────
|
|
230
|
+
|
|
231
|
+
private _enqueue( message: StreamMessage ): void {
|
|
232
|
+
if( this._messageQueue.length >= this._maxQueueDepth ){
|
|
233
|
+
// Drop oldest message (circular buffer semantics)
|
|
234
|
+
this._messageQueue.shift()
|
|
235
|
+
logger.warn(`[LiveStream] Queue full (${this._maxQueueDepth}), dropping oldest message`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this._messageQueue.push( message )
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private _drainQueue(): void {
|
|
242
|
+
while( this._messageQueue.length > 0 ){
|
|
243
|
+
const message = this._messageQueue.shift()!
|
|
244
|
+
try {
|
|
245
|
+
this._transport.send( message )
|
|
246
|
+
}
|
|
247
|
+
catch( error ){
|
|
248
|
+
logger.error('[LiveStream] Failed to send message, transport may be closed:', error )
|
|
249
|
+
// Stop trying — transport is dead
|
|
250
|
+
this._messageQueue.length = 0
|
|
251
|
+
break
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── WebSocket transport (Bun-native) ──────────────────────
|
|
258
|
+
|
|
259
|
+
export interface WebSocketConfig {
|
|
260
|
+
port: number
|
|
261
|
+
path?: string // e.g., '/live'
|
|
262
|
+
maxConnections?: number
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* FIX 3: Bun.serve().upgrade() requires an options object with `data`.
|
|
267
|
+
* The third argument is not rest parameters — it's a single options bag.
|
|
268
|
+
*/
|
|
269
|
+
export class WebSocketTransport implements LiveStreamTransport {
|
|
270
|
+
private _server: ReturnType<typeof Bun.serve> | null = null
|
|
271
|
+
private _clients: Set<{ send: ( data: string ) => void }> = new Set()
|
|
272
|
+
private _config: WebSocketConfig
|
|
273
|
+
private _maxConnections: number
|
|
274
|
+
|
|
275
|
+
constructor( config: WebSocketConfig ){
|
|
276
|
+
this._config = config
|
|
277
|
+
this._maxConnections = config.maxConnections ?? 100
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
send( message: StreamMessage ): void {
|
|
281
|
+
const data = JSON.stringify( message )
|
|
282
|
+
|
|
283
|
+
for( const client of this._clients )
|
|
284
|
+
client.send( data )
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
close(): void {
|
|
288
|
+
this._server?.stop()
|
|
289
|
+
this._clients.clear()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
onRegister(): void {
|
|
293
|
+
this._server = Bun.serve({
|
|
294
|
+
port: this._config.port,
|
|
295
|
+
fetch: ( req, server ) => {
|
|
296
|
+
// Only upgrade on the configured path
|
|
297
|
+
const url = new URL( req.url )
|
|
298
|
+
if( this._config.path && url.pathname !== this._config.path )
|
|
299
|
+
return new Response('Not found', { status: 404 } )
|
|
300
|
+
|
|
301
|
+
// Connection limit
|
|
302
|
+
if( this._clients.size >= this._maxConnections )
|
|
303
|
+
return new Response('Too many connections', { status: 503 } )
|
|
304
|
+
|
|
305
|
+
// FIX 3: upgrade() requires { data } as second argument per Bun types
|
|
306
|
+
const upgraded = server.upgrade( req, { data: {} } )
|
|
307
|
+
if( !upgraded )
|
|
308
|
+
return new Response('WebSocket upgrade failed', { status: 400 } )
|
|
309
|
+
|
|
310
|
+
return undefined // Bun handles the upgrade response
|
|
311
|
+
},
|
|
312
|
+
websocket: {
|
|
313
|
+
open: ( ws ) => {
|
|
314
|
+
this._clients.add( ws )
|
|
315
|
+
logger.info(`[WebSocket] Client connected (${this._clients.size} total)`)
|
|
316
|
+
},
|
|
317
|
+
close: ( ws ) => {
|
|
318
|
+
this._clients.delete( ws )
|
|
319
|
+
logger.info(`[WebSocket] Client disconnected (${this._clients.size} total)`)
|
|
320
|
+
},
|
|
321
|
+
message: ( _ws, _message ) => {
|
|
322
|
+
// Clients can send commands here (pause, resume, set speed, etc.)
|
|
323
|
+
// For now, read-only streaming
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
logger.info(`[WebSocket] Server listening on ws://localhost:${this._config.port}${this._config.path ?? '/'}`)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Server-Sent Events transport ──────────────────────────
|
|
333
|
+
|
|
334
|
+
export interface SSEConfig {
|
|
335
|
+
port: number
|
|
336
|
+
path?: string
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* FIX 4 & 5: ReadableStreamDefaultController (from Bun.serve fetch) is not
|
|
341
|
+
* a WritableStreamDefaultWriter. SSE uses a ReadableStream to push events
|
|
342
|
+
* to the client. We need to store the controller and use controller.enqueue()
|
|
343
|
+
* instead of writer.write().
|
|
344
|
+
*/
|
|
345
|
+
export class SSETransport implements LiveStreamTransport {
|
|
346
|
+
private _server: ReturnType<typeof Bun.serve> | null = null
|
|
347
|
+
private _clients: Set<ReadableStreamDefaultController<Uint8Array>> = new Set()
|
|
348
|
+
private _config: SSEConfig
|
|
349
|
+
private _encoder = new TextEncoder()
|
|
350
|
+
|
|
351
|
+
constructor( config: SSEConfig ){
|
|
352
|
+
this._config = config
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
send( message: StreamMessage ): void {
|
|
356
|
+
const
|
|
357
|
+
eventLine = `event: ${message.type}\n`,
|
|
358
|
+
dataLine = `data: ${JSON.stringify( message )}\n\n`,
|
|
359
|
+
encoded = this._encoder.encode( eventLine + dataLine )
|
|
360
|
+
|
|
361
|
+
for( const controller of this._clients )
|
|
362
|
+
controller.enqueue( encoded )
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
close(): void {
|
|
366
|
+
this._server?.stop()
|
|
367
|
+
for( const controller of this._clients ){
|
|
368
|
+
try { controller.close() } catch {}
|
|
369
|
+
}
|
|
370
|
+
this._clients.clear()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
onRegister(): void {
|
|
374
|
+
this._server = Bun.serve({
|
|
375
|
+
port: this._config.port,
|
|
376
|
+
fetch: async ( req ) => {
|
|
377
|
+
const url = new URL( req.url )
|
|
378
|
+
if( this._config.path && url.pathname !== this._config.path )
|
|
379
|
+
return new Response('Not found', { status: 404 } )
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* FIX 4 & 5: ReadableStream with start(controller) gives us a
|
|
383
|
+
* ReadableStreamDefaultController. We store the controller directly
|
|
384
|
+
* and use controller.enqueue() to push SSE data. The client's
|
|
385
|
+
* abort signal handles disconnection cleanup.
|
|
386
|
+
*/
|
|
387
|
+
let streamController: ReadableStreamDefaultController<Uint8Array> | null = null
|
|
388
|
+
|
|
389
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
390
|
+
start: ( controller ) => {
|
|
391
|
+
streamController = controller
|
|
392
|
+
this._clients.add( controller )
|
|
393
|
+
|
|
394
|
+
// Send initial comment to establish connection
|
|
395
|
+
controller.enqueue( this._encoder.encode(': connected\n\n') )
|
|
396
|
+
|
|
397
|
+
req.signal.addEventListener('abort', () => {
|
|
398
|
+
if( streamController ){
|
|
399
|
+
this._clients.delete( streamController )
|
|
400
|
+
logger.info(`[SSE] Client disconnected (${this._clients.size} total)`)
|
|
401
|
+
streamController = null
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
},
|
|
405
|
+
cancel: () => {
|
|
406
|
+
if( streamController ){
|
|
407
|
+
this._clients.delete( streamController )
|
|
408
|
+
streamController = null
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
return new Response( stream, {
|
|
414
|
+
headers: {
|
|
415
|
+
'Content-Type': 'text/event-stream',
|
|
416
|
+
'Cache-Control': 'no-cache',
|
|
417
|
+
'Connection': 'keep-alive',
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
},
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
logger.info(`[SSE] Server listening on http://localhost:${this._config.port}${this._config.path ?? '/'}`)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── Console transport (for development) ───────────────────
|
|
428
|
+
|
|
429
|
+
export class ConsoleTransport implements LiveStreamTransport {
|
|
430
|
+
private _verbose: boolean
|
|
431
|
+
|
|
432
|
+
constructor( verbose: boolean = false ){
|
|
433
|
+
this._verbose = verbose
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
send( message: StreamMessage ): void {
|
|
437
|
+
if( !this._verbose && message.type === 'tick-summary'){
|
|
438
|
+
// Only log every 100 ticks for summary
|
|
439
|
+
const summary = message.payload as TickSummary
|
|
440
|
+
if( summary.tick % 100 === 0 ){
|
|
441
|
+
logger.info(
|
|
442
|
+
`[Live] Tick ${summary.tick}: ${summary.entityCount} entities, ${summary.wallDuration}ms`
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
logger.info(`[Live] ${message.type}:`, message.payload )
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
close(): void {
|
|
452
|
+
// No-op for console
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── Factory helpers ───────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
export class LiveStreamFactory {
|
|
459
|
+
/**
|
|
460
|
+
* Create a WebSocket-backed live stream wired into a running simulation.
|
|
461
|
+
* Wire `stream.onTick` as `orchestratorConfig.onAfterTick`.
|
|
462
|
+
*/
|
|
463
|
+
static createWebSocket(
|
|
464
|
+
sim: {
|
|
465
|
+
eventBus: EventBus
|
|
466
|
+
stateManager: StateManager
|
|
467
|
+
orchestrator: Orchestrator
|
|
468
|
+
clock: SimulationClock
|
|
469
|
+
metrics: MetricCollector
|
|
470
|
+
},
|
|
471
|
+
config: LiveStreamConfig & WebSocketConfig
|
|
472
|
+
): LiveStreamExtension {
|
|
473
|
+
const transport = new WebSocketTransport({
|
|
474
|
+
port: config.port,
|
|
475
|
+
path: config.path,
|
|
476
|
+
maxConnections: config.maxConnections,
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
return new LiveStreamExtension(
|
|
480
|
+
transport,
|
|
481
|
+
sim.eventBus,
|
|
482
|
+
sim.stateManager,
|
|
483
|
+
sim.orchestrator,
|
|
484
|
+
sim.clock,
|
|
485
|
+
config,
|
|
486
|
+
sim.metrics
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Create an SSE-backed live stream.
|
|
492
|
+
*/
|
|
493
|
+
static createSSE(
|
|
494
|
+
sim: {
|
|
495
|
+
eventBus: EventBus
|
|
496
|
+
stateManager: StateManager
|
|
497
|
+
orchestrator: Orchestrator
|
|
498
|
+
clock: SimulationClock
|
|
499
|
+
metrics: MetricCollector
|
|
500
|
+
},
|
|
501
|
+
config: LiveStreamConfig & SSEConfig
|
|
502
|
+
): LiveStreamExtension {
|
|
503
|
+
const transport = new SSETransport({
|
|
504
|
+
port: config.port,
|
|
505
|
+
path: config.path,
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
return new LiveStreamExtension(
|
|
509
|
+
transport,
|
|
510
|
+
sim.eventBus,
|
|
511
|
+
sim.stateManager,
|
|
512
|
+
sim.orchestrator,
|
|
513
|
+
sim.clock,
|
|
514
|
+
config,
|
|
515
|
+
sim.metrics
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Quick console logger for development.
|
|
521
|
+
*/
|
|
522
|
+
static createConsole(
|
|
523
|
+
sim: {
|
|
524
|
+
eventBus: EventBus
|
|
525
|
+
stateManager: StateManager
|
|
526
|
+
orchestrator: Orchestrator
|
|
527
|
+
clock: SimulationClock
|
|
528
|
+
metrics: MetricCollector
|
|
529
|
+
},
|
|
530
|
+
config: LiveStreamConfig = {}
|
|
531
|
+
): LiveStreamExtension {
|
|
532
|
+
const transport = new ConsoleTransport()
|
|
533
|
+
|
|
534
|
+
return new LiveStreamExtension(
|
|
535
|
+
transport,
|
|
536
|
+
sim.eventBus,
|
|
537
|
+
sim.stateManager,
|
|
538
|
+
sim.orchestrator,
|
|
539
|
+
sim.clock,
|
|
540
|
+
config,
|
|
541
|
+
sim.metrics
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── Usage example ─────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
// import { DefaultSimulation } from '../core/simulation'
|
|
549
|
+
// import { LiveStreamFactory } from './live.ext'
|
|
550
|
+
//
|
|
551
|
+
// const sim = new DefaultSimulation()
|
|
552
|
+
// // ... load scenario, add engines ...
|
|
553
|
+
//
|
|
554
|
+
// const liveStream = LiveStreamFactory.createWebSocket( sim, {
|
|
555
|
+
// port: 8080,
|
|
556
|
+
// path: '/simulation-live',
|
|
557
|
+
// tickSummaryIntervalTicks: 1,
|
|
558
|
+
// streamEvents: true,
|
|
559
|
+
// snapshotIntervalTicks: 100,
|
|
560
|
+
// })
|
|
561
|
+
//
|
|
562
|
+
// // Wire into orchestrator using updateConfig — works post-construction
|
|
563
|
+
// const unsubLive = sim.orchestrator.onAfterTick( liveStream.onTick )
|
|
564
|
+
//
|
|
565
|
+
// // Later, remove one without affecting the other
|
|
566
|
+
// unsubLive()
|
|
567
|
+
//
|
|
568
|
+
// liveStream.start()
|
|
569
|
+
// await sim.run()
|
|
570
|
+
// liveStream.stop()
|