@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,626 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/core/distributed.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Distributed simulation support for multi-node execution.
|
|
7
|
+
* Supports partitioning by entity type, geographic region, or custom sharding.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { StateManager } from '#core/state.manager'
|
|
11
|
+
import type { SimulationState, SimulationContext, SimulationEntity, Tick, Timestamp, SimulationEvent, RestoreOptions, StateCommands } from '#core/types'
|
|
12
|
+
import { createContext } from '#core/utils'
|
|
13
|
+
import { wallClock } from '#core/wall.clock'
|
|
14
|
+
|
|
15
|
+
export type ShardStrategy = 'by-entity-type' | 'by-entity-id-hash' | 'by-region' | 'custom'
|
|
16
|
+
export type ConsistencyLevel = 'strong' | 'eventual' | 'causal'
|
|
17
|
+
export type DeliveryGuarantee = 'at-most-once' | 'at-least-once' | 'exactly-once'
|
|
18
|
+
|
|
19
|
+
export interface ShardConfig {
|
|
20
|
+
index: number
|
|
21
|
+
total: number
|
|
22
|
+
strategy: ShardStrategy
|
|
23
|
+
entityTypes?: string[] // for by-entity-type strategy
|
|
24
|
+
regionBounds?: { minX: number; maxX: number; minY: number; maxY: number } // for by-region
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DistributedNodeConfig {
|
|
28
|
+
nodeId: string
|
|
29
|
+
shard: ShardConfig
|
|
30
|
+
coordinatorUrl?: string
|
|
31
|
+
heartbeatIntervalMs?: number
|
|
32
|
+
syncIntervalMs?: number
|
|
33
|
+
consistencyLevel?: ConsistencyLevel
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ShardAssignment {
|
|
37
|
+
nodeId: string
|
|
38
|
+
shard: ShardConfig
|
|
39
|
+
status: 'active' | 'standby' | 'failed'
|
|
40
|
+
lastHeartbeat: Timestamp
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DistributedEvent<T = unknown> extends SimulationEvent<T, {
|
|
44
|
+
originatingNode: string
|
|
45
|
+
shardIndex: number
|
|
46
|
+
isCrossShard: boolean
|
|
47
|
+
targetShard?: number
|
|
48
|
+
}> {}
|
|
49
|
+
|
|
50
|
+
export interface RemoteNode {
|
|
51
|
+
nodeId: string
|
|
52
|
+
shard: ShardConfig
|
|
53
|
+
send( event: SimulationEvent, context: SimulationContext ): Promise<void>
|
|
54
|
+
query( request: CrossShardQuery ): Promise<CrossShardResult>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CrossShardQuery {
|
|
58
|
+
type: 'get-entity' | 'get-entities-by-type' | 'get-metric'
|
|
59
|
+
entityId?: string
|
|
60
|
+
entityType?: string
|
|
61
|
+
metricKey?: string
|
|
62
|
+
shardTarget?: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CrossShardResult {
|
|
66
|
+
success: boolean
|
|
67
|
+
data: unknown
|
|
68
|
+
fromNode: string
|
|
69
|
+
timestamp: Timestamp
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PartitionRouter {
|
|
73
|
+
getShardForEntity( entity: SimulationEntity, totalShards: number ): number
|
|
74
|
+
getShardForMetric( metricKey: string, totalShards: number ): number
|
|
75
|
+
isLocalEntity( entityId: string, shardConfig: ShardConfig ): boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class ConsistentHashRouter implements PartitionRouter {
|
|
79
|
+
private _virtualNodes: number
|
|
80
|
+
|
|
81
|
+
constructor( virtualNodes: number = 150 ){
|
|
82
|
+
this._virtualNodes = virtualNodes
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getShardForEntity( entity: SimulationEntity, totalShards: number ): number {
|
|
86
|
+
const key = `${entity.type}:${entity.id}`
|
|
87
|
+
return this._hashToShard( key, totalShards )
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getShardForMetric( metricKey: string, totalShards: number ): number {
|
|
91
|
+
return this._hashToShard(`metric:${metricKey}`, totalShards )
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
isLocalEntity( entityId: string, shardConfig: ShardConfig ): boolean {
|
|
95
|
+
const
|
|
96
|
+
hash = this._hash( entityId ),
|
|
97
|
+
shardIndex = hash % shardConfig.total
|
|
98
|
+
|
|
99
|
+
return shardIndex === shardConfig.index
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private _hashToShard( key: string, totalShards: number ): number {
|
|
103
|
+
return this._hash( key ) % totalShards
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Use FNV-1a hash for better distribution.
|
|
108
|
+
* Replaces the weak djb2 variant.
|
|
109
|
+
*/
|
|
110
|
+
private _hash( key: string ): number {
|
|
111
|
+
let hash = 2166136261 // FNV offset basis
|
|
112
|
+
for( let i = 0; i < key.length; i++ ) {
|
|
113
|
+
hash ^= key.charCodeAt( i )
|
|
114
|
+
hash = Math.imul( hash, 16777619 ) // FNV prime
|
|
115
|
+
}
|
|
116
|
+
return hash >>> 0
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class DefaultPartitionRouter implements PartitionRouter {
|
|
121
|
+
private _strategy: ShardStrategy
|
|
122
|
+
private _regionSize: number
|
|
123
|
+
|
|
124
|
+
constructor( strategy: ShardStrategy = 'by-entity-id-hash', regionSize: number = 100 ){
|
|
125
|
+
this._strategy = strategy
|
|
126
|
+
this._regionSize = regionSize
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getShardForEntity( entity: SimulationEntity, totalShards: number ): number {
|
|
130
|
+
switch( this._strategy ){
|
|
131
|
+
case 'by-entity-type': return this._hash( entity.type ) % totalShards
|
|
132
|
+
case 'by-entity-id-hash': return this._hash( entity.id ) % totalShards
|
|
133
|
+
case 'by-region': {
|
|
134
|
+
const coords = entity.metadata?.position as { x: number; y: number } | undefined
|
|
135
|
+
if( !coords ) return 0
|
|
136
|
+
|
|
137
|
+
const
|
|
138
|
+
regionX = Math.floor( coords.x / this._regionSize ),
|
|
139
|
+
regionY = Math.floor( coords.y / this._regionSize )
|
|
140
|
+
|
|
141
|
+
return ( regionX * 31 + regionY ) % totalShards
|
|
142
|
+
}
|
|
143
|
+
default: return this._hash( entity.id ) % totalShards
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getShardForMetric( metricKey: string, totalShards: number ): number {
|
|
148
|
+
return this._hash( metricKey ) % totalShards
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
isLocalEntity( entityId: string, shardConfig: ShardConfig ): boolean {
|
|
152
|
+
const
|
|
153
|
+
hash = this._hash( entityId ),
|
|
154
|
+
assignedShard = hash % shardConfig.total
|
|
155
|
+
|
|
156
|
+
return assignedShard === shardConfig.index
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private _hash( key: string ): number {
|
|
160
|
+
let hash = 5381
|
|
161
|
+
for( let i = 0; i < key.length; i++ )
|
|
162
|
+
hash = ( ( hash << 5 ) + hash ) + key.charCodeAt( i )
|
|
163
|
+
|
|
164
|
+
return Math.abs( hash )
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Transport interface with timeout, retry, circuit breaker
|
|
170
|
+
* semantics, and delivery guarantees.
|
|
171
|
+
*/
|
|
172
|
+
export interface CrossShardTransport {
|
|
173
|
+
send( targetNode: string, event: DistributedEvent, context: SimulationContext ): Promise<void>
|
|
174
|
+
broadcast( event: DistributedEvent, context: SimulationContext ): Promise<void>
|
|
175
|
+
query( targetNode: string, query: CrossShardQuery, timeoutMs?: number ): Promise<CrossShardResult>
|
|
176
|
+
registerHandler( handler: CrossShardHandler ): void
|
|
177
|
+
|
|
178
|
+
/** Configure delivery guarantee for subsequent sends. */
|
|
179
|
+
setDeliveryGuarantee( guarantee: DeliveryGuarantee ): void
|
|
180
|
+
|
|
181
|
+
/** Configure retry policy. */
|
|
182
|
+
setRetryPolicy( maxRetries: number, backoffMs: number ): void
|
|
183
|
+
|
|
184
|
+
/** Returns circuit breaker status for a node. */
|
|
185
|
+
isCircuitOpen( nodeId: string ): boolean
|
|
186
|
+
|
|
187
|
+
/** Reset circuit breaker for a node. */
|
|
188
|
+
resetCircuit( nodeId: string ): void
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type CrossShardHandler = ( event: DistributedEvent, context: SimulationContext ) => Promise<void>
|
|
192
|
+
|
|
193
|
+
export class LocalTransport implements CrossShardTransport {
|
|
194
|
+
private _nodes: Map<string, LocalTransportNode> = new Map()
|
|
195
|
+
private _handlers: Set<CrossShardHandler> = new Set()
|
|
196
|
+
private _deliveryGuarantee: DeliveryGuarantee = 'at-least-once'
|
|
197
|
+
private _maxRetries = 3
|
|
198
|
+
private _backoffMs = 100
|
|
199
|
+
private _circuitBreakers: Map<string, { failures: number; open: boolean; lastFailure: number }> = new Map()
|
|
200
|
+
|
|
201
|
+
registerNode( nodeId: string, transport: LocalTransportNode ): void {
|
|
202
|
+
this._nodes.set( nodeId, transport )
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async send( targetNode: string, event: DistributedEvent, context: SimulationContext ): Promise<void> {
|
|
206
|
+
const node = this._nodes.get( targetNode )
|
|
207
|
+
if( !node )
|
|
208
|
+
throw new Error(`Node not found: ${targetNode}`)
|
|
209
|
+
|
|
210
|
+
// Circuit breaker check
|
|
211
|
+
if( this.isCircuitOpen( targetNode ) )
|
|
212
|
+
throw new Error(`Circuit breaker open for node: ${targetNode}`)
|
|
213
|
+
|
|
214
|
+
// Retry logic
|
|
215
|
+
let lastError: Error | null = null
|
|
216
|
+
for( let attempt = 0; attempt <= this._maxRetries; attempt++ ){
|
|
217
|
+
try {
|
|
218
|
+
await node.receive( event, context )
|
|
219
|
+
this._recordSuccess( targetNode )
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
catch( error ){
|
|
223
|
+
lastError = error as Error
|
|
224
|
+
this._recordFailure( targetNode )
|
|
225
|
+
if( attempt < this._maxRetries )
|
|
226
|
+
await this._delay( this._backoffMs * Math.pow( 2, attempt ) )
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if( this._deliveryGuarantee === 'at-least-once')
|
|
231
|
+
throw lastError ?? new Error(`Failed to send to ${targetNode} after ${this._maxRetries} retries`)
|
|
232
|
+
|
|
233
|
+
// at-most-once: silently drop after retries exhausted
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async broadcast( event: DistributedEvent, context: SimulationContext ): Promise<void> {
|
|
237
|
+
await Promise.all( Array.from( this._nodes.values() ).map( node => node.receive( event, context ) ) )
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async query( targetNode: string, query: CrossShardQuery, timeoutMs: number = 5000 ): Promise<CrossShardResult> {
|
|
241
|
+
const node = this._nodes.get( targetNode )
|
|
242
|
+
if( !node )
|
|
243
|
+
return { success: false, data: null, fromNode: targetNode, timestamp: wallClock() }
|
|
244
|
+
|
|
245
|
+
// Timeout support
|
|
246
|
+
const result = await Promise.race([
|
|
247
|
+
node.handleQuery( query ),
|
|
248
|
+
new Promise<CrossShardResult>( ( _, reject ) =>
|
|
249
|
+
setTimeout( () => reject( new Error(`Query timeout to ${targetNode}`) ), timeoutMs )
|
|
250
|
+
)
|
|
251
|
+
])
|
|
252
|
+
|
|
253
|
+
return result as CrossShardResult
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
registerHandler( handler: CrossShardHandler ): void {
|
|
257
|
+
this._handlers.add( handler )
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Configuration setters ──────────────────────────────
|
|
261
|
+
|
|
262
|
+
setDeliveryGuarantee( guarantee: DeliveryGuarantee ): void {
|
|
263
|
+
this._deliveryGuarantee = guarantee
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setRetryPolicy( maxRetries: number, backoffMs: number ): void {
|
|
267
|
+
this._maxRetries = maxRetries
|
|
268
|
+
this._backoffMs = backoffMs
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
isCircuitOpen( nodeId: string ): boolean {
|
|
272
|
+
return this._circuitBreakers.get( nodeId )?.open ?? false
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
resetCircuit( nodeId: string ): void {
|
|
276
|
+
this._circuitBreakers.set( nodeId, { failures: 0, open: false, lastFailure: 0 } )
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private _recordSuccess( nodeId: string ): void {
|
|
280
|
+
this._circuitBreakers.set( nodeId, { failures: 0, open: false, lastFailure: 0 } )
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private _recordFailure( nodeId: string ): void {
|
|
284
|
+
const current = this._circuitBreakers.get( nodeId ) ?? { failures: 0, open: false, lastFailure: 0 }
|
|
285
|
+
current.failures++
|
|
286
|
+
current.lastFailure = wallClock()
|
|
287
|
+
|
|
288
|
+
// Open circuit after 5 consecutive failures
|
|
289
|
+
if( current.failures >= 5 )
|
|
290
|
+
current.open = true
|
|
291
|
+
|
|
292
|
+
this._circuitBreakers.set( nodeId, current )
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async _delay( ms: number ): Promise<void> {
|
|
296
|
+
return new Promise( resolve => setTimeout( resolve, ms ) )
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async _dispatch( event: DistributedEvent, context: SimulationContext ): Promise<void> {
|
|
300
|
+
for( const handler of this._handlers )
|
|
301
|
+
await handler( event, context )
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface LocalTransportNode {
|
|
306
|
+
receive( event: DistributedEvent, context: SimulationContext ): Promise<void>
|
|
307
|
+
handleQuery( query: CrossShardQuery ): Promise<CrossShardResult>
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export class DistributedStateManager implements StateManager, LocalTransportNode {
|
|
311
|
+
private _localState: StateManager
|
|
312
|
+
private _router: PartitionRouter
|
|
313
|
+
private _shardConfig: ShardConfig
|
|
314
|
+
private _transport: LocalTransport
|
|
315
|
+
private _nodeId: string
|
|
316
|
+
private _pendingCrossShardEvents: DistributedEvent[] = []
|
|
317
|
+
private _syncInterval: NodeJS.Timeout | null = null
|
|
318
|
+
|
|
319
|
+
constructor(
|
|
320
|
+
localState: StateManager,
|
|
321
|
+
router: PartitionRouter,
|
|
322
|
+
shardConfig: ShardConfig,
|
|
323
|
+
transport: LocalTransport,
|
|
324
|
+
nodeId: string,
|
|
325
|
+
syncIntervalMs: number = 1000
|
|
326
|
+
){
|
|
327
|
+
this._localState = localState
|
|
328
|
+
this._router = router
|
|
329
|
+
this._shardConfig = shardConfig
|
|
330
|
+
this._transport = transport
|
|
331
|
+
this._nodeId = nodeId
|
|
332
|
+
|
|
333
|
+
if( syncIntervalMs > 0 )
|
|
334
|
+
this._syncInterval = setInterval( () => this._sync(), syncIntervalMs )
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
get currentTick(): Tick { return this._localState.currentTick }
|
|
338
|
+
get currentTime(): Timestamp { return this._localState.currentTime }
|
|
339
|
+
|
|
340
|
+
updateClock( tick: Tick, time: Timestamp ): void {
|
|
341
|
+
this._localState.updateClock( tick, time )
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
getEntity<T extends SimulationEntity>( id: string ): T | undefined {
|
|
345
|
+
if( this._router.isLocalEntity( id, this._shardConfig ) )
|
|
346
|
+
return this._localState.getEntity<T>( id )
|
|
347
|
+
|
|
348
|
+
return undefined
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Async cross-shard entity access.
|
|
353
|
+
* Returns undefined for entities not found on the target shard.
|
|
354
|
+
*/
|
|
355
|
+
async getEntityAsync<T extends SimulationEntity>( id: string ): Promise<T | undefined> {
|
|
356
|
+
if( this._router.isLocalEntity( id, this._shardConfig ) )
|
|
357
|
+
return this._localState.getEntity<T>( id )
|
|
358
|
+
|
|
359
|
+
// Route to the appropriate shard
|
|
360
|
+
const dummyEntity: SimulationEntity = { id, type: '', createdAt: 0, updatedAt: 0 }
|
|
361
|
+
const targetShard = this._router.getShardForEntity( dummyEntity, this._shardConfig.total )
|
|
362
|
+
const targetNode = `node-${targetShard}`
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const result = await this._transport.query( targetNode, { type: 'get-entity', entityId: id })
|
|
366
|
+
return result.success ? ( result.data as T ) : undefined
|
|
367
|
+
}
|
|
368
|
+
catch { return undefined }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
setEntity( entity: SimulationEntity ): void {
|
|
372
|
+
const targetShard = this._router.getShardForEntity( entity, this._shardConfig.total )
|
|
373
|
+
|
|
374
|
+
if( targetShard === this._shardConfig.index )
|
|
375
|
+
this._localState.setEntity( entity )
|
|
376
|
+
|
|
377
|
+
else this._pendingCrossShardEvents.push({
|
|
378
|
+
id: crypto.randomUUID(), // determinism-ok: cross-shard coordination id at the cross-node boundary (already wallClock-stamped)
|
|
379
|
+
type: 'shard.entity.set',
|
|
380
|
+
timestamp: wallClock(),
|
|
381
|
+
tick: this.currentTick,
|
|
382
|
+
source: this._nodeId,
|
|
383
|
+
payload: entity,
|
|
384
|
+
metadata: {
|
|
385
|
+
originatingNode: this._nodeId,
|
|
386
|
+
shardIndex: this._shardConfig.index,
|
|
387
|
+
isCrossShard: true,
|
|
388
|
+
targetShard
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
deleteEntity( id: string ): boolean {
|
|
394
|
+
if( this._router.isLocalEntity( id, this._shardConfig ) )
|
|
395
|
+
return this._localState.deleteEntity( id )
|
|
396
|
+
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
getAllEntities(): IterableIterator<SimulationEntity> {
|
|
401
|
+
return this._localState.getAllEntities()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
getEntitiesByType( type: string ): SimulationEntity[] {
|
|
405
|
+
return this._localState.getEntitiesByType( type )
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
getMetric( key: string ): number | undefined {
|
|
409
|
+
const targetShard = this._router.getShardForMetric( key, this._shardConfig.total )
|
|
410
|
+
if( targetShard === this._shardConfig.index )
|
|
411
|
+
return this._localState.getMetric( key )
|
|
412
|
+
|
|
413
|
+
return undefined
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
setMetric( key: string, value: number ): void {
|
|
417
|
+
const targetShard = this._router.getShardForMetric( key, this._shardConfig.total )
|
|
418
|
+
|
|
419
|
+
if( targetShard === this._shardConfig.index )
|
|
420
|
+
this._localState.setMetric( key, value )
|
|
421
|
+
|
|
422
|
+
else this._pendingCrossShardEvents.push({
|
|
423
|
+
id: crypto.randomUUID(), // determinism-ok: cross-shard coordination id at the cross-node boundary (already wallClock-stamped)
|
|
424
|
+
type: 'shard.metric.set',
|
|
425
|
+
timestamp: wallClock(),
|
|
426
|
+
tick: this.currentTick,
|
|
427
|
+
source: this._nodeId,
|
|
428
|
+
payload: { key, value },
|
|
429
|
+
metadata: {
|
|
430
|
+
originatingNode: this._nodeId,
|
|
431
|
+
shardIndex: this._shardConfig.index,
|
|
432
|
+
isCrossShard: true,
|
|
433
|
+
targetShard
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
incrementMetric( key: string, delta: number = 1 ): number {
|
|
439
|
+
const targetShard = this._router.getShardForMetric( key, this._shardConfig.total )
|
|
440
|
+
|
|
441
|
+
if( targetShard === this._shardConfig.index )
|
|
442
|
+
return this._localState.incrementMetric( key, delta )
|
|
443
|
+
|
|
444
|
+
this._pendingCrossShardEvents.push({
|
|
445
|
+
id: crypto.randomUUID(), // determinism-ok: cross-shard coordination id at the cross-node boundary (already wallClock-stamped)
|
|
446
|
+
type: 'shard.metric.increment',
|
|
447
|
+
timestamp: wallClock(),
|
|
448
|
+
tick: this.currentTick,
|
|
449
|
+
source: this._nodeId,
|
|
450
|
+
payload: { key, delta },
|
|
451
|
+
metadata: {
|
|
452
|
+
originatingNode: this._nodeId,
|
|
453
|
+
shardIndex: this._shardConfig.index,
|
|
454
|
+
isCrossShard: true,
|
|
455
|
+
targetShard
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
const current = this._localState.getMetric( key ) ?? 0
|
|
460
|
+
return current + delta
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
snapshot(): SimulationState {
|
|
464
|
+
return this._localState.snapshot()
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
restore( snapshot: SimulationState, options?: RestoreOptions ): void {
|
|
468
|
+
this._localState.restore( snapshot, options )
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
applyCommands( commands: StateCommands ): void {
|
|
472
|
+
this._localState.applyCommands( commands )
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
clear(): void {
|
|
476
|
+
this._localState.clear()
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async receive( event: DistributedEvent, _context: SimulationContext ): Promise<void> {
|
|
480
|
+
switch( event.type ) {
|
|
481
|
+
case 'shard.entity.set':
|
|
482
|
+
this._localState.setEntity( event.payload as SimulationEntity )
|
|
483
|
+
break
|
|
484
|
+
|
|
485
|
+
case 'shard.metric.set':
|
|
486
|
+
const { key, value } = event.payload as { key: string; value: number }
|
|
487
|
+
this._localState.setMetric( key, value )
|
|
488
|
+
break
|
|
489
|
+
|
|
490
|
+
case 'shard.metric.increment':
|
|
491
|
+
const { key: incKey, delta } = event.payload as { key: string; delta: number }
|
|
492
|
+
this._localState.incrementMetric( incKey, delta )
|
|
493
|
+
break
|
|
494
|
+
|
|
495
|
+
default:
|
|
496
|
+
break
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async handleQuery( query: CrossShardQuery ): Promise<CrossShardResult> {
|
|
501
|
+
switch( query.type ){
|
|
502
|
+
case 'get-entity':
|
|
503
|
+
if( !query.entityId )
|
|
504
|
+
return { success: false, data: null, fromNode: this._nodeId, timestamp: wallClock() }
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
success: true,
|
|
508
|
+
data: this._localState.getEntity( query.entityId ),
|
|
509
|
+
fromNode: this._nodeId,
|
|
510
|
+
timestamp: wallClock()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
case 'get-entities-by-type':
|
|
514
|
+
if( !query.entityType )
|
|
515
|
+
return { success: false, data: null, fromNode: this._nodeId, timestamp: wallClock() }
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
success: true,
|
|
519
|
+
data: this._localState.getEntitiesByType( query.entityType ),
|
|
520
|
+
fromNode: this._nodeId,
|
|
521
|
+
timestamp: wallClock()
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
case 'get-metric':
|
|
525
|
+
if( !query.metricKey )
|
|
526
|
+
return { success: false, data: null, fromNode: this._nodeId, timestamp: wallClock() }
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
success: true,
|
|
530
|
+
data: this._localState.getMetric( query.metricKey ),
|
|
531
|
+
fromNode: this._nodeId,
|
|
532
|
+
timestamp: wallClock()
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
default:
|
|
536
|
+
return { success: false, data: null, fromNode: this._nodeId, timestamp: wallClock() }
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
flushPendingEvents(): DistributedEvent[] {
|
|
541
|
+
return this._pendingCrossShardEvents.splice( 0 )
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private async _sync(): Promise<void> {
|
|
545
|
+
const events = this.flushPendingEvents()
|
|
546
|
+
for( const event of events ) {
|
|
547
|
+
const targetShard = event.metadata?.targetShard
|
|
548
|
+
|
|
549
|
+
if( targetShard !== undefined && targetShard !== this._shardConfig.index ){
|
|
550
|
+
const targetNode = `node-${targetShard}`
|
|
551
|
+
await this._transport.send( targetNode, event, createContext('distributed', this._nodeId ) )
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
destroy(): void {
|
|
557
|
+
this._syncInterval && clearInterval( this._syncInterval )
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export class DistributedOrchestrator {
|
|
562
|
+
private _nodes: Map<string, DistributedNode> = new Map()
|
|
563
|
+
private _router: PartitionRouter
|
|
564
|
+
private _transport: LocalTransport
|
|
565
|
+
private _coordinatorId: string | null = null
|
|
566
|
+
|
|
567
|
+
constructor( router: PartitionRouter = new DefaultPartitionRouter() ){
|
|
568
|
+
this._router = router
|
|
569
|
+
this._transport = new LocalTransport()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
addNode( config: DistributedNodeConfig, localState: StateManager ): DistributedNode {
|
|
573
|
+
const distributedState = new DistributedStateManager(
|
|
574
|
+
localState,
|
|
575
|
+
this._router,
|
|
576
|
+
config.shard,
|
|
577
|
+
this._transport,
|
|
578
|
+
config.nodeId,
|
|
579
|
+
config.syncIntervalMs
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
const node: DistributedNode = {
|
|
583
|
+
nodeId: config.nodeId,
|
|
584
|
+
shard: config.shard,
|
|
585
|
+
stateManager: distributedState,
|
|
586
|
+
transport: this._transport,
|
|
587
|
+
status: 'active',
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
this._nodes.set( config.nodeId, node )
|
|
591
|
+
this._transport.registerNode( config.nodeId, distributedState )
|
|
592
|
+
|
|
593
|
+
if( config.coordinatorUrl && !this._coordinatorId )
|
|
594
|
+
this._coordinatorId = config.nodeId
|
|
595
|
+
|
|
596
|
+
return node
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
getNode( nodeId: string ): DistributedNode | undefined {
|
|
600
|
+
return this._nodes.get( nodeId )
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
getAllNodes(): DistributedNode[] {
|
|
604
|
+
return Array.from( this._nodes.values() )
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async broadcast( event: DistributedEvent, context: SimulationContext ): Promise<void> {
|
|
608
|
+
await this._transport.broadcast( event, context )
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async sendToShard( shardIndex: number, event: DistributedEvent, context: SimulationContext ): Promise<void> {
|
|
612
|
+
const node = Array.from( this._nodes.values() ).find( n => n.shard.index === shardIndex )
|
|
613
|
+
if( !node )
|
|
614
|
+
throw new Error(`No node found for shard ${shardIndex}`)
|
|
615
|
+
|
|
616
|
+
await this._transport.send( node.nodeId, event, context )
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export interface DistributedNode {
|
|
621
|
+
nodeId: string
|
|
622
|
+
shard: ShardConfig
|
|
623
|
+
stateManager: DistributedStateManager
|
|
624
|
+
transport: CrossShardTransport
|
|
625
|
+
status: 'active' | 'standby' | 'failed'
|
|
626
|
+
}
|