@spearwolf/shadow-objects 0.32.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 +333 -10
- package/README.md +100 -20
- 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 +43 -5
- package/src/elements/ShaeEntElement.d.ts.map +1 -1
- package/src/elements/ShaeEntElement.js +399 -170
- 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 +57 -7
- package/src/elements/ShaePropElement.d.ts.map +1 -1
- package/src/elements/ShaePropElement.js +241 -220
- 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 +230 -74
- 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 +81 -17
- package/src/in-the-dark/Kernel.d.ts.map +1 -1
- package/src/in-the-dark/Kernel.js +449 -392
- package/src/in-the-dark/Kernel.js.map +3 -3
- package/src/in-the-dark/Registry.d.ts +1 -1
- package/src/in-the-dark/Registry.d.ts.map +1 -1
- package/src/in-the-dark/Registry.js +99 -26
- package/src/in-the-dark/Registry.js.map +2 -2
- package/src/in-the-dark/ShadowObject.d.ts +1 -1
- package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
- package/src/in-the-dark/ShadowObject.js +3 -2
- 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 +8 -5
- 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 +39 -8
- 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 +54 -28
- 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 -12
- 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 +4 -2
- package/src/utils/attr-utils.js.map +2 -2
- package/src/utils/constants.js.map +1 -1
- package/src/utils/generateUUID.d.ts +12 -1
- 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 +16 -1
- package/src/utils/waitForMessageOfType.d.ts.map +1 -1
- package/src/utils/waitForMessageOfType.js +28 -3
- package/src/utils/waitForMessageOfType.js.map +2 -2
- package/src/view/ComponentChanges.d.ts +91 -1
- package/src/view/ComponentChanges.d.ts.map +1 -1
- package/src/view/ComponentChanges.js +253 -69
- package/src/view/ComponentChanges.js.map +2 -2
- package/src/view/ComponentContext.d.ts +239 -7
- package/src/view/ComponentContext.d.ts.map +1 -1
- package/src/view/ComponentContext.js +648 -115
- 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 +3 -1
- package/src/view/ComponentMemory.js.map +2 -2
- package/src/view/IShadowObjectEnvProxy.d.ts +33 -1
- package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.d.ts +2 -2
- package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.js +18 -11
- package/src/view/LocalShadowObjectEnv.js.map +2 -2
- package/src/view/RemoteWorkerEnv.d.ts +129 -3
- package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
- package/src/view/RemoteWorkerEnv.js +290 -60
- package/src/view/RemoteWorkerEnv.js.map +2 -2
- package/src/view/ShadowEnv.d.ts +73 -2
- package/src/view/ShadowEnv.d.ts.map +1 -1
- package/src/view/ShadowEnv.js +301 -64
- package/src/view/ShadowEnv.js.map +2 -2
- package/src/view/ViewComponent.d.ts +93 -8
- package/src/view/ViewComponent.d.ts.map +1 -1
- package/src/view/ViewComponent.js +148 -28
- 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 +80 -18
- 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,47 +17,120 @@ 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.#allEntitiesNeedUpdate = true;
|
|
36
|
-
this.#rootContexts = /* @__PURE__ */ new Map();
|
|
37
60
|
eventize(this);
|
|
38
61
|
this.registry = Registry.get(registry);
|
|
39
62
|
}
|
|
40
|
-
#entities;
|
|
41
|
-
#rootEntities;
|
|
42
|
-
#allEntities;
|
|
43
|
-
#allEntitiesReversed;
|
|
44
|
-
#allEntitiesNeedUpdate;
|
|
45
|
-
#rootContexts;
|
|
46
63
|
getEntity(uuid) {
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
return this.#requireEntry(uuid).entity;
|
|
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
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The entry for an entity that is expected to exist.
|
|
80
|
+
*
|
|
81
|
+
* The uuid is the caller's responsibility: `getEntity()` hands the throw on to its own
|
|
82
|
+
* callers, and `#updateShadowObjects()` is reached only for an entity the caller has just
|
|
83
|
+
* confirmed. Failing here names the uuid instead of failing later on an undefined field.
|
|
84
|
+
*/
|
|
85
|
+
#requireEntry(uuid) {
|
|
86
|
+
const entry = this.#entities.get(uuid);
|
|
87
|
+
if (entry === void 0) {
|
|
49
88
|
throw new Error(`entity with uuid "${uuid}" not found!`);
|
|
50
89
|
}
|
|
51
|
-
return
|
|
90
|
+
return entry;
|
|
52
91
|
}
|
|
53
92
|
hasEntity(uuid) {
|
|
54
93
|
return this.#entities.has(uuid);
|
|
55
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
|
+
}
|
|
56
111
|
/**
|
|
57
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.
|
|
58
121
|
*/
|
|
59
122
|
traverseLevelOrderBFS(reverse = false) {
|
|
60
123
|
if (this.#allEntitiesNeedUpdate) {
|
|
61
124
|
const lvl = /* @__PURE__ */ new Map();
|
|
125
|
+
const visited = /* @__PURE__ */ new Set();
|
|
62
126
|
const traverse = (uuid, depth) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
127
|
+
if (visited.has(uuid)) return;
|
|
128
|
+
visited.add(uuid);
|
|
129
|
+
const e = this.findEntity(uuid);
|
|
130
|
+
if (e === void 0) return;
|
|
131
|
+
const entities = lvl.get(depth);
|
|
132
|
+
if (entities) {
|
|
133
|
+
entities.push(e);
|
|
66
134
|
} else {
|
|
67
135
|
lvl.set(depth, [e]);
|
|
68
136
|
}
|
|
@@ -77,42 +145,84 @@ class Kernel {
|
|
|
77
145
|
this.#allEntitiesReversed = this.#allEntities.slice().reverse();
|
|
78
146
|
this.#allEntitiesNeedUpdate = false;
|
|
79
147
|
}
|
|
80
|
-
return reverse ? this.#allEntitiesReversed : this.#allEntities;
|
|
148
|
+
return reverse ? this.#allEntitiesReversed.slice() : this.#allEntities.slice();
|
|
81
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
|
+
*/
|
|
82
159
|
getEntityGraph() {
|
|
83
|
-
|
|
160
|
+
const visited = /* @__PURE__ */ new Set();
|
|
161
|
+
return Array.from(this.#rootEntities).map((uuid) => this.#getEntityGraphNode(uuid, visited)).filter((node) => node !== void 0);
|
|
84
162
|
}
|
|
85
|
-
getEntityGraphNode(uuid) {
|
|
86
|
-
|
|
87
|
-
|
|
163
|
+
#getEntityGraphNode(uuid, visited, omitted) {
|
|
164
|
+
const entry = this.#entities.get(uuid);
|
|
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);
|
|
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);
|
|
88
177
|
return {
|
|
89
178
|
token,
|
|
90
179
|
entity,
|
|
91
180
|
props: Object.fromEntries(entity.propEntries()),
|
|
92
|
-
children
|
|
181
|
+
children,
|
|
182
|
+
...omittedChildren.length > 0 ? { omittedChildren } : {}
|
|
93
183
|
};
|
|
94
184
|
}
|
|
95
185
|
upgradeEntities() {
|
|
96
186
|
const entityConstructors = /* @__PURE__ */ new Map();
|
|
97
187
|
for (const entity of this.traverseLevelOrderBFS(true)) {
|
|
98
|
-
|
|
188
|
+
if (!this.hasEntity(entity.uuid)) continue;
|
|
189
|
+
entityConstructors.set(entity.uuid, this.#updateShadowObjects(entity.uuid, 2 /* DestroyOnly */));
|
|
99
190
|
}
|
|
100
191
|
for (const entity of this.traverseLevelOrderBFS(false)) {
|
|
101
|
-
this.
|
|
192
|
+
if (!this.hasEntity(entity.uuid)) continue;
|
|
193
|
+
this.#updateShadowObjects(entity.uuid, 1 /* JustCreate */, entityConstructors.get(entity.uuid));
|
|
102
194
|
}
|
|
103
195
|
entityConstructors.clear();
|
|
104
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
|
+
*/
|
|
105
211
|
run(event) {
|
|
106
|
-
|
|
107
|
-
|
|
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 });
|
|
108
223
|
}
|
|
109
|
-
batch(() => {
|
|
110
|
-
for (const entry of event.changeTrail) {
|
|
111
|
-
this.parse(entry);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
224
|
}
|
|
115
|
-
parse(entry) {
|
|
225
|
+
#parse(entry) {
|
|
116
226
|
switch (entry.type) {
|
|
117
227
|
case ComponentChangeType.CreateEntities:
|
|
118
228
|
this.createEntity(
|
|
@@ -123,19 +233,15 @@ class Kernel {
|
|
|
123
233
|
entry.properties,
|
|
124
234
|
entry.autoDestructionOnParentRemoval
|
|
125
235
|
);
|
|
126
|
-
this.#allEntitiesNeedUpdate = true;
|
|
127
236
|
break;
|
|
128
237
|
case ComponentChangeType.DestroyEntities:
|
|
129
238
|
this.destroyEntity(entry.uuid);
|
|
130
|
-
this.#allEntitiesNeedUpdate = true;
|
|
131
239
|
break;
|
|
132
240
|
case ComponentChangeType.SetParent:
|
|
133
241
|
this.setParent(entry.uuid, entry.parentUuid, entry.order);
|
|
134
|
-
this.#allEntitiesNeedUpdate = true;
|
|
135
242
|
break;
|
|
136
243
|
case ComponentChangeType.UpdateOrder:
|
|
137
244
|
this.updateOrder(entry.uuid, entry.order);
|
|
138
|
-
this.#allEntitiesNeedUpdate = true;
|
|
139
245
|
break;
|
|
140
246
|
case ComponentChangeType.ChangeProperties:
|
|
141
247
|
this.changeProperties(entry.uuid, entry.properties);
|
|
@@ -149,100 +255,194 @@ class Kernel {
|
|
|
149
255
|
}
|
|
150
256
|
}
|
|
151
257
|
createEntity(uuid, token, parentUuid, order = 0, properties, autoDestructionOnParentRemoval = false) {
|
|
258
|
+
if (this.#entities.has(uuid)) {
|
|
259
|
+
throw new EntityUuidInUseError(uuid);
|
|
260
|
+
}
|
|
152
261
|
const e = new Entity(this, uuid);
|
|
153
262
|
e.order = order;
|
|
154
263
|
const entry = { token, entity: e, usedConstructors: /* @__PURE__ */ new Map() };
|
|
155
264
|
this.#entities.set(uuid, entry);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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;
|
|
165
278
|
}
|
|
166
|
-
|
|
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
|
+
);
|
|
167
303
|
}
|
|
168
304
|
destroyEntity(uuid) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
305
|
+
const entry = this.#entities.get(uuid);
|
|
306
|
+
if (entry === void 0) return;
|
|
307
|
+
const { entity, usedConstructors } = entry;
|
|
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);
|
|
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
|
+
);
|
|
178
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;
|
|
179
345
|
}
|
|
180
|
-
entity.removeFromParent();
|
|
181
|
-
emit(entity, onDestroy, entity);
|
|
182
|
-
usedConstructors.clear();
|
|
183
|
-
this.#entities.delete(entity.uuid);
|
|
184
|
-
this.#rootEntities.delete(entity.uuid);
|
|
185
|
-
this.#allEntitiesNeedUpdate = true;
|
|
186
346
|
}
|
|
187
|
-
|
|
347
|
+
/**
|
|
348
|
+
* @param order the new order, or `undefined` to keep the current one. A set-parent change
|
|
349
|
+
* only carries an order when it actually changed, so an absent order must not be read as
|
|
350
|
+
* a reset to `0` — that would silently drop the order the view side still holds.
|
|
351
|
+
*/
|
|
352
|
+
setParent(uuid, parentUuid, order) {
|
|
188
353
|
const e = this.getEntity(uuid);
|
|
189
|
-
|
|
354
|
+
const nextOrder = order ?? e.order;
|
|
355
|
+
if (e.parentUuid === parentUuid && e.order === nextOrder) return;
|
|
190
356
|
if (parentUuid && !this.#entities.has(parentUuid)) {
|
|
191
357
|
throw new Error(`entity with uuid "${parentUuid}" not found!`);
|
|
192
358
|
}
|
|
193
|
-
|
|
194
|
-
|
|
359
|
+
if (parentUuid) {
|
|
360
|
+
e.assertAttachableTo(this.getEntity(parentUuid));
|
|
361
|
+
}
|
|
362
|
+
e.removeFromParent(parentUuid == null);
|
|
363
|
+
e.order = nextOrder;
|
|
195
364
|
e.parentUuid = parentUuid;
|
|
196
|
-
|
|
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) {
|
|
197
398
|
this.#rootEntities.delete(uuid);
|
|
198
399
|
} else {
|
|
199
400
|
this.#rootEntities.add(uuid);
|
|
200
401
|
}
|
|
201
|
-
e.reSubscribeToParentContexts();
|
|
202
|
-
queueMicrotask(() => {
|
|
203
|
-
if (this.logger.isDebug) {
|
|
204
|
-
this.logger.debug("entity.onParentChanged", { uuid, parentUuid, order, entity: e });
|
|
205
|
-
}
|
|
206
|
-
emit(e, onParentChanged, e);
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
updateOrder(uuid, order) {
|
|
210
|
-
this.getEntity(uuid).order = order;
|
|
211
402
|
}
|
|
212
403
|
dispatchEventsToEntity(uuid, events) {
|
|
213
|
-
this.
|
|
404
|
+
this.findEntity(uuid)?.dispatchViewEvents(events);
|
|
214
405
|
}
|
|
215
406
|
changeProperties(uuid, properties) {
|
|
216
407
|
this.getEntity(uuid).setProperties(properties);
|
|
217
|
-
this
|
|
408
|
+
this.#updateShadowObjects(uuid);
|
|
218
409
|
}
|
|
219
410
|
changeToken(uuid, token) {
|
|
220
|
-
if (!this.#entities.has(uuid)) return;
|
|
221
411
|
const entry = this.#entities.get(uuid);
|
|
412
|
+
if (entry === void 0) return;
|
|
222
413
|
if (entry.token === token) return;
|
|
414
|
+
const previousToken = entry.token;
|
|
223
415
|
entry.token = token;
|
|
224
|
-
this
|
|
416
|
+
this.#updateShadowObjects(uuid, 0 /* CreateAndDestroy */, void 0, previousToken);
|
|
225
417
|
}
|
|
226
418
|
dispatchMessageToView(message) {
|
|
227
419
|
queueMicrotask(() => {
|
|
228
|
-
|
|
420
|
+
emitSafe(this, MessageToView, message);
|
|
229
421
|
});
|
|
230
422
|
}
|
|
231
423
|
/**
|
|
232
424
|
* Create or destroy the shadow-objects of an entity using the registered constructors.
|
|
233
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.
|
|
234
431
|
*/
|
|
235
|
-
updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors) {
|
|
236
|
-
const entry = this.#
|
|
432
|
+
#updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors, previousToken) {
|
|
433
|
+
const entry = this.#requireEntry(uuid);
|
|
237
434
|
nextConstructors ??= new Set(this.registry.findConstructors(entry.token, entry.entity.truthyProps()));
|
|
238
435
|
const shouldDestroy = action === 0 /* CreateAndDestroy */ || action === 2 /* DestroyOnly */;
|
|
239
436
|
const shouldCreate = action === 0 /* CreateAndDestroy */ || action === 1 /* JustCreate */;
|
|
437
|
+
const removed = [];
|
|
438
|
+
const created = [];
|
|
240
439
|
if (shouldDestroy) {
|
|
241
440
|
for (const [construct, shadowObjects] of entry.usedConstructors) {
|
|
242
441
|
if (!nextConstructors.has(construct)) {
|
|
243
442
|
entry.usedConstructors.delete(construct);
|
|
443
|
+
removed.push({ construct, count: shadowObjects.size });
|
|
244
444
|
for (const obj of shadowObjects) {
|
|
245
|
-
this
|
|
445
|
+
this.#destroyShadowObject(obj, entry.entity);
|
|
246
446
|
}
|
|
247
447
|
}
|
|
248
448
|
}
|
|
@@ -250,322 +450,166 @@ class Kernel {
|
|
|
250
450
|
if (shouldCreate) {
|
|
251
451
|
for (const construct of nextConstructors) {
|
|
252
452
|
if (!entry.usedConstructors.has(construct)) {
|
|
253
|
-
|
|
453
|
+
try {
|
|
454
|
+
created.push(this.#constructShadowObject(construct, entry));
|
|
455
|
+
} catch (error) {
|
|
456
|
+
this.#rollbackFailedShadowObjectUpdate(entry, created, removed, previousToken);
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
254
459
|
}
|
|
255
460
|
}
|
|
256
461
|
}
|
|
257
462
|
return nextConstructors;
|
|
258
463
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
288
511
|
);
|
|
289
512
|
}
|
|
290
|
-
return propReader;
|
|
291
|
-
};
|
|
292
|
-
const shadowObject = eventize(
|
|
293
|
-
new construct({
|
|
294
|
-
entity: entry.entity,
|
|
295
|
-
provideContext(name, sourceOrInitialValue, options) {
|
|
296
|
-
if (!provideContextOptionsDeprecatedShown && options != null && typeof options === "function") {
|
|
297
|
-
console.warn(
|
|
298
|
-
'[shadow-objects] Deprecation Warning: The "isEqual" option of "provideContext()" is now passed as {compare} argument. Please update your code accordingly.'
|
|
299
|
-
);
|
|
300
|
-
provideContextOptionsDeprecatedShown = true;
|
|
301
|
-
}
|
|
302
|
-
const opts = typeof options === "function" ? { compare: options } : options;
|
|
303
|
-
let ctxProvider = contextProviders.get(name);
|
|
304
|
-
if (ctxProvider == null) {
|
|
305
|
-
const isSig = isSignal(sourceOrInitialValue);
|
|
306
|
-
const initialValue = isSig ? void 0 : toMaybe(sourceOrInitialValue);
|
|
307
|
-
ctxProvider = createSignal(initialValue, opts?.compare ? { compare: opts.compare } : void 0);
|
|
308
|
-
if (isSig) {
|
|
309
|
-
const ln2 = link(sourceOrInitialValue, ctxProvider);
|
|
310
|
-
unsubscribeSecondary.add(ln2.destroy.bind(ln2));
|
|
311
|
-
}
|
|
312
|
-
const ln = link(ctxProvider, entry.entity.provideContext(name));
|
|
313
|
-
unsubscribeSecondary.add(ln.destroy.bind(ln));
|
|
314
|
-
contextProviders.set(name, ctxProvider);
|
|
315
|
-
}
|
|
316
|
-
if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
|
|
317
|
-
unsubscribeSecondary.add(() => {
|
|
318
|
-
ctxProvider.set(void 0);
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
return ctxProvider;
|
|
322
|
-
},
|
|
323
|
-
provideGlobalContext(name, sourceOrInitialValue, options) {
|
|
324
|
-
if (!provideGlobalContextOptionsDeprecatedShown && options != null && typeof options === "function") {
|
|
325
|
-
console.warn(
|
|
326
|
-
'[shadow-objects] Deprecation Warning: The "isEqual" option of "provideGlobalContext()" is now passed as {compare} argument. Please update your code accordingly.'
|
|
327
|
-
);
|
|
328
|
-
provideGlobalContextOptionsDeprecatedShown = true;
|
|
329
|
-
}
|
|
330
|
-
const opts = typeof options === "function" ? { compare: options } : options;
|
|
331
|
-
let ctxProvider = contextRootProviders.get(name);
|
|
332
|
-
if (ctxProvider == null) {
|
|
333
|
-
const isSig = isSignal(sourceOrInitialValue);
|
|
334
|
-
const initialValue = isSig ? void 0 : toMaybe(sourceOrInitialValue);
|
|
335
|
-
ctxProvider = createSignal(initialValue, opts?.compare ? { compare: opts.compare } : void 0);
|
|
336
|
-
if (isSig) {
|
|
337
|
-
const ln2 = link(sourceOrInitialValue, ctxProvider);
|
|
338
|
-
unsubscribeSecondary.add(ln2.destroy.bind(ln2));
|
|
339
|
-
}
|
|
340
|
-
const ln = link(ctxProvider, entry.entity.provideGlobalContext(name));
|
|
341
|
-
unsubscribeSecondary.add(ln.destroy.bind(ln));
|
|
342
|
-
contextRootProviders.set(name, ctxProvider);
|
|
343
|
-
}
|
|
344
|
-
if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
|
|
345
|
-
unsubscribeSecondary.add(() => {
|
|
346
|
-
ctxProvider.set(void 0);
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
return ctxProvider;
|
|
350
|
-
},
|
|
351
|
-
useContext(name, options) {
|
|
352
|
-
if (!useContextOptionsDeprecatedShown && options != null && typeof options === "function") {
|
|
353
|
-
console.warn(
|
|
354
|
-
'[shadow-objects] Deprecation Warning: The "isEqual" option of "useContext()" is now passed as {compare} argument. Please update your code accordingly.'
|
|
355
|
-
);
|
|
356
|
-
useContextOptionsDeprecatedShown = true;
|
|
357
|
-
}
|
|
358
|
-
const opts = typeof options === "function" ? { compare: options } : options;
|
|
359
|
-
let ctxReader = contextReaders.get(name);
|
|
360
|
-
if (ctxReader === void 0) {
|
|
361
|
-
ctxReader = createSignal(void 0, opts).get;
|
|
362
|
-
contextReaders.set(name, ctxReader);
|
|
363
|
-
contextReaderCompares.set(name, opts?.compare);
|
|
364
|
-
const ln = link(entry.entity.useContext(name), ctxReader);
|
|
365
|
-
unsubscribeSecondary.add(ln.destroy.bind(ln));
|
|
366
|
-
} else if (opts?.compare != null && contextReaderCompares.get(name) !== opts.compare) {
|
|
367
|
-
console.warn(
|
|
368
|
-
`[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.`
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
return ctxReader;
|
|
372
|
-
},
|
|
373
|
-
useParentContext(name, options) {
|
|
374
|
-
if (!useParentContextOptionsDeprecatedShown && options != null && typeof options === "function") {
|
|
375
|
-
console.warn(
|
|
376
|
-
'[shadow-objects] Deprecation Warning: The "isEqual" option of "useParentContext()" is now passed as {compare} argument. Please update your code accordingly.'
|
|
377
|
-
);
|
|
378
|
-
useParentContextOptionsDeprecatedShown = true;
|
|
379
|
-
}
|
|
380
|
-
const opts = typeof options === "function" ? { compare: options } : options;
|
|
381
|
-
let ctxReader = contextParentReaders.get(name);
|
|
382
|
-
if (ctxReader === void 0) {
|
|
383
|
-
ctxReader = createSignal(void 0, opts).get;
|
|
384
|
-
contextParentReaders.set(name, ctxReader);
|
|
385
|
-
contextParentReaderCompares.set(name, opts?.compare);
|
|
386
|
-
const ln = link(entry.entity.useParentContext(name), ctxReader);
|
|
387
|
-
unsubscribeSecondary.add(ln.destroy.bind(ln));
|
|
388
|
-
} else if (opts?.compare != null && contextParentReaderCompares.get(name) !== opts.compare) {
|
|
389
|
-
console.warn(
|
|
390
|
-
`[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.`
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
return ctxReader;
|
|
394
|
-
},
|
|
395
|
-
dispatchMessageToView(type, data, transferables, traverseChildren = false) {
|
|
396
|
-
entry.entity.dispatchMessageToView(type, data, transferables, traverseChildren);
|
|
397
|
-
},
|
|
398
|
-
useProperty: getUseProperty,
|
|
399
|
-
useProperties(props) {
|
|
400
|
-
const result = {};
|
|
401
|
-
for (const key in props) {
|
|
402
|
-
if (Object.hasOwn(props, key)) {
|
|
403
|
-
result[key] = getUseProperty(props[key]);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return result;
|
|
407
|
-
},
|
|
408
|
-
createResource(factory, cleanup) {
|
|
409
|
-
const resourceSignal = createSignal();
|
|
410
|
-
const effect = createEffect(() => {
|
|
411
|
-
const resource = toMaybe(factory());
|
|
412
|
-
resourceSignal.set(resource);
|
|
413
|
-
if (resource !== void 0 && cleanup) {
|
|
414
|
-
return () => {
|
|
415
|
-
cleanup(resource);
|
|
416
|
-
resourceSignal.set(void 0);
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
return () => {
|
|
420
|
-
resourceSignal.set(void 0);
|
|
421
|
-
};
|
|
422
|
-
});
|
|
423
|
-
unsubscribeSecondary.add(() => {
|
|
424
|
-
effect.destroy();
|
|
425
|
-
resourceSignal.set(void 0);
|
|
426
|
-
destroySignal(resourceSignal);
|
|
427
|
-
});
|
|
428
|
-
return resourceSignal;
|
|
429
|
-
},
|
|
430
|
-
createEffect(...args) {
|
|
431
|
-
const effect = createEffect(...args);
|
|
432
|
-
unsubscribeSecondary.add(effect.destroy);
|
|
433
|
-
return effect;
|
|
434
|
-
},
|
|
435
|
-
createSignal(...args) {
|
|
436
|
-
const sig = createSignal(...args);
|
|
437
|
-
unsubscribeSecondary.add(() => {
|
|
438
|
-
destroySignal(sig);
|
|
439
|
-
});
|
|
440
|
-
return sig;
|
|
441
|
-
},
|
|
442
|
-
createMemo(...args) {
|
|
443
|
-
const sig = createMemo(...args);
|
|
444
|
-
unsubscribeSecondary.add(() => {
|
|
445
|
-
destroySignal(sig);
|
|
446
|
-
});
|
|
447
|
-
return sig;
|
|
448
|
-
},
|
|
449
|
-
on(...args) {
|
|
450
|
-
const [firstArg] = args;
|
|
451
|
-
if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
|
|
452
|
-
const unsub2 = on(entry.entity, ...args);
|
|
453
|
-
unsubscribeSecondary.add(unsub2);
|
|
454
|
-
return unsub2;
|
|
455
|
-
}
|
|
456
|
-
const unsub = on(...args);
|
|
457
|
-
unsubscribeSecondary.add(unsub);
|
|
458
|
-
return Object.assign(() => {
|
|
459
|
-
unsubscribeSecondary.delete(unsub);
|
|
460
|
-
unsub();
|
|
461
|
-
}, unsub);
|
|
462
|
-
},
|
|
463
|
-
once(...args) {
|
|
464
|
-
const [firstArg] = args;
|
|
465
|
-
if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
|
|
466
|
-
const unsub2 = once(entry.entity, ...args);
|
|
467
|
-
unsubscribeSecondary.add(unsub2);
|
|
468
|
-
return unsub2;
|
|
469
|
-
}
|
|
470
|
-
const unsub = once(...args);
|
|
471
|
-
unsubscribeSecondary.add(unsub);
|
|
472
|
-
return Object.assign(() => {
|
|
473
|
-
unsubscribeSecondary.delete(unsub);
|
|
474
|
-
unsub();
|
|
475
|
-
}, unsub);
|
|
476
|
-
},
|
|
477
|
-
emit(...args) {
|
|
478
|
-
const [firstArg] = args;
|
|
479
|
-
if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
|
|
480
|
-
emit(entry.entity, ...args);
|
|
481
|
-
} else {
|
|
482
|
-
emit(...args);
|
|
483
|
-
}
|
|
484
|
-
},
|
|
485
|
-
onViewEvent(callback) {
|
|
486
|
-
const unsub = on(entry.entity, onViewEvent, (type, data) => {
|
|
487
|
-
callback(type, data);
|
|
488
|
-
});
|
|
489
|
-
unsubscribeSecondary.add(unsub);
|
|
490
|
-
},
|
|
491
|
-
onDestroy(callback) {
|
|
492
|
-
unsubscribePrimary.add(callback);
|
|
493
|
-
}
|
|
494
|
-
})
|
|
495
|
-
);
|
|
496
|
-
if (this.logger.isInfo) {
|
|
497
|
-
this.logger.info("create shadow-object", getDisplayName(construct), { shadowObject, entity: entry.entity });
|
|
498
513
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
destroySignal(sig);
|
|
523
|
-
}
|
|
524
|
-
unsubscribePrimary.clear();
|
|
525
|
-
unsubscribeSecondary.clear();
|
|
526
|
-
contextReaders.clear();
|
|
527
|
-
contextParentReaders.clear();
|
|
528
|
-
propertyReaders.clear();
|
|
529
|
-
contextProviders.clear();
|
|
530
|
-
contextRootProviders.clear();
|
|
531
|
-
const otherShadowObjects = entry.usedConstructors.get(construct);
|
|
532
|
-
if (otherShadowObjects) {
|
|
533
|
-
otherShadowObjects.delete(shadowObject);
|
|
534
|
-
if (otherShadowObjects.size === 0) {
|
|
535
|
-
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
|
+
}
|
|
536
537
|
}
|
|
537
538
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
539
|
+
);
|
|
540
|
+
const createdBy = entry.usedConstructors.get(construct);
|
|
541
|
+
if (createdBy) {
|
|
542
|
+
createdBy.add(shadowObject);
|
|
541
543
|
} else {
|
|
542
544
|
entry.usedConstructors.set(construct, /* @__PURE__ */ new Set([shadowObject]));
|
|
543
545
|
}
|
|
544
|
-
|
|
546
|
+
try {
|
|
547
|
+
this.#attachShadowObject(shadowObject, entry.entity);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
this.#destroyShadowObject(shadowObject, entry.entity);
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
545
552
|
return shadowObject;
|
|
546
553
|
}
|
|
547
|
-
createShadowObjects(
|
|
548
|
-
const entry = this.#entities.get(uuid);
|
|
554
|
+
#createShadowObjects(entry) {
|
|
549
555
|
this.registry.findConstructors(entry.token, entry.entity.truthyProps())?.forEach((construct) => {
|
|
550
|
-
this
|
|
556
|
+
this.#constructShadowObject(construct, entry);
|
|
551
557
|
});
|
|
552
558
|
}
|
|
553
559
|
findShadowObjects(uuid) {
|
|
554
|
-
|
|
555
|
-
|
|
560
|
+
const entry = this.#entities.get(uuid);
|
|
561
|
+
if (entry === void 0) return [];
|
|
562
|
+
const { usedConstructors } = entry;
|
|
556
563
|
return Array.from(new Set(Array.from(usedConstructors.values()).flatMap((objs) => Array.from(objs))));
|
|
557
564
|
}
|
|
558
|
-
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
|
+
}
|
|
559
575
|
on(entity, shadowObject);
|
|
560
576
|
if (typeof shadowObject[onCreate] === "function") {
|
|
561
577
|
shadowObject[onCreate](entity);
|
|
562
578
|
}
|
|
563
579
|
}
|
|
564
|
-
|
|
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;
|
|
565
595
|
if (typeof shadowObject[onDestroy] === "function") {
|
|
566
|
-
|
|
596
|
+
runGuarded(
|
|
597
|
+
this.logger,
|
|
598
|
+
() => shadowObject[onDestroy](entity),
|
|
599
|
+
"shadow-object onDestroy hook failed:",
|
|
600
|
+
displayName
|
|
601
|
+
);
|
|
567
602
|
}
|
|
568
|
-
|
|
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();
|
|
569
613
|
off(entity, shadowObject);
|
|
570
614
|
}
|
|
571
615
|
findOrCreateRootContext(name) {
|
|
@@ -577,13 +621,26 @@ class Kernel {
|
|
|
577
621
|
return ctx;
|
|
578
622
|
}
|
|
579
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;
|
|
580
637
|
for (const ctx of this.#rootContexts.values()) {
|
|
581
638
|
ctx.dispose();
|
|
582
639
|
}
|
|
583
640
|
this.#rootContexts.clear();
|
|
584
|
-
|
|
585
|
-
this
|
|
586
|
-
}
|
|
641
|
+
queueMicrotask(() => {
|
|
642
|
+
off(this);
|
|
643
|
+
});
|
|
587
644
|
}
|
|
588
645
|
}
|
|
589
646
|
export {
|