@spearwolf/shadow-objects 0.33.0 → 0.35.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 +350 -61
- package/README.md +66 -13
- package/bundle.js +6 -53
- package/package.json +15 -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 +26 -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 +13 -2
- package/src/constants.d.ts.map +1 -1
- package/src/constants.js +7 -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 +61 -2
- package/src/elements/ShaePropElement.d.ts.map +1 -1
- package/src/elements/ShaePropElement.js +264 -219
- package/src/elements/ShaePropElement.js.map +2 -2
- package/src/elements/ShaeWorkerElement.d.ts +59 -5
- package/src/elements/ShaeWorkerElement.d.ts.map +1 -1
- package/src/elements/ShaeWorkerElement.js +271 -73
- package/src/elements/ShaeWorkerElement.js.map +2 -2
- package/src/elements/constants.d.ts +9 -0
- package/src/elements/constants.d.ts.map +1 -1
- package/src/elements/constants.js +18 -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 +143 -3
- package/src/in-the-dark/Entity.d.ts.map +1 -1
- package/src/in-the-dark/Entity.js +330 -63
- package/src/in-the-dark/Entity.js.map +3 -3
- package/src/in-the-dark/Kernel.d.ts +98 -16
- package/src/in-the-dark/Kernel.d.ts.map +1 -1
- package/src/in-the-dark/Kernel.js +466 -379
- package/src/in-the-dark/Kernel.js.map +3 -3
- package/src/in-the-dark/Registry.d.ts +7 -1
- package/src/in-the-dark/Registry.d.ts.map +1 -1
- package/src/in-the-dark/Registry.js +112 -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 +133 -0
- package/src/in-the-dark/ShadowObjectCreationScope.d.ts.map +1 -0
- package/src/in-the-dark/ShadowObjectCreationScope.js +603 -0
- package/src/in-the-dark/ShadowObjectCreationScope.js.map +7 -0
- package/src/in-the-dark/SignalsPath.d.ts +2 -0
- package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
- package/src/in-the-dark/SignalsPath.js +6 -2
- package/src/in-the-dark/SignalsPath.js.map +2 -2
- package/src/in-the-dark/displayName.d.ts +4 -0
- package/src/in-the-dark/displayName.d.ts.map +1 -0
- package/src/in-the-dark/displayName.js +5 -0
- package/src/in-the-dark/displayName.js.map +7 -0
- 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 +9 -1
- package/src/index.d.ts.map +1 -1
- package/src/index.js +9 -1
- package/src/index.js.map +2 -2
- package/src/inspect/createKernelSnapshot.d.ts +17 -0
- package/src/inspect/createKernelSnapshot.d.ts.map +1 -0
- package/src/inspect/createKernelSnapshot.js +247 -0
- package/src/inspect/createKernelSnapshot.js.map +7 -0
- package/src/inspect/createViewSnapshot.d.ts +10 -0
- package/src/inspect/createViewSnapshot.d.ts.map +1 -0
- package/src/inspect/createViewSnapshot.js +143 -0
- package/src/inspect/createViewSnapshot.js.map +7 -0
- package/src/inspect/normalizeInspectRequest.d.ts +32 -0
- package/src/inspect/normalizeInspectRequest.d.ts.map +1 -0
- package/src/inspect/normalizeInspectRequest.js +55 -0
- package/src/inspect/normalizeInspectRequest.js.map +7 -0
- package/src/inspect/serializeValue.d.ts +13 -0
- package/src/inspect/serializeValue.d.ts.map +1 -0
- package/src/inspect/serializeValue.js +149 -0
- package/src/inspect/serializeValue.js.map +7 -0
- package/src/inspect/types.d.ts +260 -0
- package/src/inspect/types.d.ts.map +1 -0
- package/src/inspect/types.js +1 -0
- package/src/inspect/types.js.map +7 -0
- package/src/model-context/ModelContextLike.d.ts +59 -0
- package/src/model-context/ModelContextLike.d.ts.map +1 -0
- package/src/model-context/ModelContextLike.js +15 -0
- package/src/model-context/ModelContextLike.js.map +7 -0
- package/src/model-context/exposeShadowEnvsToModelContext.d.ts +48 -0
- package/src/model-context/exposeShadowEnvsToModelContext.d.ts.map +1 -0
- package/src/model-context/exposeShadowEnvsToModelContext.js +39 -0
- package/src/model-context/exposeShadowEnvsToModelContext.js.map +7 -0
- package/src/model-context/redactProps.d.ts +14 -0
- package/src/model-context/redactProps.d.ts.map +1 -0
- package/src/model-context/redactProps.js +22 -0
- package/src/model-context/redactProps.js.map +7 -0
- package/src/model-context/sharedExposure.d.ts +32 -0
- package/src/model-context/sharedExposure.d.ts.map +1 -0
- package/src/model-context/sharedExposure.js +108 -0
- package/src/model-context/sharedExposure.js.map +7 -0
- package/src/model-context/toolSupport.d.ts +72 -0
- package/src/model-context/toolSupport.d.ts.map +1 -0
- package/src/model-context/toolSupport.js +145 -0
- package/src/model-context/toolSupport.js.map +7 -0
- package/src/model-context/tools/findEntities.d.ts +15 -0
- package/src/model-context/tools/findEntities.d.ts.map +1 -0
- package/src/model-context/tools/findEntities.js +71 -0
- package/src/model-context/tools/findEntities.js.map +7 -0
- package/src/model-context/tools/getEntity.d.ts +16 -0
- package/src/model-context/tools/getEntity.d.ts.map +1 -0
- package/src/model-context/tools/getEntity.js +65 -0
- package/src/model-context/tools/getEntity.js.map +7 -0
- package/src/model-context/tools/getEntityTree.d.ts +4 -0
- package/src/model-context/tools/getEntityTree.d.ts.map +1 -0
- package/src/model-context/tools/getEntityTree.js +58 -0
- package/src/model-context/tools/getEntityTree.js.map +7 -0
- package/src/model-context/tools/getRegistry.d.ts +14 -0
- package/src/model-context/tools/getRegistry.d.ts.map +1 -0
- package/src/model-context/tools/getRegistry.js +39 -0
- package/src/model-context/tools/getRegistry.js.map +7 -0
- package/src/model-context/tools/index.d.ts +9 -0
- package/src/model-context/tools/index.d.ts.map +1 -0
- package/src/model-context/tools/index.js +16 -0
- package/src/model-context/tools/index.js.map +7 -0
- package/src/model-context/tools/listEnvs.d.ts +33 -0
- package/src/model-context/tools/listEnvs.d.ts.map +1 -0
- package/src/model-context/tools/listEnvs.js +37 -0
- package/src/model-context/tools/listEnvs.js.map +7 -0
- package/src/model-context.d.ts +13 -0
- package/src/model-context.d.ts.map +1 -0
- package/src/model-context.js +15 -0
- package/src/model-context.js.map +7 -0
- package/src/shadow-objects.d.ts +4 -0
- package/src/shadow-objects.d.ts.map +1 -1
- package/src/shadow-objects.js +6 -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/testing/TestEntity.d.ts +79 -0
- package/src/testing/TestEntity.d.ts.map +1 -0
- package/src/testing/TestEntity.js +135 -0
- package/src/testing/TestEntity.js.map +7 -0
- package/src/testing/createTestKernel.d.ts +11 -0
- package/src/testing/createTestKernel.d.ts.map +1 -0
- package/src/testing/createTestKernel.js +132 -0
- package/src/testing/createTestKernel.js.map +7 -0
- package/src/testing/mountShadowObject.d.ts +21 -0
- package/src/testing/mountShadowObject.d.ts.map +1 -0
- package/src/testing/mountShadowObject.js +72 -0
- package/src/testing/mountShadowObject.js.map +7 -0
- package/src/testing/recordKernelErrors.d.ts +21 -0
- package/src/testing/recordKernelErrors.d.ts.map +1 -0
- package/src/testing/recordKernelErrors.js +39 -0
- package/src/testing/recordKernelErrors.js.map +7 -0
- package/src/testing/settle.d.ts +15 -0
- package/src/testing/settle.d.ts.map +1 -0
- package/src/testing/settle.js +19 -0
- package/src/testing/settle.js.map +7 -0
- package/src/testing/types.d.ts +119 -0
- package/src/testing/types.d.ts.map +1 -0
- package/src/testing/types.js +1 -0
- package/src/testing/types.js.map +7 -0
- package/src/testing.d.ts +14 -0
- package/src/testing.d.ts.map +1 -0
- package/src/testing.js +11 -0
- package/src/testing.js.map +7 -0
- package/src/types.d.ts +185 -14
- 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 +11 -0
- package/src/utils/attr-utils.d.ts.map +1 -1
- package/src/utils/attr-utils.js +5 -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 +38 -3
- package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.d.ts +8 -1
- package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.js +23 -7
- package/src/view/LocalShadowObjectEnv.js.map +2 -2
- package/src/view/RemoteWorkerEnv.d.ts +108 -5
- package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
- package/src/view/RemoteWorkerEnv.js +244 -77
- package/src/view/RemoteWorkerEnv.js.map +2 -2
- package/src/view/ShadowEnv.d.ts +79 -2
- package/src/view/ShadowEnv.d.ts.map +1 -1
- package/src/view/ShadowEnv.js +323 -59
- package/src/view/ShadowEnv.js.map +3 -3
- 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 +98 -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,23 +1,82 @@
|
|
|
1
1
|
import { ChangeTrailPhase, ContextLost, GlobalNS } from "../constants.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ConsoleLogger } from "../utils/ConsoleLogger.js";
|
|
3
|
+
import { MicrotaskCollector } from "../utils/MicrotaskCollector.js";
|
|
4
|
+
import { runGuarded } from "../utils/runGuarded.js";
|
|
3
5
|
import { toNamespace } from "../utils/toNamespace.js";
|
|
4
|
-
import { ComponentChanges } from "./ComponentChanges.js";
|
|
6
|
+
import { ComponentChanges, PropertyWithoutValue } from "./ComponentChanges.js";
|
|
5
7
|
import { ComponentMemory } from "./ComponentMemory.js";
|
|
8
|
+
class OrderedUuids {
|
|
9
|
+
#uuids = [];
|
|
10
|
+
#members = /* @__PURE__ */ new Set();
|
|
11
|
+
/** The uuids in their order. Read-only: every write goes through this class. */
|
|
12
|
+
get uuids() {
|
|
13
|
+
return this.#uuids;
|
|
14
|
+
}
|
|
15
|
+
get size() {
|
|
16
|
+
return this.#uuids.length;
|
|
17
|
+
}
|
|
18
|
+
has(uuid) {
|
|
19
|
+
return this.#members.has(uuid);
|
|
20
|
+
}
|
|
21
|
+
push(uuid) {
|
|
22
|
+
this.#uuids.push(uuid);
|
|
23
|
+
this.#members.add(uuid);
|
|
24
|
+
}
|
|
25
|
+
insertAt(index, uuid) {
|
|
26
|
+
this.#uuids.splice(index, 0, uuid);
|
|
27
|
+
this.#members.add(uuid);
|
|
28
|
+
}
|
|
29
|
+
/** @returns whether the uuid was in the list */
|
|
30
|
+
delete(uuid) {
|
|
31
|
+
if (!this.#members.delete(uuid)) return false;
|
|
32
|
+
const idx = this.#uuids.indexOf(uuid);
|
|
33
|
+
if (idx !== -1) {
|
|
34
|
+
this.#uuids.splice(idx, 1);
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
clear() {
|
|
39
|
+
this.#uuids.length = 0;
|
|
40
|
+
this.#members.clear();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
6
43
|
class ComponentContextDisposedError extends Error {
|
|
7
44
|
constructor(message = "the component context has been disposed") {
|
|
8
45
|
super(message);
|
|
9
46
|
this.name = "ComponentContextDisposedError";
|
|
10
47
|
}
|
|
11
48
|
}
|
|
49
|
+
class ComponentUuidInUseError extends Error {
|
|
50
|
+
constructor(uuid) {
|
|
51
|
+
super(`the view component cannot join the component context because the uuid ${uuid} is already held by another component`);
|
|
52
|
+
this.name = "ComponentUuidInUseError";
|
|
53
|
+
this.uuid = uuid;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
12
56
|
class ComponentContext {
|
|
13
57
|
static {
|
|
14
58
|
this.ReRequestParentRoots = "re-request-parent-roots";
|
|
15
59
|
}
|
|
60
|
+
static {
|
|
61
|
+
this.ReRequestParent = "re-request-parent";
|
|
62
|
+
}
|
|
63
|
+
static {
|
|
64
|
+
/**
|
|
65
|
+
* Ask the element behind a component to let the properties hanging on it look for their host
|
|
66
|
+
* again.
|
|
67
|
+
*
|
|
68
|
+
* A context knows nothing about `<shae-prop>` — a property has no view component and cannot be
|
|
69
|
+
* addressed here. The signal still belongs next to the two above: it travels the same broadcast
|
|
70
|
+
* channel to the same receivers, and the element on the other end is the one that can reach the
|
|
71
|
+
* properties below it.
|
|
72
|
+
*/
|
|
73
|
+
this.ReRequestEntHost = "re-request-ent-host";
|
|
74
|
+
}
|
|
16
75
|
static getContextsMap() {
|
|
17
|
-
if (globalThis.
|
|
18
|
-
globalThis.
|
|
76
|
+
if (globalThis.__shadowObjectsContexts == null) {
|
|
77
|
+
globalThis.__shadowObjectsContexts = /* @__PURE__ */ new Map();
|
|
19
78
|
}
|
|
20
|
-
return globalThis.
|
|
79
|
+
return globalThis.__shadowObjectsContexts;
|
|
21
80
|
}
|
|
22
81
|
static get(namespace) {
|
|
23
82
|
const ns = toNamespace(namespace);
|
|
@@ -28,10 +87,63 @@ class ComponentContext {
|
|
|
28
87
|
return new ComponentContext(ns);
|
|
29
88
|
}
|
|
30
89
|
#components = /* @__PURE__ */ new Map();
|
|
31
|
-
|
|
32
|
-
|
|
90
|
+
// the entity tree is walked in this order, so the roots are a list and not a set
|
|
91
|
+
#rootComponents = new OrderedUuids();
|
|
33
92
|
#isDisposed = false;
|
|
93
|
+
// which instances name this ComponentContext, and the one place that can say so. #components holds one
|
|
94
|
+
// entry per uuid, and an entry outlives the departure of its component until the next change
|
|
95
|
+
// trail — so a uuid standing in that map is no statement about anyone still being a member
|
|
96
|
+
#componentInstances = /* @__PURE__ */ new Set();
|
|
97
|
+
/**
|
|
98
|
+
* The entry `component` owns, and nothing else.
|
|
99
|
+
*
|
|
100
|
+
* An entry outlives the departure of its component until the next change trail, so a later
|
|
101
|
+
* component can take the uuid over while the entry is still standing. A lookup by uuid alone
|
|
102
|
+
* would then hand back the entry of a namesake, and every write that followed would land on a
|
|
103
|
+
* live component that was never named. Each method that is given an instance asks through here,
|
|
104
|
+
* so that it acts on that instance or on nothing.
|
|
105
|
+
*/
|
|
106
|
+
#entryOf(component) {
|
|
107
|
+
const entry = this.#components.get(component.uuid);
|
|
108
|
+
return entry !== void 0 && entry.component === component ? entry : void 0;
|
|
109
|
+
}
|
|
34
110
|
#componentMemory = new ComponentMemory();
|
|
111
|
+
#logger = new ConsoleLogger("ComponentContext");
|
|
112
|
+
/**
|
|
113
|
+
* The change trail this context built last, as long as nobody has settled it yet.
|
|
114
|
+
* `owners[i]` is the bookkeeping that produced `entries[i]`; `retiring` are the components the
|
|
115
|
+
* build read as spent, which is a verdict of the build and not of the commit — a component
|
|
116
|
+
* destroyed while the trail travels owes its destruction to the *next* trail, and dropping its
|
|
117
|
+
* entry here would leave the entity standing with nothing left to take it down.
|
|
118
|
+
*/
|
|
119
|
+
#uncommittedTrail;
|
|
120
|
+
/** The components a build reads as spent: destroyed, or created and dropped without ever going out. */
|
|
121
|
+
#retiringComponents(participants) {
|
|
122
|
+
return participants.filter((changes) => changes.isDestroyed || changes.isNew && !changes.isCreated);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Carry out the verdict the build took, for the components whose every entry is settled.
|
|
126
|
+
*
|
|
127
|
+
* A component that has been claimed again since the build stays: a uuid its holder has left is
|
|
128
|
+
* free, and the entry now belongs to whoever took it over.
|
|
129
|
+
*/
|
|
130
|
+
#retireComponents(retiring, stillPending) {
|
|
131
|
+
for (const changes of retiring) {
|
|
132
|
+
if (stillPending?.has(changes)) continue;
|
|
133
|
+
if (changes.isCreated) continue;
|
|
134
|
+
this.#deleteComponent(changes.uuid, changes);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Release the record of the built trail and drop the components it retired, without folding
|
|
139
|
+
* anything into their bookkeeping.
|
|
140
|
+
*/
|
|
141
|
+
#retireBuiltTrail() {
|
|
142
|
+
const uncommitted = this.#uncommittedTrail;
|
|
143
|
+
if (uncommitted == null) return;
|
|
144
|
+
this.#uncommittedTrail = void 0;
|
|
145
|
+
this.#retireComponents(uncommitted.retiring);
|
|
146
|
+
}
|
|
35
147
|
constructor(namespace = GlobalNS) {
|
|
36
148
|
const ns = toNamespace(namespace);
|
|
37
149
|
const ctxMap = ComponentContext.getContextsMap();
|
|
@@ -51,6 +163,16 @@ class ComponentContext {
|
|
|
51
163
|
get isDisposed() {
|
|
52
164
|
return this.#isDisposed;
|
|
53
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Take a component in: it becomes a member of this {@link ComponentContext} and a
|
|
168
|
+
* `CreateEntities` change is written for its uuid.
|
|
169
|
+
*
|
|
170
|
+
* Called by the {@link ViewComponent#context} setter, and by nothing else.
|
|
171
|
+
*
|
|
172
|
+
* @throws {ComponentContextDisposedError} if this {@link ComponentContext} has been disposed
|
|
173
|
+
* @throws {ComponentUuidInUseError} if another member of this {@link ComponentContext} holds
|
|
174
|
+
* `component.uuid`
|
|
175
|
+
*/
|
|
54
176
|
addComponent(component) {
|
|
55
177
|
if (this.#isDisposed) {
|
|
56
178
|
throw new ComponentContextDisposedError(
|
|
@@ -60,16 +182,19 @@ class ComponentContext {
|
|
|
60
182
|
let viewInstance = this.#components.get(component.uuid);
|
|
61
183
|
if (viewInstance) {
|
|
62
184
|
if (viewInstance.component !== component) {
|
|
63
|
-
|
|
185
|
+
if (this.#componentInstances.has(viewInstance.component)) {
|
|
186
|
+
throw new ComponentUuidInUseError(component.uuid);
|
|
187
|
+
}
|
|
188
|
+
for (const childUuid of [...viewInstance.children.uuids]) {
|
|
64
189
|
this.#components.get(childUuid)?.component.removeFromParent();
|
|
65
190
|
}
|
|
66
191
|
}
|
|
67
192
|
viewInstance.component = component;
|
|
68
|
-
viewInstance.children
|
|
193
|
+
viewInstance.children.clear();
|
|
69
194
|
} else {
|
|
70
195
|
viewInstance = {
|
|
71
196
|
component,
|
|
72
|
-
children:
|
|
197
|
+
children: new OrderedUuids(),
|
|
73
198
|
changes: new ComponentChanges(component.uuid),
|
|
74
199
|
propIsEqual: void 0
|
|
75
200
|
};
|
|
@@ -87,75 +212,104 @@ class ComponentContext {
|
|
|
87
212
|
} else {
|
|
88
213
|
this.#appendToOrdered(component, this.#rootComponents);
|
|
89
214
|
}
|
|
215
|
+
this.#componentInstances.add(component);
|
|
90
216
|
this.#viewInstances = void 0;
|
|
91
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether `component` is a member of this {@link ComponentContext} right now.
|
|
220
|
+
*
|
|
221
|
+
* This is the question about the instance, and the only one this class answers about
|
|
222
|
+
* membership. A component that has left gets `false`, even while its entry is still standing —
|
|
223
|
+
* {@link ComponentContext.hasComponents} is what counts entries.
|
|
224
|
+
*/
|
|
92
225
|
hasComponent(component) {
|
|
93
|
-
return this.#
|
|
226
|
+
return this.#componentInstances.has(component);
|
|
94
227
|
}
|
|
95
228
|
hasComponents() {
|
|
96
229
|
return this.#components.size > 0;
|
|
97
230
|
}
|
|
98
231
|
isRootComponent(component) {
|
|
99
|
-
return this.#rootComponents.
|
|
232
|
+
return this.#entryOf(component) !== void 0 && this.#rootComponents.has(component.uuid);
|
|
100
233
|
}
|
|
101
234
|
destroyComponent(component) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
235
|
+
this.#componentInstances.delete(component);
|
|
236
|
+
const entry = this.#entryOf(component);
|
|
237
|
+
if (entry !== void 0 && !entry.changes.isDestroyed) {
|
|
238
|
+
for (const childUuid of [...entry.children.uuids]) {
|
|
105
239
|
this.#components.get(childUuid)?.component.removeFromParent();
|
|
106
240
|
}
|
|
107
241
|
entry.changes.destroy();
|
|
108
242
|
this.#viewInstances = void 0;
|
|
109
243
|
}
|
|
244
|
+
if (component.context === this) {
|
|
245
|
+
component.destroy();
|
|
246
|
+
}
|
|
110
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* The children of `component`, in sort order — a fresh array each call, and an empty one for
|
|
250
|
+
* an instance that does not own its entry.
|
|
251
|
+
*
|
|
252
|
+
* Uuids without a matching view instance are skipped instead of dereferenced: `addToChildren()`
|
|
253
|
+
* writes a uuid into a children list without the parent link of the child following along, so
|
|
254
|
+
* `#deleteComponent()` has no list to take that uuid out of and the entry behind it goes while
|
|
255
|
+
* the list keeps naming it. `#appendToOrdered()` and `#traverseLevelOrderBFS()` read a children
|
|
256
|
+
* list the same way.
|
|
257
|
+
*/
|
|
111
258
|
getChildren(component) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
const childIdx = parentEntry.children.indexOf(childUuid);
|
|
120
|
-
if (childIdx !== -1) {
|
|
121
|
-
parentEntry.children.splice(childIdx, 1);
|
|
122
|
-
childEntry.changes.setParent(void 0);
|
|
259
|
+
const children = this.#entryOf(component)?.children;
|
|
260
|
+
if (children === void 0) return [];
|
|
261
|
+
const result = [];
|
|
262
|
+
for (const uuid of children.uuids) {
|
|
263
|
+
const entry = this.#components.get(uuid);
|
|
264
|
+
if (entry !== void 0) {
|
|
265
|
+
result.push(entry.component);
|
|
123
266
|
}
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
removeFromParent(component, parent) {
|
|
271
|
+
const parentEntry = this.#entryOf(parent);
|
|
272
|
+
if (parentEntry === void 0) return;
|
|
273
|
+
const childEntry = this.#entryOf(component);
|
|
274
|
+
if (childEntry === void 0) return;
|
|
275
|
+
if (parentEntry.children.delete(component.uuid)) {
|
|
276
|
+
childEntry.changes.setParent(void 0);
|
|
124
277
|
this.#appendToOrdered(childEntry.component, this.#rootComponents);
|
|
125
278
|
this.#viewInstances = void 0;
|
|
126
279
|
}
|
|
127
280
|
}
|
|
128
|
-
moveToRoot(
|
|
129
|
-
const childEntry = this.#
|
|
130
|
-
if (childEntry)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
281
|
+
moveToRoot(component) {
|
|
282
|
+
const childEntry = this.#entryOf(component);
|
|
283
|
+
if (childEntry === void 0) return;
|
|
284
|
+
const parentEntry = component.parent ? this.#entryOf(component.parent) : void 0;
|
|
285
|
+
parentEntry?.children.delete(component.uuid);
|
|
286
|
+
childEntry.changes.setParent(void 0);
|
|
287
|
+
this.#appendToOrdered(childEntry.component, this.#rootComponents);
|
|
134
288
|
this.#viewInstances = void 0;
|
|
135
289
|
}
|
|
136
290
|
changeToken(component, token) {
|
|
137
|
-
this.#
|
|
291
|
+
this.#entryOf(component)?.changes.changeToken(token);
|
|
138
292
|
}
|
|
139
293
|
isChildOf(child, parent) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return entry.children.includes(child.uuid);
|
|
143
|
-
}
|
|
144
|
-
return false;
|
|
294
|
+
const parentEntry = this.#entryOf(parent);
|
|
295
|
+
return parentEntry !== void 0 && this.#entryOf(child) !== void 0 && parentEntry.children.has(child.uuid);
|
|
145
296
|
}
|
|
146
297
|
addToChildren(parent, child) {
|
|
147
|
-
const entry = this.#
|
|
298
|
+
const entry = this.#entryOf(parent);
|
|
148
299
|
if (entry) {
|
|
300
|
+
const childEntry = this.#entryOf(child);
|
|
301
|
+
if (childEntry === void 0) return;
|
|
149
302
|
this.#appendToOrdered(child, entry.children);
|
|
150
|
-
|
|
151
|
-
|
|
303
|
+
childEntry.changes.setParent(parent.uuid);
|
|
304
|
+
this.#rootComponents.delete(child.uuid);
|
|
152
305
|
this.#viewInstances = void 0;
|
|
153
306
|
} else {
|
|
154
|
-
throw new Error(`the view component ${parent.uuid} cannot have a child added to it because
|
|
307
|
+
throw new Error(`the view component ${parent.uuid} cannot have a child added to it because this context does not hold it`);
|
|
155
308
|
}
|
|
156
309
|
}
|
|
157
310
|
/**
|
|
158
311
|
* Destroy a component and all its descendants without writing anything to a change trail.
|
|
312
|
+
* Each of them is detached from this context and reports {@link ViewComponent#isDestroyed}.
|
|
159
313
|
*
|
|
160
314
|
* @see {@link ComponentContext.clear}
|
|
161
315
|
*/
|
|
@@ -167,18 +321,18 @@ class ComponentContext {
|
|
|
167
321
|
visited.add(uuid);
|
|
168
322
|
const entry = this.#components.get(uuid);
|
|
169
323
|
if (entry) {
|
|
170
|
-
for (const childUuid of entry.children.
|
|
324
|
+
for (const childUuid of [...entry.children.uuids]) {
|
|
171
325
|
this.#removeSubTree(childUuid, visited);
|
|
172
326
|
}
|
|
173
|
-
this.destroyComponent(entry.component);
|
|
174
327
|
this.#deleteComponent(uuid);
|
|
328
|
+
this.destroyComponent(entry.component);
|
|
175
329
|
}
|
|
176
330
|
}
|
|
177
331
|
/**
|
|
178
332
|
* @returns `true` if the value differs from the last value written to a change trail
|
|
179
333
|
*/
|
|
180
334
|
setProperty(component, propKey, value, isEqual) {
|
|
181
|
-
const vi = this.#
|
|
335
|
+
const vi = this.#entryOf(component);
|
|
182
336
|
if (vi != null) {
|
|
183
337
|
if (isEqual != null) {
|
|
184
338
|
vi.propIsEqual ??= /* @__PURE__ */ new Map();
|
|
@@ -190,18 +344,86 @@ class ComponentContext {
|
|
|
190
344
|
}
|
|
191
345
|
return false;
|
|
192
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Mark `propKey` on `component` as set without giving it a value: the change trail carries
|
|
349
|
+
* an entry that names only the key, and the entity behind it reads the property as
|
|
350
|
+
* `undefined` with the key in place. {@link ComponentContext.setProperty} with `undefined`
|
|
351
|
+
* is the other thing — a removal.
|
|
352
|
+
*
|
|
353
|
+
* An equality rule registered for the key is forgotten, the way it is for a
|
|
354
|
+
* {@link ComponentContext.setProperty} that comes without one: there is no value here for a
|
|
355
|
+
* rule to compare.
|
|
356
|
+
*
|
|
357
|
+
* @returns `true` when this differs from the last value written to a change trail, and
|
|
358
|
+
* `false` for an instance that does not own its entry
|
|
359
|
+
*/
|
|
360
|
+
setPropertyWithoutValue(component, propKey) {
|
|
361
|
+
const vi = this.#entryOf(component);
|
|
362
|
+
if (vi != null) {
|
|
363
|
+
vi.propIsEqual?.delete(propKey);
|
|
364
|
+
return vi.changes.setPropertyWithoutValue(propKey);
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
193
368
|
removeProperty(component, propKey) {
|
|
194
|
-
this.#
|
|
369
|
+
this.#entryOf(component)?.changes.removeProperty(propKey);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Hand the properties of `component` over to the context it has just joined.
|
|
373
|
+
*
|
|
374
|
+
* Called by the {@link ViewComponent#context} setter, and by nothing else. It reads the state
|
|
375
|
+
* this context still holds for the component: leaving a context destroys it, but the entry
|
|
376
|
+
* survives until the next {@link ComponentContext.buildChangeTrails}, so the properties are
|
|
377
|
+
* still there to be read once the join has happened.
|
|
378
|
+
*
|
|
379
|
+
* The equality function travels with each property, because it is a rule about the key and not
|
|
380
|
+
* about the value — the same key means the same notion of "unchanged" wherever it lives. It does
|
|
381
|
+
* not get to decide about the arrival, though: the target holds nothing for the key yet, so it
|
|
382
|
+
* would be asked whether the value equals `undefined`, and a function that says yes would drop
|
|
383
|
+
* the property instead of carrying it over. The value is therefore written first and the rule
|
|
384
|
+
* registered after it — the other way round, the write would delete the entry it just made,
|
|
385
|
+
* because {@link ComponentContext.setProperty} keeps only what its caller passes.
|
|
386
|
+
*
|
|
387
|
+
* What the registered rule is for: {@link ComponentContext.reCreateChanges} is its one reader,
|
|
388
|
+
* and it compares with it when it rebuilds a component from the memory. A later
|
|
389
|
+
* {@link ComponentContext.setProperty} does not consult it — it uses its own argument, or none.
|
|
390
|
+
*
|
|
391
|
+
* A property that is set without a value goes over as one: handing it on as the value
|
|
392
|
+
* `undefined` would have the target read it as a removal, and the key would not arrive.
|
|
393
|
+
*
|
|
394
|
+
* Built like {@link ComponentChanges.transferEventsTo}, with one difference: the properties are
|
|
395
|
+
* copied, not moved. What stays behind goes down with the entity in this context.
|
|
396
|
+
*/
|
|
397
|
+
transferPropertiesTo(component, target) {
|
|
398
|
+
const vi = this.#entryOf(component);
|
|
399
|
+
if (vi === void 0) return;
|
|
400
|
+
for (const [key, value] of vi.changes.getProperties()) {
|
|
401
|
+
if (value === PropertyWithoutValue) {
|
|
402
|
+
target.setPropertyWithoutValue(component, key);
|
|
403
|
+
} else {
|
|
404
|
+
target.setProperty(component, key, value);
|
|
405
|
+
}
|
|
406
|
+
const isEqual = vi.propIsEqual?.get(key);
|
|
407
|
+
if (isEqual != null) {
|
|
408
|
+
target.#registerPropIsEqual(component, key, isEqual);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
#registerPropIsEqual(component, propKey, isEqual) {
|
|
413
|
+
const vi = this.#entryOf(component);
|
|
414
|
+
if (vi === void 0) return;
|
|
415
|
+
vi.propIsEqual ??= /* @__PURE__ */ new Map();
|
|
416
|
+
vi.propIsEqual.set(propKey, isEqual);
|
|
195
417
|
}
|
|
196
418
|
changeOrder(component) {
|
|
197
|
-
const entry = this.#
|
|
419
|
+
const entry = this.#entryOf(component);
|
|
198
420
|
if (entry === void 0) return;
|
|
199
|
-
const parentEntry = component.parent ? this.#
|
|
421
|
+
const parentEntry = component.parent ? this.#entryOf(component.parent) : void 0;
|
|
200
422
|
if (parentEntry !== void 0) {
|
|
201
|
-
|
|
423
|
+
parentEntry.children.delete(component.uuid);
|
|
202
424
|
this.#appendToOrdered(component, parentEntry.children);
|
|
203
425
|
} else if (component.parent == null) {
|
|
204
|
-
|
|
426
|
+
this.#rootComponents.delete(component.uuid);
|
|
205
427
|
this.#appendToOrdered(component, this.#rootComponents);
|
|
206
428
|
}
|
|
207
429
|
entry.changes.changeOrder(component.order);
|
|
@@ -217,7 +439,7 @@ class ComponentContext {
|
|
|
217
439
|
* Dispatch an event to the shadow objects linked to the view component
|
|
218
440
|
*/
|
|
219
441
|
dispatchShadowObjectsEvent(component, type, data, transferables) {
|
|
220
|
-
this.#
|
|
442
|
+
this.#entryOf(component)?.changes.createEvent(type, data, transferables);
|
|
221
443
|
}
|
|
222
444
|
/**
|
|
223
445
|
* Dispatch an event to all view components
|
|
@@ -234,56 +456,258 @@ class ComponentContext {
|
|
|
234
456
|
this.#components.get(uuid)?.component.dispatchEvent(type, data, traverseChildren);
|
|
235
457
|
}
|
|
236
458
|
/**
|
|
237
|
-
* Inform all root components that they should re-request their parents
|
|
459
|
+
* Inform all root components that they should re-request their parents.
|
|
460
|
+
*
|
|
461
|
+
* Every root is asked, because there is nothing here to ask about: the message carries no
|
|
462
|
+
* sender, so this side cannot narrow the set down at all. The receiver is the one that can:
|
|
463
|
+
* it re-asks the element tree, and whoever answers first wins.
|
|
464
|
+
*
|
|
465
|
+
* Delivery is immediate: the round is over by the time the call returns. Two paths inside the
|
|
466
|
+
* library end here — {@link ComponentContext.dispatchReRequestParentSiblings} for a component
|
|
467
|
+
* without a parent, whose candidate set is the roots, and the delivery of a collected round for
|
|
468
|
+
* that same set. What does not come this way is an entity that has just arrived: it hands its
|
|
469
|
+
* round to the collector below. The method is public because running a round at a moment of
|
|
470
|
+
* one's own choosing is what it is for.
|
|
238
471
|
*/
|
|
239
472
|
dispatchReRequestParentRoots() {
|
|
240
|
-
for (const uuid of this.#rootComponents) {
|
|
473
|
+
for (const uuid of [...this.#rootComponents.uuids]) {
|
|
241
474
|
this.dispatchMessage(uuid, ComponentContext.ReRequestParentRoots);
|
|
242
475
|
}
|
|
243
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Inform the children of `component` that they should re-request their parents.
|
|
479
|
+
*
|
|
480
|
+
* The receivers are the components hanging on `component` right now, and every one of them has
|
|
481
|
+
* to let go: the message carries no sender and needs no filter, because there is nothing here to
|
|
482
|
+
* decide. {@link ComponentContext.ReRequestParentRoots} is the signal for it — it means "drop
|
|
483
|
+
* your parent and ask again", which is what the roots happen to be asked for most often, not
|
|
484
|
+
* what it says.
|
|
485
|
+
*/
|
|
486
|
+
dispatchReRequestParentChildren(component) {
|
|
487
|
+
for (const child of this.getChildren(component)) {
|
|
488
|
+
this.dispatchMessage(child.uuid, ComponentContext.ReRequestParentRoots);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Inform the siblings of `component` that they should re-request their parents.
|
|
493
|
+
*
|
|
494
|
+
* This narrows the candidate set for one specific question: which components could have bound
|
|
495
|
+
* to an ancestor further away than they should have, because `component` was not yet answering
|
|
496
|
+
* when they asked? Such a component sits below `component` in the element tree and shares its
|
|
497
|
+
* parent — any entity in between would have answered first. As long as `component` has no
|
|
498
|
+
* parent of its own, the roots are that same set.
|
|
499
|
+
*
|
|
500
|
+
* The signal alone does not decide anything: the receiver re-asks, and whoever answers first
|
|
501
|
+
* wins. Which components exist as candidates is what this method knows; whether one of them
|
|
502
|
+
* really sits below `component` is settled on the receiving side, which is where the element
|
|
503
|
+
* tree is visible — `data` is what carries the sender's identity there.
|
|
504
|
+
*
|
|
505
|
+
* The two signals differ in the order they work in, and that decides more than it looks like:
|
|
506
|
+
* {@link ComponentContext.ReRequestParentRoots} releases the parent before asking, so a
|
|
507
|
+
* component that gets the same answer back re-joins its parent's children at the end;
|
|
508
|
+
* {@link ComponentContext.ReRequestParent} asks first and releases only where nobody answers,
|
|
509
|
+
* which leaves every component that is already bound where it stands, order included.
|
|
510
|
+
*
|
|
511
|
+
* Delivery is immediate, and nothing inside the library calls it: like
|
|
512
|
+
* {@link ComponentContext.dispatchReRequestParentRoots} it is public so that a round can be run
|
|
513
|
+
* at a moment of one's own choosing.
|
|
514
|
+
*/
|
|
515
|
+
dispatchReRequestParentSiblings(component, data = void 0) {
|
|
516
|
+
const parent = component.parent;
|
|
517
|
+
if (parent == null) {
|
|
518
|
+
this.dispatchReRequestParentRoots();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
for (const child of this.getChildren(parent)) {
|
|
522
|
+
if (child !== component) {
|
|
523
|
+
this.dispatchMessage(child.uuid, ComponentContext.ReRequestParent, data);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* The peers waiting for a round: sender by sender in arrival order, each with the data the
|
|
529
|
+
* round it asks for would carry.
|
|
530
|
+
*/
|
|
531
|
+
#peerReRequests = new MicrotaskCollector((pending) => this.#deliverPeerReRequests(pending));
|
|
532
|
+
/**
|
|
533
|
+
* Take a peer re-request from `sender` — an entity that has just become one and may be the new
|
|
534
|
+
* closest ancestor for entities that bound while it was not yet answering.
|
|
535
|
+
*
|
|
536
|
+
* Everything that arrives in the same task shares one round. Alone, each arrival is a broadcast
|
|
537
|
+
* over the whole candidate set: the root channel carries no sender and has to ask every root,
|
|
538
|
+
* the sibling channel asks every child of one parent. n entities coming up together then cost
|
|
539
|
+
* n(n+1)/2 messages, and each message is a full ancestor request through the DOM — for 600 roots
|
|
540
|
+
* in one namespace that is 180 300 messages and some 257 ms on top of a build that takes 42 ms
|
|
541
|
+
* with the channel switched off. One round per task costs 600 messages and 2 ms on top. The
|
|
542
|
+
* point where a round per arrival costs more than a frame lies at about 145 roots in one
|
|
543
|
+
* namespace; one round per task stays clear of it over the whole measured range, up to 600.
|
|
544
|
+
*
|
|
545
|
+
* Numbers measured 2026-08-22 in Chromium via Playwright 1.62.1 — a snapshot, not a guarantee;
|
|
546
|
+
* the size series is in `packages/shadow-objects/docs/guides.md`, under "How Many Entities Fit
|
|
547
|
+
* in One Namespace".
|
|
548
|
+
*
|
|
549
|
+
* @param data travels with the round as long as `sender` is the only one in it — see
|
|
550
|
+
* {@link ComponentContext.dispatchReRequestParentSiblings} for what a receiver does with it.
|
|
551
|
+
*
|
|
552
|
+
* @internal
|
|
553
|
+
*/
|
|
554
|
+
collectPeerReRequest(sender, data = void 0) {
|
|
555
|
+
this.#peerReRequests.add(sender, data);
|
|
556
|
+
}
|
|
557
|
+
/** Run the collected rounds, one per candidate set. */
|
|
558
|
+
#deliverPeerReRequests(pending) {
|
|
559
|
+
const rounds = /* @__PURE__ */ new Map();
|
|
560
|
+
for (const [sender, data] of pending) {
|
|
561
|
+
if (!this.#componentInstances.has(sender)) continue;
|
|
562
|
+
const key = sender.parent ?? null;
|
|
563
|
+
const round = rounds.get(key);
|
|
564
|
+
if (round === void 0) {
|
|
565
|
+
rounds.set(key, { data });
|
|
566
|
+
} else {
|
|
567
|
+
round.data = void 0;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const [parent, round] of rounds) {
|
|
571
|
+
runGuarded(
|
|
572
|
+
this.#logger,
|
|
573
|
+
() => {
|
|
574
|
+
if (parent == null) {
|
|
575
|
+
this.dispatchReRequestParentRoots();
|
|
576
|
+
} else {
|
|
577
|
+
for (const child of this.getChildren(parent)) {
|
|
578
|
+
this.dispatchMessage(child.uuid, ComponentContext.ReRequestParent, round.data);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
"a peer re-request round could not be delivered:",
|
|
583
|
+
parent?.uuid ?? "roots"
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
244
587
|
/**
|
|
245
588
|
* Create the component change trails at this point in time.
|
|
246
589
|
* The next call will only return the differences from the previous call.
|
|
247
590
|
*
|
|
591
|
+
* @param commit whether the trail counts as applied the moment it is built. Pass `false` when
|
|
592
|
+
* the trail still has to travel to a Shadow Environment that may refuse it, and settle it
|
|
593
|
+
* afterwards with {@link ComponentContext.commitChangeTrail}.
|
|
594
|
+
*
|
|
595
|
+
* @see {@link ComponentContext.commitChangeTrail}
|
|
248
596
|
* @see {@link ComponentContext.reCreateChanges}
|
|
249
597
|
*/
|
|
250
|
-
buildChangeTrails(
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
changes.buildChangeTrail(
|
|
262
|
-
|
|
263
|
-
|
|
598
|
+
buildChangeTrails(commit = true) {
|
|
599
|
+
this.#peerReRequests.flush();
|
|
600
|
+
if (this.#uncommittedTrail != null) {
|
|
601
|
+
this.#logger.debug("committing an open change trail because a second one is being built", this.#uncommittedTrail.entries);
|
|
602
|
+
this.commitChangeTrail(this.#uncommittedTrail.entries.length);
|
|
603
|
+
}
|
|
604
|
+
const entries = [];
|
|
605
|
+
const owners = [];
|
|
606
|
+
const participants = this.hasComponents() ? this.#buildPathOfChanges() : [];
|
|
607
|
+
const build = (changes, phase) => {
|
|
608
|
+
const before = entries.length;
|
|
609
|
+
changes.buildChangeTrail(entries, phase);
|
|
610
|
+
for (let i = before; i < entries.length; i++) {
|
|
611
|
+
owners[i] = changes;
|
|
264
612
|
}
|
|
265
|
-
|
|
613
|
+
};
|
|
614
|
+
for (const changes of participants) build(changes, ChangeTrailPhase.StructuralChanges);
|
|
615
|
+
for (const changes of participants) build(changes, ChangeTrailPhase.ContentUpdates);
|
|
616
|
+
for (const changes of participants) build(changes, ChangeTrailPhase.Removal);
|
|
617
|
+
this.#uncommittedTrail = { entries, owners, retiring: this.#retiringComponents(participants) };
|
|
618
|
+
if (commit) this.commitChangeTrail(entries.length);
|
|
619
|
+
return entries;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Fold the first `appliedCount` entries of the change trail this context built last into the
|
|
623
|
+
* state the next trail is diffed against, and write them to the Component Memory. Every entry
|
|
624
|
+
* behind that line stays pending and goes out again with the next trail.
|
|
625
|
+
*
|
|
626
|
+
* A component is retired -- its entry dropped -- only where the build read it as spent and every
|
|
627
|
+
* entry it contributed is settled. One component can hold a creation and an event in the same
|
|
628
|
+
* trail, and a creation sent twice is refused by the Shadow Environment, which already holds an
|
|
629
|
+
* entity behind that uuid.
|
|
630
|
+
*
|
|
631
|
+
* @param appliedCount how many entries the Shadow Environment applied, counted from the front.
|
|
632
|
+
* Clamped to the length of the trail.
|
|
633
|
+
* @param changeTrail the trail this call settles. Given, the call is ignored unless it is the
|
|
634
|
+
* very trail this context built last -- a cycle that lost its trail to a later build must not
|
|
635
|
+
* draw a line through that later trail.
|
|
636
|
+
*/
|
|
637
|
+
commitChangeTrail(appliedCount, changeTrail) {
|
|
638
|
+
const uncommitted = this.#uncommittedTrail;
|
|
639
|
+
if (uncommitted == null) return;
|
|
640
|
+
if (changeTrail != null && changeTrail !== uncommitted.entries) return;
|
|
641
|
+
const { entries, owners, retiring } = uncommitted;
|
|
642
|
+
const count = Math.min(Math.max(appliedCount, 0), entries.length);
|
|
643
|
+
this.#uncommittedTrail = void 0;
|
|
644
|
+
const stillPending = new Set(owners.slice(count));
|
|
645
|
+
for (let i = 0; i < count; i++) {
|
|
646
|
+
owners[i].commitChange(entries[i]);
|
|
266
647
|
}
|
|
267
|
-
this.#componentMemory.write(
|
|
268
|
-
|
|
648
|
+
this.#componentMemory.write(count === entries.length ? entries : entries.slice(0, count));
|
|
649
|
+
this.#retireComponents(retiring, stillPending);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Whether the Component Memory holds a state for `uuid` — the state
|
|
653
|
+
* {@link ComponentContext.reCreateChanges} would rebuild that component from.
|
|
654
|
+
*/
|
|
655
|
+
hasComponentState(uuid) {
|
|
656
|
+
return this.#componentMemory.hasComponentState(uuid);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* The state the Component Memory holds for `uuid`, or `undefined` where it holds none.
|
|
660
|
+
*
|
|
661
|
+
* A snapshot, not the record: the property list of the record is rewritten in place as
|
|
662
|
+
* trails come in, so a caller holding on to it would be reading a moving value — or
|
|
663
|
+
* writing one. What it is a snapshot of is the memory at the moment of the call, and the
|
|
664
|
+
* memory moves under several hands: {@link ComponentContext.commitChangeTrail} folds a
|
|
665
|
+
* settled trail into it, {@link ComponentContext.buildChangeTrails} does the same for the
|
|
666
|
+
* trail it builds unless it is told not to commit, {@link ComponentContext.reCreateChanges}
|
|
667
|
+
* writes the trail it builds and empties the memory afterwards, and
|
|
668
|
+
* {@link ComponentContext.clear} empties it outright. A snapshot taken before any of those
|
|
669
|
+
* describes a state that is over; take it again to see the current one.
|
|
670
|
+
*
|
|
671
|
+
* This is the window a test or a diagnosis needs, and the one that leaves the context as it
|
|
672
|
+
* found it — reading the same thing out of {@link ComponentContext.reCreateChanges} costs a
|
|
673
|
+
* rebuild of every component in the namespace.
|
|
674
|
+
*/
|
|
675
|
+
getComponentState(uuid) {
|
|
676
|
+
const state = this.#componentMemory.getComponentState(uuid);
|
|
677
|
+
if (state === void 0) return void 0;
|
|
678
|
+
return {
|
|
679
|
+
...state,
|
|
680
|
+
properties: state.properties?.map((entry) => entry.length === 1 ? [entry[0]] : [entry[0], entry[1]])
|
|
681
|
+
};
|
|
269
682
|
}
|
|
270
683
|
/**
|
|
271
684
|
* Resets the internal component change states so that all view components are regenerated with the next change trail.
|
|
272
685
|
* The outstanding events are taken over.
|
|
273
686
|
*
|
|
687
|
+
* The trail this produces belongs to a Shadow Environment that holds none of these uuids -- a
|
|
688
|
+
* fresh proxy. One whose Kernel still holds them refuses the first re-created creation it reads,
|
|
689
|
+
* and every cycle that follows, because a uuid names one entity at a time.
|
|
690
|
+
*
|
|
274
691
|
* @see {@link ComponentContext.buildChangeTrails}
|
|
275
692
|
*/
|
|
276
693
|
reCreateChanges() {
|
|
277
694
|
if (this.#componentMemory.isEmpty()) return;
|
|
278
|
-
this.buildChangeTrails(false);
|
|
695
|
+
const trails = this.buildChangeTrails(false);
|
|
696
|
+
this.#componentMemory.write(trails);
|
|
697
|
+
this.#retireBuiltTrail();
|
|
279
698
|
for (const [uuid, cMem] of this.#componentMemory) {
|
|
280
699
|
const c = this.#components.get(uuid);
|
|
281
700
|
if (c) {
|
|
282
701
|
const changes = new ComponentChanges(uuid);
|
|
283
702
|
changes.create(cMem.token, cMem.parentUuid, cMem.order, cMem.autoDestructionOnParentRemoval);
|
|
284
703
|
if (cMem.properties) {
|
|
285
|
-
for (const
|
|
286
|
-
|
|
704
|
+
for (const entry of cMem.properties) {
|
|
705
|
+
const key = entry[0];
|
|
706
|
+
if (entry.length === 1) {
|
|
707
|
+
changes.setPropertyWithoutValue(key);
|
|
708
|
+
} else {
|
|
709
|
+
changes.changeProperty(key, entry[1], c.propIsEqual?.get(key));
|
|
710
|
+
}
|
|
287
711
|
}
|
|
288
712
|
}
|
|
289
713
|
c.changes.transferEventsTo(changes);
|
|
@@ -295,18 +719,26 @@ class ComponentContext {
|
|
|
295
719
|
this.broadcastEvent(ContextLost);
|
|
296
720
|
}
|
|
297
721
|
/**
|
|
298
|
-
* Remove all components without writing anything to a change trail.
|
|
299
|
-
*
|
|
722
|
+
* Remove all components without writing anything to a change trail. Every
|
|
723
|
+
* {@link ViewComponent} this context holds is destroyed, so each of them reports
|
|
724
|
+
* {@link ViewComponent#isDestroyed} and holds no context afterwards. The context itself stays
|
|
725
|
+
* registered under its namespace and can be used again — assigning it to a component takes
|
|
726
|
+
* that component back in under the same uuid.
|
|
300
727
|
*
|
|
301
728
|
* @see {@link ComponentContext.dispose} for the final teardown
|
|
302
729
|
*/
|
|
303
730
|
clear() {
|
|
304
731
|
this.#viewInstances = void 0;
|
|
305
732
|
this.#componentMemory.clear();
|
|
306
|
-
|
|
733
|
+
this.#uncommittedTrail = void 0;
|
|
734
|
+
for (const component of Array.from(this.#componentInstances)) {
|
|
735
|
+
component.destroy();
|
|
736
|
+
}
|
|
737
|
+
this.#componentInstances.clear();
|
|
738
|
+
for (const uuid of [...this.#rootComponents.uuids]) {
|
|
307
739
|
this.removeSubTree(uuid);
|
|
308
740
|
}
|
|
309
|
-
if (this.#rootComponents.
|
|
741
|
+
if (this.#rootComponents.size !== 0) {
|
|
310
742
|
throw new Error("component-context panic: #rootComponents is not empty!");
|
|
311
743
|
}
|
|
312
744
|
if (this.#components.size !== 0) {
|
|
@@ -322,14 +754,11 @@ class ComponentContext {
|
|
|
322
754
|
* produces empty change trails, and rejects any component that tries to join it. Calling
|
|
323
755
|
* it more than once is a no-op.
|
|
324
756
|
*
|
|
325
|
-
* A
|
|
757
|
+
* A `ShadowEnv` bound to this context keeps its reference; destroy the environment
|
|
326
758
|
* first if you want the namespace released on both sides.
|
|
327
759
|
*/
|
|
328
760
|
dispose() {
|
|
329
761
|
if (this.#isDisposed) return;
|
|
330
|
-
for (const { component } of Array.from(this.#components.values())) {
|
|
331
|
-
component.destroy();
|
|
332
|
-
}
|
|
333
762
|
this.clear();
|
|
334
763
|
this.#isDisposed = true;
|
|
335
764
|
const ctxMap = ComponentContext.getContextsMap();
|
|
@@ -337,18 +766,26 @@ class ComponentContext {
|
|
|
337
766
|
ctxMap.delete(this.ns);
|
|
338
767
|
}
|
|
339
768
|
}
|
|
340
|
-
|
|
769
|
+
/**
|
|
770
|
+
* Drop the component entry behind `uuid`, and with it the uuid's place in every children list.
|
|
771
|
+
*
|
|
772
|
+
* @param expectedChanges the bookkeeping that asked for the deletion. A round trip lies between
|
|
773
|
+
* the build of a change trail and its commit, and an entry that a different bookkeeping stands
|
|
774
|
+
* behind by then belongs to a component that joined in the meantime.
|
|
775
|
+
*/
|
|
776
|
+
#deleteComponent(uuid, expectedChanges) {
|
|
341
777
|
const entry = this.#components.get(uuid);
|
|
342
778
|
if (entry === void 0) return;
|
|
779
|
+
if (expectedChanges !== void 0 && entry.changes !== expectedChanges) return;
|
|
343
780
|
const parentUuid = entry.component.parent?.uuid;
|
|
344
781
|
if (parentUuid !== void 0) {
|
|
345
782
|
const parentEntry = this.#components.get(parentUuid);
|
|
346
783
|
if (parentEntry !== void 0) {
|
|
347
|
-
|
|
784
|
+
parentEntry.children.delete(uuid);
|
|
348
785
|
}
|
|
349
786
|
}
|
|
350
787
|
this.#components.delete(uuid);
|
|
351
|
-
|
|
788
|
+
this.#rootComponents.delete(uuid);
|
|
352
789
|
this.#viewInstances = void 0;
|
|
353
790
|
}
|
|
354
791
|
#buildPathOfChanges() {
|
|
@@ -363,16 +800,32 @@ class ComponentContext {
|
|
|
363
800
|
*
|
|
364
801
|
* Uuids without a matching view instance are skipped instead of dereferenced, so a
|
|
365
802
|
* partially torn down list can never turn a reordering into an exception.
|
|
803
|
+
*
|
|
804
|
+
* Two questions decide what one insertion costs, and a list that carries nothing but its uuids
|
|
805
|
+
* has to be walked for both: once to see whether the uuid already stands in it, once to find the
|
|
806
|
+
* place it sorts into. Filling such a list with n components costs n(n+1)/2 steps. `OrderedUuids`
|
|
807
|
+
* answers the membership in one step, and the shortcut below answers the place in one more for
|
|
808
|
+
* every component that sorts at or after the last member; a component that sorts ahead of it
|
|
809
|
+
* still walks the list. Building n roots one after another, all on the default order, that is
|
|
810
|
+
* 1.01 ms for 600 and 2.03 ms for 1200, against 4.44 ms and 16.04 ms for the walked list.
|
|
811
|
+
* Numbers measured 2026-08-31 on node v25.9.0, on the context alone with no DOM around
|
|
812
|
+
* it -- a snapshot, not a guarantee. The parent-resolution series in
|
|
813
|
+
* `packages/shadow-objects/docs/guides.md` was measured in a browser and covers a different question.
|
|
366
814
|
*/
|
|
367
815
|
#appendToOrdered(component, childUuids) {
|
|
368
|
-
if (childUuids.
|
|
816
|
+
if (childUuids.has(component.uuid)) {
|
|
369
817
|
return;
|
|
370
818
|
}
|
|
371
819
|
const { order } = component;
|
|
372
|
-
|
|
373
|
-
|
|
820
|
+
const last = childUuids.size > 0 ? this.#components.get(childUuids.uuids[childUuids.size - 1])?.component : void 0;
|
|
821
|
+
if (last !== void 0 && order >= last.order) {
|
|
822
|
+
childUuids.push(component.uuid);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
for (let i = 0; i < childUuids.size; i++) {
|
|
826
|
+
const other = this.#components.get(childUuids.uuids[i])?.component;
|
|
374
827
|
if (other !== void 0 && order < other.order) {
|
|
375
|
-
childUuids.
|
|
828
|
+
childUuids.insertAt(i, component.uuid);
|
|
376
829
|
return;
|
|
377
830
|
}
|
|
378
831
|
}
|
|
@@ -382,7 +835,10 @@ class ComponentContext {
|
|
|
382
835
|
#traverseLevelOrderBFS() {
|
|
383
836
|
if (this.#viewInstances) return this.#viewInstances;
|
|
384
837
|
const lvl = /* @__PURE__ */ new Map();
|
|
838
|
+
const visited = /* @__PURE__ */ new Set();
|
|
385
839
|
const traverse = (uuid, depth) => {
|
|
840
|
+
if (visited.has(uuid)) return;
|
|
841
|
+
visited.add(uuid);
|
|
386
842
|
const viewInstance = this.#components.get(uuid);
|
|
387
843
|
if (viewInstance == null) return;
|
|
388
844
|
const atDepth = lvl.get(depth);
|
|
@@ -391,11 +847,11 @@ class ComponentContext {
|
|
|
391
847
|
} else {
|
|
392
848
|
lvl.set(depth, [viewInstance]);
|
|
393
849
|
}
|
|
394
|
-
for (const childUuid of viewInstance.children) {
|
|
850
|
+
for (const childUuid of viewInstance.children.uuids) {
|
|
395
851
|
traverse(childUuid, depth + 1);
|
|
396
852
|
}
|
|
397
853
|
};
|
|
398
|
-
for (const uuid of this.#rootComponents) {
|
|
854
|
+
for (const uuid of this.#rootComponents.uuids) {
|
|
399
855
|
traverse(uuid, 0);
|
|
400
856
|
}
|
|
401
857
|
this.#viewInstances = Array.from(lvl.entries()).sort((a, b) => a[0] - b[0]).flatMap(([, vi]) => vi);
|
|
@@ -404,6 +860,7 @@ class ComponentContext {
|
|
|
404
860
|
}
|
|
405
861
|
export {
|
|
406
862
|
ComponentContext,
|
|
407
|
-
ComponentContextDisposedError
|
|
863
|
+
ComponentContextDisposedError,
|
|
864
|
+
ComponentUuidInUseError
|
|
408
865
|
};
|
|
409
866
|
//# sourceMappingURL=ComponentContext.js.map
|