@arcforge/cognet 2.0.111 → 2.0.113
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/package.json +3 -3
- package/src/host.ts +57 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcforge/cognet",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.113",
|
|
4
4
|
"description": "A cognitive engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"deploy": "echo \"This package is published ONLY by apps/tui/scripts/release.ts, which pins workspace:* deps to concrete versions first. Publishing it directly ships an unresolvable dependency.\" && exit 1"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@arcforge/err": "2.0.
|
|
22
|
-
"@arcforge/types": "2.0.
|
|
21
|
+
"@arcforge/err": "2.0.113",
|
|
22
|
+
"@arcforge/types": "2.0.113",
|
|
23
23
|
"hookable": "^5.5.3"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
package/src/host.ts
CHANGED
|
@@ -47,7 +47,6 @@ type LoopBody = (ctx: LoopCtx) => Promise<void>
|
|
|
47
47
|
|
|
48
48
|
let registered: LoopBody | null = null
|
|
49
49
|
let boundKernel: KernelAbi | null = null
|
|
50
|
-
let currentClock: ReturnType<typeof Clock> | null = null
|
|
51
50
|
let loaded = false
|
|
52
51
|
|
|
53
52
|
type CognetAmbientScope = {
|
|
@@ -55,6 +54,20 @@ type CognetAmbientScope = {
|
|
|
55
54
|
loop(body: LoopBody): void
|
|
56
55
|
phase<T>(name: string, fn: () => Promise<T>): Promise<T>
|
|
57
56
|
system<T>(name: string, fn: () => Promise<T>): Promise<T>
|
|
57
|
+
/**
|
|
58
|
+
* The clock of the wake this async context belongs to.
|
|
59
|
+
*
|
|
60
|
+
* Carried in the scope rather than a module-level `currentClock`, because
|
|
61
|
+
* that single mutable assumed one wake existed at a time. A continuous
|
|
62
|
+
* cognet ticks whether or not the previous wake finished, so two bodies
|
|
63
|
+
* overlap routinely: the second assigned `currentClock`, the first's
|
|
64
|
+
* phase() then timed against the wrong clock, and whichever finished
|
|
65
|
+
* first set it null — leaving the other to throw mid-flight. Every
|
|
66
|
+
* symptom silent, and all of it invisible until a wake outlived a tick.
|
|
67
|
+
*
|
|
68
|
+
* Null outside a wake (load, shutdown), where phase() is illegal anyway.
|
|
69
|
+
*/
|
|
70
|
+
clock: ReturnType<typeof Clock> | null
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
// Every compiled cognet carries an inlined copy of this module, but all of
|
|
@@ -84,8 +97,9 @@ function kernelOrThrow(): KernelAbi {
|
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
function clockOrThrow(): ReturnType<typeof Clock> {
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
const clock = ambientStorage.getStore()?.clock
|
|
101
|
+
if (!clock) throw err("COGNET_ACCESSED_BEFORE_LOAD", { detail: "phase()/system() are wake-scoped — call them inside the loop body" })
|
|
102
|
+
return clock
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
// ── ambient globals ──────────────────────────────────────────────────────────
|
|
@@ -136,15 +150,35 @@ const localKernel = {
|
|
|
136
150
|
get: (key: never) => kernelOrThrow().store.get(key),
|
|
137
151
|
set: (key: never, value: never) => kernelOrThrow().store.set(key, value),
|
|
138
152
|
},
|
|
153
|
+
wake: () => kernelOrThrow().wake(),
|
|
154
|
+
clock: () => kernelOrThrow().clock(),
|
|
155
|
+
// A getter, not a captured value: the bound kernel arrives at load(), and
|
|
156
|
+
// capturing this at module scope would freeze an empty map from before
|
|
157
|
+
// the brain was given anything.
|
|
158
|
+
get models() { return kernelOrThrow().models },
|
|
139
159
|
} satisfies KernelAbi
|
|
140
160
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
/**
|
|
162
|
+
* The ambient scope for one execution. Everything in it is shared except the
|
|
163
|
+
* clock, which belongs to the wake that created it — see CognetAmbientScope.
|
|
164
|
+
*
|
|
165
|
+
* A function rather than a singleton: concurrent wakes each need their own
|
|
166
|
+
* store, and `ambientStorage.run(sharedObject, ...)` would have given them
|
|
167
|
+
* one object to fight over.
|
|
168
|
+
*/
|
|
169
|
+
function scopeFor(clock: ReturnType<typeof Clock> | null): CognetAmbientScope {
|
|
170
|
+
return {
|
|
171
|
+
kernel: localKernel,
|
|
172
|
+
loop: registerLoop,
|
|
173
|
+
phase: <T>(name: string, fn: () => Promise<T>) => clockOrThrow().runPhase(name, fn),
|
|
174
|
+
system: <T>(name: string, fn: () => Promise<T>) => clockOrThrow().runSystem(name, fn),
|
|
175
|
+
clock,
|
|
176
|
+
}
|
|
146
177
|
}
|
|
147
178
|
|
|
179
|
+
/** Load, boot and shutdown run outside any wake, so they carry no clock. */
|
|
180
|
+
const localScope: CognetAmbientScope = scopeFor(null)
|
|
181
|
+
|
|
148
182
|
// Stable process-wide facades. They never capture one cognet instance; every
|
|
149
183
|
// operation resolves the current async scope at the moment it is performed.
|
|
150
184
|
globals.loop = (body: LoopBody): void => ambientOrThrow().loop(body)
|
|
@@ -160,6 +194,13 @@ globals.kernel = {
|
|
|
160
194
|
get: (key: never) => ambientOrThrow().kernel.store.get(key),
|
|
161
195
|
set: (key: never, value: never) => ambientOrThrow().kernel.store.set(key, value),
|
|
162
196
|
},
|
|
197
|
+
// Resolved through kernelOrThrow(), not ambientOrThrow(): a plugin's
|
|
198
|
+
// clock lives in a setInterval registered during boot, and that callback
|
|
199
|
+
// fires outside every ambient scope. The bound kernel is module state
|
|
200
|
+
// that outlives each wake, which is exactly what a driver needs.
|
|
201
|
+
wake: () => kernelOrThrow().wake(),
|
|
202
|
+
clock: () => kernelOrThrow().clock(),
|
|
203
|
+
get models() { return kernelOrThrow().models },
|
|
163
204
|
} satisfies KernelAbi
|
|
164
205
|
|
|
165
206
|
globals.phase = <T>(name: string, fn: () => Promise<T>) => ambientOrThrow().phase(name, fn)
|
|
@@ -202,16 +243,20 @@ export function CognetHost(config: CognetConfig, main: () => Promise<void>): Cog
|
|
|
202
243
|
},
|
|
203
244
|
|
|
204
245
|
async wake(wake) {
|
|
205
|
-
return ambientStorage.run(localScope, async () => {
|
|
206
246
|
const body = registered
|
|
207
247
|
if (!body) throw err("COGNET_NO_LOOP", { detail: `${config.name} woken before load()`, context: { name: config.name } })
|
|
208
248
|
|
|
249
|
+
// Built BEFORE entering the scope, because the scope carries it.
|
|
250
|
+
// Two overlapping wakes get two clocks and two stores, so each
|
|
251
|
+
// body's phase() resolves its own no matter how the awaits
|
|
252
|
+
// interleave. `emit` is bound through kernelOrThrow(), which
|
|
253
|
+
// reads module state that outlives every wake — safe to close
|
|
254
|
+
// over here.
|
|
209
255
|
const clock = Clock({ emit: (type, data) => kernelOrThrow().emit(type, data), signal: wake.signal })
|
|
210
|
-
currentClock = clock
|
|
211
256
|
|
|
212
|
-
|
|
257
|
+
return ambientStorage.run(scopeFor(clock), async () => {
|
|
258
|
+
await hooks.callHook("wake", wake)
|
|
213
259
|
|
|
214
|
-
try {
|
|
215
260
|
let stopped = false
|
|
216
261
|
const ctx: LoopCtx = { ...wake, stop: () => { stopped = true } }
|
|
217
262
|
const maxTicks = config.maxTicksPerWake ?? Infinity
|
|
@@ -228,9 +273,6 @@ export function CognetHost(config: CognetConfig, main: () => Promise<void>): Cog
|
|
|
228
273
|
await body(ctx)
|
|
229
274
|
})
|
|
230
275
|
}
|
|
231
|
-
} finally {
|
|
232
|
-
currentClock = null
|
|
233
|
-
}
|
|
234
276
|
})
|
|
235
277
|
},
|
|
236
278
|
|