@spearwolf/shadow-objects 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +320 -61
- package/README.md +57 -13
- package/bundle.js +5 -54
- package/package.json +7 -3
- package/src/ChangeTrailRefusedError.d.ts +27 -0
- package/src/ChangeTrailRefusedError.d.ts.map +1 -0
- package/src/ChangeTrailRefusedError.js +12 -0
- package/src/ChangeTrailRefusedError.js.map +7 -0
- package/src/EntityUuidInUseError.d.ts +19 -0
- package/src/EntityUuidInUseError.d.ts.map +1 -0
- package/src/EntityUuidInUseError.js +11 -0
- package/src/EntityUuidInUseError.js.map +7 -0
- package/src/WorkerTimeoutError.d.ts +25 -0
- package/src/WorkerTimeoutError.d.ts.map +1 -0
- package/src/WorkerTimeoutError.js +12 -0
- package/src/WorkerTimeoutError.js.map +7 -0
- package/src/bundle.d.ts +1 -1
- package/src/bundle.d.ts.map +1 -1
- package/src/bundle.js +1 -1
- package/src/bundle.js.map +2 -2
- package/src/constants.d.ts +9 -2
- package/src/constants.d.ts.map +1 -1
- package/src/constants.js +1 -1
- package/src/constants.js.map +2 -2
- package/src/create-worker.bundle.d.ts +1 -1
- package/src/create-worker.bundle.d.ts.map +1 -1
- package/src/create-worker.bundle.js.map +1 -1
- package/src/create-worker.d.ts +1 -1
- package/src/create-worker.d.ts.map +1 -1
- package/src/create-worker.js.map +1 -1
- package/src/elements/ShaeElement.d.ts +70 -2
- package/src/elements/ShaeElement.d.ts.map +1 -1
- package/src/elements/ShaeElement.js +129 -25
- package/src/elements/ShaeElement.js.map +3 -3
- package/src/elements/ShaeEntElement.d.ts +39 -1
- package/src/elements/ShaeEntElement.d.ts.map +1 -1
- package/src/elements/ShaeEntElement.js +397 -167
- package/src/elements/ShaeEntElement.js.map +2 -2
- package/src/elements/ShaeLifecycleElement.d.ts +71 -0
- package/src/elements/ShaeLifecycleElement.d.ts.map +1 -0
- package/src/elements/ShaeLifecycleElement.js +83 -0
- package/src/elements/ShaeLifecycleElement.js.map +7 -0
- package/src/elements/ShaePropElement.d.ts +52 -2
- package/src/elements/ShaePropElement.d.ts.map +1 -1
- package/src/elements/ShaePropElement.js +240 -219
- package/src/elements/ShaePropElement.js.map +2 -2
- package/src/elements/ShaeWorkerElement.d.ts +49 -5
- package/src/elements/ShaeWorkerElement.d.ts.map +1 -1
- package/src/elements/ShaeWorkerElement.js +204 -73
- package/src/elements/ShaeWorkerElement.js.map +2 -2
- package/src/elements/constants.d.ts +6 -0
- package/src/elements/constants.d.ts.map +1 -1
- package/src/elements/constants.js +12 -0
- package/src/elements/constants.js.map +2 -2
- package/src/elements/deferredTeardown.d.ts +24 -0
- package/src/elements/deferredTeardown.d.ts.map +1 -0
- package/src/elements/deferredTeardown.js +30 -0
- package/src/elements/deferredTeardown.js.map +7 -0
- package/src/elements/displayContentsRule.d.ts +25 -0
- package/src/elements/displayContentsRule.d.ts.map +1 -0
- package/src/elements/displayContentsRule.js +37 -0
- package/src/elements/displayContentsRule.js.map +7 -0
- package/src/elements/events.d.ts +10 -7
- package/src/elements/events.d.ts.map +1 -1
- package/src/elements/forwardCustomEvents.d.ts +19 -0
- package/src/elements/forwardCustomEvents.d.ts.map +1 -0
- package/src/elements/forwardCustomEvents.js +47 -0
- package/src/elements/forwardCustomEvents.js.map +7 -0
- package/src/elements/hostedSlots.d.ts +36 -0
- package/src/elements/hostedSlots.d.ts.map +1 -0
- package/src/elements/hostedSlots.js +118 -0
- package/src/elements/hostedSlots.js.map +7 -0
- package/src/elements/parentRemoval.d.ts +18 -0
- package/src/elements/parentRemoval.d.ts.map +1 -0
- package/src/elements/parentRemoval.js +60 -0
- package/src/elements/parentRemoval.js.map +7 -0
- package/src/elements/propValueConverters.d.ts +3 -0
- package/src/elements/propValueConverters.d.ts.map +1 -0
- package/src/elements/propValueConverters.js +54 -0
- package/src/elements/propValueConverters.js.map +7 -0
- package/src/elements/requestEntAncestor.d.ts +32 -0
- package/src/elements/requestEntAncestor.d.ts.map +1 -0
- package/src/elements/requestEntAncestor.js +16 -0
- package/src/elements/requestEntAncestor.js.map +7 -0
- package/src/elements.js.map +1 -1
- package/src/in-the-dark/Entity.d.ts +113 -3
- package/src/in-the-dark/Entity.d.ts.map +1 -1
- package/src/in-the-dark/Entity.js +292 -63
- package/src/in-the-dark/Entity.js.map +3 -3
- package/src/in-the-dark/Kernel.d.ts +76 -17
- package/src/in-the-dark/Kernel.d.ts.map +1 -1
- package/src/in-the-dark/Kernel.js +410 -378
- package/src/in-the-dark/Kernel.js.map +3 -3
- package/src/in-the-dark/Registry.d.ts.map +1 -1
- package/src/in-the-dark/Registry.js +83 -14
- package/src/in-the-dark/Registry.js.map +2 -2
- package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
- package/src/in-the-dark/ShadowObject.js +1 -0
- package/src/in-the-dark/ShadowObject.js.map +2 -2
- package/src/in-the-dark/ShadowObjectCreationScope.d.ts +126 -0
- package/src/in-the-dark/ShadowObjectCreationScope.d.ts.map +1 -0
- package/src/in-the-dark/ShadowObjectCreationScope.js +596 -0
- package/src/in-the-dark/ShadowObjectCreationScope.js.map +7 -0
- package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
- package/src/in-the-dark/SignalsPath.js +2 -2
- package/src/in-the-dark/SignalsPath.js.map +2 -2
- package/src/in-the-dark/events.js.map +1 -1
- package/src/in-the-dark/importModule.d.ts +7 -0
- package/src/in-the-dark/importModule.d.ts.map +1 -1
- package/src/in-the-dark/importModule.js +5 -2
- package/src/in-the-dark/importModule.js.map +2 -2
- package/src/index.d.ts +7 -1
- package/src/index.d.ts.map +1 -1
- package/src/index.js +5 -1
- package/src/index.js.map +2 -2
- package/src/shadow-objects.d.ts +2 -0
- package/src/shadow-objects.d.ts.map +1 -1
- package/src/shadow-objects.js +2 -0
- package/src/shadow-objects.js.map +2 -2
- package/src/shadow-objects.worker.js +0 -1
- package/src/shadow-objects.worker.js.map +2 -2
- package/src/shae-ent.js.map +1 -1
- package/src/shae-prop.js +2 -2
- package/src/shae-prop.js.map +2 -2
- package/src/shae-worker.js.map +1 -1
- package/src/types.d.ts +35 -4
- package/src/types.d.ts.map +1 -1
- package/src/utils/ConsoleLogger.d.ts +45 -10
- package/src/utils/ConsoleLogger.d.ts.map +1 -1
- package/src/utils/ConsoleLogger.js +51 -26
- package/src/utils/ConsoleLogger.js.map +2 -2
- package/src/utils/FrameLoop.d.ts +42 -3
- package/src/utils/FrameLoop.d.ts.map +1 -1
- package/src/utils/FrameLoop.js +71 -10
- package/src/utils/FrameLoop.js.map +2 -2
- package/src/utils/MicrotaskCollector.d.ts +45 -0
- package/src/utils/MicrotaskCollector.d.ts.map +1 -0
- package/src/utils/MicrotaskCollector.js +33 -0
- package/src/utils/MicrotaskCollector.js.map +7 -0
- package/src/utils/MicrotaskGate.d.ts +17 -0
- package/src/utils/MicrotaskGate.d.ts.map +1 -0
- package/src/utils/MicrotaskGate.js +20 -0
- package/src/utils/MicrotaskGate.js.map +7 -0
- package/src/utils/array-utils.js.map +1 -1
- package/src/utils/attr-utils.d.ts +6 -0
- package/src/utils/attr-utils.d.ts.map +1 -1
- package/src/utils/attr-utils.js +3 -1
- package/src/utils/attr-utils.js.map +2 -2
- package/src/utils/constants.js.map +1 -1
- package/src/utils/generateUUID.d.ts +11 -0
- package/src/utils/generateUUID.d.ts.map +1 -1
- package/src/utils/generateUUID.js +31 -266
- package/src/utils/generateUUID.js.map +2 -2
- package/src/utils/props-utils.d.ts +3 -2
- package/src/utils/props-utils.d.ts.map +1 -1
- package/src/utils/props-utils.js +13 -24
- package/src/utils/props-utils.js.map +2 -2
- package/src/utils/runGuarded.d.ts +27 -0
- package/src/utils/runGuarded.d.ts.map +1 -0
- package/src/utils/runGuarded.js +11 -0
- package/src/utils/runGuarded.js.map +7 -0
- package/src/utils/toMaybe.d.ts.map +1 -1
- package/src/utils/toMaybe.js.map +1 -1
- package/src/utils/toNamespace.d.ts.map +1 -1
- package/src/utils/toNamespace.js.map +1 -1
- package/src/utils/toUrlString.d.ts +12 -0
- package/src/utils/toUrlString.d.ts.map +1 -1
- package/src/utils/toUrlString.js.map +2 -2
- package/src/utils/waitForMessageOfType.d.ts +12 -1
- package/src/utils/waitForMessageOfType.d.ts.map +1 -1
- package/src/utils/waitForMessageOfType.js +16 -1
- package/src/utils/waitForMessageOfType.js.map +2 -2
- package/src/view/ComponentChanges.d.ts +88 -1
- package/src/view/ComponentChanges.d.ts.map +1 -1
- package/src/view/ComponentChanges.js +243 -73
- package/src/view/ComponentChanges.js.map +2 -2
- package/src/view/ComponentContext.d.ts +206 -16
- package/src/view/ComponentContext.d.ts.map +1 -1
- package/src/view/ComponentContext.js +548 -91
- package/src/view/ComponentContext.js.map +2 -2
- package/src/view/ComponentMemory.d.ts +3 -3
- package/src/view/ComponentMemory.d.ts.map +1 -1
- package/src/view/ComponentMemory.js.map +1 -1
- package/src/view/IShadowObjectEnvProxy.d.ts +29 -3
- package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.d.ts +1 -1
- package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.js +8 -6
- package/src/view/LocalShadowObjectEnv.js.map +2 -2
- package/src/view/RemoteWorkerEnv.d.ts +89 -5
- package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
- package/src/view/RemoteWorkerEnv.js +195 -77
- package/src/view/RemoteWorkerEnv.js.map +2 -2
- package/src/view/ShadowEnv.d.ts +49 -2
- package/src/view/ShadowEnv.d.ts.map +1 -1
- package/src/view/ShadowEnv.js +223 -60
- package/src/view/ShadowEnv.js.map +2 -2
- package/src/view/ViewComponent.d.ts +78 -14
- package/src/view/ViewComponent.d.ts.map +1 -1
- package/src/view/ViewComponent.js +90 -14
- package/src/view/ViewComponent.js.map +2 -2
- package/src/view/cloneChangeTrail.js.map +2 -2
- package/src/worker/MessageRouter.d.ts +14 -0
- package/src/worker/MessageRouter.d.ts.map +1 -1
- package/src/worker/MessageRouter.js +75 -15
- package/src/worker/MessageRouter.js.map +2 -2
- package/src/worker/WorkerRuntime.d.ts +22 -1
- package/src/worker/WorkerRuntime.d.ts.map +1 -1
- package/src/worker/WorkerRuntime.js +60 -8
- package/src/worker/WorkerRuntime.js.map +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { emitStrict, once, retain } from "@spearwolf/eventize";
|
|
2
|
+
import { ChangeTrailRefusedError } from "../ChangeTrailRefusedError.js";
|
|
2
3
|
import {
|
|
3
4
|
AppliedChangeTrail,
|
|
4
5
|
ChangeTrail,
|
|
@@ -14,24 +15,24 @@ import {
|
|
|
14
15
|
WorkerLoadTimeout
|
|
15
16
|
} from "../constants.js";
|
|
16
17
|
import createWorker from "../create-worker.js";
|
|
17
|
-
import { CONSOLE_LOGGER, ConsoleLogger } from "../utils/ConsoleLogger.js";
|
|
18
|
+
import { CONSOLE_LOGGER, ConsoleLogger, consoleLoggerConfigKey, loadConsoleLoggerConfig } from "../utils/ConsoleLogger.js";
|
|
18
19
|
import { toUrlString } from "../utils/toUrlString.js";
|
|
19
|
-
import { waitForMessageOfType } from "../utils/waitForMessageOfType.js";
|
|
20
|
-
const
|
|
20
|
+
import { isTimeout, MaxWorkerTimeout, waitForMessageOfType } from "../utils/waitForMessageOfType.js";
|
|
21
|
+
const splitTransferables = (changeTrail) => {
|
|
22
|
+
if (!Array.isArray(changeTrail)) return { changeTrail };
|
|
23
|
+
let outbound;
|
|
21
24
|
let transferables;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
delete changeItem.transferables;
|
|
31
|
-
}
|
|
25
|
+
for (let i = 0; i < changeTrail.length; i++) {
|
|
26
|
+
const changeItem = changeTrail[i];
|
|
27
|
+
if (changeItem?.transferables) {
|
|
28
|
+
transferables = transferables ? [...transferables, ...changeItem.transferables] : [...changeItem.transferables];
|
|
29
|
+
outbound ??= [...changeTrail];
|
|
30
|
+
const withoutTransferables = { ...changeItem };
|
|
31
|
+
delete withoutTransferables.transferables;
|
|
32
|
+
outbound[i] = withoutTransferables;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
return transferables;
|
|
35
|
+
return { changeTrail: outbound ?? changeTrail, transferables };
|
|
35
36
|
};
|
|
36
37
|
class WorkerFailedError extends Error {
|
|
37
38
|
constructor(type, message, options) {
|
|
@@ -46,16 +47,59 @@ class WorkerDestroyedError extends Error {
|
|
|
46
47
|
this.name = "WorkerDestroyedError";
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
class WorkerReportedError extends Error {
|
|
51
|
+
constructor(name, message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = name || "Error";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const DefaultWorkerTimeouts = {
|
|
57
|
+
loadTimeout: WorkerLoadTimeout,
|
|
58
|
+
configureTimeout: WorkerConfigureTimeout,
|
|
59
|
+
changeTrailTimeout: WorkerChangeTrailTimeout,
|
|
60
|
+
destroyTimeout: WorkerDestroyTimeout
|
|
61
|
+
};
|
|
62
|
+
const resolveTimeouts = (options, logger) => {
|
|
63
|
+
const resolved = { ...DefaultWorkerTimeouts };
|
|
64
|
+
if (options == null) return resolved;
|
|
65
|
+
for (const key of Object.keys(DefaultWorkerTimeouts)) {
|
|
66
|
+
if (!Object.hasOwn(options, key)) continue;
|
|
67
|
+
const value = options[key];
|
|
68
|
+
if (value === void 0) continue;
|
|
69
|
+
if (isTimeout(value)) {
|
|
70
|
+
resolved[key] = value;
|
|
71
|
+
} else {
|
|
72
|
+
logger.error(`ignoring the ${key} option: expected a number of milliseconds from 1 to ${MaxWorkerTimeout}, got`, value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return resolved;
|
|
76
|
+
};
|
|
49
77
|
class RemoteWorkerEnv {
|
|
50
|
-
constructor() {
|
|
78
|
+
constructor(options) {
|
|
51
79
|
this.#isDestroyed = false;
|
|
52
80
|
this.#changeTrailSerial = 0;
|
|
53
81
|
/**
|
|
54
|
-
* Aborted exactly once, with the {@link WorkerFailedError}
|
|
82
|
+
* Aborted exactly once, with the error that ends this environment: a {@link WorkerFailedError}
|
|
83
|
+
* when the worker broke down, a {@link WorkerDestroyedError} when it was torn down. Whichever
|
|
84
|
+
* comes first keeps the signal, so a teardown never buries the failure that provoked it.
|
|
55
85
|
* It settles every request already waiting for a reply and turns away every later one.
|
|
56
86
|
*/
|
|
57
87
|
this.#workerFailure = new AbortController();
|
|
58
|
-
this
|
|
88
|
+
this.#logger = new ConsoleLogger("RemoteWorkerEnv");
|
|
89
|
+
this.onWorkerError = (event) => {
|
|
90
|
+
this.handleWorkerFailure("error", event, event.message || "the worker reported an error");
|
|
91
|
+
};
|
|
92
|
+
this.onWorkerMessageError = (event) => {
|
|
93
|
+
this.handleWorkerFailure("messageerror", event, "the worker sent a message that could not be deserialized");
|
|
94
|
+
};
|
|
95
|
+
this.onMessageFromWorker = (event) => {
|
|
96
|
+
if (event.data?.type === MessageToView) {
|
|
97
|
+
this.onMessageToView?.(event.data.data);
|
|
98
|
+
} else {
|
|
99
|
+
this.logger.debug("message from worker", event);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
this.#timeouts = Object.freeze(resolveTimeouts(options, this.logger));
|
|
59
103
|
retain(this, RemoteWorkerEnv.WorkerLoaded);
|
|
60
104
|
retain(this, RemoteWorkerEnv.WorkerFailed);
|
|
61
105
|
}
|
|
@@ -69,6 +113,20 @@ class RemoteWorkerEnv {
|
|
|
69
113
|
#isDestroyed;
|
|
70
114
|
#changeTrailSerial;
|
|
71
115
|
#workerFailure;
|
|
116
|
+
#logger;
|
|
117
|
+
/** The logger this environment reports through. */
|
|
118
|
+
get logger() {
|
|
119
|
+
return this.#logger;
|
|
120
|
+
}
|
|
121
|
+
#timeouts;
|
|
122
|
+
/**
|
|
123
|
+
* The four timeouts this environment holds itself to, resolved once when it is built. The
|
|
124
|
+
* object is frozen and the slot holds no setter, so the constructor is the one way in --
|
|
125
|
+
* and `resolveTimeouts()` vets every value that goes through it.
|
|
126
|
+
*/
|
|
127
|
+
get timeouts() {
|
|
128
|
+
return this.#timeouts;
|
|
129
|
+
}
|
|
72
130
|
get isDestroyed() {
|
|
73
131
|
return this.#isDestroyed;
|
|
74
132
|
}
|
|
@@ -80,6 +138,8 @@ class RemoteWorkerEnv {
|
|
|
80
138
|
* `catch()` — otherwise a worker failure surfaces as an unhandled rejection.
|
|
81
139
|
*
|
|
82
140
|
* @throws {WorkerFailedError} if the worker fails before or after that happens
|
|
141
|
+
* @throws {WorkerDestroyedError} once the environment has been torn down — including every
|
|
142
|
+
* read after a load that had already succeeded, because there is no environment left to hand out
|
|
83
143
|
*/
|
|
84
144
|
get workerLoaded() {
|
|
85
145
|
const { signal } = this.#workerFailure;
|
|
@@ -106,11 +166,8 @@ class RemoteWorkerEnv {
|
|
|
106
166
|
async start() {
|
|
107
167
|
const { signal } = this.#workerFailure;
|
|
108
168
|
if (signal.aborted) throw signal.reason;
|
|
109
|
-
if (this.#isDestroyed) throw new WorkerDestroyedError();
|
|
110
169
|
if (this.#worker) {
|
|
111
|
-
|
|
112
|
-
this.logger.warn("already started");
|
|
113
|
-
}
|
|
170
|
+
this.logger.warn("already started");
|
|
114
171
|
return this.workerLoaded.then(() => {
|
|
115
172
|
if (this.isDestroyed) {
|
|
116
173
|
throw new WorkerDestroyedError();
|
|
@@ -118,22 +175,23 @@ class RemoteWorkerEnv {
|
|
|
118
175
|
});
|
|
119
176
|
}
|
|
120
177
|
const worker = this.#worker = createWorker();
|
|
121
|
-
worker.addEventListener("error", this.onWorkerError
|
|
122
|
-
worker.addEventListener("messageerror", this.onWorkerMessageError
|
|
178
|
+
worker.addEventListener("error", this.onWorkerError);
|
|
179
|
+
worker.addEventListener("messageerror", this.onWorkerMessageError);
|
|
123
180
|
this.configureConsoleLogger(worker);
|
|
124
181
|
try {
|
|
125
|
-
await waitForMessageOfType(worker, Loaded,
|
|
182
|
+
await waitForMessageOfType(worker, Loaded, this.timeouts.loadTimeout, void 0, signal);
|
|
126
183
|
if (this.isDestroyed) {
|
|
127
|
-
throw new WorkerDestroyedError();
|
|
184
|
+
throw signal.aborted ? signal.reason : new WorkerDestroyedError();
|
|
128
185
|
}
|
|
129
|
-
worker.addEventListener("message", this.onMessageFromWorker
|
|
186
|
+
worker.addEventListener("message", this.onMessageFromWorker);
|
|
130
187
|
queueMicrotask(() => {
|
|
131
|
-
|
|
188
|
+
this.announceLoaded();
|
|
132
189
|
});
|
|
133
190
|
} catch (error) {
|
|
134
191
|
if (error !== signal.reason) {
|
|
135
192
|
this.logger.error("failed to start", error);
|
|
136
193
|
}
|
|
194
|
+
this.stopListeningTo(worker);
|
|
137
195
|
this.#worker = void 0;
|
|
138
196
|
if (!this.#isDestroyed) {
|
|
139
197
|
worker.terminate();
|
|
@@ -146,27 +204,32 @@ class RemoteWorkerEnv {
|
|
|
146
204
|
if (signal.aborted) return Promise.reject(signal.reason);
|
|
147
205
|
const worker = this.#worker;
|
|
148
206
|
if (worker == null) return Promise.reject(new WorkerDestroyedError());
|
|
149
|
-
const transferables =
|
|
150
|
-
const message = { type: ChangeTrail, changeTrail };
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
message.serial = serial;
|
|
154
|
-
}
|
|
155
|
-
worker.postMessage(message, transferables ?? []);
|
|
156
|
-
if (waitForConfirmation) {
|
|
157
|
-
return waitForMessageOfType(
|
|
158
|
-
worker,
|
|
159
|
-
AppliedChangeTrail,
|
|
160
|
-
WorkerChangeTrailTimeout,
|
|
161
|
-
(data) => {
|
|
162
|
-
if (data.error) throw data.error;
|
|
163
|
-
return data.serial === serial;
|
|
164
|
-
},
|
|
165
|
-
signal
|
|
166
|
-
);
|
|
167
|
-
} else {
|
|
207
|
+
const { changeTrail: outbound, transferables } = splitTransferables(changeTrail);
|
|
208
|
+
const message = { type: ChangeTrail, changeTrail: outbound };
|
|
209
|
+
if (!waitForConfirmation) {
|
|
210
|
+
worker.postMessage(message, transferables ?? []);
|
|
168
211
|
return Promise.resolve();
|
|
169
212
|
}
|
|
213
|
+
const serial = ++this.#changeTrailSerial;
|
|
214
|
+
message.serial = serial;
|
|
215
|
+
worker.postMessage(message, transferables ?? []);
|
|
216
|
+
return waitForMessageOfType(
|
|
217
|
+
worker,
|
|
218
|
+
AppliedChangeTrail,
|
|
219
|
+
this.timeouts.changeTrailTimeout,
|
|
220
|
+
(data) => {
|
|
221
|
+
if (data.serial !== serial) return false;
|
|
222
|
+
if (data.error) {
|
|
223
|
+
const reason = new WorkerReportedError(data.errorName, data.error);
|
|
224
|
+
if (typeof data.appliedCount === "number") {
|
|
225
|
+
throw new ChangeTrailRefusedError(data.appliedCount, changeTrail.length, { cause: reason });
|
|
226
|
+
}
|
|
227
|
+
throw reason;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
},
|
|
231
|
+
signal
|
|
232
|
+
);
|
|
170
233
|
}
|
|
171
234
|
importScript(url) {
|
|
172
235
|
const { signal } = this.#workerFailure;
|
|
@@ -178,30 +241,29 @@ class RemoteWorkerEnv {
|
|
|
178
241
|
return waitForMessageOfType(
|
|
179
242
|
worker,
|
|
180
243
|
ImportedModule,
|
|
181
|
-
|
|
244
|
+
this.timeouts.configureTimeout,
|
|
182
245
|
(data) => {
|
|
183
|
-
if (data.
|
|
184
|
-
|
|
246
|
+
if (data.url !== url) return false;
|
|
247
|
+
if (data.error) throw new WorkerReportedError(data.errorName, data.error);
|
|
248
|
+
return true;
|
|
185
249
|
},
|
|
186
250
|
signal
|
|
187
251
|
);
|
|
188
252
|
}
|
|
189
253
|
destroy() {
|
|
190
|
-
|
|
254
|
+
this.#isDestroyed = true;
|
|
191
255
|
const worker = this.#worker;
|
|
192
256
|
this.#worker = void 0;
|
|
193
|
-
this.#
|
|
257
|
+
this.#workerFailure.abort(new WorkerDestroyedError());
|
|
258
|
+
if (worker == null) return;
|
|
259
|
+
this.stopListeningTo(worker);
|
|
194
260
|
worker.postMessage({ type: Destroy });
|
|
195
|
-
waitForMessageOfType(worker, Destroyed,
|
|
261
|
+
waitForMessageOfType(worker, Destroyed, this.timeouts.destroyTimeout).catch((error) => {
|
|
262
|
+
this.logger.warn("the worker did not acknowledge the teardown", error);
|
|
263
|
+
}).finally(() => {
|
|
196
264
|
worker.terminate();
|
|
197
265
|
});
|
|
198
266
|
}
|
|
199
|
-
onWorkerError(event) {
|
|
200
|
-
this.handleWorkerFailure("error", event, event.message || "the worker reported an error");
|
|
201
|
-
}
|
|
202
|
-
onWorkerMessageError(event) {
|
|
203
|
-
this.handleWorkerFailure("messageerror", event, "the worker sent a message that could not be deserialized");
|
|
204
|
-
}
|
|
205
267
|
handleWorkerFailure(type, event, message) {
|
|
206
268
|
if (this.#isDestroyed) return;
|
|
207
269
|
this.#isDestroyed = true;
|
|
@@ -211,6 +273,7 @@ class RemoteWorkerEnv {
|
|
|
211
273
|
this.logger.error(message, event);
|
|
212
274
|
const worker = this.#worker;
|
|
213
275
|
this.#worker = void 0;
|
|
276
|
+
if (worker != null) this.stopListeningTo(worker);
|
|
214
277
|
this.#workerFailure.abort(reason);
|
|
215
278
|
worker?.terminate();
|
|
216
279
|
try {
|
|
@@ -218,27 +281,59 @@ class RemoteWorkerEnv {
|
|
|
218
281
|
} catch (error) {
|
|
219
282
|
this.logger.error("the proxy-failed callback threw", error);
|
|
220
283
|
}
|
|
221
|
-
|
|
222
|
-
env: this,
|
|
223
|
-
type,
|
|
224
|
-
message,
|
|
225
|
-
reason,
|
|
226
|
-
event
|
|
227
|
-
});
|
|
284
|
+
this.announceFailure({ env: this, type, message, reason, event });
|
|
228
285
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Announces the completed handshake to the consumers.
|
|
288
|
+
*
|
|
289
|
+
* `emitStrict()` rather than `emit()`, and the retain policy is what decides it. `workerLoaded`
|
|
290
|
+
* reads the retained value, and eventize writes that value only once the dispatch has run
|
|
291
|
+
* through: under the plain dispatch a single listener that throws would take the replay for
|
|
292
|
+
* every later subscriber with it, and every later read of `workerLoaded` would wait for a
|
|
293
|
+
* failure or a teardown instead of resolving. The guarded dispatch serves every listener and
|
|
294
|
+
* writes the retained value, because the event was delivered.
|
|
295
|
+
*
|
|
296
|
+
* What the listeners threw arrives here afterwards -- one of them unchanged, several as an
|
|
297
|
+
* `AggregateError` in dispatch order -- and is reported. It goes no further: this runs from a
|
|
298
|
+
* microtask, where a throw has no caller left to reach and would surface as an unhandled error.
|
|
299
|
+
*/
|
|
300
|
+
announceLoaded() {
|
|
301
|
+
try {
|
|
302
|
+
emitStrict(this, RemoteWorkerEnv.WorkerLoaded, this);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
this.logger.error("a workerLoaded listener threw", error);
|
|
234
305
|
}
|
|
235
306
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Announces the failure to the consumers. The same guarded dispatch as
|
|
309
|
+
* {@link RemoteWorkerEnv.announceLoaded}, for the same reason: `WorkerFailed` is documented as
|
|
310
|
+
* retained, and a consumer that subscribes only after the failure has to be able to hear about
|
|
311
|
+
* it. Every listener is served, the retained value is written, and whatever was thrown is
|
|
312
|
+
* reported here and reaches no caller.
|
|
313
|
+
*/
|
|
314
|
+
announceFailure(payload) {
|
|
315
|
+
try {
|
|
316
|
+
emitStrict(this, RemoteWorkerEnv.WorkerFailed, payload);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
this.logger.error("a workerFailed listener threw", error);
|
|
241
319
|
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Takes every listener of this environment off a worker. Whoever registered them takes them
|
|
323
|
+
* off again -- between the teardown and the `terminate()` that ends it the worker stays
|
|
324
|
+
* alive, and through its listeners it keeps this environment and everything it references
|
|
325
|
+
* reachable for exactly that long.
|
|
326
|
+
*/
|
|
327
|
+
stopListeningTo(worker) {
|
|
328
|
+
worker.removeEventListener("error", this.onWorkerError);
|
|
329
|
+
worker.removeEventListener("messageerror", this.onWorkerMessageError);
|
|
330
|
+
worker.removeEventListener("message", this.onMessageFromWorker);
|
|
331
|
+
}
|
|
332
|
+
configureConsoleLogger(worker) {
|
|
333
|
+
const configKey = ["RemoteWorkerEnv", "workerConfig"];
|
|
334
|
+
const storageKey = consoleLoggerConfigKey(configKey);
|
|
335
|
+
const workerConfig = this.readWorkerConfig(configKey, storageKey);
|
|
336
|
+
this.logger.info("load console-logger worker config", { storageKey, workerConfig });
|
|
242
337
|
worker.postMessage({
|
|
243
338
|
type: CONSOLE_LOGGER,
|
|
244
339
|
config: {
|
|
@@ -249,10 +344,33 @@ class RemoteWorkerEnv {
|
|
|
249
344
|
}
|
|
250
345
|
});
|
|
251
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* The console-logger config for the worker, read from the storage key this environment
|
|
349
|
+
* announces. The key is a diagnostic switch, set by hand and sharing its namespace with
|
|
350
|
+
* everything else on this origin, so its content decides nothing about whether the worker
|
|
351
|
+
* comes up: anything that is not a plain JSON object counts as no config at all, exactly
|
|
352
|
+
* like an absent key.
|
|
353
|
+
*/
|
|
354
|
+
readWorkerConfig(configKey, storageKey) {
|
|
355
|
+
const stored = loadConsoleLoggerConfig(configKey, "{}");
|
|
356
|
+
let parsed;
|
|
357
|
+
try {
|
|
358
|
+
parsed = JSON.parse(stored);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
this.logger.error(`ignoring the unreadable console-logger worker config at "${storageKey}"`, stored, error);
|
|
361
|
+
return {};
|
|
362
|
+
}
|
|
363
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
364
|
+
this.logger.error(`ignoring the console-logger worker config at "${storageKey}": expected a JSON object`, stored);
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
252
369
|
}
|
|
253
370
|
export {
|
|
254
371
|
RemoteWorkerEnv,
|
|
255
372
|
WorkerDestroyedError,
|
|
256
|
-
WorkerFailedError
|
|
373
|
+
WorkerFailedError,
|
|
374
|
+
WorkerReportedError
|
|
257
375
|
};
|
|
258
376
|
//# sourceMappingURL=RemoteWorkerEnv.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/view/RemoteWorkerEnv.ts"],
|
|
4
|
-
"sourcesContent": ["import {emit, once, retain} from '@spearwolf/eventize';\nimport {\n AppliedChangeTrail,\n ChangeTrail,\n Configure,\n Destroy,\n Destroyed,\n ImportedModule,\n Loaded,\n MessageToView,\n WorkerChangeTrailTimeout,\n WorkerConfigureTimeout,\n WorkerDestroyTimeout,\n WorkerLoadTimeout,\n} from '../constants.js';\nimport createWorker from '../create-worker.js';\nimport type {AppliedChangeTrailEvent, ChangeTrailType, ImportedModuleEvent, TransferablesType} from '../types.js';\nimport {CONSOLE_LOGGER, ConsoleLogger} from '../utils/ConsoleLogger.js';\nimport {toUrlString} from '../utils/toUrlString.js';\nimport {waitForMessageOfType} from '../utils/waitForMessageOfType.js';\nimport type {IShadowObjectEnvProxy} from './IShadowObjectEnvProxy.js';\n\nconst removeTransferables = (changeTrail: ChangeTrailType): TransferablesType | undefined => {\n let transferables: TransferablesType | undefined;\n\n if (changeTrail != null && Array.isArray(changeTrail)) {\n for (const changeItem of changeTrail) {\n if (changeItem.transferables) {\n if (!transferables) {\n transferables = changeItem.transferables;\n } else {\n transferables = [...transferables, ...changeItem.transferables];\n }\n delete changeItem.transferables;\n }\n }\n }\n\n return transferables;\n};\n\n/**\n * The reason every pending and every later worker request is rejected with\n * once the worker has failed.\n */\nexport class WorkerFailedError extends Error {\n /** `'error'` when the worker itself threw, `'messageerror'` when it sent something that could not be deserialized. */\n readonly type: 'error' | 'messageerror';\n\n constructor(type: 'error' | 'messageerror', message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'WorkerFailedError';\n this.type = type;\n }\n}\n\n/**\n * The reason a request is rejected with once the environment has been torn down\n * with {@link RemoteWorkerEnv.destroy}. The worker is gone; no reply can arrive.\n */\nexport class WorkerDestroyedError extends Error {\n constructor(message = 'the worker environment has been destroyed') {\n super(message);\n this.name = 'WorkerDestroyedError';\n }\n}\n\n/** What {@link RemoteWorkerEnv.WorkerFailed} carries. */\nexport interface WorkerFailedEvent {\n /** the environment whose worker failed */\n env: RemoteWorkerEnv;\n /** what the worker reported */\n type: 'error' | 'messageerror';\n /** a readable description of the failure */\n message: string;\n /** the error every pending and every later request is rejected with */\n reason: WorkerFailedError;\n /** the worker event the failure was read from */\n event: ErrorEvent | MessageEvent;\n}\n\nexport class RemoteWorkerEnv implements IShadowObjectEnvProxy {\n static WorkerLoaded = 'workerLoaded';\n static WorkerFailed = 'workerFailed';\n\n #worker?: Worker;\n #isDestroyed = false;\n #changeTrailSerial = 0;\n\n /**\n * Aborted exactly once, with the {@link WorkerFailedError} that describes the failure.\n * It settles every request already waiting for a reply and turns away every later one.\n */\n readonly #workerFailure = new AbortController();\n\n readonly logger = new ConsoleLogger('RemoteWorkerEnv');\n\n get isDestroyed(): boolean {\n return this.#isDestroyed;\n }\n\n /**\n * Resolves once the worker is up.\n *\n * Every read hands out a promise that can reject, so a caller that only wants to\n * kick the environment off and never awaits the result should still attach a\n * `catch()` \u2014 otherwise a worker failure surfaces as an unhandled rejection.\n *\n * @throws {WorkerFailedError} if the worker fails before or after that happens\n */\n get workerLoaded(): Promise<RemoteWorkerEnv> {\n const {signal} = this.#workerFailure;\n if (signal.aborted) return Promise.reject(signal.reason);\n\n return new Promise<RemoteWorkerEnv>((resolve, reject) => {\n let unsubscribeLoaded: (() => void) | undefined;\n let settled = false;\n\n const onFailure = () => {\n if (settled) return;\n settled = true;\n unsubscribeLoaded?.();\n reject(signal.reason);\n };\n\n unsubscribeLoaded = once(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerLoaded, (env: RemoteWorkerEnv) => {\n if (settled) return;\n settled = true;\n signal.removeEventListener('abort', onFailure);\n resolve(env);\n });\n\n // a retained WorkerLoaded is replayed inside once(), before it returns \u2014\n // then there is nothing left to wait for and no abort listener to attach\n if (settled) return;\n\n signal.addEventListener('abort', onFailure, {once: true});\n });\n }\n\n constructor() {\n retain(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerLoaded);\n // a consumer that subscribes only after the failure still gets to hear about it\n retain(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerFailed);\n }\n\n async start(): Promise<void> {\n // both guards are needed, and in this order: a failure aborts the signal *and* marks the\n // environment destroyed, so asking about the teardown first would bury the more precise\n // reason \u2014 while a teardown leaves the signal untouched and would slip past it\n const {signal} = this.#workerFailure;\n if (signal.aborted) throw signal.reason;\n\n // a torn-down environment must not spawn a worker: nothing would ever reach it, and\n // nothing would ever shut it down again\n if (this.#isDestroyed) throw new WorkerDestroyedError();\n\n if (this.#worker) {\n if (this.logger.isWarn) {\n this.logger.warn('already started');\n }\n\n return this.workerLoaded.then((): void => {\n if (this.isDestroyed) {\n throw new WorkerDestroyedError();\n }\n });\n }\n\n const worker = (this.#worker = createWorker());\n\n // registered before the load handshake begins: a worker that dies while it is\n // still coming up must not leave the caller waiting for the load timeout\n worker.addEventListener('error', this.onWorkerError.bind(this));\n worker.addEventListener('messageerror', this.onWorkerMessageError.bind(this));\n\n this.configureConsoleLogger(worker);\n\n try {\n await waitForMessageOfType(worker, Loaded, WorkerLoadTimeout, undefined, signal);\n\n if (this.isDestroyed) {\n throw new WorkerDestroyedError();\n }\n\n worker.addEventListener('message', this.onMessageFromWorker.bind(this));\n\n queueMicrotask(() => {\n emit(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerLoaded, this);\n });\n } catch (error) {\n // the failure path reports with the worker event in hand; anything else is on us\n if (error !== signal.reason) {\n this.logger.error('failed to start', error);\n }\n\n this.#worker = undefined;\n\n // only a start that nobody else is unwinding owns this worker: the reference is gone,\n // nobody can reach it any more, and leaving it running would keep a thread open for an\n // environment that never started. A failure has already terminated it, and a teardown\n // in flight is waiting for its Destroyed reply \u2014 cutting that short here would leave\n // destroy() to sit out its timeout and skip the worker-side teardown entirely.\n if (!this.#isDestroyed) {\n worker.terminate();\n }\n\n throw error;\n }\n }\n\n applyChangeTrail(changeTrail: ChangeTrailType, waitForConfirmation: boolean): Promise<void> {\n const {signal} = this.#workerFailure;\n if (signal.aborted) return Promise.reject(signal.reason);\n\n const worker = this.#worker;\n if (worker == null) return Promise.reject(new WorkerDestroyedError());\n\n const transferables = removeTransferables(changeTrail);\n const message = {type: ChangeTrail, changeTrail} as any;\n\n const serial = ++this.#changeTrailSerial;\n if (waitForConfirmation) {\n message.serial = serial;\n }\n\n worker.postMessage(message, transferables ?? []);\n\n if (waitForConfirmation) {\n return waitForMessageOfType(\n worker,\n AppliedChangeTrail,\n WorkerChangeTrailTimeout,\n (data: AppliedChangeTrailEvent) => {\n if (data.error) throw data.error;\n return data.serial === serial;\n },\n signal,\n );\n } else {\n return Promise.resolve();\n }\n }\n\n importScript(url: URL | string): Promise<void> {\n const {signal} = this.#workerFailure;\n if (signal.aborted) return Promise.reject(signal.reason);\n\n const worker = this.#worker;\n if (worker == null) return Promise.reject(new WorkerDestroyedError());\n\n url = toUrlString(url);\n worker.postMessage({type: Configure, importModule: url});\n return waitForMessageOfType(\n worker,\n ImportedModule,\n WorkerConfigureTimeout,\n (data: ImportedModuleEvent) => {\n if (data.error) throw data.error;\n return data.url === url;\n },\n signal,\n );\n }\n\n destroy(): void {\n if (!this.#worker) return;\n\n const worker = this.#worker;\n this.#worker = undefined;\n\n this.#isDestroyed = true;\n\n worker.postMessage({type: Destroy});\n\n waitForMessageOfType(worker, Destroyed, WorkerDestroyTimeout).finally(() => {\n worker.terminate();\n });\n }\n\n private onWorkerError(event: ErrorEvent) {\n this.handleWorkerFailure('error', event, event.message || 'the worker reported an error');\n }\n\n private onWorkerMessageError(event: MessageEvent) {\n this.handleWorkerFailure('messageerror', event, 'the worker sent a message that could not be deserialized');\n }\n\n private handleWorkerFailure(type: 'error' | 'messageerror', event: ErrorEvent | MessageEvent, message: string): void {\n // a deliberate teardown, or a failure that was already reported: the first one wins\n if (this.#isDestroyed) return;\n\n this.#isDestroyed = true;\n\n const reason = new WorkerFailedError(type, message, {\n cause: type === 'error' ? ((event as ErrorEvent).error ?? event) : event,\n });\n\n this.logger.error(message, event);\n\n const worker = this.#worker;\n this.#worker = undefined;\n\n // settle everyone waiting for a reply before the worker goes away \u2014 it can no longer arrive\n this.#workerFailure.abort(reason);\n\n worker?.terminate();\n\n // the ShadowEnv is told first, so that the state it derives from this is already\n // settled when consumers hear about it. Whatever happens down that path stays\n // there \u2014 the public event is announced either way\n try {\n (this as IShadowObjectEnvProxy).onProxyFailed?.(reason);\n } catch (error) {\n this.logger.error('the proxy-failed callback threw', error);\n }\n\n emit(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerFailed, {\n env: this,\n type,\n message,\n reason,\n event,\n } as WorkerFailedEvent);\n }\n\n private onMessageFromWorker(event: MessageEvent) {\n if (event.data?.type === MessageToView) {\n (this as IShadowObjectEnvProxy).onMessageToView?.(event.data.data);\n } else if (this.logger.isDebug) {\n this.logger.debug('message from worker', event);\n }\n }\n\n private configureConsoleLogger(worker: Worker) {\n const workerConfigKey = `${CONSOLE_LOGGER}.RemoteWorkerEnv.workerConfig`;\n const workerConfig = JSON.parse(localStorage.getItem(workerConfigKey) ?? '{}');\n\n if (this.logger.isInfo) {\n this.logger.info('load console-logger worker config', {localStorageKey: workerConfigKey, workerConfig});\n }\n\n worker.postMessage({\n type: CONSOLE_LOGGER,\n config: {\n ...ConsoleLogger.sharedConfig,\n enable: this.logger.isEnabled,\n ...workerConfig,\n ...(ConsoleLogger.isEnabled ? {} : {enable: false}),\n },\n });\n }\n}"],
|
|
5
|
-
"mappings": "AAAA,SAAQ,
|
|
4
|
+
"sourcesContent": ["import {emitStrict, once, retain} from '@spearwolf/eventize';\nimport {ChangeTrailRefusedError} from '../ChangeTrailRefusedError.js';\nimport {\n AppliedChangeTrail,\n ChangeTrail,\n Configure,\n Destroy,\n Destroyed,\n ImportedModule,\n Loaded,\n MessageToView,\n WorkerChangeTrailTimeout,\n WorkerConfigureTimeout,\n WorkerDestroyTimeout,\n WorkerLoadTimeout,\n} from '../constants.js';\nimport createWorker from '../create-worker.js';\nimport type {AppliedChangeTrailEvent, ChangeTrailType, ImportedModuleEvent, SyncEvent, TransferablesType} from '../types.js';\nimport {CONSOLE_LOGGER, ConsoleLogger, consoleLoggerConfigKey, loadConsoleLoggerConfig} from '../utils/ConsoleLogger.js';\nimport {toUrlString} from '../utils/toUrlString.js';\nimport {isTimeout, MaxWorkerTimeout, waitForMessageOfType} from '../utils/waitForMessageOfType.js';\nimport type {IShadowObjectEnvProxy} from './IShadowObjectEnvProxy.js';\n\n/**\n * Splits the transferables out of a change trail without writing to it. The trail handed in is a\n * snapshot that travels on after this call -- to the `ShadowEnv.AfterSync` consumers when the\n * environment applies it, to the `ShadowEnv.SyncFailed` consumers when it refuses it -- so an\n * entry carrying transferables is replaced by a shallow copy without them and every other\n * entry is passed through as it is. A trail that carries none is handed back object for\n * object.\n */\nconst splitTransferables = (\n changeTrail: ChangeTrailType,\n): {changeTrail: ChangeTrailType; transferables?: TransferablesType | undefined} => {\n if (!Array.isArray(changeTrail)) return {changeTrail};\n\n let outbound: ChangeTrailType | undefined;\n let transferables: TransferablesType | undefined;\n\n for (let i = 0; i < changeTrail.length; i++) {\n const changeItem = changeTrail[i];\n if (changeItem?.transferables) {\n transferables = transferables ? [...transferables, ...changeItem.transferables] : [...changeItem.transferables];\n\n outbound ??= [...changeTrail];\n const withoutTransferables = {...changeItem};\n delete withoutTransferables.transferables;\n outbound[i] = withoutTransferables;\n }\n }\n\n return {changeTrail: outbound ?? changeTrail, transferables};\n};\n\n/**\n * What actually goes on the wire for a `ChangeTrail` message. `SyncEvent` describes the shape a\n * change trail carries once it is inside the worker, without the `type` discriminant that gets\n * it there \u2014 its two neighbors on the wire (`ImportedModuleEvent`, `AppliedChangeTrailEvent`)\n * both require that field, so adding it to `SyncEvent` itself would either make it mandatory\n * where `LocalShadowObjectEnv` never sets it, or optional on a public type whose neighbors need\n * it required. Kept local instead: it says exactly what this message is without touching the\n * exported type surface.\n */\ninterface ChangeTrailMessage extends SyncEvent {\n type: typeof ChangeTrail;\n}\n\n/**\n * The reason every pending and every later worker request is rejected with\n * once the worker has failed.\n */\nexport class WorkerFailedError extends Error {\n /** `'error'` when the worker itself threw, `'messageerror'` when it sent something that could not be deserialized. */\n readonly type: 'error' | 'messageerror';\n\n constructor(type: 'error' | 'messageerror', message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'WorkerFailedError';\n this.type = type;\n }\n}\n\n/**\n * The reason a request is rejected with once the environment has been torn down\n * with {@link RemoteWorkerEnv.destroy}. The worker is gone; no reply can arrive.\n */\nexport class WorkerDestroyedError extends Error {\n constructor(message = 'the worker environment has been destroyed') {\n super(message);\n this.name = 'WorkerDestroyedError';\n }\n}\n\n/**\n * The reason a request is rejected with when the worker reported a failure of its own -- a\n * module that would not import, a change trail its Kernel refused. An error does not survive\n * structured cloning as the object it is, so the two fields that do are rebuilt here, and\n * `name` is the name the error called itself inside the worker: a caller reads it the same\n * way it reads the name of an error a `LocalShadowObjectEnv` hands it. A name that is missing\n * or empty names no class, and `Error` is what such a reason is called -- the same name a\n * plain `new Error(message)` carries on the local side.\n *\n * What does not come across is the class and everything it added. `instanceof EntityUuidInUseError`\n * is `false` here and there is no `uuid` field, whatever `name` says; `instanceof WorkerReportedError`\n * is what tells such a reason apart from one raised on this side.\n */\nexport class WorkerReportedError extends Error {\n constructor(name: string | undefined, message: string) {\n super(message);\n this.name = name || 'Error';\n }\n}\n\n/**\n * How long a {@link RemoteWorkerEnv} waits for each of the four replies a worker owes it,\n * in milliseconds.\n */\nexport interface WorkerTimeouts {\n /** the `Loaded` handshake at the start */\n loadTimeout: number;\n /** the `ImportedModule` reply to an `importScript()` */\n configureTimeout: number;\n /** the `AppliedChangeTrail` confirmation of a change trail sent with `waitForConfirmation` */\n changeTrailTimeout: number;\n /** the `Destroyed` acknowledgement of a teardown */\n destroyTimeout: number;\n}\n\n/**\n * What {@link RemoteWorkerEnv} takes. Every value left out keeps its default \u2014 the four\n * `Worker*Timeout` constants. A value that is not a number of milliseconds from 1 to\n * 2147483647 \u2014 close to 25 days, the longest delay a timer keeps \u2014 is reported through the\n * logger and the default applies.\n */\nexport type RemoteWorkerEnvOptions = Partial<WorkerTimeouts>;\n\nconst DefaultWorkerTimeouts: WorkerTimeouts = {\n loadTimeout: WorkerLoadTimeout,\n configureTimeout: WorkerConfigureTimeout,\n changeTrailTimeout: WorkerChangeTrailTimeout,\n destroyTimeout: WorkerDestroyTimeout,\n};\n\n/**\n * Resolves the options against the defaults. A timeout is a number of milliseconds from 1 to\n * {@link MaxWorkerTimeout}; anything else is refused and reported, and the default stays.\n *\n * Zero and Infinity are refused along with the rest, and that is the point of the rule rather\n * than an oversight: `waitForMessageOfType()` arms no timer for either of them, so a teardown\n * given one of the two would wait for an acknowledgement a dead worker never sends \u2014 and the\n * `terminate()` that ends the teardown hangs on that very chain. One rule for all four values\n * is easier to hold on to than three that allow it and one that does not, and {@link MaxWorkerTimeout}\n * \u2014 close to 25 days \u2014 is as long as a wait can honestly be made.\n */\nconst resolveTimeouts = (options: RemoteWorkerEnvOptions | undefined, logger: ConsoleLogger): WorkerTimeouts => {\n const resolved: WorkerTimeouts = {...DefaultWorkerTimeouts};\n if (options == null) return resolved;\n\n for (const key of Object.keys(DefaultWorkerTimeouts) as (keyof WorkerTimeouts)[]) {\n // own keys only: what an object inherits from its prototype is not what its author wrote down\n if (!Object.hasOwn(options, key)) continue;\n\n const value = options[key];\n if (value === undefined) continue;\n\n if (isTimeout(value)) {\n resolved[key] = value;\n } else {\n logger.error(`ignoring the ${key} option: expected a number of milliseconds from 1 to ${MaxWorkerTimeout}, got`, value);\n }\n }\n\n return resolved;\n};\n\n/** What {@link RemoteWorkerEnv.WorkerFailed} carries. */\nexport interface WorkerFailedEvent {\n /** the environment whose worker failed */\n env: RemoteWorkerEnv;\n /** what the worker reported */\n type: 'error' | 'messageerror';\n /** a readable description of the failure */\n message: string;\n /** the error every pending and every later request is rejected with */\n reason: WorkerFailedError;\n /** the worker event the failure was read from */\n event: ErrorEvent | MessageEvent;\n}\n\nexport class RemoteWorkerEnv implements IShadowObjectEnvProxy {\n static WorkerLoaded = 'workerLoaded';\n static WorkerFailed = 'workerFailed';\n\n #worker?: Worker | undefined;\n #isDestroyed = false;\n #changeTrailSerial = 0;\n\n /**\n * Aborted exactly once, with the error that ends this environment: a {@link WorkerFailedError}\n * when the worker broke down, a {@link WorkerDestroyedError} when it was torn down. Whichever\n * comes first keeps the signal, so a teardown never buries the failure that provoked it.\n * It settles every request already waiting for a reply and turns away every later one.\n */\n readonly #workerFailure = new AbortController();\n\n readonly #logger = new ConsoleLogger('RemoteWorkerEnv');\n\n /** The logger this environment reports through. */\n get logger(): ConsoleLogger {\n return this.#logger;\n }\n\n readonly #timeouts: Readonly<WorkerTimeouts>;\n\n /**\n * The four timeouts this environment holds itself to, resolved once when it is built. The\n * object is frozen and the slot holds no setter, so the constructor is the one way in --\n * and `resolveTimeouts()` vets every value that goes through it.\n */\n get timeouts(): Readonly<WorkerTimeouts> {\n return this.#timeouts;\n }\n\n get isDestroyed(): boolean {\n return this.#isDestroyed;\n }\n\n /**\n * Resolves once the worker is up.\n *\n * Every read hands out a promise that can reject, so a caller that only wants to\n * kick the environment off and never awaits the result should still attach a\n * `catch()` \u2014 otherwise a worker failure surfaces as an unhandled rejection.\n *\n * @throws {WorkerFailedError} if the worker fails before or after that happens\n * @throws {WorkerDestroyedError} once the environment has been torn down \u2014 including every\n * read after a load that had already succeeded, because there is no environment left to hand out\n */\n get workerLoaded(): Promise<RemoteWorkerEnv> {\n const {signal} = this.#workerFailure;\n if (signal.aborted) return Promise.reject(signal.reason);\n\n return new Promise<RemoteWorkerEnv>((resolve, reject) => {\n let unsubscribeLoaded: (() => void) | undefined;\n let settled = false;\n\n const onFailure = () => {\n if (settled) return;\n settled = true;\n unsubscribeLoaded?.();\n reject(signal.reason);\n };\n\n unsubscribeLoaded = once(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerLoaded, (env: RemoteWorkerEnv) => {\n if (settled) return;\n settled = true;\n signal.removeEventListener('abort', onFailure);\n resolve(env);\n });\n\n // a retained WorkerLoaded is replayed inside once(), before it returns \u2014\n // then there is nothing left to wait for and no abort listener to attach\n if (settled) return;\n\n signal.addEventListener('abort', onFailure, {once: true});\n });\n }\n\n constructor(options?: RemoteWorkerEnvOptions) {\n // the logger is a field initializer and is therefore already in place\n this.#timeouts = Object.freeze(resolveTimeouts(options, this.logger));\n\n retain(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerLoaded);\n // a consumer that subscribes only after the failure still gets to hear about it\n retain(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerFailed);\n }\n\n async start(): Promise<void> {\n // both endings of this environment abort the signal, and it keeps the reason of whichever\n // came first. A finished environment must not spawn a worker: nothing would ever reach it,\n // and nothing would ever shut it down again\n const {signal} = this.#workerFailure;\n if (signal.aborted) throw signal.reason;\n\n if (this.#worker) {\n this.logger.warn('already started');\n\n return this.workerLoaded.then((): void => {\n if (this.isDestroyed) {\n throw new WorkerDestroyedError();\n }\n });\n }\n\n const worker = (this.#worker = createWorker());\n\n // registered before the load handshake begins: a worker that dies while it is\n // still coming up must not leave the caller waiting for the load timeout\n worker.addEventListener('error', this.onWorkerError);\n worker.addEventListener('messageerror', this.onWorkerMessageError);\n\n this.configureConsoleLogger(worker);\n\n try {\n await waitForMessageOfType(worker, Loaded, this.timeouts.loadTimeout, undefined, signal);\n\n // the handshake completed but the environment ended while its reply was on the way here.\n // The reason of that ending is what the caller gets \u2014 a fresh error of our own would read\n // as a start that went wrong, and the catch below would report it as one\n if (this.isDestroyed) {\n throw signal.aborted ? signal.reason : new WorkerDestroyedError();\n }\n\n worker.addEventListener('message', this.onMessageFromWorker);\n\n queueMicrotask(() => {\n this.announceLoaded();\n });\n } catch (error) {\n // the failure path reports with the worker event in hand; anything else is on us\n if (error !== signal.reason) {\n this.logger.error('failed to start', error);\n }\n\n this.stopListeningTo(worker);\n this.#worker = undefined;\n\n // only a start that nobody else is unwinding owns this worker: the reference is gone,\n // nobody can reach it any more, and leaving it running would keep a thread open for an\n // environment that never started. A failure has already terminated it, and a teardown\n // in flight is waiting for its Destroyed reply \u2014 cutting that short here would leave\n // destroy() to sit out its timeout and skip the worker-side teardown entirely.\n if (!this.#isDestroyed) {\n worker.terminate();\n }\n\n throw error;\n }\n }\n\n applyChangeTrail(changeTrail: ChangeTrailType, waitForConfirmation: boolean): Promise<void> {\n const {signal} = this.#workerFailure;\n if (signal.aborted) return Promise.reject(signal.reason);\n\n const worker = this.#worker;\n if (worker == null) return Promise.reject(new WorkerDestroyedError());\n\n const {changeTrail: outbound, transferables} = splitTransferables(changeTrail);\n const message: ChangeTrailMessage = {type: ChangeTrail, changeTrail: outbound};\n\n if (!waitForConfirmation) {\n worker.postMessage(message, transferables ?? []);\n return Promise.resolve();\n }\n\n // the counter moves only where a number actually goes on the wire: it is the sole link\n // between a request and its confirmation, and a sequence with invisible jumps in it tells\n // a later diagnosis nothing\n const serial = ++this.#changeTrailSerial;\n message.serial = serial;\n\n worker.postMessage(message, transferables ?? []);\n\n return waitForMessageOfType(\n worker,\n AppliedChangeTrail,\n this.timeouts.changeTrailTimeout,\n (data: AppliedChangeTrailEvent) => {\n // the serial decides who a confirmation concerns -- an error belonging to another\n // trail would otherwise reject the request that happens to be waiting here\n if (data.serial !== serial) return false;\n if (data.error) {\n const reason = new WorkerReportedError(data.errorName, data.error);\n // Only a confirmation that names the count can move the line the view draws between\n // applied and pending; one that does not carries no more than the reason itself, and\n // is handed on as that reason.\n if (typeof data.appliedCount === 'number') {\n throw new ChangeTrailRefusedError(data.appliedCount, changeTrail.length, {cause: reason});\n }\n throw reason;\n }\n return true;\n },\n signal,\n );\n }\n\n importScript(url: URL | string): Promise<void> {\n const {signal} = this.#workerFailure;\n if (signal.aborted) return Promise.reject(signal.reason);\n\n const worker = this.#worker;\n if (worker == null) return Promise.reject(new WorkerDestroyedError());\n\n url = toUrlString(url);\n worker.postMessage({type: Configure, importModule: url});\n return waitForMessageOfType(\n worker,\n ImportedModule,\n this.timeouts.configureTimeout,\n (data: ImportedModuleEvent) => {\n if (data.url !== url) return false;\n if (data.error) throw new WorkerReportedError(data.errorName, data.error);\n return true;\n },\n signal,\n );\n }\n\n destroy(): void {\n // the teardown counts even when there is no worker to tear down: everything that comes\n // afterwards has to meet an environment that is gone\n this.#isDestroyed = true;\n\n // releasing the reference here is what makes the teardown idempotent: a repeated call finds\n // nothing left, sends no second Destroy and terminates nothing twice\n const worker = this.#worker;\n this.#worker = undefined;\n\n // settles everything still waiting for a reply: a start() in the middle of its load handshake\n // would otherwise sit out its load timeout, and workerLoaded would never hear anything again\n this.#workerFailure.abort(new WorkerDestroyedError());\n\n if (worker == null) return;\n\n this.stopListeningTo(worker);\n\n worker.postMessage({type: Destroy});\n\n waitForMessageOfType(worker, Destroyed, this.timeouts.destroyTimeout)\n .catch((error) => {\n // `.finally()` passes a rejection on, so without this the silence of a worker ends as an\n // unhandled rejection once the destroy timeout is up. It is terminated either way, and\n // by then there is nobody left to hand the error to\n this.logger.warn('the worker did not acknowledge the teardown', error);\n })\n .finally(() => {\n worker.terminate();\n });\n }\n\n private readonly onWorkerError = (event: ErrorEvent): void => {\n this.handleWorkerFailure('error', event, event.message || 'the worker reported an error');\n };\n\n private readonly onWorkerMessageError = (event: MessageEvent): void => {\n this.handleWorkerFailure('messageerror', event, 'the worker sent a message that could not be deserialized');\n };\n\n private handleWorkerFailure(type: 'error' | 'messageerror', event: ErrorEvent | MessageEvent, message: string): void {\n // a deliberate teardown, or a failure that was already reported: the first one wins\n if (this.#isDestroyed) return;\n\n this.#isDestroyed = true;\n\n const reason = new WorkerFailedError(type, message, {\n cause: type === 'error' ? ((event as ErrorEvent).error ?? event) : event,\n });\n\n this.logger.error(message, event);\n\n const worker = this.#worker;\n this.#worker = undefined;\n\n if (worker != null) this.stopListeningTo(worker);\n\n // settle everyone waiting for a reply before the worker goes away \u2014 it can no longer arrive\n this.#workerFailure.abort(reason);\n\n worker?.terminate();\n\n // the ShadowEnv is told first, so that the state it derives from this is already\n // settled when consumers hear about it. Whatever happens down that path stays\n // there \u2014 the public event is announced either way\n try {\n (this as IShadowObjectEnvProxy).onProxyFailed?.(reason);\n } catch (error) {\n this.logger.error('the proxy-failed callback threw', error);\n }\n\n this.announceFailure({env: this, type, message, reason, event});\n }\n\n /**\n * Announces the completed handshake to the consumers.\n *\n * `emitStrict()` rather than `emit()`, and the retain policy is what decides it. `workerLoaded`\n * reads the retained value, and eventize writes that value only once the dispatch has run\n * through: under the plain dispatch a single listener that throws would take the replay for\n * every later subscriber with it, and every later read of `workerLoaded` would wait for a\n * failure or a teardown instead of resolving. The guarded dispatch serves every listener and\n * writes the retained value, because the event was delivered.\n *\n * What the listeners threw arrives here afterwards -- one of them unchanged, several as an\n * `AggregateError` in dispatch order -- and is reported. It goes no further: this runs from a\n * microtask, where a throw has no caller left to reach and would surface as an unhandled error.\n */\n private announceLoaded(): void {\n try {\n emitStrict(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerLoaded, this);\n } catch (error) {\n this.logger.error('a workerLoaded listener threw', error);\n }\n }\n\n /**\n * Announces the failure to the consumers. The same guarded dispatch as\n * {@link RemoteWorkerEnv.announceLoaded}, for the same reason: `WorkerFailed` is documented as\n * retained, and a consumer that subscribes only after the failure has to be able to hear about\n * it. Every listener is served, the retained value is written, and whatever was thrown is\n * reported here and reaches no caller.\n */\n private announceFailure(payload: WorkerFailedEvent): void {\n try {\n emitStrict(this as RemoteWorkerEnv, RemoteWorkerEnv.WorkerFailed, payload);\n } catch (error) {\n this.logger.error('a workerFailed listener threw', error);\n }\n }\n\n private readonly onMessageFromWorker = (event: MessageEvent): void => {\n if (event.data?.type === MessageToView) {\n (this as IShadowObjectEnvProxy).onMessageToView?.(event.data.data);\n } else {\n this.logger.debug('message from worker', event);\n }\n };\n\n /**\n * Takes every listener of this environment off a worker. Whoever registered them takes them\n * off again -- between the teardown and the `terminate()` that ends it the worker stays\n * alive, and through its listeners it keeps this environment and everything it references\n * reachable for exactly that long.\n */\n private stopListeningTo(worker: Worker): void {\n worker.removeEventListener('error', this.onWorkerError);\n worker.removeEventListener('messageerror', this.onWorkerMessageError);\n worker.removeEventListener('message', this.onMessageFromWorker);\n }\n\n private configureConsoleLogger(worker: Worker) {\n const configKey = ['RemoteWorkerEnv', 'workerConfig'];\n const storageKey = consoleLoggerConfigKey(configKey);\n const workerConfig = this.readWorkerConfig(configKey, storageKey);\n\n this.logger.info('load console-logger worker config', {storageKey, workerConfig});\n\n worker.postMessage({\n type: CONSOLE_LOGGER,\n config: {\n ...ConsoleLogger.sharedConfig,\n enable: this.logger.isEnabled,\n ...workerConfig,\n ...(ConsoleLogger.isEnabled ? {} : {enable: false}),\n },\n });\n }\n\n /**\n * The console-logger config for the worker, read from the storage key this environment\n * announces. The key is a diagnostic switch, set by hand and sharing its namespace with\n * everything else on this origin, so its content decides nothing about whether the worker\n * comes up: anything that is not a plain JSON object counts as no config at all, exactly\n * like an absent key.\n */\n private readWorkerConfig(configKey: string[], storageKey: string): Record<string, unknown> {\n const stored = loadConsoleLoggerConfig(configKey, '{}');\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(stored);\n } catch (error) {\n // error, not warn: whoever writes this key wants the worker to talk, and a silent\n // fallback would send them looking for the reason inside the worker\n this.logger.error(`ignoring the unreadable console-logger worker config at \"${storageKey}\"`, stored, error);\n return {};\n }\n\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n this.logger.error(`ignoring the console-logger worker config at \"${storageKey}\": expected a JSON object`, stored);\n return {};\n }\n\n return parsed as Record<string, unknown>;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/src/view/ShadowEnv.d.ts
CHANGED
|
@@ -15,11 +15,19 @@ export declare class ShadowEnvDestroyedError extends Error {
|
|
|
15
15
|
export declare class ShadowEnv {
|
|
16
16
|
#private;
|
|
17
17
|
static AfterSync: string;
|
|
18
|
+
static SyncFailed: string;
|
|
18
19
|
static ContextLost: string;
|
|
19
20
|
static ContextCreated: string;
|
|
20
21
|
static ProxyFailed: string;
|
|
21
22
|
static get(ns: NamespaceType): ShadowEnv | undefined;
|
|
22
|
-
|
|
23
|
+
/** The logger this environment reports through. */
|
|
24
|
+
get logger(): ConsoleLogger;
|
|
25
|
+
/**
|
|
26
|
+
* The namespace of the {@link ComponentContext} this environment observes, and `undefined`
|
|
27
|
+
* while it observes none. The `view` setter writes it, so it carries the name
|
|
28
|
+
* {@link ShadowEnv.get} finds this environment under, unless another environment has since
|
|
29
|
+
* taken that namespace over; a `destroy()` leaves it on `undefined`.
|
|
30
|
+
*/
|
|
23
31
|
readonly ns$: import("@spearwolf/signalize").Signal<NamespaceType | undefined>;
|
|
24
32
|
accessor viewReady: boolean;
|
|
25
33
|
accessor proxyReady: boolean;
|
|
@@ -36,11 +44,50 @@ export declare class ShadowEnv {
|
|
|
36
44
|
* @throws {ShadowEnvDestroyedError} if the environment is destroyed before that happens
|
|
37
45
|
*/
|
|
38
46
|
readonly ready: () => Promise<ShadowEnv>;
|
|
47
|
+
/**
|
|
48
|
+
* Send the change trail without asking the Shadow Environment to confirm it --
|
|
49
|
+
* {@link ShadowEnv.syncWait} is the call that asks.
|
|
50
|
+
*
|
|
51
|
+
* What becomes of a refusal is then the proxy's decision, and the two shipped ones differ.
|
|
52
|
+
* `LocalShadowObjectEnv` runs the Kernel synchronously, inside the call the environment makes to
|
|
53
|
+
* the proxy -- one microtask after `sync()` returned, not inside the call to `sync()` itself --
|
|
54
|
+
* and rejects with what it threw, so a refusal reaches {@link ShadowEnv.SyncFailed} here as well,
|
|
55
|
+
* carrying its number.
|
|
56
|
+
* `RemoteWorkerEnv` sends no serial without a confirmation and gets no answer back, so a
|
|
57
|
+
* refusal stays in the worker: it is written to the console there, `SyncFailed` stays silent, and
|
|
58
|
+
* the whole trail is booked as applied.
|
|
59
|
+
*
|
|
60
|
+
* {@link ShadowEnv.syncWait} is the way both proxies answer on.
|
|
61
|
+
*/
|
|
39
62
|
sync(): void;
|
|
40
63
|
/**
|
|
41
64
|
* Like {@link ShadowEnv.sync}, but resolves with the change trail once the cycle completed.
|
|
42
65
|
*
|
|
43
|
-
*
|
|
66
|
+
* A cycle whose change trail the Shadow Environment could not apply rejects instead, with the
|
|
67
|
+
* reason the proxy gave. Where the Kernel itself refused the trail that reason is a
|
|
68
|
+
* {@link ChangeTrailRefusedError}: it names how many entries the Kernel applied, this side folds
|
|
69
|
+
* exactly those into its bookkeeping, and everything behind that line stays pending and goes out
|
|
70
|
+
* again with the next cycle. A trail whose cause of refusal stays put is refused every time, which
|
|
71
|
+
* is what {@link ShadowEnv.SyncFailed} is the place to act on.
|
|
72
|
+
*
|
|
73
|
+
* A reason that says nothing about how far the Kernel got -- a confirmation window that ran out,
|
|
74
|
+
* a proxy whose environment is gone -- counts the whole trail as applied, because a Shadow
|
|
75
|
+
* Environment that fell silent may well hold all of it. Then the trail is gone, and only a
|
|
76
|
+
* re-creation from the Component Memory brings it back. That re-creation belongs to a fresh
|
|
77
|
+
* proxy: an environment that still holds the entities refuses a creation for a uuid it already
|
|
78
|
+
* has. Handing {@link ShadowEnv.envProxy} a new proxy is therefore the way back -- the Shadow
|
|
79
|
+
* Environment calls {@link ComponentContext.reCreateChanges} itself once the new proxy is ready.
|
|
80
|
+
* Making that call is the consumer's decision, the same way recovering from a
|
|
81
|
+
* {@link ShadowEnv.ProxyFailed} is.
|
|
82
|
+
*
|
|
83
|
+
* Which cycle a caller gets is decided when the change trail is built. Everyone who arrives
|
|
84
|
+
* before that point waits on the same promise -- they all ride the same trail. From the build
|
|
85
|
+
* on the trail is fixed, and a call after it belongs to the next cycle, the one that carries
|
|
86
|
+
* the changes made since. That holds inside a listener of {@link ShadowEnv.AfterSync} or
|
|
87
|
+
* {@link ShadowEnv.SyncFailed} as well: the cycle it was told about is over, so the call opens
|
|
88
|
+
* the one behind it.
|
|
89
|
+
*
|
|
90
|
+
* @throws {ShadowEnvDestroyedError} if the environment is destroyed before the cycle completes
|
|
44
91
|
*/
|
|
45
92
|
syncWait(): Promise<ChangeTrailType>;
|
|
46
93
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ShadowEnv.d.ts","sourceRoot":"","sources":["../../../src/view/ShadowEnv.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ShadowEnv.d.ts","sourceRoot":"","sources":["../../../src/view/ShadowEnv.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAC,eAAe,EAAE,aAAa,EAAC,MAAM,aAAa,CAAC;AAChE,OAAO,EAAC,aAAa,EAAC,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,4BAA4B,CAAC;AAEtE,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,YAAY,EAAE,GAAG,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC;CAC7D;AAED;;;GAGG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,YAAY,OAAO,SAA8C,EAGhE;CACF;AAcD,qBAAa,SAAS;;IACpB,MAAM,CAAC,SAAS,SAAe;IAC/B,MAAM,CAAC,UAAU,SAAgB;IACjC,MAAM,CAAC,WAAW,SAAiB;IACnC,MAAM,CAAC,cAAc,SAAoB;IACzC,MAAM,CAAC,WAAW,SAAiB;IAEnC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,aAAa,GAAG,SAAS,GAAG,SAAS,CAGnD;IAWD,mDAAmD;IACnD,IAAI,MAAM,IAAI,aAAa,CAE1B;IAED;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,mEAA6C;IAE/C,QAAQ,CAAC,SAAS,UAAS;IAC3B,QAAQ,CAAC,UAAU,UAAS;IAItC,IAAI,WAAW,YAEd;IAED,cAWC;IAoDD,IAAI,IAAI,IAAI,gBAAgB,GAAG,SAAS,CAEvC;IAED,IAAI,IAAI,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EA0BhD;IAkBD,IAAI,QAAQ,IAAI,qBAAqB,GAAG,SAAS,CAEhD;IAMD,IAAI,QAAQ,CAAC,KAAK,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,EA+C3D;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAuBD;;;;OAIG;IACH,QAAQ,CAAC,KAAK,QAAa,OAAO,CAAC,SAAS,CAAC,CAI3C;IAEF;;;;;;;;;;;;;;OAcG;IACH,IAAI,IAAI,IAAI,CASX;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,QAAQ,IAAI,OAAO,CAAC,eAAe,CAAC,CAanC;IAwBD;;;;;OAKG;IACH,OAAO,SAgCN;CA2JF"}
|