@spearwolf/shadow-objects 0.33.0 → 0.34.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/CHANGELOG.md +320 -61
- package/README.md +57 -13
- package/bundle.js +5 -54
- package/package.json +7 -3
- package/src/ChangeTrailRefusedError.d.ts +27 -0
- package/src/ChangeTrailRefusedError.d.ts.map +1 -0
- package/src/ChangeTrailRefusedError.js +12 -0
- package/src/ChangeTrailRefusedError.js.map +7 -0
- package/src/EntityUuidInUseError.d.ts +19 -0
- package/src/EntityUuidInUseError.d.ts.map +1 -0
- package/src/EntityUuidInUseError.js +11 -0
- package/src/EntityUuidInUseError.js.map +7 -0
- package/src/WorkerTimeoutError.d.ts +25 -0
- package/src/WorkerTimeoutError.d.ts.map +1 -0
- package/src/WorkerTimeoutError.js +12 -0
- package/src/WorkerTimeoutError.js.map +7 -0
- package/src/bundle.d.ts +1 -1
- package/src/bundle.d.ts.map +1 -1
- package/src/bundle.js +1 -1
- package/src/bundle.js.map +2 -2
- package/src/constants.d.ts +9 -2
- package/src/constants.d.ts.map +1 -1
- package/src/constants.js +1 -1
- package/src/constants.js.map +2 -2
- package/src/create-worker.bundle.d.ts +1 -1
- package/src/create-worker.bundle.d.ts.map +1 -1
- package/src/create-worker.bundle.js.map +1 -1
- package/src/create-worker.d.ts +1 -1
- package/src/create-worker.d.ts.map +1 -1
- package/src/create-worker.js.map +1 -1
- package/src/elements/ShaeElement.d.ts +70 -2
- package/src/elements/ShaeElement.d.ts.map +1 -1
- package/src/elements/ShaeElement.js +129 -25
- package/src/elements/ShaeElement.js.map +3 -3
- package/src/elements/ShaeEntElement.d.ts +39 -1
- package/src/elements/ShaeEntElement.d.ts.map +1 -1
- package/src/elements/ShaeEntElement.js +397 -167
- package/src/elements/ShaeEntElement.js.map +2 -2
- package/src/elements/ShaeLifecycleElement.d.ts +71 -0
- package/src/elements/ShaeLifecycleElement.d.ts.map +1 -0
- package/src/elements/ShaeLifecycleElement.js +83 -0
- package/src/elements/ShaeLifecycleElement.js.map +7 -0
- package/src/elements/ShaePropElement.d.ts +52 -2
- package/src/elements/ShaePropElement.d.ts.map +1 -1
- package/src/elements/ShaePropElement.js +240 -219
- package/src/elements/ShaePropElement.js.map +2 -2
- package/src/elements/ShaeWorkerElement.d.ts +49 -5
- package/src/elements/ShaeWorkerElement.d.ts.map +1 -1
- package/src/elements/ShaeWorkerElement.js +204 -73
- package/src/elements/ShaeWorkerElement.js.map +2 -2
- package/src/elements/constants.d.ts +6 -0
- package/src/elements/constants.d.ts.map +1 -1
- package/src/elements/constants.js +12 -0
- package/src/elements/constants.js.map +2 -2
- package/src/elements/deferredTeardown.d.ts +24 -0
- package/src/elements/deferredTeardown.d.ts.map +1 -0
- package/src/elements/deferredTeardown.js +30 -0
- package/src/elements/deferredTeardown.js.map +7 -0
- package/src/elements/displayContentsRule.d.ts +25 -0
- package/src/elements/displayContentsRule.d.ts.map +1 -0
- package/src/elements/displayContentsRule.js +37 -0
- package/src/elements/displayContentsRule.js.map +7 -0
- package/src/elements/events.d.ts +10 -7
- package/src/elements/events.d.ts.map +1 -1
- package/src/elements/forwardCustomEvents.d.ts +19 -0
- package/src/elements/forwardCustomEvents.d.ts.map +1 -0
- package/src/elements/forwardCustomEvents.js +47 -0
- package/src/elements/forwardCustomEvents.js.map +7 -0
- package/src/elements/hostedSlots.d.ts +36 -0
- package/src/elements/hostedSlots.d.ts.map +1 -0
- package/src/elements/hostedSlots.js +118 -0
- package/src/elements/hostedSlots.js.map +7 -0
- package/src/elements/parentRemoval.d.ts +18 -0
- package/src/elements/parentRemoval.d.ts.map +1 -0
- package/src/elements/parentRemoval.js +60 -0
- package/src/elements/parentRemoval.js.map +7 -0
- package/src/elements/propValueConverters.d.ts +3 -0
- package/src/elements/propValueConverters.d.ts.map +1 -0
- package/src/elements/propValueConverters.js +54 -0
- package/src/elements/propValueConverters.js.map +7 -0
- package/src/elements/requestEntAncestor.d.ts +32 -0
- package/src/elements/requestEntAncestor.d.ts.map +1 -0
- package/src/elements/requestEntAncestor.js +16 -0
- package/src/elements/requestEntAncestor.js.map +7 -0
- package/src/elements.js.map +1 -1
- package/src/in-the-dark/Entity.d.ts +113 -3
- package/src/in-the-dark/Entity.d.ts.map +1 -1
- package/src/in-the-dark/Entity.js +292 -63
- package/src/in-the-dark/Entity.js.map +3 -3
- package/src/in-the-dark/Kernel.d.ts +76 -17
- package/src/in-the-dark/Kernel.d.ts.map +1 -1
- package/src/in-the-dark/Kernel.js +410 -378
- package/src/in-the-dark/Kernel.js.map +3 -3
- package/src/in-the-dark/Registry.d.ts.map +1 -1
- package/src/in-the-dark/Registry.js +83 -14
- package/src/in-the-dark/Registry.js.map +2 -2
- package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
- package/src/in-the-dark/ShadowObject.js +1 -0
- package/src/in-the-dark/ShadowObject.js.map +2 -2
- package/src/in-the-dark/ShadowObjectCreationScope.d.ts +126 -0
- package/src/in-the-dark/ShadowObjectCreationScope.d.ts.map +1 -0
- package/src/in-the-dark/ShadowObjectCreationScope.js +596 -0
- package/src/in-the-dark/ShadowObjectCreationScope.js.map +7 -0
- package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
- package/src/in-the-dark/SignalsPath.js +2 -2
- package/src/in-the-dark/SignalsPath.js.map +2 -2
- package/src/in-the-dark/events.js.map +1 -1
- package/src/in-the-dark/importModule.d.ts +7 -0
- package/src/in-the-dark/importModule.d.ts.map +1 -1
- package/src/in-the-dark/importModule.js +5 -2
- package/src/in-the-dark/importModule.js.map +2 -2
- package/src/index.d.ts +7 -1
- package/src/index.d.ts.map +1 -1
- package/src/index.js +5 -1
- package/src/index.js.map +2 -2
- package/src/shadow-objects.d.ts +2 -0
- package/src/shadow-objects.d.ts.map +1 -1
- package/src/shadow-objects.js +2 -0
- package/src/shadow-objects.js.map +2 -2
- package/src/shadow-objects.worker.js +0 -1
- package/src/shadow-objects.worker.js.map +2 -2
- package/src/shae-ent.js.map +1 -1
- package/src/shae-prop.js +2 -2
- package/src/shae-prop.js.map +2 -2
- package/src/shae-worker.js.map +1 -1
- package/src/types.d.ts +35 -4
- package/src/types.d.ts.map +1 -1
- package/src/utils/ConsoleLogger.d.ts +45 -10
- package/src/utils/ConsoleLogger.d.ts.map +1 -1
- package/src/utils/ConsoleLogger.js +51 -26
- package/src/utils/ConsoleLogger.js.map +2 -2
- package/src/utils/FrameLoop.d.ts +42 -3
- package/src/utils/FrameLoop.d.ts.map +1 -1
- package/src/utils/FrameLoop.js +71 -10
- package/src/utils/FrameLoop.js.map +2 -2
- package/src/utils/MicrotaskCollector.d.ts +45 -0
- package/src/utils/MicrotaskCollector.d.ts.map +1 -0
- package/src/utils/MicrotaskCollector.js +33 -0
- package/src/utils/MicrotaskCollector.js.map +7 -0
- package/src/utils/MicrotaskGate.d.ts +17 -0
- package/src/utils/MicrotaskGate.d.ts.map +1 -0
- package/src/utils/MicrotaskGate.js +20 -0
- package/src/utils/MicrotaskGate.js.map +7 -0
- package/src/utils/array-utils.js.map +1 -1
- package/src/utils/attr-utils.d.ts +6 -0
- package/src/utils/attr-utils.d.ts.map +1 -1
- package/src/utils/attr-utils.js +3 -1
- package/src/utils/attr-utils.js.map +2 -2
- package/src/utils/constants.js.map +1 -1
- package/src/utils/generateUUID.d.ts +11 -0
- package/src/utils/generateUUID.d.ts.map +1 -1
- package/src/utils/generateUUID.js +31 -266
- package/src/utils/generateUUID.js.map +2 -2
- package/src/utils/props-utils.d.ts +3 -2
- package/src/utils/props-utils.d.ts.map +1 -1
- package/src/utils/props-utils.js +13 -24
- package/src/utils/props-utils.js.map +2 -2
- package/src/utils/runGuarded.d.ts +27 -0
- package/src/utils/runGuarded.d.ts.map +1 -0
- package/src/utils/runGuarded.js +11 -0
- package/src/utils/runGuarded.js.map +7 -0
- package/src/utils/toMaybe.d.ts.map +1 -1
- package/src/utils/toMaybe.js.map +1 -1
- package/src/utils/toNamespace.d.ts.map +1 -1
- package/src/utils/toNamespace.js.map +1 -1
- package/src/utils/toUrlString.d.ts +12 -0
- package/src/utils/toUrlString.d.ts.map +1 -1
- package/src/utils/toUrlString.js.map +2 -2
- package/src/utils/waitForMessageOfType.d.ts +12 -1
- package/src/utils/waitForMessageOfType.d.ts.map +1 -1
- package/src/utils/waitForMessageOfType.js +16 -1
- package/src/utils/waitForMessageOfType.js.map +2 -2
- package/src/view/ComponentChanges.d.ts +88 -1
- package/src/view/ComponentChanges.d.ts.map +1 -1
- package/src/view/ComponentChanges.js +243 -73
- package/src/view/ComponentChanges.js.map +2 -2
- package/src/view/ComponentContext.d.ts +206 -16
- package/src/view/ComponentContext.d.ts.map +1 -1
- package/src/view/ComponentContext.js +548 -91
- package/src/view/ComponentContext.js.map +2 -2
- package/src/view/ComponentMemory.d.ts +3 -3
- package/src/view/ComponentMemory.d.ts.map +1 -1
- package/src/view/ComponentMemory.js.map +1 -1
- package/src/view/IShadowObjectEnvProxy.d.ts +29 -3
- package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.d.ts +1 -1
- package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.js +8 -6
- package/src/view/LocalShadowObjectEnv.js.map +2 -2
- package/src/view/RemoteWorkerEnv.d.ts +89 -5
- package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
- package/src/view/RemoteWorkerEnv.js +195 -77
- package/src/view/RemoteWorkerEnv.js.map +2 -2
- package/src/view/ShadowEnv.d.ts +49 -2
- package/src/view/ShadowEnv.d.ts.map +1 -1
- package/src/view/ShadowEnv.js +223 -60
- package/src/view/ShadowEnv.js.map +2 -2
- package/src/view/ViewComponent.d.ts +78 -14
- package/src/view/ViewComponent.d.ts.map +1 -1
- package/src/view/ViewComponent.js +90 -14
- package/src/view/ViewComponent.js.map +2 -2
- package/src/view/cloneChangeTrail.js.map +2 -2
- package/src/worker/MessageRouter.d.ts +14 -0
- package/src/worker/MessageRouter.d.ts.map +1 -1
- package/src/worker/MessageRouter.js +75 -15
- package/src/worker/MessageRouter.js.map +2 -2
- package/src/worker/WorkerRuntime.d.ts +22 -1
- package/src/worker/WorkerRuntime.d.ts.map +1 -1
- package/src/worker/WorkerRuntime.js +60 -8
- package/src/worker/WorkerRuntime.js.map +2 -2
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
createEffect,
|
|
5
|
-
createMemo,
|
|
6
|
-
createSignal,
|
|
7
|
-
destroySignal,
|
|
8
|
-
isSignal,
|
|
9
|
-
link
|
|
10
|
-
} from "@spearwolf/signalize";
|
|
1
|
+
import { emitSafe, emitStrict, eventize, off, on } from "@spearwolf/eventize";
|
|
2
|
+
import { batch } from "@spearwolf/signalize";
|
|
3
|
+
import { ChangeTrailRefusedError } from "../ChangeTrailRefusedError.js";
|
|
11
4
|
import { ComponentChangeType, MessageToView } from "../constants.js";
|
|
5
|
+
import { EntityUuidInUseError } from "../EntityUuidInUseError.js";
|
|
12
6
|
import { ConsoleLogger } from "../utils/ConsoleLogger.js";
|
|
13
|
-
import {
|
|
7
|
+
import { runGuarded } from "../utils/runGuarded.js";
|
|
14
8
|
import { Entity } from "./Entity.js";
|
|
15
9
|
import { onCreate, onDestroy, onParentChanged, onViewEvent } from "./events.js";
|
|
16
10
|
import { Registry } from "./Registry.js";
|
|
11
|
+
import { ShadowObjectCreationScope } from "./ShadowObjectCreationScope.js";
|
|
17
12
|
import { SignalsPath } from "./SignalsPath.js";
|
|
18
13
|
var ShadowObjectAction = /* @__PURE__ */ ((ShadowObjectAction2) => {
|
|
19
14
|
ShadowObjectAction2[ShadowObjectAction2["CreateAndDestroy"] = 0] = "CreateAndDestroy";
|
|
@@ -22,37 +17,69 @@ var ShadowObjectAction = /* @__PURE__ */ ((ShadowObjectAction2) => {
|
|
|
22
17
|
return ShadowObjectAction2;
|
|
23
18
|
})(ShadowObjectAction || {});
|
|
24
19
|
const getDisplayName = (construct) => construct.displayName || construct.name;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
const LIFECYCLE_HOOKS = [
|
|
21
|
+
["onCreate", onCreate],
|
|
22
|
+
["onDestroy", onDestroy],
|
|
23
|
+
["onParentChanged", onParentChanged],
|
|
24
|
+
["onViewEvent", onViewEvent]
|
|
25
|
+
];
|
|
30
26
|
class Kernel {
|
|
27
|
+
#logger = new ConsoleLogger("Kernel");
|
|
28
|
+
/** The logger this Kernel reports through. */
|
|
29
|
+
get logger() {
|
|
30
|
+
return this.#logger;
|
|
31
|
+
}
|
|
32
|
+
#entities = /* @__PURE__ */ new Map();
|
|
33
|
+
#rootEntities = /* @__PURE__ */ new Set();
|
|
34
|
+
#allEntities = [];
|
|
35
|
+
#allEntitiesReversed = [];
|
|
36
|
+
#allEntitiesNeedUpdate = true;
|
|
37
|
+
#rootContexts = /* @__PURE__ */ new Map();
|
|
38
|
+
/**
|
|
39
|
+
* The creation scope of a shadow-object, keyed by the shadow-object itself.
|
|
40
|
+
*
|
|
41
|
+
* `#destroyShadowObject()` needs to reach the scope from the outside when a shadow-object leaves the
|
|
42
|
+
* constructor set of an entity that stays alive. The scope removes its own entry when it tears down,
|
|
43
|
+
* so nothing here outlives the shadow-object it belongs to.
|
|
44
|
+
*
|
|
45
|
+
* The key is the shadow-object instance, which assumes one instance per construction. A
|
|
46
|
+
* constructor handing out the same instance twice — to a second entity, or to the same one
|
|
47
|
+
* under another token — leaves only the later scope reachable from here; the earlier one
|
|
48
|
+
* still tears down when its entity is destroyed.
|
|
49
|
+
*/
|
|
50
|
+
#shadowObjectScopes = /* @__PURE__ */ new WeakMap();
|
|
51
|
+
// The member names whose deprecation report this kernel has already made, one entry each. Handed
|
|
52
|
+
// to every creation scope this kernel builds, which is what gives the report the lifetime of the
|
|
53
|
+
// kernel rather than that of this module: an application running two shadow environments would
|
|
54
|
+
// otherwise report the deprecated call form to whichever of them got there first and to no other.
|
|
55
|
+
// One entry per name rather than a single flag, because a flag would swallow the reports of the
|
|
56
|
+
// four members that come after the first. `destroy()` leaves the set alone -- a kernel that has
|
|
57
|
+
// said it once has said it.
|
|
58
|
+
#shownDeprecations = /* @__PURE__ */ new Set();
|
|
31
59
|
constructor(registry) {
|
|
32
|
-
this.logger = new ConsoleLogger("Kernel");
|
|
33
|
-
this.#entities = /* @__PURE__ */ new Map();
|
|
34
|
-
this.#rootEntities = /* @__PURE__ */ new Set();
|
|
35
|
-
this.#allEntities = [];
|
|
36
|
-
this.#allEntitiesReversed = [];
|
|
37
|
-
this.#allEntitiesNeedUpdate = true;
|
|
38
|
-
this.#rootContexts = /* @__PURE__ */ new Map();
|
|
39
60
|
eventize(this);
|
|
40
61
|
this.registry = Registry.get(registry);
|
|
41
62
|
}
|
|
42
|
-
#entities;
|
|
43
|
-
#rootEntities;
|
|
44
|
-
#allEntities;
|
|
45
|
-
#allEntitiesReversed;
|
|
46
|
-
#allEntitiesNeedUpdate;
|
|
47
|
-
#rootContexts;
|
|
48
63
|
getEntity(uuid) {
|
|
49
64
|
return this.#requireEntry(uuid).entity;
|
|
50
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The entity behind a uuid, or `undefined` when the kernel does not hold one.
|
|
68
|
+
*
|
|
69
|
+
* The counterpart to `getEntity()`, which throws. Which of the two a caller wants follows from what an
|
|
70
|
+
* absent entity means to it: a change that describes the entity tree -- a new parent, a new order, new
|
|
71
|
+
* properties -- names an entity the view believes to be there, and a uuid the kernel does not know is a
|
|
72
|
+
* disagreement the caller has to hear about. An event is the other case: it carries no structure, and the
|
|
73
|
+
* entity it was meant for may have been torn down between the two sides.
|
|
74
|
+
*/
|
|
75
|
+
findEntity(uuid) {
|
|
76
|
+
return this.#entities.get(uuid)?.entity;
|
|
77
|
+
}
|
|
51
78
|
/**
|
|
52
79
|
* The entry for an entity that is expected to exist.
|
|
53
80
|
*
|
|
54
81
|
* The uuid is the caller's responsibility: `getEntity()` hands the throw on to its own
|
|
55
|
-
* callers, and
|
|
82
|
+
* callers, and `#updateShadowObjects()` is reached only for an entity the caller has just
|
|
56
83
|
* confirmed. Failing here names the uuid instead of failing later on an undefined field.
|
|
57
84
|
*/
|
|
58
85
|
#requireEntry(uuid) {
|
|
@@ -65,14 +92,42 @@ class Kernel {
|
|
|
65
92
|
hasEntity(uuid) {
|
|
66
93
|
return this.#entities.has(uuid);
|
|
67
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* How many entities the kernel is holding, one count per store, read without waiting on a garbage
|
|
97
|
+
* collector: a test can destroy a kernel and see that it let go of every one of them, rather than
|
|
98
|
+
* inferring it from what a collector happened to do. The two traversal counts are the cached
|
|
99
|
+
* breadth-first order and its reverse.
|
|
100
|
+
*
|
|
101
|
+
* @internal
|
|
102
|
+
*/
|
|
103
|
+
get debugEntityCounts() {
|
|
104
|
+
return {
|
|
105
|
+
entities: this.#entities.size,
|
|
106
|
+
rootEntities: this.#rootEntities.size,
|
|
107
|
+
traversal: this.#allEntities.length,
|
|
108
|
+
traversalReversed: this.#allEntitiesReversed.length
|
|
109
|
+
};
|
|
110
|
+
}
|
|
68
111
|
/**
|
|
69
112
|
* @returns all entities in breadth-first order
|
|
113
|
+
*
|
|
114
|
+
* The list belongs to the caller: sorting or reversing it changes nothing about what the next call
|
|
115
|
+
* hands out. The walk behind it visits every entity once, which also carries it over a children list
|
|
116
|
+
* that points back at an ancestor -- `Entity.addChild()` writes such a list without touching the parent
|
|
117
|
+
* link, so no check along the parent chain can cover it.
|
|
118
|
+
*
|
|
119
|
+
* A uuid the kernel no longer holds drops out, and what hangs below it with it -- `getEntityGraph()`
|
|
120
|
+
* walks the same tree by the same rule.
|
|
70
121
|
*/
|
|
71
122
|
traverseLevelOrderBFS(reverse = false) {
|
|
72
123
|
if (this.#allEntitiesNeedUpdate) {
|
|
73
124
|
const lvl = /* @__PURE__ */ new Map();
|
|
125
|
+
const visited = /* @__PURE__ */ new Set();
|
|
74
126
|
const traverse = (uuid, depth) => {
|
|
75
|
-
|
|
127
|
+
if (visited.has(uuid)) return;
|
|
128
|
+
visited.add(uuid);
|
|
129
|
+
const e = this.findEntity(uuid);
|
|
130
|
+
if (e === void 0) return;
|
|
76
131
|
const entities = lvl.get(depth);
|
|
77
132
|
if (entities) {
|
|
78
133
|
entities.push(e);
|
|
@@ -90,46 +145,84 @@ class Kernel {
|
|
|
90
145
|
this.#allEntitiesReversed = this.#allEntities.slice().reverse();
|
|
91
146
|
this.#allEntitiesNeedUpdate = false;
|
|
92
147
|
}
|
|
93
|
-
return reverse ? this.#allEntitiesReversed : this.#allEntities;
|
|
148
|
+
return reverse ? this.#allEntitiesReversed.slice() : this.#allEntities.slice();
|
|
94
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* The entity tree as nested nodes, one node per entity. The visited set is what makes that count
|
|
152
|
+
* hold: a children list that points back at an ancestor would otherwise be walked forever, and this
|
|
153
|
+
* is the one traversal a caller reaches for while debugging a tree that is already broken. A child
|
|
154
|
+
* the walk leaves out is named at its parent under `omittedChildren`, with the reason -- a uuid the
|
|
155
|
+
* kernel does not hold, or one the walk has already placed elsewhere in the graph. A root the walk
|
|
156
|
+
* has already reached through another root's children falls off the top level without a note; it
|
|
157
|
+
* still stands in the graph, just at the other position.
|
|
158
|
+
*/
|
|
95
159
|
getEntityGraph() {
|
|
96
|
-
|
|
160
|
+
const visited = /* @__PURE__ */ new Set();
|
|
161
|
+
return Array.from(this.#rootEntities).map((uuid) => this.#getEntityGraphNode(uuid, visited)).filter((node) => node !== void 0);
|
|
97
162
|
}
|
|
98
|
-
getEntityGraphNode(uuid) {
|
|
163
|
+
#getEntityGraphNode(uuid, visited, omitted) {
|
|
99
164
|
const entry = this.#entities.get(uuid);
|
|
100
|
-
if (entry === void 0)
|
|
165
|
+
if (entry === void 0) {
|
|
166
|
+
omitted?.push({ uuid, reason: "not-in-kernel" });
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
if (visited.has(uuid)) {
|
|
170
|
+
omitted?.push({ uuid, reason: "already-in-graph" });
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
visited.add(uuid);
|
|
101
174
|
const { token, entity } = entry;
|
|
175
|
+
const omittedChildren = [];
|
|
176
|
+
const children = entity.children.map((child) => this.#getEntityGraphNode(child.uuid, visited, omittedChildren)).filter((node) => node !== void 0);
|
|
102
177
|
return {
|
|
103
178
|
token,
|
|
104
179
|
entity,
|
|
105
180
|
props: Object.fromEntries(entity.propEntries()),
|
|
106
|
-
|
|
107
|
-
|
|
181
|
+
children,
|
|
182
|
+
...omittedChildren.length > 0 ? { omittedChildren } : {}
|
|
108
183
|
};
|
|
109
184
|
}
|
|
110
185
|
upgradeEntities() {
|
|
111
186
|
const entityConstructors = /* @__PURE__ */ new Map();
|
|
112
187
|
for (const entity of this.traverseLevelOrderBFS(true)) {
|
|
113
188
|
if (!this.hasEntity(entity.uuid)) continue;
|
|
114
|
-
entityConstructors.set(entity.uuid, this
|
|
189
|
+
entityConstructors.set(entity.uuid, this.#updateShadowObjects(entity.uuid, 2 /* DestroyOnly */));
|
|
115
190
|
}
|
|
116
191
|
for (const entity of this.traverseLevelOrderBFS(false)) {
|
|
117
192
|
if (!this.hasEntity(entity.uuid)) continue;
|
|
118
|
-
this
|
|
193
|
+
this.#updateShadowObjects(entity.uuid, 1 /* JustCreate */, entityConstructors.get(entity.uuid));
|
|
119
194
|
}
|
|
120
195
|
entityConstructors.clear();
|
|
121
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Apply a change trail, entry by entry, and say how far it got if it cannot be applied in full.
|
|
199
|
+
*
|
|
200
|
+
* The loop ends at the entry that throws, so what the kernel holds afterwards is a prefix of the
|
|
201
|
+
* trail; the {@link ChangeTrailRefusedError} names its length. The counter sits behind
|
|
202
|
+
* `#parse` and therefore counts only entries that returned normally.
|
|
203
|
+
*
|
|
204
|
+
* An effect the batch defers belongs to no single entry: the batch releases it once the loop is
|
|
205
|
+
* through, and a throw from there arrives with every entry already counted. `appliedCount` then
|
|
206
|
+
* equals `entryCount` — everything was applied, something failed nevertheless, and both
|
|
207
|
+
* statements are true at once.
|
|
208
|
+
*
|
|
209
|
+
* @throws {ChangeTrailRefusedError} if any entry, or any effect the batch deferred, threw
|
|
210
|
+
*/
|
|
122
211
|
run(event) {
|
|
123
|
-
|
|
124
|
-
|
|
212
|
+
this.logger.debug("sync", event);
|
|
213
|
+
let appliedCount = 0;
|
|
214
|
+
try {
|
|
215
|
+
batch(() => {
|
|
216
|
+
for (const entry of event.changeTrail) {
|
|
217
|
+
this.#parse(entry);
|
|
218
|
+
appliedCount++;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw new ChangeTrailRefusedError(appliedCount, event.changeTrail.length, { cause: error });
|
|
125
223
|
}
|
|
126
|
-
batch(() => {
|
|
127
|
-
for (const entry of event.changeTrail) {
|
|
128
|
-
this.parse(entry);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
224
|
}
|
|
132
|
-
parse(entry) {
|
|
225
|
+
#parse(entry) {
|
|
133
226
|
switch (entry.type) {
|
|
134
227
|
case ComponentChangeType.CreateEntities:
|
|
135
228
|
this.createEntity(
|
|
@@ -140,19 +233,15 @@ class Kernel {
|
|
|
140
233
|
entry.properties,
|
|
141
234
|
entry.autoDestructionOnParentRemoval
|
|
142
235
|
);
|
|
143
|
-
this.#allEntitiesNeedUpdate = true;
|
|
144
236
|
break;
|
|
145
237
|
case ComponentChangeType.DestroyEntities:
|
|
146
238
|
this.destroyEntity(entry.uuid);
|
|
147
|
-
this.#allEntitiesNeedUpdate = true;
|
|
148
239
|
break;
|
|
149
240
|
case ComponentChangeType.SetParent:
|
|
150
241
|
this.setParent(entry.uuid, entry.parentUuid, entry.order);
|
|
151
|
-
this.#allEntitiesNeedUpdate = true;
|
|
152
242
|
break;
|
|
153
243
|
case ComponentChangeType.UpdateOrder:
|
|
154
244
|
this.updateOrder(entry.uuid, entry.order);
|
|
155
|
-
this.#allEntitiesNeedUpdate = true;
|
|
156
245
|
break;
|
|
157
246
|
case ComponentChangeType.ChangeProperties:
|
|
158
247
|
this.changeProperties(entry.uuid, entry.properties);
|
|
@@ -166,41 +255,94 @@ class Kernel {
|
|
|
166
255
|
}
|
|
167
256
|
}
|
|
168
257
|
createEntity(uuid, token, parentUuid, order = 0, properties, autoDestructionOnParentRemoval = false) {
|
|
258
|
+
if (this.#entities.has(uuid)) {
|
|
259
|
+
throw new EntityUuidInUseError(uuid);
|
|
260
|
+
}
|
|
169
261
|
const e = new Entity(this, uuid);
|
|
170
262
|
e.order = order;
|
|
171
263
|
const entry = { token, entity: e, usedConstructors: /* @__PURE__ */ new Map() };
|
|
172
264
|
this.#entities.set(uuid, entry);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
265
|
+
this.noteEntityTreeChange(uuid);
|
|
266
|
+
try {
|
|
267
|
+
if (parentUuid) {
|
|
268
|
+
e.parentUuid = parentUuid;
|
|
269
|
+
}
|
|
270
|
+
e.autoDestructionOnParentRemoval = autoDestructionOnParentRemoval;
|
|
271
|
+
if (properties) {
|
|
272
|
+
e.setProperties(properties);
|
|
273
|
+
}
|
|
274
|
+
this.#createShadowObjects(entry);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.#rollbackFailedCreation(uuid);
|
|
277
|
+
throw error;
|
|
182
278
|
}
|
|
183
|
-
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Takes back what a creation managed before it failed. The teardown is the regular one, so a
|
|
282
|
+
* shadow-object that already stands hears its `onDestroy` and its creation scope ends the way
|
|
283
|
+
* it would on any other destruction; the entry, the root registration and the link to the
|
|
284
|
+
* parent go with it. `destroyEntity()` clears the bookkeeping of the entity whatever a callback
|
|
285
|
+
* along the way does, so there is nothing left here to take back by hand; only a failure of the
|
|
286
|
+
* teardown itself is reported, because the creation error is the one the caller is waiting for.
|
|
287
|
+
*
|
|
288
|
+
* The rollback reaches this one entity, not the kernel around it. What a constructor did to other
|
|
289
|
+
* entities before it threw stands, and the teardown even adds to it, because `destroyEntity()` walks
|
|
290
|
+
* the children list: an entity the constructor hung under this one survives as a root, and one it
|
|
291
|
+
* moved under this one is taken off the parent it came from -- or destroyed outright, where it
|
|
292
|
+
* carries `autoDestructionOnParentRemoval`. Both leave an entity in a state that is neither the one
|
|
293
|
+
* before the call nor the one the constructor built. A rollback that covers those cases needs a
|
|
294
|
+
* snapshot of the kernel, which this path does not take.
|
|
295
|
+
*/
|
|
296
|
+
#rollbackFailedCreation(uuid) {
|
|
297
|
+
runGuarded(
|
|
298
|
+
this.logger,
|
|
299
|
+
() => this.destroyEntity(uuid),
|
|
300
|
+
"rollback of a failed entity creation could not destroy the entity:",
|
|
301
|
+
uuid
|
|
302
|
+
);
|
|
184
303
|
}
|
|
185
304
|
destroyEntity(uuid) {
|
|
186
305
|
const entry = this.#entities.get(uuid);
|
|
187
306
|
if (entry === void 0) return;
|
|
188
307
|
const { entity, usedConstructors } = entry;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
308
|
+
try {
|
|
309
|
+
const childrenSnapshot = [...entity.children];
|
|
310
|
+
for (const child of childrenSnapshot) {
|
|
311
|
+
runGuarded(
|
|
312
|
+
this.logger,
|
|
313
|
+
() => {
|
|
314
|
+
if (child.autoDestructionOnParentRemoval) {
|
|
315
|
+
this.destroyEntity(child.uuid);
|
|
316
|
+
} else {
|
|
317
|
+
child.removeFromParent();
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
"child of a destroyed entity could not be handed on:",
|
|
321
|
+
child.uuid
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
runGuarded(this.logger, () => entity.removeFromParent(), "entity could not be detached from its parent:", entity.uuid);
|
|
325
|
+
const shadowObjects = this.findShadowObjects(entity.uuid);
|
|
326
|
+
for (const shadowObject of shadowObjects) {
|
|
327
|
+
off(entity, shadowObject);
|
|
328
|
+
this.#notifyShadowObjectDestroy(shadowObject, entity);
|
|
196
329
|
}
|
|
330
|
+
runGuarded(this.logger, () => emitStrict(entity, onDestroy, entity), "entity onDestroy notification failed:", entity.uuid);
|
|
331
|
+
for (const shadowObject of shadowObjects) {
|
|
332
|
+
runGuarded(
|
|
333
|
+
this.logger,
|
|
334
|
+
() => this.#shadowObjectScopes.get(shadowObject)?.tearDown(),
|
|
335
|
+
"creation scope teardown of a destroyed entity failed:",
|
|
336
|
+
entity.uuid
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
runGuarded(this.logger, () => entity[onDestroy](), "entity release failed:", entity.uuid);
|
|
340
|
+
} finally {
|
|
341
|
+
usedConstructors.clear();
|
|
342
|
+
this.#entities.delete(entity.uuid);
|
|
343
|
+
this.#rootEntities.delete(entity.uuid);
|
|
344
|
+
this.#allEntitiesNeedUpdate = true;
|
|
197
345
|
}
|
|
198
|
-
entity.removeFromParent();
|
|
199
|
-
emit(entity, onDestroy, entity);
|
|
200
|
-
usedConstructors.clear();
|
|
201
|
-
this.#entities.delete(entity.uuid);
|
|
202
|
-
this.#rootEntities.delete(entity.uuid);
|
|
203
|
-
this.#allEntitiesNeedUpdate = true;
|
|
204
346
|
}
|
|
205
347
|
/**
|
|
206
348
|
* @param order the new order, or `undefined` to keep the current one. A set-parent change
|
|
@@ -214,59 +356,93 @@ class Kernel {
|
|
|
214
356
|
if (parentUuid && !this.#entities.has(parentUuid)) {
|
|
215
357
|
throw new Error(`entity with uuid "${parentUuid}" not found!`);
|
|
216
358
|
}
|
|
217
|
-
|
|
359
|
+
if (parentUuid) {
|
|
360
|
+
e.assertAttachableTo(this.getEntity(parentUuid));
|
|
361
|
+
}
|
|
362
|
+
e.removeFromParent(parentUuid == null);
|
|
218
363
|
e.order = nextOrder;
|
|
219
364
|
e.parentUuid = parentUuid;
|
|
220
|
-
|
|
365
|
+
this.logger.debug("entity.onParentChanged", { uuid, parentUuid, order: nextOrder, entity: e });
|
|
366
|
+
runGuarded(this.logger, () => emitStrict(e, onParentChanged, e), "entity onParentChanged notification failed:", uuid);
|
|
367
|
+
}
|
|
368
|
+
updateOrder(uuid, order) {
|
|
369
|
+
this.getEntity(uuid).order = order;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Records that an entity has changed its place in the entity tree: it took a parent, lost one,
|
|
373
|
+
* or moved among its siblings. Two pieces of bookkeeping follow from that -- the cached
|
|
374
|
+
* traversal is dropped, and an entity without a parent joins the set of root entities while one
|
|
375
|
+
* with a parent leaves it.
|
|
376
|
+
*
|
|
377
|
+
* `Entity` reports here from every write that moves it, because those writes are reachable from
|
|
378
|
+
* a shadow-object with no kernel call in between: `entity.parent`, `entity.parentUuid`,
|
|
379
|
+
* `entity.order` and `entity.removeFromParent()`. The state is read off the entity rather than
|
|
380
|
+
* taken as an argument, so a caller cannot describe a move the entity did not make.
|
|
381
|
+
*
|
|
382
|
+
* A uuid the kernel does not hold reaches the cache and stops there: an entity joins the root
|
|
383
|
+
* set when the kernel takes it in and leaves it when the kernel lets go.
|
|
384
|
+
*
|
|
385
|
+
* That such a uuid passes in silence, where `setParent()` and `updateOrder()` throw at one, is the
|
|
386
|
+
* difference between a request and a report. Those two are asked to move an entity and cannot do it
|
|
387
|
+
* without one, so an unknown uuid is a disagreement the caller has to hear about. This is an entity
|
|
388
|
+
* saying where it stands, and it says so while the kernel is still taking it in: `createEntity()`
|
|
389
|
+
* writes the order on the entity before the entry is registered, so an entity created with an order
|
|
390
|
+
* of its own reports once before the kernel holds it. A throw there would cost the creation its own
|
|
391
|
+
* bookkeeping, and there is nothing to disagree about -- dropping the cache is right either way.
|
|
392
|
+
*/
|
|
393
|
+
noteEntityTreeChange(uuid) {
|
|
394
|
+
this.#allEntitiesNeedUpdate = true;
|
|
395
|
+
const entry = this.#entities.get(uuid);
|
|
396
|
+
if (entry === void 0) return;
|
|
397
|
+
if (entry.entity.hasParent) {
|
|
221
398
|
this.#rootEntities.delete(uuid);
|
|
222
399
|
} else {
|
|
223
400
|
this.#rootEntities.add(uuid);
|
|
224
401
|
}
|
|
225
|
-
e.reSubscribeToParentContexts();
|
|
226
|
-
queueMicrotask(() => {
|
|
227
|
-
if (this.logger.isDebug) {
|
|
228
|
-
this.logger.debug("entity.onParentChanged", { uuid, parentUuid, order: nextOrder, entity: e });
|
|
229
|
-
}
|
|
230
|
-
emit(e, onParentChanged, e);
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
updateOrder(uuid, order) {
|
|
234
|
-
this.getEntity(uuid).order = order;
|
|
235
402
|
}
|
|
236
403
|
dispatchEventsToEntity(uuid, events) {
|
|
237
|
-
this.
|
|
404
|
+
this.findEntity(uuid)?.dispatchViewEvents(events);
|
|
238
405
|
}
|
|
239
406
|
changeProperties(uuid, properties) {
|
|
240
407
|
this.getEntity(uuid).setProperties(properties);
|
|
241
|
-
this
|
|
408
|
+
this.#updateShadowObjects(uuid);
|
|
242
409
|
}
|
|
243
410
|
changeToken(uuid, token) {
|
|
244
411
|
const entry = this.#entities.get(uuid);
|
|
245
412
|
if (entry === void 0) return;
|
|
246
413
|
if (entry.token === token) return;
|
|
414
|
+
const previousToken = entry.token;
|
|
247
415
|
entry.token = token;
|
|
248
|
-
this
|
|
416
|
+
this.#updateShadowObjects(uuid, 0 /* CreateAndDestroy */, void 0, previousToken);
|
|
249
417
|
}
|
|
250
418
|
dispatchMessageToView(message) {
|
|
251
419
|
queueMicrotask(() => {
|
|
252
|
-
|
|
420
|
+
emitSafe(this, MessageToView, message);
|
|
253
421
|
});
|
|
254
422
|
}
|
|
255
423
|
/**
|
|
256
424
|
* Create or destroy the shadow-objects of an entity using the registered constructors.
|
|
257
425
|
* After a token change or registry changes, an entity may be given different shadow-objects.
|
|
426
|
+
*
|
|
427
|
+
* @param previousToken the token the entry carried before the caller wrote the new one, or
|
|
428
|
+
* `undefined` where the caller left the token alone. A rebuild that does not get through puts it
|
|
429
|
+
* back before it restores the shadow-objects, so an entity is never built against a token it does
|
|
430
|
+
* not carry.
|
|
258
431
|
*/
|
|
259
|
-
updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors) {
|
|
432
|
+
#updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors, previousToken) {
|
|
260
433
|
const entry = this.#requireEntry(uuid);
|
|
261
434
|
nextConstructors ??= new Set(this.registry.findConstructors(entry.token, entry.entity.truthyProps()));
|
|
262
435
|
const shouldDestroy = action === 0 /* CreateAndDestroy */ || action === 2 /* DestroyOnly */;
|
|
263
436
|
const shouldCreate = action === 0 /* CreateAndDestroy */ || action === 1 /* JustCreate */;
|
|
437
|
+
const removed = [];
|
|
438
|
+
const created = [];
|
|
264
439
|
if (shouldDestroy) {
|
|
265
440
|
for (const [construct, shadowObjects] of entry.usedConstructors) {
|
|
266
441
|
if (!nextConstructors.has(construct)) {
|
|
267
442
|
entry.usedConstructors.delete(construct);
|
|
443
|
+
removed.push({ construct, count: shadowObjects.size });
|
|
268
444
|
for (const obj of shadowObjects) {
|
|
269
|
-
this
|
|
445
|
+
this.#destroyShadowObject(obj, entry.entity);
|
|
270
446
|
}
|
|
271
447
|
}
|
|
272
448
|
}
|
|
@@ -274,304 +450,110 @@ class Kernel {
|
|
|
274
450
|
if (shouldCreate) {
|
|
275
451
|
for (const construct of nextConstructors) {
|
|
276
452
|
if (!entry.usedConstructors.has(construct)) {
|
|
277
|
-
|
|
453
|
+
try {
|
|
454
|
+
created.push(this.#constructShadowObject(construct, entry));
|
|
455
|
+
} catch (error) {
|
|
456
|
+
this.#rollbackFailedShadowObjectUpdate(entry, created, removed, previousToken);
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
278
459
|
}
|
|
279
460
|
}
|
|
280
461
|
}
|
|
281
462
|
return nextConstructors;
|
|
282
463
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Takes a rebuild of the shadow-objects of one entity back to where the call found it.
|
|
466
|
+
*
|
|
467
|
+
* The identity check comes first, because a constructor may destroy the entity it is coming to life
|
|
468
|
+
* on. Where the kernel no longer holds this entry -- including where another entry stands under the
|
|
469
|
+
* same uuid by now -- `destroyEntity()` has already notified every shadow-object of the entry and
|
|
470
|
+
* cleared `usedConstructors`; both halves of the rollback would find nothing to work with.
|
|
471
|
+
*
|
|
472
|
+
* The token before the shadow-objects: what is rebuilt is built against the token the entity carries
|
|
473
|
+
* afterwards.
|
|
474
|
+
*
|
|
475
|
+
* Backwards down, forwards up: the new shadow-objects leave before the previous ones come back, so
|
|
476
|
+
* the two sets never stand on the entity at the same time.
|
|
477
|
+
*
|
|
478
|
+
* Every step behind a guard of its own and nothing re-thrown -- the caller is waiting for the error
|
|
479
|
+
* of the construction, not for one from the way back.
|
|
480
|
+
*
|
|
481
|
+
* What the rollback does not reach is named rather than passed over. `changeProperties()` has written its
|
|
482
|
+
* properties before it gets here, and they stay written -- a constructor restored below can therefore
|
|
483
|
+
* stand on an entity whose properties no longer route to it, until the next re-resolution of the set
|
|
484
|
+
* leaves it out and takes it down. And `upgradeEntities()` takes its shadow-objects down in one pass over
|
|
485
|
+
* the entity tree and builds in the next, so a rollback in the second pass knows nothing of what the first
|
|
486
|
+
* one took -- at this entity as much as at any other; the entity keeps the token it came in with, an
|
|
487
|
+
* upgrade having written none, and is left with whatever the first pass spared. Both would need a snapshot
|
|
488
|
+
* of the kernel, which this path does not take.
|
|
489
|
+
*/
|
|
490
|
+
#rollbackFailedShadowObjectUpdate(entry, created, removed, previousToken) {
|
|
491
|
+
if (this.#entities.get(entry.entity.uuid) !== entry) return;
|
|
492
|
+
if (previousToken !== void 0) {
|
|
493
|
+
entry.token = previousToken;
|
|
494
|
+
}
|
|
495
|
+
for (let i = created.length - 1; i >= 0; i--) {
|
|
496
|
+
runGuarded(
|
|
497
|
+
this.logger,
|
|
498
|
+
() => this.#destroyShadowObject(created[i], entry.entity),
|
|
499
|
+
"rollback of a failed shadow-object update could not remove a new shadow-object:",
|
|
500
|
+
entry.entity.uuid
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
for (const { construct, count } of removed) {
|
|
504
|
+
for (let i = 0; i < count; i++) {
|
|
505
|
+
runGuarded(
|
|
506
|
+
this.logger,
|
|
507
|
+
() => this.#constructShadowObject(construct, entry),
|
|
508
|
+
"rollback of a failed shadow-object update could not restore a shadow-object:",
|
|
509
|
+
getDisplayName(construct),
|
|
510
|
+
entry.entity.uuid
|
|
312
511
|
);
|
|
313
512
|
}
|
|
314
|
-
return propReader;
|
|
315
|
-
};
|
|
316
|
-
const shadowObject = eventize(
|
|
317
|
-
new construct({
|
|
318
|
-
entity: entry.entity,
|
|
319
|
-
provideContext(name, sourceOrInitialValue, options) {
|
|
320
|
-
if (!provideContextOptionsDeprecatedShown && options != null && typeof options === "function") {
|
|
321
|
-
console.warn(
|
|
322
|
-
'[shadow-objects] Deprecation Warning: The "isEqual" option of "provideContext()" is now passed as {compare} argument. Please update your code accordingly.'
|
|
323
|
-
);
|
|
324
|
-
provideContextOptionsDeprecatedShown = true;
|
|
325
|
-
}
|
|
326
|
-
const opts = typeof options === "function" ? { compare: options } : options;
|
|
327
|
-
let ctxProvider = contextProviders.get(name);
|
|
328
|
-
if (ctxProvider == null) {
|
|
329
|
-
const isSig = isSignal(sourceOrInitialValue);
|
|
330
|
-
const initialValue = isSig ? void 0 : toMaybe(sourceOrInitialValue);
|
|
331
|
-
ctxProvider = createSignal(initialValue, opts?.compare ? { compare: opts.compare } : void 0);
|
|
332
|
-
if (isSig) {
|
|
333
|
-
const ln2 = link(sourceOrInitialValue, ctxProvider);
|
|
334
|
-
unsubscribeSecondary.add(ln2.destroy.bind(ln2));
|
|
335
|
-
}
|
|
336
|
-
const ln = link(ctxProvider, entry.entity.provideContext(name));
|
|
337
|
-
unsubscribeSecondary.add(ln.destroy.bind(ln));
|
|
338
|
-
contextProviders.set(name, ctxProvider);
|
|
339
|
-
}
|
|
340
|
-
if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
|
|
341
|
-
unsubscribeSecondary.add(() => {
|
|
342
|
-
ctxProvider.set(void 0);
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
return ctxProvider;
|
|
346
|
-
},
|
|
347
|
-
provideGlobalContext(name, sourceOrInitialValue, options) {
|
|
348
|
-
if (!provideGlobalContextOptionsDeprecatedShown && options != null && typeof options === "function") {
|
|
349
|
-
console.warn(
|
|
350
|
-
'[shadow-objects] Deprecation Warning: The "isEqual" option of "provideGlobalContext()" is now passed as {compare} argument. Please update your code accordingly.'
|
|
351
|
-
);
|
|
352
|
-
provideGlobalContextOptionsDeprecatedShown = true;
|
|
353
|
-
}
|
|
354
|
-
const opts = typeof options === "function" ? { compare: options } : options;
|
|
355
|
-
let ctxProvider = contextRootProviders.get(name);
|
|
356
|
-
if (ctxProvider == null) {
|
|
357
|
-
const isSig = isSignal(sourceOrInitialValue);
|
|
358
|
-
const initialValue = isSig ? void 0 : toMaybe(sourceOrInitialValue);
|
|
359
|
-
ctxProvider = createSignal(initialValue, opts?.compare ? { compare: opts.compare } : void 0);
|
|
360
|
-
if (isSig) {
|
|
361
|
-
const ln2 = link(sourceOrInitialValue, ctxProvider);
|
|
362
|
-
unsubscribeSecondary.add(ln2.destroy.bind(ln2));
|
|
363
|
-
}
|
|
364
|
-
const ln = link(ctxProvider, entry.entity.provideGlobalContext(name));
|
|
365
|
-
unsubscribeSecondary.add(ln.destroy.bind(ln));
|
|
366
|
-
contextRootProviders.set(name, ctxProvider);
|
|
367
|
-
}
|
|
368
|
-
if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
|
|
369
|
-
unsubscribeSecondary.add(() => {
|
|
370
|
-
ctxProvider.set(void 0);
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
return ctxProvider;
|
|
374
|
-
},
|
|
375
|
-
useContext(name, options) {
|
|
376
|
-
if (!useContextOptionsDeprecatedShown && options != null && typeof options === "function") {
|
|
377
|
-
console.warn(
|
|
378
|
-
'[shadow-objects] Deprecation Warning: The "isEqual" option of "useContext()" is now passed as {compare} argument. Please update your code accordingly.'
|
|
379
|
-
);
|
|
380
|
-
useContextOptionsDeprecatedShown = true;
|
|
381
|
-
}
|
|
382
|
-
const opts = typeof options === "function" ? { compare: options } : options;
|
|
383
|
-
let ctxReader = contextReaders.get(name);
|
|
384
|
-
if (ctxReader === void 0) {
|
|
385
|
-
ctxReader = createSignal(void 0, opts).get;
|
|
386
|
-
contextReaders.set(name, ctxReader);
|
|
387
|
-
contextReaderCompares.set(name, opts?.compare);
|
|
388
|
-
const ln = link(entry.entity.useContext(name), ctxReader);
|
|
389
|
-
unsubscribeSecondary.add(ln.destroy.bind(ln));
|
|
390
|
-
} else if (opts?.compare != null && contextReaderCompares.get(name) !== opts.compare) {
|
|
391
|
-
console.warn(
|
|
392
|
-
`[shadow-objects] useContext("${String(name)}"): the cached signal already exists with a different (or no) {compare} function \u2014 the new options are ignored. Pass options only on the first call per context.`
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
return ctxReader;
|
|
396
|
-
},
|
|
397
|
-
useParentContext(name, options) {
|
|
398
|
-
if (!useParentContextOptionsDeprecatedShown && options != null && typeof options === "function") {
|
|
399
|
-
console.warn(
|
|
400
|
-
'[shadow-objects] Deprecation Warning: The "isEqual" option of "useParentContext()" is now passed as {compare} argument. Please update your code accordingly.'
|
|
401
|
-
);
|
|
402
|
-
useParentContextOptionsDeprecatedShown = true;
|
|
403
|
-
}
|
|
404
|
-
const opts = typeof options === "function" ? { compare: options } : options;
|
|
405
|
-
let ctxReader = contextParentReaders.get(name);
|
|
406
|
-
if (ctxReader === void 0) {
|
|
407
|
-
ctxReader = createSignal(void 0, opts).get;
|
|
408
|
-
contextParentReaders.set(name, ctxReader);
|
|
409
|
-
contextParentReaderCompares.set(name, opts?.compare);
|
|
410
|
-
const ln = link(entry.entity.useParentContext(name), ctxReader);
|
|
411
|
-
unsubscribeSecondary.add(ln.destroy.bind(ln));
|
|
412
|
-
} else if (opts?.compare != null && contextParentReaderCompares.get(name) !== opts.compare) {
|
|
413
|
-
console.warn(
|
|
414
|
-
`[shadow-objects] useParentContext("${String(name)}"): the cached signal already exists with a different (or no) {compare} function \u2014 the new options are ignored. Pass options only on the first call per parent context.`
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
return ctxReader;
|
|
418
|
-
},
|
|
419
|
-
dispatchMessageToView(type, data, transferables, traverseChildren = false) {
|
|
420
|
-
entry.entity.dispatchMessageToView(type, data, transferables, traverseChildren);
|
|
421
|
-
},
|
|
422
|
-
useProperty: getUseProperty,
|
|
423
|
-
useProperties(props) {
|
|
424
|
-
const result = {};
|
|
425
|
-
for (const key in props) {
|
|
426
|
-
if (Object.hasOwn(props, key)) {
|
|
427
|
-
result[key] = getUseProperty(props[key]);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
return result;
|
|
431
|
-
},
|
|
432
|
-
createResource(factory, cleanup) {
|
|
433
|
-
const resourceSignal = createSignal();
|
|
434
|
-
const effect = createEffect(() => {
|
|
435
|
-
const resource = toMaybe(factory());
|
|
436
|
-
resourceSignal.set(resource);
|
|
437
|
-
if (resource !== void 0 && cleanup) {
|
|
438
|
-
return () => {
|
|
439
|
-
cleanup(resource);
|
|
440
|
-
resourceSignal.set(void 0);
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
return () => {
|
|
444
|
-
resourceSignal.set(void 0);
|
|
445
|
-
};
|
|
446
|
-
});
|
|
447
|
-
unsubscribeSecondary.add(() => {
|
|
448
|
-
effect.destroy();
|
|
449
|
-
resourceSignal.set(void 0);
|
|
450
|
-
destroySignal(resourceSignal);
|
|
451
|
-
});
|
|
452
|
-
return resourceSignal;
|
|
453
|
-
},
|
|
454
|
-
createEffect(...args) {
|
|
455
|
-
const effect = createEffect(...args);
|
|
456
|
-
unsubscribeSecondary.add(effect.destroy);
|
|
457
|
-
return effect;
|
|
458
|
-
},
|
|
459
|
-
createSignal(...args) {
|
|
460
|
-
const sig = createSignal(...args);
|
|
461
|
-
unsubscribeSecondary.add(() => {
|
|
462
|
-
destroySignal(sig);
|
|
463
|
-
});
|
|
464
|
-
return sig;
|
|
465
|
-
},
|
|
466
|
-
createMemo(...args) {
|
|
467
|
-
const sig = createMemo(...args);
|
|
468
|
-
unsubscribeSecondary.add(() => {
|
|
469
|
-
destroySignal(sig);
|
|
470
|
-
});
|
|
471
|
-
return sig;
|
|
472
|
-
},
|
|
473
|
-
on(...args) {
|
|
474
|
-
const [firstArg] = args;
|
|
475
|
-
if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
|
|
476
|
-
const unsub2 = on(entry.entity, ...args);
|
|
477
|
-
unsubscribeSecondary.add(unsub2);
|
|
478
|
-
return unsub2;
|
|
479
|
-
}
|
|
480
|
-
const unsub = on(...args);
|
|
481
|
-
unsubscribeSecondary.add(unsub);
|
|
482
|
-
return Object.assign(() => {
|
|
483
|
-
unsubscribeSecondary.delete(unsub);
|
|
484
|
-
unsub();
|
|
485
|
-
}, unsub);
|
|
486
|
-
},
|
|
487
|
-
once(...args) {
|
|
488
|
-
const [firstArg] = args;
|
|
489
|
-
if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
|
|
490
|
-
const unsub2 = once(entry.entity, ...args);
|
|
491
|
-
unsubscribeSecondary.add(unsub2);
|
|
492
|
-
return unsub2;
|
|
493
|
-
}
|
|
494
|
-
const unsub = once(...args);
|
|
495
|
-
unsubscribeSecondary.add(unsub);
|
|
496
|
-
return Object.assign(() => {
|
|
497
|
-
unsubscribeSecondary.delete(unsub);
|
|
498
|
-
unsub();
|
|
499
|
-
}, unsub);
|
|
500
|
-
},
|
|
501
|
-
emit(...args) {
|
|
502
|
-
const [firstArg] = args;
|
|
503
|
-
if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
|
|
504
|
-
emit(entry.entity, ...args);
|
|
505
|
-
} else {
|
|
506
|
-
emit(...args);
|
|
507
|
-
}
|
|
508
|
-
},
|
|
509
|
-
onViewEvent(callback) {
|
|
510
|
-
const unsub = on(entry.entity, onViewEvent, (type, data) => {
|
|
511
|
-
callback(type, data);
|
|
512
|
-
});
|
|
513
|
-
unsubscribeSecondary.add(unsub);
|
|
514
|
-
},
|
|
515
|
-
onDestroy(callback) {
|
|
516
|
-
unsubscribePrimary.add(callback);
|
|
517
|
-
}
|
|
518
|
-
})
|
|
519
|
-
);
|
|
520
|
-
if (this.logger.isInfo) {
|
|
521
|
-
this.logger.info("create shadow-object", getDisplayName(construct), { shadowObject, entity: entry.entity });
|
|
522
513
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
destroySignal(sig);
|
|
547
|
-
}
|
|
548
|
-
unsubscribePrimary.clear();
|
|
549
|
-
unsubscribeSecondary.clear();
|
|
550
|
-
contextReaders.clear();
|
|
551
|
-
contextParentReaders.clear();
|
|
552
|
-
propertyReaders.clear();
|
|
553
|
-
contextProviders.clear();
|
|
554
|
-
contextRootProviders.clear();
|
|
555
|
-
const otherShadowObjects = entry.usedConstructors.get(construct);
|
|
556
|
-
if (otherShadowObjects) {
|
|
557
|
-
otherShadowObjects.delete(shadowObject);
|
|
558
|
-
if (otherShadowObjects.size === 0) {
|
|
559
|
-
entry.usedConstructors.delete(construct);
|
|
514
|
+
}
|
|
515
|
+
#constructShadowObject(construct, entry) {
|
|
516
|
+
const scope = new ShadowObjectCreationScope(entry.entity, this.logger, getDisplayName(construct), this.#shownDeprecations);
|
|
517
|
+
let shadowObject;
|
|
518
|
+
try {
|
|
519
|
+
shadowObject = eventize(new construct(scope.createAPI()));
|
|
520
|
+
} catch (error) {
|
|
521
|
+
scope.tearDown();
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
this.#shadowObjectScopes.set(shadowObject, scope);
|
|
525
|
+
scope.bindTo(
|
|
526
|
+
shadowObject,
|
|
527
|
+
() => {
|
|
528
|
+
this.#shadowObjectScopes.delete(shadowObject);
|
|
529
|
+
},
|
|
530
|
+
() => {
|
|
531
|
+
const otherShadowObjects = entry.usedConstructors.get(construct);
|
|
532
|
+
if (otherShadowObjects) {
|
|
533
|
+
otherShadowObjects.delete(shadowObject);
|
|
534
|
+
if (otherShadowObjects.size === 0) {
|
|
535
|
+
entry.usedConstructors.delete(construct);
|
|
536
|
+
}
|
|
560
537
|
}
|
|
561
538
|
}
|
|
562
|
-
|
|
539
|
+
);
|
|
563
540
|
const createdBy = entry.usedConstructors.get(construct);
|
|
564
541
|
if (createdBy) {
|
|
565
542
|
createdBy.add(shadowObject);
|
|
566
543
|
} else {
|
|
567
544
|
entry.usedConstructors.set(construct, /* @__PURE__ */ new Set([shadowObject]));
|
|
568
545
|
}
|
|
569
|
-
|
|
546
|
+
try {
|
|
547
|
+
this.#attachShadowObject(shadowObject, entry.entity);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
this.#destroyShadowObject(shadowObject, entry.entity);
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
570
552
|
return shadowObject;
|
|
571
553
|
}
|
|
572
|
-
createShadowObjects(entry) {
|
|
554
|
+
#createShadowObjects(entry) {
|
|
573
555
|
this.registry.findConstructors(entry.token, entry.entity.truthyProps())?.forEach((construct) => {
|
|
574
|
-
this
|
|
556
|
+
this.#constructShadowObject(construct, entry);
|
|
575
557
|
});
|
|
576
558
|
}
|
|
577
559
|
findShadowObjects(uuid) {
|
|
@@ -580,17 +562,54 @@ class Kernel {
|
|
|
580
562
|
const { usedConstructors } = entry;
|
|
581
563
|
return Array.from(new Set(Array.from(usedConstructors.values()).flatMap((objs) => Array.from(objs))));
|
|
582
564
|
}
|
|
583
|
-
attachShadowObject(shadowObject, entity) {
|
|
565
|
+
#attachShadowObject(shadowObject, entity) {
|
|
566
|
+
for (const [name, symbol] of LIFECYCLE_HOOKS) {
|
|
567
|
+
if (typeof shadowObject[name] === "function" && typeof shadowObject[symbol] !== "function") {
|
|
568
|
+
const displayName = this.#shadowObjectScopes.get(shadowObject)?.displayName;
|
|
569
|
+
this.logger.error(
|
|
570
|
+
`the "${name}" lifecycle hook is a plain method and is never called; use the [${name}] symbol from "@spearwolf/shadow-objects/shadow-objects.js":`,
|
|
571
|
+
displayName
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
584
575
|
on(entity, shadowObject);
|
|
585
576
|
if (typeof shadowObject[onCreate] === "function") {
|
|
586
577
|
shadowObject[onCreate](entity);
|
|
587
578
|
}
|
|
588
579
|
}
|
|
589
|
-
|
|
580
|
+
/**
|
|
581
|
+
* Tells one shadow-object that it is about to end, in both halves the notification has: the
|
|
582
|
+
* class-side `[onDestroy]` hook and the event other objects can listen for.
|
|
583
|
+
*
|
|
584
|
+
* Each half stands behind a guard of its own rather than a shared one, because neither is allowed to
|
|
585
|
+
* cost the other: a hook that throws still lets the event go out, and a listener that throws leaves
|
|
586
|
+
* the hook it came after untouched. Within the event half the same rule holds one level down, through
|
|
587
|
+
* `emitStrict()`: a listener that throws costs no other listener its turn. Nothing is re-thrown -- the
|
|
588
|
+
* shadow-objects of one entity reach their end in one sweep, and a failure at one of them may not take
|
|
589
|
+
* the sweep with it. Reports are
|
|
590
|
+
* keyed by the name the scope carries rather than the one the instance would give -- see
|
|
591
|
+
* `ShadowObjectCreationScope.displayName` for where the two part ways.
|
|
592
|
+
*/
|
|
593
|
+
#notifyShadowObjectDestroy(shadowObject, entity) {
|
|
594
|
+
const displayName = this.#shadowObjectScopes.get(shadowObject)?.displayName;
|
|
590
595
|
if (typeof shadowObject[onDestroy] === "function") {
|
|
591
|
-
|
|
596
|
+
runGuarded(
|
|
597
|
+
this.logger,
|
|
598
|
+
() => shadowObject[onDestroy](entity),
|
|
599
|
+
"shadow-object onDestroy hook failed:",
|
|
600
|
+
displayName
|
|
601
|
+
);
|
|
592
602
|
}
|
|
593
|
-
|
|
603
|
+
runGuarded(
|
|
604
|
+
this.logger,
|
|
605
|
+
() => emitStrict(shadowObject, onDestroy, entity),
|
|
606
|
+
"shadow-object onDestroy notification failed:",
|
|
607
|
+
displayName
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
#destroyShadowObject(shadowObject, entity) {
|
|
611
|
+
this.#notifyShadowObjectDestroy(shadowObject, entity);
|
|
612
|
+
this.#shadowObjectScopes.get(shadowObject)?.tearDown();
|
|
594
613
|
off(entity, shadowObject);
|
|
595
614
|
}
|
|
596
615
|
findOrCreateRootContext(name) {
|
|
@@ -602,13 +621,26 @@ class Kernel {
|
|
|
602
621
|
return ctx;
|
|
603
622
|
}
|
|
604
623
|
destroy() {
|
|
624
|
+
for (const entity of this.traverseLevelOrderBFS(true)) {
|
|
625
|
+
runGuarded(
|
|
626
|
+
this.logger,
|
|
627
|
+
() => this.destroyEntity(entity.uuid),
|
|
628
|
+
"entity teardown during kernel destroy failed:",
|
|
629
|
+
entity.uuid
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
this.#entities.clear();
|
|
633
|
+
this.#rootEntities.clear();
|
|
634
|
+
this.#allEntities.length = 0;
|
|
635
|
+
this.#allEntitiesReversed.length = 0;
|
|
636
|
+
this.#allEntitiesNeedUpdate = true;
|
|
605
637
|
for (const ctx of this.#rootContexts.values()) {
|
|
606
638
|
ctx.dispose();
|
|
607
639
|
}
|
|
608
640
|
this.#rootContexts.clear();
|
|
609
|
-
|
|
610
|
-
this
|
|
611
|
-
}
|
|
641
|
+
queueMicrotask(() => {
|
|
642
|
+
off(this);
|
|
643
|
+
});
|
|
612
644
|
}
|
|
613
645
|
}
|
|
614
646
|
export {
|