@vibecook/ice 0.4.0 → 0.6.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-ENMRSITH.js → chunk-BFT7YWS2.js} +16 -3
- package/dist/chunk-BFT7YWS2.js.map +1 -0
- package/dist/{chunk-3PHPWRB2.js → chunk-ELCBCTKY.js} +7 -4
- package/dist/chunk-ELCBCTKY.js.map +1 -0
- package/dist/{chunk-7LZAXQAO.js → chunk-JLXETATP.js} +5699 -3099
- package/dist/chunk-JLXETATP.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 +33 -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 +55 -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 +88 -0
- package/dist/types/packages/core/src/behavior/types.d.ts +312 -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 +196 -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 +16 -0
- package/dist/types/packages/core/src/guards/guarded-tx.d.ts +57 -0
- package/dist/types/packages/core/src/index.d.ts +10 -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,91 @@
|
|
|
1
|
+
import type { Component, Entity, OrderPlace, Relation, Tag, World } from "@vibecook/strata-ecs";
|
|
2
|
+
import type { GuardedTx } from "../guards/guarded-tx";
|
|
3
|
+
export type RecOp = {
|
|
4
|
+
readonly kind: "set";
|
|
5
|
+
readonly e: Entity;
|
|
6
|
+
readonly c: Component;
|
|
7
|
+
readonly v: unknown;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: "add";
|
|
10
|
+
readonly e: Entity;
|
|
11
|
+
readonly c: Component;
|
|
12
|
+
readonly v: unknown;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "removeComponent";
|
|
15
|
+
readonly e: Entity;
|
|
16
|
+
readonly c: Component;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "destroy";
|
|
19
|
+
readonly e: Entity;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "addTag";
|
|
22
|
+
readonly e: Entity;
|
|
23
|
+
readonly t: Tag;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "removeTag";
|
|
26
|
+
readonly e: Entity;
|
|
27
|
+
readonly t: Tag;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: "setRelation";
|
|
30
|
+
readonly e: Entity;
|
|
31
|
+
readonly r: Relation;
|
|
32
|
+
readonly target: Entity;
|
|
33
|
+
readonly place?: OrderPlace;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: "addRelation";
|
|
36
|
+
readonly e: Entity;
|
|
37
|
+
readonly r: Relation;
|
|
38
|
+
readonly target: Entity;
|
|
39
|
+
} | {
|
|
40
|
+
readonly kind: "removeRelation";
|
|
41
|
+
readonly e: Entity;
|
|
42
|
+
readonly r: Relation;
|
|
43
|
+
readonly target?: Entity;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: "moveRelation";
|
|
46
|
+
readonly e: Entity;
|
|
47
|
+
readonly r: Relation;
|
|
48
|
+
readonly place: OrderPlace;
|
|
49
|
+
} | {
|
|
50
|
+
readonly kind: "move";
|
|
51
|
+
readonly e: Entity;
|
|
52
|
+
readonly to: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
};
|
|
56
|
+
readonly animateMs: number;
|
|
57
|
+
};
|
|
58
|
+
export interface DiffDeps {
|
|
59
|
+
readonly world: World;
|
|
60
|
+
/** The DOC's value for a cell — what a commit would actually be compared to. */
|
|
61
|
+
docValue(e: Entity, c: Component): unknown;
|
|
62
|
+
/** The behavior's declared `writes:` targets (the derived-output test). */
|
|
63
|
+
readonly writeTargets: ReadonlySet<Component>;
|
|
64
|
+
}
|
|
65
|
+
export interface DiffResult {
|
|
66
|
+
readonly ops: readonly RecOp[];
|
|
67
|
+
/**
|
|
68
|
+
* The commit touches a declared `writes:` target — it is derived OUTPUT, so
|
|
69
|
+
* it is forced non-undoable. A commit that only touches the behavior's OWN
|
|
70
|
+
* data is an authorial config edit and keeps normal undo.
|
|
71
|
+
*/
|
|
72
|
+
readonly derivedOutput: boolean;
|
|
73
|
+
/** Entities this commit wrote — the next drain's subtraction set. */
|
|
74
|
+
readonly writtenEntities: readonly Entity[];
|
|
75
|
+
/**
|
|
76
|
+
* The VALUES the surviving ops commit, per entity per component. The echo
|
|
77
|
+
* snapshot needs these rather than a post-seal world read: a durable write
|
|
78
|
+
* lands in the doc now but projects into the world at the NEXT `world.sync()`,
|
|
79
|
+
* so reading the world here would capture the value the commit is replacing.
|
|
80
|
+
*/
|
|
81
|
+
readonly writtenValues: ReadonlyMap<Entity, ReadonlyMap<Component, unknown>>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Record a DERIVED commit body's ops without performing any of them. Only the
|
|
85
|
+
* derived path buffers: a plain durable behavior commits straight through, so
|
|
86
|
+
* it keeps `tx.spawn` and pays no recording cost.
|
|
87
|
+
*/
|
|
88
|
+
export declare function recordDerivedOps(behaviorName: string, build: (rec: GuardedTx) => void): RecOp[];
|
|
89
|
+
export declare function diffOps(ops: readonly RecOp[], deps: DiffDeps): DiffResult;
|
|
90
|
+
/** Replay diffed ops onto the real transaction. */
|
|
91
|
+
export declare function replayOps(ops: readonly RecOp[], tx: GuardedTx): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createBehaviorHarness` — the testing story, shipped WITH the framework
|
|
3
|
+
* rather than after it (design-009 §7).
|
|
4
|
+
*
|
|
5
|
+
* A behavior is frame-cadence, collaboration-native, transaction-writing code.
|
|
6
|
+
* Testing that without a rig means standing up an engine, a document, a
|
|
7
|
+
* pipeline and a hand-driven clock — enough ceremony that most authors would
|
|
8
|
+
* simply not test the interesting parts. The interesting parts are exactly the
|
|
9
|
+
* ones they will get wrong:
|
|
10
|
+
*
|
|
11
|
+
* - `claim(e)` fakes a live gesture claim, so an author can see their derived
|
|
12
|
+
* behavior go quiet during a drag and coalesce at the settle. Suppression
|
|
13
|
+
* is the single most surprising rule in the framework.
|
|
14
|
+
* - `pair()` gives a SECOND engine on the same document. Convergence bugs are
|
|
15
|
+
* invisible on one peer by definition, and "it works for me" is what a
|
|
16
|
+
* collaborative editor's bug reports look like.
|
|
17
|
+
* - `commits` records what actually reached the store, which is how you tell
|
|
18
|
+
* "the differ dropped it" from "it never ran".
|
|
19
|
+
*/
|
|
20
|
+
import { type Entity, type World } from "@vibecook/strata-ecs";
|
|
21
|
+
import { type Engine } from "../engine/engine";
|
|
22
|
+
import { type DocSession } from "../doc/doc-kit";
|
|
23
|
+
import { type BehaviorRuntime } from "./runtime";
|
|
24
|
+
import type { AnyBehaviorDef } from "./types";
|
|
25
|
+
/** One commit the harness observed reaching the store. */
|
|
26
|
+
export interface HarnessCommit {
|
|
27
|
+
readonly undoable: boolean;
|
|
28
|
+
/** `{ behavior, label }` for a `ctx.commit`; undefined for anything else. */
|
|
29
|
+
readonly meta: Record<string, unknown> | undefined;
|
|
30
|
+
}
|
|
31
|
+
export interface BehaviorHarnessOpts {
|
|
32
|
+
/**
|
|
33
|
+
* Attach a document. Default TRUE — durable behaviors need one, and a
|
|
34
|
+
* runtime behavior is unharmed by having it. `false` exercises the doc-less
|
|
35
|
+
* path deliberately (where `ctx.commit` refuses, by design).
|
|
36
|
+
*/
|
|
37
|
+
readonly docs?: boolean;
|
|
38
|
+
/** Extra behaviors to register beside the one under test. */
|
|
39
|
+
readonly also?: readonly AnyBehaviorDef[];
|
|
40
|
+
}
|
|
41
|
+
export interface BehaviorHarness {
|
|
42
|
+
readonly world: World;
|
|
43
|
+
readonly engine: Engine;
|
|
44
|
+
readonly runtime: BehaviorRuntime;
|
|
45
|
+
doc(): DocSession | undefined;
|
|
46
|
+
/** Drive `n` frames (default 1). The clock advances 16ms per frame. */
|
|
47
|
+
step(n?: number): void;
|
|
48
|
+
/** A bare durable node with Position + Size, for behaviors to ride. */
|
|
49
|
+
spawn(x?: number, y?: number): Entity;
|
|
50
|
+
/** Attach the behavior under test — durable through a transaction, else direct. */
|
|
51
|
+
attach(e: Entity, data?: Record<string, unknown>): void;
|
|
52
|
+
detach(e: Entity): void;
|
|
53
|
+
/** The behavior's live instances, as the runtime sees them. */
|
|
54
|
+
instances(): readonly Entity[];
|
|
55
|
+
/** Every commit that reached the store since construction. */
|
|
56
|
+
readonly commits: readonly HarnessCommit[];
|
|
57
|
+
/** Simulate a live gesture claim on `e`. Returns the release. */
|
|
58
|
+
claim(e: Entity): () => void;
|
|
59
|
+
/** A SECOND engine on this document — the two-peer rig. */
|
|
60
|
+
pair(): BehaviorHarness;
|
|
61
|
+
/** Exchange updates with every paired harness, both ways. */
|
|
62
|
+
sync(): void;
|
|
63
|
+
dispose(): void;
|
|
64
|
+
}
|
|
65
|
+
export declare function createBehaviorHarness(b: AnyBehaviorDef, opts?: BehaviorHarnessOpts): BehaviorHarness;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior schema evolution (design-009 §5.5–§5.6, BF-D14) — M13g.
|
|
3
|
+
*
|
|
4
|
+
* Markers are `engine.behavior.<name>.<v>` write-once keys: a monotone key SET,
|
|
5
|
+
* never a mutated cell, so concurrent peers converge on the union instead of
|
|
6
|
+
* racing a single slot. They are a SEPARATE FAMILY from `engine.pack.*`, and
|
|
7
|
+
* that separation is the whole point of this file.
|
|
8
|
+
*
|
|
9
|
+
* **Why not fold them into the pack compare.** The pack compare classifies a
|
|
10
|
+
* doc marker with no local counterpart as `newerInDoc`, which read-onlys the
|
|
11
|
+
* document. For prefabs that is right — an unknown prefab means the doc was
|
|
12
|
+
* written by a newer build. For behaviors it is catastrophic: behaviors ship in
|
|
13
|
+
* PLUGINS, so "no local counterpart" is the ordinary state of every document
|
|
14
|
+
* whose author had a plugin this reader does not. Folding them in would brick a
|
|
15
|
+
* document because a plugin is missing — design-008's exact failure shape,
|
|
16
|
+
* which cost us a release to learn.
|
|
17
|
+
*
|
|
18
|
+
* So behaviors get their own compare with the **absent-is-not-newer** rule:
|
|
19
|
+
* - a locally-unregistered behavior name is IGNORED ENTIRELY (its cells sit
|
|
20
|
+
* in the doc un-projected and dormant — strata's projection is
|
|
21
|
+
* name-registry-driven, so they are inert, not corrupt);
|
|
22
|
+
* - `v > installed` for an INSTALLED behavior triggers per-behavior DORMANCY
|
|
23
|
+
* (refuse hook delivery — projection is version-blind, so newer-shaped data
|
|
24
|
+
* WOULD otherwise flow into older hooks), never a doc verdict;
|
|
25
|
+
* - `v < installed` runs the migration chain.
|
|
26
|
+
*
|
|
27
|
+
* Behavior version state NEVER affects the document's gate verdict. That is a
|
|
28
|
+
* standing guarantee, and the anti-brick test in CI is what keeps it one.
|
|
29
|
+
*/
|
|
30
|
+
import type { Entity, World } from "@vibecook/strata-ecs";
|
|
31
|
+
import type { LiveWriter } from "../guards/live-writer";
|
|
32
|
+
import type { AnyBehaviorDef } from "./types";
|
|
33
|
+
export declare const BEHAVIOR_MARKER_PREFIX = "engine.behavior.";
|
|
34
|
+
export declare function behaviorMarkerKey(name: string, version: number): string;
|
|
35
|
+
export interface BehaviorMeta {
|
|
36
|
+
readMeta(key: string): string | undefined;
|
|
37
|
+
metaTransaction(fn: (meta: {
|
|
38
|
+
get(key: string): unknown;
|
|
39
|
+
set(key: string, value: string): void;
|
|
40
|
+
}) => void): void;
|
|
41
|
+
}
|
|
42
|
+
/** The highest marker version present for `name` (0 = never stamped). */
|
|
43
|
+
export declare function docBehaviorVersion(meta: BehaviorMeta, name: string, installed: number): number;
|
|
44
|
+
/** Stamp the marker for `version` if it is not already present (write-once). */
|
|
45
|
+
export declare function stampBehaviorMarker(meta: BehaviorMeta, name: string, version: number): void;
|
|
46
|
+
export type BehaviorVerdict = {
|
|
47
|
+
readonly kind: "ok";
|
|
48
|
+
}
|
|
49
|
+
/** The doc is at a NEWER version than this build installs — refuse delivery. */
|
|
50
|
+
| {
|
|
51
|
+
readonly kind: "dormant";
|
|
52
|
+
readonly docVersion: number;
|
|
53
|
+
readonly installed: number;
|
|
54
|
+
}
|
|
55
|
+
/** The doc is OLDER — the chain has work to do. */
|
|
56
|
+
| {
|
|
57
|
+
readonly kind: "migrate";
|
|
58
|
+
readonly from: number;
|
|
59
|
+
readonly installed: number;
|
|
60
|
+
}
|
|
61
|
+
/** Older, but the chain cannot reach `installed` from there. */
|
|
62
|
+
| {
|
|
63
|
+
readonly kind: "unreachable";
|
|
64
|
+
readonly from: number;
|
|
65
|
+
readonly installed: number;
|
|
66
|
+
};
|
|
67
|
+
export declare function classifyBehaviorVersion(b: AnyBehaviorDef, docVersion: number): BehaviorVerdict;
|
|
68
|
+
export interface MigrateDeps {
|
|
69
|
+
readonly world: World;
|
|
70
|
+
readonly meta: BehaviorMeta;
|
|
71
|
+
/** The doc store, for the one `{undoable: false}` transaction. */
|
|
72
|
+
readonly store: any;
|
|
73
|
+
readonly liveWriter?: LiveWriter;
|
|
74
|
+
/** Every entity currently carrying the behavior's component. */
|
|
75
|
+
carriers(): readonly Entity[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Run `b`'s migrate chain over every carrier, in ONE non-undoable transaction,
|
|
79
|
+
* then stamp the new marker.
|
|
80
|
+
*
|
|
81
|
+
* A SECOND plan+write pass, not a reuse of the M9 prefab runner: that one
|
|
82
|
+
* filters by `PrefabId`, and a behavior rides entities of any kind — its own
|
|
83
|
+
* component is the only thing that identifies its cells.
|
|
84
|
+
*
|
|
85
|
+
* Returns the number of migrated carriers.
|
|
86
|
+
*/
|
|
87
|
+
export declare function runBehaviorMigration(b: AnyBehaviorDef, from: number, deps: MigrateDeps): number;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CURATED public read surface for behaviors (design-009 §9).
|
|
3
|
+
*
|
|
4
|
+
* The engine catalog is exposed to behaviors DELIBERATELY, not accidentally.
|
|
5
|
+
* Every name on this list is a published contract: it is what a behavior may
|
|
6
|
+
* declare in `reads:` and expect to keep working across engine versions. Adding
|
|
7
|
+
* a name here is a product decision — it is a promise about a shape, not a
|
|
8
|
+
* convenience.
|
|
9
|
+
*
|
|
10
|
+
* Everything else in the catalog is ENGINE VOCABULARY: recognizers, claims,
|
|
11
|
+
* gesture bookkeeping, one-tick markers. Reading it from a behavior is not
|
|
12
|
+
* forbidden (the escape hatch stays open for engine-grade code), but it
|
|
13
|
+
* dev-WARNS, because those shapes change whenever the interaction stack does
|
|
14
|
+
* and no one will think to tell a plugin author.
|
|
15
|
+
*
|
|
16
|
+
* Two notes authors need and will not guess:
|
|
17
|
+
* - One-tick markers (`WentDown`, the `Just*` family) are already CLEARED by
|
|
18
|
+
* the time the publish slot runs, so an ephemeral behavior can never see
|
|
19
|
+
* them. That is not a bug to route around; it is what "one tick" means.
|
|
20
|
+
* - `Grab` and the claim relations are deliberately OFF the list. A behavior
|
|
21
|
+
* that wants to know "is this being dragged right now" is usually asking
|
|
22
|
+
* for claim-scoped suppression, which it already gets for free.
|
|
23
|
+
*/
|
|
24
|
+
import type { Component, Relation, Resource, Tag } from "@vibecook/strata-ecs";
|
|
25
|
+
/**
|
|
26
|
+
* The published set. Ordered by what an author reaches for first: geometry,
|
|
27
|
+
* then structure, then state, then the view.
|
|
28
|
+
*/
|
|
29
|
+
export declare const PUBLIC_READS: ReadonlySet<Component | Tag | Relation | Resource>;
|
|
30
|
+
/** Is this handle part of the published behavior read surface? */
|
|
31
|
+
export declare function isPublicRead(handle: Component | Tag | Relation | Resource): boolean;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Component, Entity, World } from "@vibecook/strata-ecs";
|
|
2
|
+
import type { GuestRegistry } from "../engine/guests";
|
|
3
|
+
import type { PhaseGroup } from "../engine/pipeline";
|
|
4
|
+
import type { LiveWriter } from "../guards/live-writer";
|
|
5
|
+
import { type BehaviorMeta } from "./migrate";
|
|
6
|
+
import type { AnyBehaviorDef } from "./types";
|
|
7
|
+
/** The slice of a doc session the runtime needs (structural: tests fake it). */
|
|
8
|
+
export interface BehaviorSession {
|
|
9
|
+
readonly readOnly: boolean;
|
|
10
|
+
readonly liveWriter: LiveWriter;
|
|
11
|
+
readonly store: {
|
|
12
|
+
transaction(fn: (tx: never) => void, opts?: {
|
|
13
|
+
undoable?: boolean;
|
|
14
|
+
}): void;
|
|
15
|
+
keyOf(e: Entity): unknown;
|
|
16
|
+
resolve(key: never): Entity | undefined;
|
|
17
|
+
getComponent<S>(e: Entity, c: Component<S>): S | undefined;
|
|
18
|
+
/** The version-marker surface (behavior/migrate.ts). Absent = no markers. */
|
|
19
|
+
readonly snapshot?: BehaviorMeta;
|
|
20
|
+
metaTransaction?(fn: (meta: {
|
|
21
|
+
get(key: string): unknown;
|
|
22
|
+
set(key: string, value: string): void;
|
|
23
|
+
}) => void): void;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The slice of a presence session an ephemeral behavior needs. Structural so
|
|
28
|
+
* tests can fake it, and a FUNCTION on the opts so it can swap with the doc —
|
|
29
|
+
* presence attaches and detaches with `docs.join`, and a behavior installed
|
|
30
|
+
* before either must simply lie dormant until one exists.
|
|
31
|
+
*/
|
|
32
|
+
export interface BehaviorPresence {
|
|
33
|
+
/** The local peer entity. A GETTER upstream: a world reset re-mints it. */
|
|
34
|
+
readonly localPeer: Entity;
|
|
35
|
+
readonly eph: {
|
|
36
|
+
addComponent(e: Entity, c: Component<unknown>, v: unknown): void;
|
|
37
|
+
removeComponent(e: Entity, c: Component): void;
|
|
38
|
+
edit(e: Entity): {
|
|
39
|
+
set(c: Component<unknown>, v: unknown): unknown;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface BehaviorRuntimeOpts {
|
|
44
|
+
readonly world: World;
|
|
45
|
+
readonly engine: {
|
|
46
|
+
addSystems(group: PhaseGroup, ...systems: readonly never[]): () => void;
|
|
47
|
+
readonly guests: GuestRegistry;
|
|
48
|
+
onPublish(hook: (world: World) => void): () => void;
|
|
49
|
+
};
|
|
50
|
+
/** The FORWARDING session seam — the target swaps per doc open/close. */
|
|
51
|
+
readonly session?: () => BehaviorSession | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The presence seam. Absent (or returning undefined) leaves every ephemeral
|
|
54
|
+
* behavior DORMANT — installed, costing nothing, publishing nothing. That is
|
|
55
|
+
* honest state, not a failure: a presence-less engine has no peer to be.
|
|
56
|
+
*/
|
|
57
|
+
readonly presence?: () => BehaviorPresence | undefined;
|
|
58
|
+
readonly onLog?: (behavior: string, message: string, rest: readonly unknown[]) => void;
|
|
59
|
+
/**
|
|
60
|
+
* A contained hook fault. Defaults to `console.error`. Hosts route this: a
|
|
61
|
+
* quarantined instance in a derived behavior means part of the document
|
|
62
|
+
* silently stops updating, which reads to a user as a broken document.
|
|
63
|
+
*/
|
|
64
|
+
readonly onFault?: (behavior: string, hook: string, entity: Entity | undefined, err: unknown) => void;
|
|
65
|
+
}
|
|
66
|
+
export interface BehaviorStatus {
|
|
67
|
+
readonly name: string;
|
|
68
|
+
readonly store: string;
|
|
69
|
+
readonly phase: string;
|
|
70
|
+
readonly instances: number;
|
|
71
|
+
/** Instances quarantined by repeated hook throws (BF-D18). */
|
|
72
|
+
readonly failed: number;
|
|
73
|
+
readonly ticking: number;
|
|
74
|
+
readonly suspended: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface BehaviorRuntime {
|
|
77
|
+
/** Compile + install a behavior. Returns an uninstaller. */
|
|
78
|
+
register(b: AnyBehaviorDef): () => void;
|
|
79
|
+
registered(): readonly AnyBehaviorDef[];
|
|
80
|
+
/** Runtime/ephemeral attach from OUTSIDE a hook (the facade surface). */
|
|
81
|
+
attach(e: Entity, b: AnyBehaviorDef, data?: Record<string, unknown>): void;
|
|
82
|
+
detach(e: Entity, b: AnyBehaviorDef): void;
|
|
83
|
+
has(e: Entity, b: AnyBehaviorDef): boolean;
|
|
84
|
+
read(e: Entity, b: AnyBehaviorDef): Record<string, unknown> | undefined;
|
|
85
|
+
list(): readonly BehaviorStatus[];
|
|
86
|
+
dispose(): void;
|
|
87
|
+
}
|
|
88
|
+
export declare function createBehaviorRuntime(opts: BehaviorRuntimeOpts): BehaviorRuntime;
|
|
@@ -0,0 +1,312 @@
|
|
|
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, 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
|
+
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;
|
|
35
|
+
/**
|
|
36
|
+
* The hook-facing data view. `p.json` fields arrive PARSED (the cell is a
|
|
37
|
+
* string; the snapshot carries the value) — parsing happens once per delivery,
|
|
38
|
+
* and delivery only runs on change, so the cost is keyed to churn rather than
|
|
39
|
+
* to frame rate. `ctx.write`/`tx.write` serialize back.
|
|
40
|
+
*/
|
|
41
|
+
export type DataOf<Sch extends BehaviorSchema> = {
|
|
42
|
+
readonly [K in keyof Sch]: ValueOfSpec<Sch[K]>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Anything a behavior may declare in `reads:` — components, tags, relations,
|
|
46
|
+
* resources, or another behavior (its data component). Each kind is routed to
|
|
47
|
+
* a different change mechanism by the compiler (§5.2), which is why the
|
|
48
|
+
* partition is compiler work and not an author concern.
|
|
49
|
+
*/
|
|
50
|
+
export type BehaviorRead = Component | Tag | Relation | Resource | AnyBehaviorDef;
|
|
51
|
+
/** The drained journal summary, valid only for the duration of the hook call. */
|
|
52
|
+
export interface BehaviorChanges {
|
|
53
|
+
/** Rebuild everything: first run, `world.reset()`, coarse writes, or a poll fired. */
|
|
54
|
+
readonly full: boolean;
|
|
55
|
+
/** Journaled entities — re-read current state to act. */
|
|
56
|
+
readonly changed: readonly Entity[];
|
|
57
|
+
/** DESTROYED entities (packed handles; safe as removal keys). */
|
|
58
|
+
readonly removed: readonly Entity[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `ctx.world` is a HOST-BUILT WRAPPER, not a `World` alias and not
|
|
62
|
+
* `ReadonlyWorld` (BF-D19): that type is `Omit<World, WorldMutatorName>`, which
|
|
63
|
+
* keeps `.runtime` — the full mutable store — so it is a naming convention, not
|
|
64
|
+
* a sandbox. This interface exposes exactly the read surface and nothing else.
|
|
65
|
+
*
|
|
66
|
+
* Honesty note: point reads through this wrapper are ATTRIBUTION-ONLY — strata
|
|
67
|
+
* enforces declared reads solely at `batch.col()`, so the compiled delivery
|
|
68
|
+
* walk (`ctx.query`) is where enforcement is real.
|
|
69
|
+
*/
|
|
70
|
+
export interface BehaviorWorld {
|
|
71
|
+
read<S>(e: Entity, c: Component<S>): S;
|
|
72
|
+
get<S>(e: Entity, c: Component<S>): S | undefined;
|
|
73
|
+
readField<S, K extends keyof S & string>(e: Entity, c: Component<S>, field: K): S[K] | undefined;
|
|
74
|
+
has(e: Entity, c: Component): boolean;
|
|
75
|
+
hasTag(e: Entity, t: Tag): boolean;
|
|
76
|
+
getRelation(e: Entity, r: Relation): Entity | undefined;
|
|
77
|
+
getRelations(e: Entity, r: Relation): Entity[];
|
|
78
|
+
getReverse(e: Entity, r: Relation): Entity[];
|
|
79
|
+
/** Sibling-sequence version for one parent — ordered relations (petition 8). */
|
|
80
|
+
orderStamp(parent: Entity, r: Relation): number;
|
|
81
|
+
getResource<S>(res: Resource<S>): S | undefined;
|
|
82
|
+
componentsOf(e: Entity): readonly Component[];
|
|
83
|
+
tagsOf(e: Entity): readonly Tag[];
|
|
84
|
+
isAlive(e: Entity): boolean;
|
|
85
|
+
}
|
|
86
|
+
/** A reads-scoped query walk. Terms are restricted to the behavior's `reads`. */
|
|
87
|
+
export interface BehaviorQuery {
|
|
88
|
+
/** Every matching entity. The array is FRESH per call — retaining it is legal. */
|
|
89
|
+
entities(): Entity[];
|
|
90
|
+
/** Iterate matches one at a time (no array allocated). */
|
|
91
|
+
each(fn: (e: Entity) => void): void;
|
|
92
|
+
/** First match, or undefined. */
|
|
93
|
+
first(): Entity | undefined;
|
|
94
|
+
}
|
|
95
|
+
/** Query terms a behavior may build — its declared reads, positively or negatively. */
|
|
96
|
+
export interface BehaviorQuerySpec {
|
|
97
|
+
/** Entities must carry ALL of these (components/tags from `reads`). */
|
|
98
|
+
readonly all?: readonly (Component | Tag | AnyBehaviorDef)[];
|
|
99
|
+
/** Entities must carry NONE of these. */
|
|
100
|
+
readonly none?: readonly (Component | Tag | AnyBehaviorDef)[];
|
|
101
|
+
}
|
|
102
|
+
/** Frame facts a `tick` hook reads. */
|
|
103
|
+
export interface BehaviorFrame {
|
|
104
|
+
readonly dtMs: number;
|
|
105
|
+
readonly tick: number;
|
|
106
|
+
/** Accumulated CLAMPED dt — under-reports across freezes; never wall time. */
|
|
107
|
+
readonly clock: number;
|
|
108
|
+
}
|
|
109
|
+
/** One remote peer's facet of an ephemeral behavior (`ctx.peers()`). */
|
|
110
|
+
export interface PeerFacet<Data> {
|
|
111
|
+
/** The remote presence-peer entity (runtime handle — do not persist). */
|
|
112
|
+
readonly entity: Entity;
|
|
113
|
+
readonly data: Readonly<Data>;
|
|
114
|
+
}
|
|
115
|
+
export interface BehaviorCtxBase<Data> {
|
|
116
|
+
/** The read wrapper (above). */
|
|
117
|
+
readonly world: BehaviorWorld;
|
|
118
|
+
/**
|
|
119
|
+
* A reads-SCOPED query. The framework compiles and caches one Query per
|
|
120
|
+
* distinct spec and walks it ATTRIBUTED — strata's only enforced read path,
|
|
121
|
+
* and its only zero-allocation bulk path. Without this, navigation was
|
|
122
|
+
* instances-outward only and `changed` got abused as a discovery channel.
|
|
123
|
+
*
|
|
124
|
+
* The navigation model, stated: behaviors navigate from their instances
|
|
125
|
+
* outward via relations, the change journal, and reads-scoped queries. There
|
|
126
|
+
* is no global search.
|
|
127
|
+
*/
|
|
128
|
+
query(spec: BehaviorQuerySpec): BehaviorQuery;
|
|
129
|
+
/** This behavior's own instances, this frame (snapshot — do not retain). */
|
|
130
|
+
entities(): readonly Entity[];
|
|
131
|
+
/** The drained journal summary. Meaningful during `changed`; do not retain. */
|
|
132
|
+
changes(): BehaviorChanges;
|
|
133
|
+
/** The journal ∩ this behavior's instances — the exact set. Do not retain. */
|
|
134
|
+
changedEntities(): readonly Entity[];
|
|
135
|
+
/** Durable doc key for an entity (undefined when not doc-bound). */
|
|
136
|
+
keyOf(e: Entity): string | undefined;
|
|
137
|
+
/** The entity for a durable key — undefined while it is not yet projected. */
|
|
138
|
+
entityFor(key: string): Entity | undefined;
|
|
139
|
+
/** Aborts at generation teardown (`world.reset`), detach, or engine dispose. */
|
|
140
|
+
readonly signal: AbortSignal;
|
|
141
|
+
/** Namespaced logging — routed to the host's diagnostics, never raw console. */
|
|
142
|
+
log(message: string, ...rest: readonly unknown[]): void;
|
|
143
|
+
}
|
|
144
|
+
export interface DurableBehaviorCtx<Data> extends BehaviorCtxBase<Data> {
|
|
145
|
+
/**
|
|
146
|
+
* ONE labeled transaction. The transaction opens INSIDE the hook, in-system
|
|
147
|
+
* (design-002-blessed): values land synchronously and this frame's `notify`
|
|
148
|
+
* sees them — never queued past the tick. `label` is a stable MACHINE id
|
|
149
|
+
* (namespaced), never display text: it feeds undo and in-CRDT provenance.
|
|
150
|
+
*
|
|
151
|
+
* Refuses on a read-only document. `tx.setResource` is MASKED (the §9
|
|
152
|
+
* resource-write fence, made mechanical) — a behavior calling it DEV-throws
|
|
153
|
+
* naming the `singleton` v1.1 seam.
|
|
154
|
+
*/
|
|
155
|
+
commit(label: string, fn: (tx: BehaviorTx<Data>) => void, opts?: {
|
|
156
|
+
undoable?: boolean;
|
|
157
|
+
}): void;
|
|
158
|
+
}
|
|
159
|
+
export interface RuntimeBehaviorCtx<Data> extends BehaviorCtxBase<Data> {
|
|
160
|
+
/** Write this behavior's OWN data on `e` (immediate; session-local). */
|
|
161
|
+
write(e: Entity, patch: Partial<Data>): void;
|
|
162
|
+
/** Write a declared runtime-store `writes:` target (immediate). */
|
|
163
|
+
set<S>(e: Entity, c: Component<S>, v: S): void;
|
|
164
|
+
attach(e: Entity, data?: Partial<Data>): void;
|
|
165
|
+
detach(e: Entity): void;
|
|
166
|
+
/** A runtime behavior may also commit durable state (a settle writing finals once). */
|
|
167
|
+
commit(label: string, fn: (tx: BehaviorTx<Data>) => void, opts?: {
|
|
168
|
+
undoable?: boolean;
|
|
169
|
+
}): void;
|
|
170
|
+
}
|
|
171
|
+
export interface EphemeralBehaviorCtx<Data> extends BehaviorCtxBase<Data> {
|
|
172
|
+
/**
|
|
173
|
+
* Publish THIS PEER's facet. No entity argument — the instance IS the local
|
|
174
|
+
* presence peer, and there is exactly one. Legal here because ephemeral hooks
|
|
175
|
+
* run at the publish slot, post-tick, where eph mutation is allowed.
|
|
176
|
+
*/
|
|
177
|
+
write(patch: Partial<Data>): void;
|
|
178
|
+
/** REMOTE peers' facets of this behavior (read-only; `Not(Local)` projections). */
|
|
179
|
+
peers(): readonly PeerFacet<Data>[];
|
|
180
|
+
}
|
|
181
|
+
export type BehaviorCtxFor<S extends BehaviorStore, Data> = S extends "durable" ? DurableBehaviorCtx<Data> : S extends "runtime" ? RuntimeBehaviorCtx<Data> : EphemeralBehaviorCtx<Data>;
|
|
182
|
+
/**
|
|
183
|
+
* The door's transaction: `GuardedTx` plus behavior attachment and typed
|
|
184
|
+
* own-data writes. `setResource` is absent by construction (§4.4's mask).
|
|
185
|
+
*/
|
|
186
|
+
export interface BehaviorTx<Data> extends Omit<GuardedTx, "setResource"> {
|
|
187
|
+
/** Write this behavior's own data on `e` (absolute, whole-value). */
|
|
188
|
+
write(e: Entity, patch: Partial<Data>): void;
|
|
189
|
+
/** Attach a behavior as a DOCUMENT op — syncs, undoes, runs on every peer. */
|
|
190
|
+
attach<D>(e: Entity, behavior: BehaviorHandle<D>, data?: Partial<D>): void;
|
|
191
|
+
detach(e: Entity, behavior: AnyBehaviorDef): void;
|
|
192
|
+
}
|
|
193
|
+
export interface BehaviorHooks<S extends BehaviorStore, Data> {
|
|
194
|
+
/**
|
|
195
|
+
* Instance appeared: attached locally, projected in (REMOTE included), or
|
|
196
|
+
* existing at generation start. Runs before that instance's first `tick`.
|
|
197
|
+
* A throw here marks the INSTANCE failed — no further hooks until re-attach.
|
|
198
|
+
*/
|
|
199
|
+
init?(e: Entity, data: Readonly<Data>, ctx: BehaviorCtxFor<S, Data>): void;
|
|
200
|
+
/**
|
|
201
|
+
* OWN declared data changed since the last delivery — by ANY writer: this
|
|
202
|
+
* behavior's commit, another plugin's tx, UNDO, or a remote peer.
|
|
203
|
+
* Per-instance; `prev` is the last-delivered snapshot. "update is about you."
|
|
204
|
+
*/
|
|
205
|
+
update?(e: Entity, data: Readonly<Data>, prev: Readonly<Data>, ctx: BehaviorCtxFor<S, Data>): void;
|
|
206
|
+
/**
|
|
207
|
+
* The reads-set changed. ONCE PER BEHAVIOR per frame, not per instance —
|
|
208
|
+
* N instances × one tag flip must not mean N journal scans.
|
|
209
|
+
* "changed is about the world."
|
|
210
|
+
*/
|
|
211
|
+
changed?(ctx: BehaviorCtxFor<S, Data>): void;
|
|
212
|
+
/**
|
|
213
|
+
* Per frame per instance — OPT-IN cost. Default is ALL instances (Law 14: an
|
|
214
|
+
* off-screen spring still settles); `tick: { while: "visible" }` is the
|
|
215
|
+
* author's DECLARED suspension. `init`/`update`/`changed`/`dispose` never
|
|
216
|
+
* suspend: culling is not lifecycle, and state must be right the moment it
|
|
217
|
+
* becomes visible again.
|
|
218
|
+
*/
|
|
219
|
+
tick?(e: Entity, data: Readonly<Data>, frame: BehaviorFrame, ctx: BehaviorCtxFor<S, Data>): void;
|
|
220
|
+
/**
|
|
221
|
+
* Instance gone: detached, despawned, disabled, or generation end. `e` may
|
|
222
|
+
* already be dead (world access is DEV-guarded) — this is for cache cleanup.
|
|
223
|
+
* Runs for ALL instances BEFORE `ctx.signal` aborts.
|
|
224
|
+
*/
|
|
225
|
+
dispose?(e: Entity, ctx: BehaviorCtxFor<S, Data>): void;
|
|
226
|
+
}
|
|
227
|
+
export interface BehaviorSpec<S extends BehaviorStore, Sch extends BehaviorSchema> {
|
|
228
|
+
/** REQUIRED — routes everything (§3). */
|
|
229
|
+
readonly store: S;
|
|
230
|
+
/**
|
|
231
|
+
* This behavior's durable writes are COMPUTED from other state. Durable only.
|
|
232
|
+
* Bundles three protections: non-undoable output, the differ, and
|
|
233
|
+
* claim-scoped delivery suppression (§5.3).
|
|
234
|
+
*/
|
|
235
|
+
readonly derived?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Opt out of suppression: derive live during gestures (costs accepted).
|
|
238
|
+
* `derived` only.
|
|
239
|
+
*/
|
|
240
|
+
readonly deriveDuringGesture?: boolean;
|
|
241
|
+
/** Durable only — the `engine.behavior.<name>.<v>` marker family (§5.6). */
|
|
242
|
+
readonly version?: number;
|
|
243
|
+
readonly schema?: Sch;
|
|
244
|
+
/** Durable only: fromVersion → idempotent absolute transform. */
|
|
245
|
+
readonly migrate?: Readonly<Record<number, (prev: Record<string, unknown>) => Record<string, unknown>>>;
|
|
246
|
+
/** Defaults per store (§4.4); validated against the legal set. */
|
|
247
|
+
readonly phase?: BehaviorPhase;
|
|
248
|
+
/** Per-frame budget request, ms. The breaker caps it at the seam ceiling. */
|
|
249
|
+
readonly budgetMs?: number;
|
|
250
|
+
readonly reads?: readonly BehaviorRead[];
|
|
251
|
+
readonly writes?: readonly Component[];
|
|
252
|
+
/**
|
|
253
|
+
* `tick` scope. `{ while: "visible" }` compiles a `Not(Culled)` term into the
|
|
254
|
+
* chunked tick system — declared suspension, never discovered.
|
|
255
|
+
*/
|
|
256
|
+
readonly tick?: {
|
|
257
|
+
readonly while?: "visible" | "all";
|
|
258
|
+
};
|
|
259
|
+
readonly on?: BehaviorHooks<S, DataOf<Sch>>;
|
|
260
|
+
}
|
|
261
|
+
/** A behavior pre-attachment: `Layout.with({ gapX: 32 })`. */
|
|
262
|
+
export interface BehaviorAttachSpec<Data = Record<string, unknown>> {
|
|
263
|
+
readonly behavior: BehaviorHandle<Data>;
|
|
264
|
+
readonly data: Partial<Data>;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The compiled declaration. Usable wherever a behavior is expected —
|
|
268
|
+
* `reads: [Glow]`, `behaviors: [Layout]`, `tx.attach(e, Layout)` — plus
|
|
269
|
+
* `.with(data)` for pre-attachment and `.component` for advanced read paths.
|
|
270
|
+
*/
|
|
271
|
+
export interface BehaviorHandle<Data> {
|
|
272
|
+
/** `<namespace>:<name>` — the process-global identity. */
|
|
273
|
+
readonly name: string;
|
|
274
|
+
readonly namespace: string;
|
|
275
|
+
readonly store: BehaviorStore;
|
|
276
|
+
readonly derived: boolean;
|
|
277
|
+
readonly deriveDuringGesture: boolean;
|
|
278
|
+
readonly version: number;
|
|
279
|
+
readonly phase: BehaviorPhase;
|
|
280
|
+
readonly budgetMs: number | undefined;
|
|
281
|
+
readonly schema: BehaviorSchema;
|
|
282
|
+
/** The generated data component (`behavior:<name>`). Advanced read paths. */
|
|
283
|
+
readonly component: Component<Record<string, string | number | boolean>>;
|
|
284
|
+
/** Every field at its declared default (the attach fill). */
|
|
285
|
+
readonly defaults: Readonly<Data>;
|
|
286
|
+
readonly reads: readonly BehaviorRead[];
|
|
287
|
+
readonly writes: readonly Component[];
|
|
288
|
+
readonly migrate: Readonly<Record<number, (prev: Record<string, unknown>) => Record<string, unknown>>>;
|
|
289
|
+
readonly tickWhile: "all" | "visible";
|
|
290
|
+
/**
|
|
291
|
+
* The hook set. MUTABLE by design: a same-shape re-definition (hot reload,
|
|
292
|
+
* a test re-importing a module) refreshes these in place, so the cached
|
|
293
|
+
* handle keeps its component and collector identity while the new code
|
|
294
|
+
* actually runs. Stale-hook HMR is the wart this avoids.
|
|
295
|
+
*/
|
|
296
|
+
on: BehaviorHooks<BehaviorStore, Data>;
|
|
297
|
+
/** Pre-attachment with data: `behaviors: [Layout.with({ gapX: 32 })]`. */
|
|
298
|
+
with(data: Partial<Data>): BehaviorAttachSpec<Data>;
|
|
299
|
+
/** @internal brand — how `reads:` tells a behavior from a component. */
|
|
300
|
+
readonly __behavior: true;
|
|
301
|
+
}
|
|
302
|
+
/** A behavior handle of unknown data shape (registries, `reads:` lists). */
|
|
303
|
+
export type AnyBehaviorDef = BehaviorHandle<any>;
|
|
304
|
+
/**
|
|
305
|
+
* A pre-attachment of unknown data shape — what a LIST of them has to be typed
|
|
306
|
+
* as. `BehaviorAttachSpec<never>` looks like the shape-agnostic form and is
|
|
307
|
+
* not: `Layout.with({gapX: 32})` produces `BehaviorAttachSpec<{gapX: number}>`,
|
|
308
|
+
* which is not assignable to it, so the documented `with()` form would not
|
|
309
|
+
* typecheck in `defineWidget({behaviors})` at all.
|
|
310
|
+
*/
|
|
311
|
+
export type AnyBehaviorAttachSpec = BehaviorAttachSpec<any>;
|
|
312
|
+
export {};
|