@vibecook/ice 0.4.0 → 0.7.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/README.md +4 -4
- package/dist/{chunk-3PHPWRB2.js → chunk-55QCOE74.js} +7 -4
- package/dist/chunk-55QCOE74.js.map +1 -0
- package/dist/{chunk-ENMRSITH.js → chunk-5OU3OWDH.js} +16 -3
- package/dist/chunk-5OU3OWDH.js.map +1 -0
- package/dist/{chunk-7LZAXQAO.js → chunk-LATXYFMO.js} +6823 -3926
- package/dist/chunk-LATXYFMO.js.map +1 -0
- package/dist/devtools.js +5 -2
- package/dist/devtools.js.map +1 -1
- package/dist/dom.js +2 -2
- package/dist/ground.js +1 -1
- package/dist/index.js +35 -1
- package/dist/r3f.js +3 -3
- package/dist/react.js +5 -3
- package/dist/types/packages/core/src/behavior/compile.d.ts +74 -0
- package/dist/types/packages/core/src/behavior/define-behavior.d.ts +56 -0
- package/dist/types/packages/core/src/behavior/differ.d.ts +91 -0
- package/dist/types/packages/core/src/behavior/harness.d.ts +65 -0
- package/dist/types/packages/core/src/behavior/migrate.d.ts +87 -0
- package/dist/types/packages/core/src/behavior/public-reads.d.ts +31 -0
- package/dist/types/packages/core/src/behavior/runtime.d.ts +102 -0
- package/dist/types/packages/core/src/behavior/types.d.ts +358 -0
- package/dist/types/packages/core/src/behavior/world-view.d.ts +36 -0
- package/dist/types/packages/core/src/engine/engine.d.ts +38 -1
- package/dist/types/packages/core/src/engine/guests.d.ts +199 -0
- package/dist/types/packages/core/src/engine/reflectors.d.ts +8 -0
- package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +26 -0
- package/dist/types/packages/core/src/guards/guarded-tx.d.ts +57 -0
- package/dist/types/packages/core/src/index.d.ts +11 -3
- package/dist/types/packages/core/src/widget/define-widget.d.ts +28 -0
- package/dist/types/packages/core/src/widget/props.d.ts +35 -1
- package/dist/types/packages/core/src/widget/spawn.d.ts +14 -0
- package/dist/types/packages/dom/src/loop.d.ts +9 -0
- package/dist/types/packages/react/src/hooks.d.ts +12 -1
- package/dist/types/packages/react/src/index.d.ts +1 -1
- package/package.json +31 -65
- package/dist/chunk-3PHPWRB2.js.map +0 -1
- package/dist/chunk-7LZAXQAO.js.map +0 -1
- package/dist/chunk-ENMRSITH.js.map +0 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineBehavior` — the type surface (design-009 §3–§4, the pinned API).
|
|
3
|
+
*
|
|
4
|
+
* ICE's userland surface is a triad: `defineWidget` (faces) · **behaviors**
|
|
5
|
+
* (logic + state) · `defineTool` (input policy). A behavior is the A-Frame-class
|
|
6
|
+
* unit — ONE named declaration carrying a data schema, a REQUIRED store class,
|
|
7
|
+
* and lifecycle hooks the engine runs on a curated phase set.
|
|
8
|
+
*
|
|
9
|
+
* `store:` is the load-bearing word: it routes where data lives, who sees it,
|
|
10
|
+
* what survives, WHICH WRITE VOCABULARY the hooks receive, at which cadence.
|
|
11
|
+
* That last routing is the reason the ctx types below are three interfaces and
|
|
12
|
+
* not one with optional members — a durable behavior must not see `ctx.write`
|
|
13
|
+
* in autocomplete at all, or "store routes everything" is a doc claim rather
|
|
14
|
+
* than a property of the product.
|
|
15
|
+
*/
|
|
16
|
+
import type { Component, Entity, Relation, Resource, Tag } from "@vibecook/strata-ecs";
|
|
17
|
+
import type { GuardedTx } from "../guards/guarded-tx";
|
|
18
|
+
import type { BooleanSpec, EntityKeySpec, EnumSpec, JsonShape, JsonSpec, NumberSpec, PropSpec, StringSpec } from "../widget/props";
|
|
19
|
+
export type BehaviorStore = "durable" | "runtime" | "ephemeral";
|
|
20
|
+
/**
|
|
21
|
+
* The CURATED phase set (design-009 §4.4, BF-D8) — a deliberate subset of the
|
|
22
|
+
* eleven pipeline groups. Authors get the four that mean something to logic;
|
|
23
|
+
* the control sweep (`ctl:*`), `input`, `react` and `cleanup` stay engine
|
|
24
|
+
* vocabulary. `publish` is not a pipeline group at all — it is the post-tick
|
|
25
|
+
* slot where ephemeral mutation is legal.
|
|
26
|
+
*/
|
|
27
|
+
export type BehaviorPhase = "simulate" | "derive" | "present" | "publish";
|
|
28
|
+
/** Legal `phase` per store class — the §4.1 validation table, as data. */
|
|
29
|
+
export declare const BEHAVIOR_PHASES: Readonly<Record<BehaviorStore, readonly BehaviorPhase[]>>;
|
|
30
|
+
/** Default phase per store (the one most declarations want). */
|
|
31
|
+
export declare const BEHAVIOR_DEFAULT_PHASE: Readonly<Record<BehaviorStore, BehaviorPhase>>;
|
|
32
|
+
/** A behavior's declared data: the SAME `p.*` DSL `defineWidget` uses. */
|
|
33
|
+
export type BehaviorSchema = Readonly<Record<string, PropSpec>>;
|
|
34
|
+
export type BehaviorHookName = "init" | "update" | "changed" | "tick" | "dispose";
|
|
35
|
+
export type BehaviorPropDescription = {
|
|
36
|
+
readonly kind: "string";
|
|
37
|
+
readonly default?: string;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: "number";
|
|
40
|
+
readonly default?: number;
|
|
41
|
+
readonly min?: number;
|
|
42
|
+
readonly max?: number;
|
|
43
|
+
} | {
|
|
44
|
+
readonly kind: "boolean";
|
|
45
|
+
readonly default?: boolean;
|
|
46
|
+
} | {
|
|
47
|
+
readonly kind: "enum";
|
|
48
|
+
readonly options: readonly string[];
|
|
49
|
+
readonly default?: string;
|
|
50
|
+
} | {
|
|
51
|
+
readonly kind: "json";
|
|
52
|
+
readonly inner: JsonShape;
|
|
53
|
+
readonly default?: string;
|
|
54
|
+
} | {
|
|
55
|
+
readonly kind: "entityKey";
|
|
56
|
+
readonly default?: string;
|
|
57
|
+
};
|
|
58
|
+
export interface BehaviorReadDescription {
|
|
59
|
+
readonly kind: "behavior" | "component" | "tag" | "relation" | "resource";
|
|
60
|
+
readonly name: string;
|
|
61
|
+
}
|
|
62
|
+
export interface BehaviorDescription {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly store: BehaviorStore;
|
|
65
|
+
readonly derived: boolean;
|
|
66
|
+
readonly deriveDuringGesture: boolean;
|
|
67
|
+
readonly version: number;
|
|
68
|
+
readonly phase: BehaviorPhase;
|
|
69
|
+
readonly budgetMs?: number;
|
|
70
|
+
readonly tickWhile: "all" | "visible";
|
|
71
|
+
readonly schema: readonly {
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly spec: BehaviorPropDescription;
|
|
74
|
+
}[];
|
|
75
|
+
readonly reads: readonly BehaviorReadDescription[];
|
|
76
|
+
readonly writes: readonly string[];
|
|
77
|
+
readonly migrationFrom: readonly number[];
|
|
78
|
+
readonly hooks: readonly BehaviorHookName[];
|
|
79
|
+
}
|
|
80
|
+
type ValueOfSpec<S extends PropSpec> = S extends NumberSpec ? number : S extends BooleanSpec ? boolean : S extends EnumSpec ? string : S extends JsonSpec ? unknown : S extends StringSpec | EntityKeySpec ? string : never;
|
|
81
|
+
/**
|
|
82
|
+
* The hook-facing data view. `p.json` fields arrive PARSED (the cell is a
|
|
83
|
+
* string; the snapshot carries the value) — parsing happens once per delivery,
|
|
84
|
+
* and delivery only runs on change, so the cost is keyed to churn rather than
|
|
85
|
+
* to frame rate. `ctx.write`/`tx.write` serialize back.
|
|
86
|
+
*/
|
|
87
|
+
export type DataOf<Sch extends BehaviorSchema> = {
|
|
88
|
+
readonly [K in keyof Sch]: ValueOfSpec<Sch[K]>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Anything a behavior may declare in `reads:` — components, tags, relations,
|
|
92
|
+
* resources, or another behavior (its data component). Each kind is routed to
|
|
93
|
+
* a different change mechanism by the compiler (§5.2), which is why the
|
|
94
|
+
* partition is compiler work and not an author concern.
|
|
95
|
+
*/
|
|
96
|
+
export type BehaviorRead = Component | Tag | Relation | Resource | AnyBehaviorDef;
|
|
97
|
+
/** The drained journal summary, valid only for the duration of the hook call. */
|
|
98
|
+
export interface BehaviorChanges {
|
|
99
|
+
/** Rebuild everything: first run, `world.reset()`, coarse writes, or a poll fired. */
|
|
100
|
+
readonly full: boolean;
|
|
101
|
+
/** Journaled entities — re-read current state to act. */
|
|
102
|
+
readonly changed: readonly Entity[];
|
|
103
|
+
/** DESTROYED entities (packed handles; safe as removal keys). */
|
|
104
|
+
readonly removed: readonly Entity[];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `ctx.world` is a HOST-BUILT WRAPPER, not a `World` alias and not
|
|
108
|
+
* `ReadonlyWorld` (BF-D19): that type is `Omit<World, WorldMutatorName>`, which
|
|
109
|
+
* keeps `.runtime` — the full mutable store — so it is a naming convention, not
|
|
110
|
+
* a sandbox. This interface exposes exactly the read surface and nothing else.
|
|
111
|
+
*
|
|
112
|
+
* Honesty note: point reads through this wrapper are ATTRIBUTION-ONLY — strata
|
|
113
|
+
* enforces declared reads solely at `batch.col()`, so the compiled delivery
|
|
114
|
+
* walk (`ctx.query`) is where enforcement is real.
|
|
115
|
+
*/
|
|
116
|
+
export interface BehaviorWorld {
|
|
117
|
+
read<S>(e: Entity, c: Component<S>): S;
|
|
118
|
+
get<S>(e: Entity, c: Component<S>): S | undefined;
|
|
119
|
+
readField<S, K extends keyof S & string>(e: Entity, c: Component<S>, field: K): S[K] | undefined;
|
|
120
|
+
has(e: Entity, c: Component): boolean;
|
|
121
|
+
hasTag(e: Entity, t: Tag): boolean;
|
|
122
|
+
getRelation(e: Entity, r: Relation): Entity | undefined;
|
|
123
|
+
getRelations(e: Entity, r: Relation): Entity[];
|
|
124
|
+
getReverse(e: Entity, r: Relation): Entity[];
|
|
125
|
+
/** Sibling-sequence version for one parent — ordered relations (petition 8). */
|
|
126
|
+
orderStamp(parent: Entity, r: Relation): number;
|
|
127
|
+
getResource<S>(res: Resource<S>): S | undefined;
|
|
128
|
+
componentsOf(e: Entity): readonly Component[];
|
|
129
|
+
tagsOf(e: Entity): readonly Tag[];
|
|
130
|
+
isAlive(e: Entity): boolean;
|
|
131
|
+
}
|
|
132
|
+
/** A reads-scoped query walk. Terms are restricted to the behavior's `reads`. */
|
|
133
|
+
export interface BehaviorQuery {
|
|
134
|
+
/** Every matching entity. The array is FRESH per call — retaining it is legal. */
|
|
135
|
+
entities(): Entity[];
|
|
136
|
+
/** Iterate matches one at a time (no array allocated). */
|
|
137
|
+
each(fn: (e: Entity) => void): void;
|
|
138
|
+
/** First match, or undefined. */
|
|
139
|
+
first(): Entity | undefined;
|
|
140
|
+
}
|
|
141
|
+
/** Query terms a behavior may build — its declared reads, positively or negatively. */
|
|
142
|
+
export interface BehaviorQuerySpec {
|
|
143
|
+
/** Entities must carry ALL of these (components/tags from `reads`). */
|
|
144
|
+
readonly all?: readonly (Component | Tag | AnyBehaviorDef)[];
|
|
145
|
+
/** Entities must carry NONE of these. */
|
|
146
|
+
readonly none?: readonly (Component | Tag | AnyBehaviorDef)[];
|
|
147
|
+
}
|
|
148
|
+
/** Frame facts a `tick` hook reads. */
|
|
149
|
+
export interface BehaviorFrame {
|
|
150
|
+
readonly dtMs: number;
|
|
151
|
+
readonly tick: number;
|
|
152
|
+
/** Accumulated CLAMPED dt — under-reports across freezes; never wall time. */
|
|
153
|
+
readonly clock: number;
|
|
154
|
+
}
|
|
155
|
+
/** One remote peer's facet of an ephemeral behavior (`ctx.peers()`). */
|
|
156
|
+
export interface PeerFacet<Data> {
|
|
157
|
+
/** The remote presence-peer entity (runtime handle — do not persist). */
|
|
158
|
+
readonly entity: Entity;
|
|
159
|
+
readonly data: Readonly<Data>;
|
|
160
|
+
}
|
|
161
|
+
export interface BehaviorCtxBase<Data> {
|
|
162
|
+
/** The read wrapper (above). */
|
|
163
|
+
readonly world: BehaviorWorld;
|
|
164
|
+
/**
|
|
165
|
+
* A reads-SCOPED query. The framework compiles and caches one Query per
|
|
166
|
+
* distinct spec and walks it ATTRIBUTED — strata's only enforced read path,
|
|
167
|
+
* and its only zero-allocation bulk path. Without this, navigation was
|
|
168
|
+
* instances-outward only and `changed` got abused as a discovery channel.
|
|
169
|
+
*
|
|
170
|
+
* The navigation model, stated: behaviors navigate from their instances
|
|
171
|
+
* outward via relations, the change journal, and reads-scoped queries. There
|
|
172
|
+
* is no global search.
|
|
173
|
+
*/
|
|
174
|
+
query(spec: BehaviorQuerySpec): BehaviorQuery;
|
|
175
|
+
/** This behavior's own instances, this frame (snapshot — do not retain). */
|
|
176
|
+
entities(): readonly Entity[];
|
|
177
|
+
/** The drained journal summary. Meaningful during `changed`; do not retain. */
|
|
178
|
+
changes(): BehaviorChanges;
|
|
179
|
+
/** The journal ∩ this behavior's instances — the exact set. Do not retain. */
|
|
180
|
+
changedEntities(): readonly Entity[];
|
|
181
|
+
/** Durable doc key for an entity (undefined when not doc-bound). */
|
|
182
|
+
keyOf(e: Entity): string | undefined;
|
|
183
|
+
/** The entity for a durable key — undefined while it is not yet projected. */
|
|
184
|
+
entityFor(key: string): Entity | undefined;
|
|
185
|
+
/** Aborts at generation teardown (`world.reset`), detach, or engine dispose. */
|
|
186
|
+
readonly signal: AbortSignal;
|
|
187
|
+
/** Namespaced logging — routed to the host's diagnostics, never raw console. */
|
|
188
|
+
log(message: string, ...rest: readonly unknown[]): void;
|
|
189
|
+
}
|
|
190
|
+
export interface DurableBehaviorCtx<Data> extends BehaviorCtxBase<Data> {
|
|
191
|
+
/**
|
|
192
|
+
* ONE labeled transaction. The transaction opens INSIDE the hook, in-system
|
|
193
|
+
* (design-002-blessed): values land synchronously and this frame's `notify`
|
|
194
|
+
* sees them — never queued past the tick. `label` is a stable MACHINE id
|
|
195
|
+
* (namespaced), never display text: it feeds undo and in-CRDT provenance.
|
|
196
|
+
*
|
|
197
|
+
* Refuses on a read-only document. `tx.setResource` is MASKED (the §9
|
|
198
|
+
* resource-write fence, made mechanical) — a behavior calling it DEV-throws
|
|
199
|
+
* naming the `singleton` v1.1 seam.
|
|
200
|
+
*/
|
|
201
|
+
commit(label: string, fn: (tx: BehaviorTx<Data>) => void, opts?: {
|
|
202
|
+
undoable?: boolean;
|
|
203
|
+
}): void;
|
|
204
|
+
}
|
|
205
|
+
export interface RuntimeBehaviorCtx<Data> extends BehaviorCtxBase<Data> {
|
|
206
|
+
/** Write this behavior's OWN data on `e` (immediate; session-local). */
|
|
207
|
+
write(e: Entity, patch: Partial<Data>): void;
|
|
208
|
+
/** Write a declared runtime-store `writes:` target (immediate). */
|
|
209
|
+
set<S>(e: Entity, c: Component<S>, v: S): void;
|
|
210
|
+
attach(e: Entity, data?: Partial<Data>): void;
|
|
211
|
+
detach(e: Entity): void;
|
|
212
|
+
/** A runtime behavior may also commit durable state (a settle writing finals once). */
|
|
213
|
+
commit(label: string, fn: (tx: BehaviorTx<Data>) => void, opts?: {
|
|
214
|
+
undoable?: boolean;
|
|
215
|
+
}): void;
|
|
216
|
+
}
|
|
217
|
+
export interface EphemeralBehaviorCtx<Data> extends BehaviorCtxBase<Data> {
|
|
218
|
+
/**
|
|
219
|
+
* Publish THIS PEER's facet. No entity argument — the instance IS the local
|
|
220
|
+
* presence peer, and there is exactly one. Legal here because ephemeral hooks
|
|
221
|
+
* run at the publish slot, post-tick, where eph mutation is allowed.
|
|
222
|
+
*/
|
|
223
|
+
write(patch: Partial<Data>): void;
|
|
224
|
+
/** REMOTE peers' facets of this behavior (read-only; `Not(Local)` projections). */
|
|
225
|
+
peers(): readonly PeerFacet<Data>[];
|
|
226
|
+
}
|
|
227
|
+
export type BehaviorCtxFor<S extends BehaviorStore, Data> = S extends "durable" ? DurableBehaviorCtx<Data> : S extends "runtime" ? RuntimeBehaviorCtx<Data> : EphemeralBehaviorCtx<Data>;
|
|
228
|
+
/**
|
|
229
|
+
* The door's transaction: `GuardedTx` plus behavior attachment and typed
|
|
230
|
+
* own-data writes. `setResource` is absent by construction (§4.4's mask).
|
|
231
|
+
*/
|
|
232
|
+
export interface BehaviorTx<Data> extends Omit<GuardedTx, "setResource"> {
|
|
233
|
+
/** Write this behavior's own data on `e` (absolute, whole-value). */
|
|
234
|
+
write(e: Entity, patch: Partial<Data>): void;
|
|
235
|
+
/** Attach a behavior as a DOCUMENT op — syncs, undoes, runs on every peer. */
|
|
236
|
+
attach<D>(e: Entity, behavior: BehaviorHandle<D>, data?: Partial<D>): void;
|
|
237
|
+
detach(e: Entity, behavior: AnyBehaviorDef): void;
|
|
238
|
+
}
|
|
239
|
+
export interface BehaviorHooks<S extends BehaviorStore, Data> {
|
|
240
|
+
/**
|
|
241
|
+
* Instance appeared: attached locally, projected in (REMOTE included), or
|
|
242
|
+
* existing at generation start. Runs before that instance's first `tick`.
|
|
243
|
+
* A throw here marks the INSTANCE failed — no further hooks until re-attach.
|
|
244
|
+
*/
|
|
245
|
+
init?(e: Entity, data: Readonly<Data>, ctx: BehaviorCtxFor<S, Data>): void;
|
|
246
|
+
/**
|
|
247
|
+
* OWN declared data changed since the last delivery — by ANY writer: this
|
|
248
|
+
* behavior's commit, another plugin's tx, UNDO, or a remote peer.
|
|
249
|
+
* Per-instance; `prev` is the last-delivered snapshot. "update is about you."
|
|
250
|
+
*/
|
|
251
|
+
update?(e: Entity, data: Readonly<Data>, prev: Readonly<Data>, ctx: BehaviorCtxFor<S, Data>): void;
|
|
252
|
+
/**
|
|
253
|
+
* The reads-set changed. ONCE PER BEHAVIOR per frame, not per instance —
|
|
254
|
+
* N instances × one tag flip must not mean N journal scans.
|
|
255
|
+
* "changed is about the world."
|
|
256
|
+
*/
|
|
257
|
+
changed?(ctx: BehaviorCtxFor<S, Data>): void;
|
|
258
|
+
/**
|
|
259
|
+
* Per frame per instance — OPT-IN cost. Default is ALL instances (Law 14: an
|
|
260
|
+
* off-screen spring still settles); `tick: { while: "visible" }` is the
|
|
261
|
+
* author's DECLARED suspension. `init`/`update`/`changed`/`dispose` never
|
|
262
|
+
* suspend: culling is not lifecycle, and state must be right the moment it
|
|
263
|
+
* becomes visible again.
|
|
264
|
+
*/
|
|
265
|
+
tick?(e: Entity, data: Readonly<Data>, frame: BehaviorFrame, ctx: BehaviorCtxFor<S, Data>): void;
|
|
266
|
+
/**
|
|
267
|
+
* Instance gone: detached, despawned, disabled, or generation end. `e` may
|
|
268
|
+
* already be dead (world access is DEV-guarded) — this is for cache cleanup.
|
|
269
|
+
* Runs for ALL instances BEFORE `ctx.signal` aborts.
|
|
270
|
+
*/
|
|
271
|
+
dispose?(e: Entity, ctx: BehaviorCtxFor<S, Data>): void;
|
|
272
|
+
}
|
|
273
|
+
export interface BehaviorSpec<S extends BehaviorStore, Sch extends BehaviorSchema> {
|
|
274
|
+
/** REQUIRED — routes everything (§3). */
|
|
275
|
+
readonly store: S;
|
|
276
|
+
/**
|
|
277
|
+
* This behavior's durable writes are COMPUTED from other state. Durable only.
|
|
278
|
+
* Bundles three protections: non-undoable output, the differ, and
|
|
279
|
+
* claim-scoped delivery suppression (§5.3).
|
|
280
|
+
*/
|
|
281
|
+
readonly derived?: boolean;
|
|
282
|
+
/**
|
|
283
|
+
* Opt out of suppression: derive live during gestures (costs accepted).
|
|
284
|
+
* `derived` only.
|
|
285
|
+
*/
|
|
286
|
+
readonly deriveDuringGesture?: boolean;
|
|
287
|
+
/** Durable only — the `engine.behavior.<name>.<v>` marker family (§5.6). */
|
|
288
|
+
readonly version?: number;
|
|
289
|
+
readonly schema?: Sch;
|
|
290
|
+
/** Durable only: fromVersion → idempotent absolute transform. */
|
|
291
|
+
readonly migrate?: Readonly<Record<number, (prev: Record<string, unknown>) => Record<string, unknown>>>;
|
|
292
|
+
/** Defaults per store (§4.4); validated against the legal set. */
|
|
293
|
+
readonly phase?: BehaviorPhase;
|
|
294
|
+
/** Per-frame budget request, ms. The breaker caps it at the seam ceiling. */
|
|
295
|
+
readonly budgetMs?: number;
|
|
296
|
+
readonly reads?: readonly BehaviorRead[];
|
|
297
|
+
readonly writes?: readonly Component[];
|
|
298
|
+
/**
|
|
299
|
+
* `tick` scope. `{ while: "visible" }` compiles a `Not(Culled)` term into the
|
|
300
|
+
* chunked tick system — declared suspension, never discovered.
|
|
301
|
+
*/
|
|
302
|
+
readonly tick?: {
|
|
303
|
+
readonly while?: "visible" | "all";
|
|
304
|
+
};
|
|
305
|
+
readonly on?: BehaviorHooks<S, DataOf<Sch>>;
|
|
306
|
+
}
|
|
307
|
+
/** A behavior pre-attachment: `Layout.with({ gapX: 32 })`. */
|
|
308
|
+
export interface BehaviorAttachSpec<Data = Record<string, unknown>> {
|
|
309
|
+
readonly behavior: BehaviorHandle<Data>;
|
|
310
|
+
readonly data: Partial<Data>;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* The compiled declaration. Usable wherever a behavior is expected —
|
|
314
|
+
* `reads: [Glow]`, `behaviors: [Layout]`, `tx.attach(e, Layout)` — plus
|
|
315
|
+
* `.with(data)` for pre-attachment and `.component` for advanced read paths.
|
|
316
|
+
*/
|
|
317
|
+
export interface BehaviorHandle<Data> {
|
|
318
|
+
/** `<namespace>:<name>` — the process-global identity. */
|
|
319
|
+
readonly name: string;
|
|
320
|
+
readonly namespace: string;
|
|
321
|
+
readonly store: BehaviorStore;
|
|
322
|
+
readonly derived: boolean;
|
|
323
|
+
readonly deriveDuringGesture: boolean;
|
|
324
|
+
readonly version: number;
|
|
325
|
+
readonly phase: BehaviorPhase;
|
|
326
|
+
readonly budgetMs: number | undefined;
|
|
327
|
+
readonly schema: BehaviorSchema;
|
|
328
|
+
/** The generated data component (`behavior:<name>`). Advanced read paths. */
|
|
329
|
+
readonly component: Component<Record<string, string | number | boolean>>;
|
|
330
|
+
/** Every field at its declared default (the attach fill). */
|
|
331
|
+
readonly defaults: Readonly<Data>;
|
|
332
|
+
readonly reads: readonly BehaviorRead[];
|
|
333
|
+
readonly writes: readonly Component[];
|
|
334
|
+
readonly migrate: Readonly<Record<number, (prev: Record<string, unknown>) => Record<string, unknown>>>;
|
|
335
|
+
readonly tickWhile: "all" | "visible";
|
|
336
|
+
/**
|
|
337
|
+
* The hook set. MUTABLE by design: a same-shape re-definition (hot reload,
|
|
338
|
+
* a test re-importing a module) refreshes these in place, so the cached
|
|
339
|
+
* handle keeps its component and collector identity while the new code
|
|
340
|
+
* actually runs. Stale-hook HMR is the wart this avoids.
|
|
341
|
+
*/
|
|
342
|
+
on: BehaviorHooks<BehaviorStore, Data>;
|
|
343
|
+
/** Pre-attachment with data: `behaviors: [Layout.with({ gapX: 32 })]`. */
|
|
344
|
+
with(data: Partial<Data>): BehaviorAttachSpec<Data>;
|
|
345
|
+
/** @internal brand — how `reads:` tells a behavior from a component. */
|
|
346
|
+
readonly __behavior: true;
|
|
347
|
+
}
|
|
348
|
+
/** A behavior handle of unknown data shape (registries, `reads:` lists). */
|
|
349
|
+
export type AnyBehaviorDef = BehaviorHandle<any>;
|
|
350
|
+
/**
|
|
351
|
+
* A pre-attachment of unknown data shape — what a LIST of them has to be typed
|
|
352
|
+
* as. `BehaviorAttachSpec<never>` looks like the shape-agnostic form and is
|
|
353
|
+
* not: `Layout.with({gapX: 32})` produces `BehaviorAttachSpec<{gapX: number}>`,
|
|
354
|
+
* which is not assignable to it, so the documented `with()` form would not
|
|
355
|
+
* typecheck in `defineWidget({behaviors})` at all.
|
|
356
|
+
*/
|
|
357
|
+
export type AnyBehaviorAttachSpec = BehaviorAttachSpec<any>;
|
|
358
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Component, Entity, Query, Relation, Resource, SystemCtx, Tag, World } from "@vibecook/strata-ecs";
|
|
2
|
+
import type { AnyBehaviorDef, BehaviorQuery, BehaviorQuerySpec, BehaviorWorld } from "./types";
|
|
3
|
+
export declare function createBehaviorWorld(world: World): BehaviorWorld;
|
|
4
|
+
/**
|
|
5
|
+
* The compiled-query cache for one behavior. Keyed on the term identity list,
|
|
6
|
+
* so the common case (a behavior calling `ctx.query` with the same spec every
|
|
7
|
+
* frame) compiles ONCE — `defineQuery` identity is strata's own cache key for
|
|
8
|
+
* the matched-archetype list, so re-compiling per call would defeat two caches.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createQueryScope(behavior: AnyBehaviorDef, allowed: ReadonlySet<Component | Tag>): (walker: BehaviorWalker, spec: BehaviorQuerySpec) => BehaviorQuery;
|
|
11
|
+
/**
|
|
12
|
+
* Where a behavior's walks and structural ops go, which depends on WHERE it is
|
|
13
|
+
* running — and the difference is not cosmetic.
|
|
14
|
+
*
|
|
15
|
+
* In a pipeline phase the behavior is inside a system: walks go through
|
|
16
|
+
* `SystemCtx` (attributed — strata's enforced read path), and structural ops
|
|
17
|
+
* are DEFERRED to the phase boundary because immediate `world.*` structural
|
|
18
|
+
* mutation throws at iteration depth > 0.
|
|
19
|
+
*
|
|
20
|
+
* At the publish slot there is no running system and no iteration bracket:
|
|
21
|
+
* walks are plain `world.query` (unattributed — nothing to attribute them to),
|
|
22
|
+
* and structural ops are immediate and legal. Ephemeral behaviors live here,
|
|
23
|
+
* which is exactly why eph structural writes are legal for them and illegal
|
|
24
|
+
* for everyone else.
|
|
25
|
+
*/
|
|
26
|
+
export interface BehaviorWalker {
|
|
27
|
+
each(q: Query, fn: (e: Entity) => void): void;
|
|
28
|
+
first(q: Query): Entity | undefined;
|
|
29
|
+
addComponent(e: Entity, c: Component, v: unknown): void;
|
|
30
|
+
removeComponent(e: Entity, c: Component): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function systemWalker(ctx: SystemCtx): BehaviorWalker;
|
|
33
|
+
export declare function publishWalker(world: World): BehaviorWalker;
|
|
34
|
+
/** The term set a behavior may query: its own component ∪ declared components/tags. */
|
|
35
|
+
export declare function allowedQueryTerms(behavior: AnyBehaviorDef, readComponents: readonly Component[], readTags: readonly Tag[]): ReadonlySet<Component | Tag>;
|
|
36
|
+
export type { Relation, Resource };
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* setFrameInfo FrameInfo resource — value write, pre-tick
|
|
6
6
|
* world.sync() durable + presence drains land; projections place
|
|
7
7
|
* world.tick(...) the phase-group pipeline (pipeline.ts)
|
|
8
|
+
* guests [2026-08-15, petition I14] the NAMED guest slot:
|
|
9
|
+
* plugin/host frame work under a circuit breaker
|
|
10
|
+
* (guests.ts). BEFORE publish so derived state has
|
|
11
|
+
* settled when presence I/O reads it, and before
|
|
12
|
+
* notify so the same frame reflects it. Guests are
|
|
13
|
+
* fault-ISOLATED — the one tier here that is.
|
|
8
14
|
* publish hooks POST-TICK, PRE-NOTIFY: presence I/O home — eph
|
|
9
15
|
* structural ops are illegal for the whole tick, this
|
|
10
16
|
* step is theirs; `world.*` is legal here (outside the
|
|
@@ -22,6 +28,7 @@
|
|
|
22
28
|
import type { World } from "@vibecook/strata-ecs";
|
|
23
29
|
import type { AnySystem } from "./pipeline";
|
|
24
30
|
import { type FrameControl } from "./frame-control";
|
|
31
|
+
import { type GuestRegistry } from "./guests";
|
|
25
32
|
import { type PhaseGroup } from "./pipeline";
|
|
26
33
|
import { type ReflectorDef } from "./reflectors";
|
|
27
34
|
export type PublishHook = (world: World) => void;
|
|
@@ -38,10 +45,29 @@ export interface FrameTelemetry {
|
|
|
38
45
|
readonly systems: readonly SystemRunRecord[];
|
|
39
46
|
readonly phaseFlushMicros: ReadonlyMap<string, number>;
|
|
40
47
|
readonly reflectorsFlushed: readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* Total reflector-flush cost, µs (post-notify — OUTSIDE the tick, which is
|
|
50
|
+
* why it can never appear in `phaseFlushMicros`: that map is keyed by strata
|
|
51
|
+
* PHASE names and reflect is not a phase. Devtools' "reflect" lane read that
|
|
52
|
+
* map until 2026-08-15 and therefore never reported at all).
|
|
53
|
+
*/
|
|
54
|
+
readonly reflectMicros: number;
|
|
55
|
+
/** Per-reflector flush cost, µs, for the same frame (registration order). */
|
|
56
|
+
readonly reflectorMicros: ReadonlyMap<string, number>;
|
|
41
57
|
}
|
|
42
58
|
export interface EngineOpts {
|
|
43
59
|
/** Reflector fault sink (default: console.error, skip this frame). */
|
|
44
60
|
readonly onReflectorFault?: (name: string, err: unknown) => void;
|
|
61
|
+
/** Guest fault sink (default: console.error). A guest throwing is contained
|
|
62
|
+
* by the breaker; hosts route this to their own diagnostics. */
|
|
63
|
+
readonly onGuestFault?: (id: string, err: unknown) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Guest breaker notices — suspensions and lenient-mode overruns (default:
|
|
66
|
+
* console.warn). Hosts SHOULD route this: a suspended derived guest means
|
|
67
|
+
* derived state silently stops updating, which reads to a user as a broken
|
|
68
|
+
* document rather than a broken plugin (design-009 §7).
|
|
69
|
+
*/
|
|
70
|
+
readonly onGuestNotice?: (message: string) => void;
|
|
45
71
|
}
|
|
46
72
|
export interface Engine {
|
|
47
73
|
readonly world: World;
|
|
@@ -53,6 +79,13 @@ export interface Engine {
|
|
|
53
79
|
reflectorNames(): readonly string[];
|
|
54
80
|
/** Register a publish-step hook (post-tick, pre-notify presence I/O). Returns a remover. */
|
|
55
81
|
onPublish(hook: PublishHook): () => void;
|
|
82
|
+
/**
|
|
83
|
+
* The guest runtime (petition I14): frame-cadence work the engine did not
|
|
84
|
+
* write — plugin systems, host derived-state jobs, compiled behaviors — run
|
|
85
|
+
* in a named slot between the tick and the publish hooks, each fault-isolated
|
|
86
|
+
* and budgeted by a circuit breaker (guests.ts).
|
|
87
|
+
*/
|
|
88
|
+
readonly guests: GuestRegistry;
|
|
56
89
|
/** Run one full frame. `now` in ms (rAF timestamp or a test counter). */
|
|
57
90
|
step(now: number): void;
|
|
58
91
|
/**
|
|
@@ -65,8 +98,12 @@ export interface Engine {
|
|
|
65
98
|
* hand exactly as before.
|
|
66
99
|
*/
|
|
67
100
|
readonly frame: FrameControl;
|
|
68
|
-
/** Arm per-system run/skip + µs collection (idempotent; costs timer calls per system).
|
|
101
|
+
/** Arm per-system run/skip + µs collection (idempotent; costs timer calls per system).
|
|
102
|
+
* Also puts the guest breaker in LENIENT mode — devtools attached means a
|
|
103
|
+
* debugger breakpoint must not suspend a healthy guest. */
|
|
69
104
|
enableTelemetry(): void;
|
|
105
|
+
/** Tear down guest instances (the facade calls this from `dispose()`). */
|
|
106
|
+
disposeGuests(): void;
|
|
70
107
|
/** The most recent completed frame's readout (undefined until telemetry armed + one step). */
|
|
71
108
|
lastFrame(): FrameTelemetry | undefined;
|
|
72
109
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guest runtime (petition I14 — the literal PA-28 "spine scheduler").
|
|
3
|
+
*
|
|
4
|
+
* A GUEST is frame-cadence work the engine did not write: a plugin's declared
|
|
5
|
+
* system, a host's derived-state job, and (from design-009) every compiled
|
|
6
|
+
* behavior. Guests get a NAMED slot in the frame contract — design-002 §1 as
|
|
7
|
+
* amended 2026-08-15:
|
|
8
|
+
*
|
|
9
|
+
* setFrameInfo → sync → tick → GUESTS → publish hooks → notify → reflect
|
|
10
|
+
*
|
|
11
|
+
* Guests run BEFORE the publish hooks so derived state has settled by the time
|
|
12
|
+
* presence I/O reads it, and before `notify()` so the same frame reflects their
|
|
13
|
+
* writes. Three properties the engine owes anything it hosts here:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Fault isolation.** A throwing guest is caught, struck, and skipped — it
|
|
16
|
+
* never kills the loop, its neighbours, or the frame. (Engine systems and
|
|
17
|
+
* publish hooks keep propagating loudly by design; guests are the untrusted
|
|
18
|
+
* tier, and the breaker below is what makes hosting them honest.)
|
|
19
|
+
* 2. **The snapshot rule.** `runAll` iterates a snapshot with a liveness check,
|
|
20
|
+
* so a guest that adds or removes guests mid-run never skips a neighbour —
|
|
21
|
+
* the bug this same release fixed in the publish-hook and reflector loops.
|
|
22
|
+
* A guest added during a run starts NEXT frame.
|
|
23
|
+
* 3. **A circuit breaker, named honestly.** A main-thread body cannot be
|
|
24
|
+
* preempted: by the time we measure, the frame is already spent. What the
|
|
25
|
+
* breaker guarantees is that no guest hurts the canvas TWICE sustained.
|
|
26
|
+
*
|
|
27
|
+
* GENERATIONS. `make(gen)` builds a per-generation instance; the generation
|
|
28
|
+
* boundary in ICE is `world.reset()` (doc close/switch — the world is reset in
|
|
29
|
+
* place, so systems, observers and this registry all survive while every entity
|
|
30
|
+
* dies). On reset we mark the generation dirty and rebuild at the TOP of the
|
|
31
|
+
* next guest step — never inside the observer emit, where structural writes are
|
|
32
|
+
* illegal and user code has no business running. `gen.signal` aborts when the
|
|
33
|
+
* generation ends or the guest is removed.
|
|
34
|
+
*
|
|
35
|
+
* THE LEDGER IS HOST-INJECTABLE. Downstream (VibeField) an engine is per-doc, so
|
|
36
|
+
* a registry-local ledger would reset on every doc switch and hand chronic
|
|
37
|
+
* offenders a fresh probation forever. `add({ledger})` seeds prior state and
|
|
38
|
+
* `onLedgerChange` reports every transition, so the host persists across
|
|
39
|
+
* generations it owns.
|
|
40
|
+
*/
|
|
41
|
+
import type { World } from "@vibecook/strata-ecs";
|
|
42
|
+
/** Frame facts a guest may read (FrameInfo, pre-read by the engine). */
|
|
43
|
+
export interface GuestFrame {
|
|
44
|
+
readonly dtMs: number;
|
|
45
|
+
readonly tick: number;
|
|
46
|
+
/** Accumulated CLAMPED dt — under-reports across freezes; never accumulate
|
|
47
|
+
* wall time from it (frame-info.ts). */
|
|
48
|
+
readonly clock: number;
|
|
49
|
+
}
|
|
50
|
+
export type GuestRun = (frame: GuestFrame) => void;
|
|
51
|
+
export interface GuestInstance {
|
|
52
|
+
run: GuestRun;
|
|
53
|
+
dispose?: () => void;
|
|
54
|
+
/**
|
|
55
|
+
* "I still owe visible work." Feeds the frame gate's settle walk, so a
|
|
56
|
+
* freeze taken mid-derive parks only AFTER the guest has caught up — the
|
|
57
|
+
* canvas is never photographed between a structural change and its reflow.
|
|
58
|
+
* A guest with no owed-work notion simply omits it (the frame gate then
|
|
59
|
+
* treats it as quiet, which is the honest default).
|
|
60
|
+
*/
|
|
61
|
+
busy?: () => boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface GuestGeneration {
|
|
64
|
+
readonly world: World;
|
|
65
|
+
/** Aborts at generation teardown (world reset), removal, or engine dispose. */
|
|
66
|
+
readonly signal: AbortSignal;
|
|
67
|
+
}
|
|
68
|
+
export type GuestFactory = (gen: GuestGeneration) => GuestInstance | GuestRun;
|
|
69
|
+
/** The persistable half of a guest's breaker state. */
|
|
70
|
+
export interface GuestLedgerRecord {
|
|
71
|
+
/** Cumulative over-budget/fault strikes (never reset by resume — honesty). */
|
|
72
|
+
readonly strikes: number;
|
|
73
|
+
readonly suspended: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface GuestSpec {
|
|
76
|
+
/** Stable id — the ledger key, the profiler lane name, the doctor's row. */
|
|
77
|
+
readonly id: string;
|
|
78
|
+
readonly make: GuestFactory;
|
|
79
|
+
/** Per-invocation budget request; the effective budget is
|
|
80
|
+
* `min(budgetMs, seamCapMs)` — no single guest may claim the whole seam. */
|
|
81
|
+
readonly budgetMs?: number;
|
|
82
|
+
/** RESERVED. Hosted guests run in the publish slot only — and that is now the
|
|
83
|
+
* settled shape, not a v1 placeholder: M13 reached pipeline cadence through
|
|
84
|
+
* `addDriven` (behaviors run as pipeline systems and CHARGE this breaker)
|
|
85
|
+
* instead of widening this field. A wider value would mean a second way to
|
|
86
|
+
* put foreign code in the tick that `addDriven` already provides. */
|
|
87
|
+
readonly phase?: "publish";
|
|
88
|
+
/** Seed prior breaker state (host-persisted across engine generations). */
|
|
89
|
+
readonly ledger?: GuestLedgerRecord;
|
|
90
|
+
}
|
|
91
|
+
export interface GuestStatus {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly status: "running" | "suspended" | "failed";
|
|
94
|
+
/** Last invocation, ms. */
|
|
95
|
+
readonly lastMs: number;
|
|
96
|
+
/** p95 over the rolling window, ms. */
|
|
97
|
+
readonly p95Ms: number;
|
|
98
|
+
readonly strikes: number;
|
|
99
|
+
readonly budgetMs: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A guest whose CADENCE its owner drives (design-009 §7): compiled behaviors
|
|
103
|
+
* run as ordinary pipeline systems, in their declared phase, inside the tick —
|
|
104
|
+
* so nothing here can call their bodies. What they still need is everything
|
|
105
|
+
* ELSE a guest gets: one breaker, one ledger, one doctor row, one seam total.
|
|
106
|
+
* That is what this handle is — the breaker, detached from the scheduler.
|
|
107
|
+
*/
|
|
108
|
+
export interface DrivenGuest {
|
|
109
|
+
/** Breaker verdict. Cheap and never throws — poll it from a system `runIf`. */
|
|
110
|
+
suspended(): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Run `body` inside this guest's fault domain, ACCRUING its cost into the
|
|
113
|
+
* current frame's charge. Returns false when the guest is suspended (the
|
|
114
|
+
* body did not run) or the body threw (contained).
|
|
115
|
+
*
|
|
116
|
+
* Accrual, not per-call recording, is what makes the budget mean "per frame"
|
|
117
|
+
* for a CHUNKED body: a tick system's body runs once per matching chunk, and
|
|
118
|
+
* recording each chunk as its own invocation would compare a 2ms frame budget
|
|
119
|
+
* against a fraction of the work it was meant to bound. The accrued total is
|
|
120
|
+
* closed as ONE invocation at the guest slot, every frame.
|
|
121
|
+
*/
|
|
122
|
+
run(body: () => void): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Report a fault the caller already CONTAINED — a per-instance hook throw
|
|
125
|
+
* that the delivery loop caught so one bad node cannot kill a 1000-node
|
|
126
|
+
* behavior (BF-D18). It takes the ordinary throw ladder.
|
|
127
|
+
*/
|
|
128
|
+
fault(err: unknown): void;
|
|
129
|
+
/** Idempotent. */
|
|
130
|
+
remove(): void;
|
|
131
|
+
}
|
|
132
|
+
export interface DrivenGuestSpec {
|
|
133
|
+
readonly id: string;
|
|
134
|
+
readonly budgetMs?: number;
|
|
135
|
+
readonly ledger?: GuestLedgerRecord;
|
|
136
|
+
/** Owed-work reporter for the frame gate's settle walk. */
|
|
137
|
+
readonly busy?: () => boolean;
|
|
138
|
+
}
|
|
139
|
+
export interface GuestRegistry {
|
|
140
|
+
/** Register a guest. Returns a remover (idempotent; disposes the instance). */
|
|
141
|
+
add(spec: GuestSpec): () => void;
|
|
142
|
+
/**
|
|
143
|
+
* Register a guest the registry does NOT call (see {@link DrivenGuest}).
|
|
144
|
+
* Its cost still lands in this frame's seam total, its strikes in the same
|
|
145
|
+
* ledger, and its row in `list()`.
|
|
146
|
+
*/
|
|
147
|
+
addDriven(spec: DrivenGuestSpec): DrivenGuest;
|
|
148
|
+
/** Live status for devtools/doctor — never throws, safe to poll. */
|
|
149
|
+
list(): readonly GuestStatus[];
|
|
150
|
+
/** Fires whenever a guest's persistable breaker state changes. */
|
|
151
|
+
onLedgerChange(cb: (id: string, record: GuestLedgerRecord) => void): () => void;
|
|
152
|
+
/** Clear a suspension (the doctor's explicit action). Cumulative strikes
|
|
153
|
+
* survive; the rolling window is cleared so probation starts fresh. */
|
|
154
|
+
resume(id: string): void;
|
|
155
|
+
}
|
|
156
|
+
/** The engine-facing half (step drives it; hosts never see these). */
|
|
157
|
+
export interface GuestRegistryInternal extends GuestRegistry {
|
|
158
|
+
runAll(frame: GuestFrame): void;
|
|
159
|
+
/** Any live guest reporting owed work — the engine's settle reporter. */
|
|
160
|
+
anyBusy(): boolean;
|
|
161
|
+
dispose(): void;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Breaker constants (design-009 §12 / door ED-D3). Exported so tests and the
|
|
165
|
+
* downstream doctor cite ONE source rather than re-deriving thresholds.
|
|
166
|
+
*/
|
|
167
|
+
export declare const GUEST_BUDGET: {
|
|
168
|
+
/** Default per-invocation budget when a guest declares none. */
|
|
169
|
+
readonly defaultBudgetMs: 2;
|
|
170
|
+
/** The whole guest seam's sustained budget — also the per-guest ceiling. */
|
|
171
|
+
readonly seamCapMs: 8;
|
|
172
|
+
/** Rolling window length, in invocations. */
|
|
173
|
+
readonly window: 120;
|
|
174
|
+
/** Trip: more than this many of the last `window` invocations over budget. */
|
|
175
|
+
readonly overBudgetTrip: 30;
|
|
176
|
+
/** Trip: this many CONSECUTIVE invocations at ≥ `heavyFactor` × budget. */
|
|
177
|
+
readonly consecutiveHeavy: 3;
|
|
178
|
+
readonly heavyFactor: 4;
|
|
179
|
+
/** Trip: this many CONSECUTIVE invocations over `stallMs` (one GC pause or
|
|
180
|
+
* debugger breakpoint must never suspend — hence 2, not 1). */
|
|
181
|
+
readonly consecutiveStall: 2;
|
|
182
|
+
readonly stallMs: 50;
|
|
183
|
+
/** Trip: this many consecutive throws. */
|
|
184
|
+
readonly consecutiveThrows: 3;
|
|
185
|
+
/** Frames the whole seam may sit over `seamCapMs` before the worst offender
|
|
186
|
+
* is suspended. */
|
|
187
|
+
readonly seamOverFrames: 3;
|
|
188
|
+
};
|
|
189
|
+
export interface GuestRegistryOpts {
|
|
190
|
+
/**
|
|
191
|
+
* Dev leniency: while true, TIMING strikes log and never suspend (the throw
|
|
192
|
+
* ladder still bites). Wired to `engine.enableTelemetry()` — i.e. devtools
|
|
193
|
+
* attached — so stepping in a debugger cannot suspend a healthy guest.
|
|
194
|
+
*/
|
|
195
|
+
readonly lenient?: () => boolean;
|
|
196
|
+
readonly onFault?: (id: string, err: unknown) => void;
|
|
197
|
+
readonly onNotice?: (message: string) => void;
|
|
198
|
+
}
|
|
199
|
+
export declare function createGuestRegistry(world: World, opts?: GuestRegistryOpts): GuestRegistryInternal;
|