@aethervtt/shard-runtime 0.0.1 → 0.1.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/dist/app.d.ts +124 -0
- package/dist/app.js +303 -16
- package/dist/control.d.ts +2 -0
- package/dist/control.js +7 -1
- package/dist/demand.d.ts +47 -0
- package/dist/demand.js +68 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +9 -1
- package/dist/plugin.d.ts +12 -0
- package/dist/runners.d.ts +17 -2
- package/dist/runners.js +130 -48
- package/dist/testing.d.ts +22 -0
- package/dist/testing.js +64 -0
- package/dist/time.d.ts +4 -0
- package/dist/time.js +5 -0
- package/dist/write-check.d.ts +22 -0
- package/dist/write-check.js +54 -0
- package/package.json +7 -3
package/dist/app.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface AppOptions {
|
|
|
11
11
|
now?: () => number;
|
|
12
12
|
/** Seed for the `GlobalRng` resource. Default 0. */
|
|
13
13
|
seed?: number;
|
|
14
|
+
/**
|
|
15
|
+
* A development build: missing assets show loudly (checkers), and dev-only checks and issues
|
|
16
|
+
* run (shader cache misses, color-space mismatches, GL error checks). Read it from `DevMode`.
|
|
17
|
+
* Default false.
|
|
18
|
+
*/
|
|
19
|
+
dev?: boolean;
|
|
14
20
|
}
|
|
15
21
|
export interface AppDescription {
|
|
16
22
|
plugins: {
|
|
@@ -39,6 +45,39 @@ export interface AppMethod {
|
|
|
39
45
|
world: World;
|
|
40
46
|
}, params: Record<string, unknown>): unknown;
|
|
41
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Code the app runs happens inside its scopes: plugin builds and ready hooks, frames, pumps, and
|
|
50
|
+
* disposal. The render plugin uses one to count GPU objects against the app (0052).
|
|
51
|
+
*/
|
|
52
|
+
export interface AppScope {
|
|
53
|
+
enter(): void;
|
|
54
|
+
exit(): void;
|
|
55
|
+
}
|
|
56
|
+
/** What a loop runner gives the app while it drives it (0052). */
|
|
57
|
+
export interface FrameDriver {
|
|
58
|
+
/** Schedules a frame if none is coming (on-demand); a no-op for runners that run every frame. */
|
|
59
|
+
requestFrame(): void;
|
|
60
|
+
/** Stops the loop for good. */
|
|
61
|
+
stop(): void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Waits until what the last frame drew is on screen (0062), resolving with when that was on the
|
|
65
|
+
* app's clock if it knows better than "now". The render plugin sets one: the frame's GPU work done,
|
|
66
|
+
* then the next animation frame, whose timestamp it resolves with. Without one, a frame counts as
|
|
67
|
+
* presented when it ends.
|
|
68
|
+
*/
|
|
69
|
+
export type FramePresenter = () => Promise<number | undefined>;
|
|
70
|
+
/** When the app started and became usable, in ms on its clock (`AppOptions.now`) (0062). */
|
|
71
|
+
export interface StartupTimings {
|
|
72
|
+
/** `init()` was called. NaN before. */
|
|
73
|
+
initStart: number;
|
|
74
|
+
/** `init()` resolved. NaN before. */
|
|
75
|
+
initEnd: number;
|
|
76
|
+
/** The host called `markUsable()`. NaN before. */
|
|
77
|
+
usableMarked: number;
|
|
78
|
+
/** The first frame after `markUsable()` was presented. NaN before. */
|
|
79
|
+
usable: number;
|
|
80
|
+
}
|
|
42
81
|
/**
|
|
43
82
|
* Owns the world, plugins, and schedules, and runs frames. Hosts drive it with a runner
|
|
44
83
|
* (animation frame, headless, or manual `update` calls).
|
|
@@ -50,6 +89,11 @@ export declare class App {
|
|
|
50
89
|
private readonly now;
|
|
51
90
|
private readonly plugins;
|
|
52
91
|
private readonly pending;
|
|
92
|
+
/** Plugins in the order they were built; disposal runs it backwards. */
|
|
93
|
+
private readonly built;
|
|
94
|
+
private readonly scopes;
|
|
95
|
+
private driver;
|
|
96
|
+
private disposing;
|
|
53
97
|
private readonly schedules;
|
|
54
98
|
private readonly systemNames;
|
|
55
99
|
private readonly states;
|
|
@@ -59,6 +103,12 @@ export declare class App {
|
|
|
59
103
|
private startupDone;
|
|
60
104
|
private accumulator;
|
|
61
105
|
private readonly frameListeners;
|
|
106
|
+
private presenter;
|
|
107
|
+
/** Traces stamped since the last frame started; the next frame carries them. */
|
|
108
|
+
private readonly traces;
|
|
109
|
+
private readonly traceListeners;
|
|
110
|
+
private readonly usableWaiters;
|
|
111
|
+
private readonly startup_;
|
|
62
112
|
private readonly appMethods;
|
|
63
113
|
/** The plugin whose `build` is running, so registrations can be attributed to it. */
|
|
64
114
|
private building;
|
|
@@ -95,8 +145,68 @@ export declare class App {
|
|
|
95
145
|
init(): Promise<void>;
|
|
96
146
|
/** Initializes, then hands control to the runner (manual if none was set). */
|
|
97
147
|
run(): Promise<void>;
|
|
148
|
+
/**
|
|
149
|
+
* Stops the runner, waits for every plugin's work in flight (`beforeDispose`), then disposes
|
|
150
|
+
* plugins in reverse build order, so each releases what it created: GPU objects, surfaces,
|
|
151
|
+
* listeners, workers (0052). Idempotent; afterwards any other
|
|
152
|
+
* call throws `runtime/disposed`. A plugin whose dispose throws is logged and the rest still run.
|
|
153
|
+
*/
|
|
154
|
+
dispose(): Promise<void>;
|
|
155
|
+
/** True once `dispose()` was called. */
|
|
156
|
+
get disposed(): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Adds a scope around everything the app runs (see `AppScope`). Returns a function that removes
|
|
159
|
+
* it; don't call that from inside the app's own code (a frame, a plugin's dispose): disposal
|
|
160
|
+
* drops every scope once plugins are done. Scopes enter in the order added and exit in reverse.
|
|
161
|
+
*/
|
|
162
|
+
addScope(scope: AppScope): () => void;
|
|
163
|
+
/**
|
|
164
|
+
* Called by a loop runner as it starts. `requestFrame` and `dispose` reach the loop through it.
|
|
165
|
+
*/
|
|
166
|
+
attachDriver(driver: FrameDriver): void;
|
|
167
|
+
/**
|
|
168
|
+
* Asks for a frame: an on-demand runner schedules one if none is coming (0052). Host code that
|
|
169
|
+
* changes what's on screen without writing the world (a canvas style, a DOM overlay) calls it.
|
|
170
|
+
*/
|
|
171
|
+
requestFrame(): void;
|
|
172
|
+
/** Sets how a frame waits to be on screen (see `FramePresenter`). The render plugin calls it. */
|
|
173
|
+
setPresenter(presenter: FramePresenter | undefined): void;
|
|
174
|
+
/** When init started and ended and when the app became usable, in ms on the app's clock. */
|
|
175
|
+
get startup(): Readonly<StartupTimings>;
|
|
176
|
+
/**
|
|
177
|
+
* The host says the app is usable (for a VTT, once the scene is interactive). The first complete
|
|
178
|
+
* frame presented after this is the first usable frame (`startup.usable`, `whenUsable()`). Later
|
|
179
|
+
* calls do nothing.
|
|
180
|
+
*/
|
|
181
|
+
markUsable(): void;
|
|
182
|
+
/** Resolves with `startup.usable` once the first usable frame is presented. */
|
|
183
|
+
whenUsable(): Promise<number>;
|
|
184
|
+
/**
|
|
185
|
+
* Stamps a host write (a patch, a token move) for latency: resolves with the ms from now until the
|
|
186
|
+
* frame that carries it is presented. Call it right after the write. The next frame to start
|
|
187
|
+
* carries every trace stamped before it; a frame that ends still loading (`LOADING_DEMAND`: a
|
|
188
|
+
* pipeline compiling, a mesh not uploaded) hands them on, so the latency runs until the write is
|
|
189
|
+
* visible. `onTrace` listeners see each result.
|
|
190
|
+
*/
|
|
191
|
+
trace(label: string): Promise<number>;
|
|
192
|
+
/**
|
|
193
|
+
* Requests a frame and resolves (with the ms waited) once a complete frame is presented, as
|
|
194
|
+
* `trace` does, but no `onTrace` listener sees it: for tools waiting on the screen, not for
|
|
195
|
+
* latency.
|
|
196
|
+
*/
|
|
197
|
+
whenPresented(): Promise<number>;
|
|
198
|
+
/** Called with every resolved trace: its label and latency in ms. Returns an unsubscribe function. */
|
|
199
|
+
onTrace(listener: (label: string, ms: number) => void): () => void;
|
|
200
|
+
/**
|
|
201
|
+
* Drops accumulated fixed-step time, so the next frame runs FixedUpdate for its own delta only.
|
|
202
|
+
* On-demand runners call it on waking, so a scene idle for a minute doesn't simulate the minute.
|
|
203
|
+
*/
|
|
204
|
+
resetFixedTime(): void;
|
|
98
205
|
/** Runs one frame. `delta` is in seconds. */
|
|
99
206
|
update(delta: number): void;
|
|
207
|
+
private frame;
|
|
208
|
+
/** Resolves traces and the usable mark once this frame is on screen. */
|
|
209
|
+
private present;
|
|
100
210
|
/** Called after every frame with the number of frames completed. Returns an unsubscribe function. */
|
|
101
211
|
onFrame(listener: (frame: number) => void): () => void;
|
|
102
212
|
/**
|
|
@@ -106,6 +216,20 @@ export declare class App {
|
|
|
106
216
|
pump(): void;
|
|
107
217
|
runSchedule(label: ScheduleLabel): void;
|
|
108
218
|
describe(): AppDescription;
|
|
219
|
+
private assertLive;
|
|
220
|
+
/**
|
|
221
|
+
* Runs `fn` inside the app's scopes, as build and ready hooks run. For the part of an async ready
|
|
222
|
+
* hook after its first await, which runs outside them: what it makes counts against the app.
|
|
223
|
+
*/
|
|
224
|
+
scoped<T>(fn: () => T): T;
|
|
225
|
+
/** Enters every scope; returns how many, for the matching `exit`. */
|
|
226
|
+
private enter;
|
|
227
|
+
/** Exits the first `n` scopes, so one added inside the section never sees an unmatched exit. */
|
|
228
|
+
private exit;
|
|
229
|
+
private buildPlugin;
|
|
230
|
+
/** Awaits a ready hook; the part that runs before its first await is inside the app's scopes. */
|
|
231
|
+
private readyPlugin;
|
|
232
|
+
private disposeAll;
|
|
109
233
|
private schedule;
|
|
110
234
|
private applyStateTransitions;
|
|
111
235
|
private dependencyError;
|
package/dist/app.js
CHANGED
|
@@ -14,9 +14,16 @@ import {
|
|
|
14
14
|
World
|
|
15
15
|
} from "@aethervtt/shard-core";
|
|
16
16
|
import { AppControl, AppControlResource } from "./control.js";
|
|
17
|
+
import { FrameDemand, FrameDemandState, LOADING_DEMAND } from "./demand.js";
|
|
17
18
|
import { Log, LogResource } from "./log.js";
|
|
18
19
|
import { OnEnter, OnExit } from "./state.js";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
DevMode,
|
|
22
|
+
DisplayRate,
|
|
23
|
+
FixedTime,
|
|
24
|
+
GlobalRng,
|
|
25
|
+
Time
|
|
26
|
+
} from "./time.js";
|
|
20
27
|
const FRAME_SCHEDULES = [Startup, First, PreUpdate, FixedUpdate, Update, PostUpdate, Last];
|
|
21
28
|
const EPSILON = 1e-9;
|
|
22
29
|
const TimePlugin = {
|
|
@@ -34,6 +41,11 @@ class App {
|
|
|
34
41
|
now;
|
|
35
42
|
plugins = /* @__PURE__ */ new Map();
|
|
36
43
|
pending = [];
|
|
44
|
+
/** Plugins in the order they were built; disposal runs it backwards. */
|
|
45
|
+
built = [];
|
|
46
|
+
scopes = [];
|
|
47
|
+
driver;
|
|
48
|
+
disposing;
|
|
37
49
|
schedules = /* @__PURE__ */ new Map();
|
|
38
50
|
systemNames = /* @__PURE__ */ new Set();
|
|
39
51
|
states = [];
|
|
@@ -43,6 +55,17 @@ class App {
|
|
|
43
55
|
startupDone = false;
|
|
44
56
|
accumulator = 0;
|
|
45
57
|
frameListeners = /* @__PURE__ */ new Set();
|
|
58
|
+
presenter;
|
|
59
|
+
/** Traces stamped since the last frame started; the next frame carries them. */
|
|
60
|
+
traces = [];
|
|
61
|
+
traceListeners = /* @__PURE__ */ new Set();
|
|
62
|
+
usableWaiters = [];
|
|
63
|
+
startup_ = {
|
|
64
|
+
initStart: Number.NaN,
|
|
65
|
+
initEnd: Number.NaN,
|
|
66
|
+
usableMarked: Number.NaN,
|
|
67
|
+
usable: Number.NaN
|
|
68
|
+
};
|
|
46
69
|
appMethods = /* @__PURE__ */ new Map();
|
|
47
70
|
/** The plugin whose `build` is running, so registrations can be attributed to it. */
|
|
48
71
|
building;
|
|
@@ -53,15 +76,20 @@ class App {
|
|
|
53
76
|
this.now = options.now ?? (() => performance.now());
|
|
54
77
|
this.world.insertResource(ProfilerResource, this.profiler);
|
|
55
78
|
this.world.insertResource(GlobalRng, new Rng(options.seed ?? 0));
|
|
79
|
+
this.world.insertResource(DevMode, { enabled: options.dev ?? false });
|
|
56
80
|
const log = new Log();
|
|
57
81
|
log.now = () => this.world.tryResource(Time)?.elapsed ?? 0;
|
|
58
82
|
this.world.insertResource(LogResource, log);
|
|
59
|
-
|
|
83
|
+
const control = new AppControl();
|
|
84
|
+
control.onRequest = () => this.driver?.requestFrame();
|
|
85
|
+
this.world.insertResource(AppControlResource, control);
|
|
86
|
+
this.world.insertResource(FrameDemand, new FrameDemandState(this.now));
|
|
60
87
|
this.addPlugin(TimePlugin);
|
|
61
88
|
}
|
|
62
89
|
// --- building --------------------------------------------------------------
|
|
63
90
|
/** Adds plugins. They build in dependency order during `init()`. */
|
|
64
91
|
addPlugin(...plugins) {
|
|
92
|
+
this.assertLive();
|
|
65
93
|
for (const plugin of plugins) {
|
|
66
94
|
if (this.plugins.has(plugin.name)) {
|
|
67
95
|
throw new ShardError("app/duplicate-plugin", `Plugin "${plugin.name}" was added twice`);
|
|
@@ -73,6 +101,7 @@ class App {
|
|
|
73
101
|
}
|
|
74
102
|
/** Adds protocol methods (see `AppMethod`). A second method with a taken name throws. */
|
|
75
103
|
addMethod(...methods) {
|
|
104
|
+
this.assertLive();
|
|
76
105
|
for (const method of methods) {
|
|
77
106
|
if (this.appMethods.has(method.name)) {
|
|
78
107
|
throw new ShardError("app/duplicate-method", `Method "${method.name}" was added twice`);
|
|
@@ -86,6 +115,7 @@ class App {
|
|
|
86
115
|
return [...this.appMethods.values()];
|
|
87
116
|
}
|
|
88
117
|
addSystems(schedule, ...systems) {
|
|
118
|
+
this.assertLive();
|
|
89
119
|
const target = this.schedule(schedule);
|
|
90
120
|
for (const system of systems) {
|
|
91
121
|
const name = "system" in system ? system.system.name : system.name;
|
|
@@ -101,6 +131,7 @@ class App {
|
|
|
101
131
|
return this;
|
|
102
132
|
}
|
|
103
133
|
configureSets(schedule, ...sets) {
|
|
134
|
+
this.assertLive();
|
|
104
135
|
const target = this.schedule(schedule);
|
|
105
136
|
for (const set of sets) {
|
|
106
137
|
target.configureSet(set);
|
|
@@ -135,7 +166,11 @@ class App {
|
|
|
135
166
|
* itself. Resources and entities stay. For hot reload; returns the removed system names.
|
|
136
167
|
*/
|
|
137
168
|
unloadPlugin(name) {
|
|
169
|
+
this.assertLive();
|
|
138
170
|
const owned = this.owned.get(name);
|
|
171
|
+
const plugin = this.plugins.get(name);
|
|
172
|
+
const at = plugin ? this.built.indexOf(plugin) : -1;
|
|
173
|
+
if (at !== -1) this.built.splice(at, 1);
|
|
139
174
|
this.plugins.delete(name);
|
|
140
175
|
this.owned.delete(name);
|
|
141
176
|
if (!owned) return [];
|
|
@@ -159,37 +194,38 @@ class App {
|
|
|
159
194
|
* first run. Startup systems it adds don't run: startup already happened.
|
|
160
195
|
*/
|
|
161
196
|
async loadPlugin(plugin) {
|
|
197
|
+
this.assertLive();
|
|
162
198
|
if (this.plugins.has(plugin.name)) {
|
|
163
199
|
throw new ShardError("app/duplicate-plugin", `Plugin "${plugin.name}" is already loaded`, {
|
|
164
200
|
hint: "Unload it first (unloadPlugin)."
|
|
165
201
|
});
|
|
166
202
|
}
|
|
167
203
|
this.plugins.set(plugin.name, plugin);
|
|
168
|
-
this.
|
|
169
|
-
|
|
170
|
-
plugin.build(this);
|
|
171
|
-
} finally {
|
|
172
|
-
this.building = void 0;
|
|
173
|
-
}
|
|
174
|
-
await plugin.ready?.(this);
|
|
204
|
+
this.buildPlugin(plugin);
|
|
205
|
+
await this.readyPlugin(plugin);
|
|
175
206
|
}
|
|
176
207
|
insertResource(def, value) {
|
|
208
|
+
this.assertLive();
|
|
177
209
|
this.world.insertResource(def, value);
|
|
178
210
|
return this;
|
|
179
211
|
}
|
|
180
212
|
/** Registers a state machine. `OnEnter(initial)` runs at the start of the first frame. */
|
|
181
213
|
initState(state, initial = state.values[0]) {
|
|
214
|
+
this.assertLive();
|
|
182
215
|
this.world.insertResource(state.resource, { current: initial, next: void 0 });
|
|
183
216
|
this.states.push({ state, entered: false });
|
|
184
217
|
return this;
|
|
185
218
|
}
|
|
186
219
|
setRunner(runner) {
|
|
220
|
+
this.assertLive();
|
|
187
221
|
this.runner = runner;
|
|
188
222
|
return this;
|
|
189
223
|
}
|
|
190
224
|
/** Builds plugins in dependency order, then awaits their `ready` hooks. */
|
|
191
225
|
async init() {
|
|
226
|
+
this.assertLive();
|
|
192
227
|
if (this.initialized) return;
|
|
228
|
+
if (Number.isNaN(this.startup_.initStart)) this.startup_.initStart = this.now();
|
|
193
229
|
const built = [];
|
|
194
230
|
const builtNames = /* @__PURE__ */ new Set();
|
|
195
231
|
while (this.pending.length > 0) {
|
|
@@ -198,32 +234,145 @@ class App {
|
|
|
198
234
|
);
|
|
199
235
|
if (index === -1) throw this.dependencyError(builtNames);
|
|
200
236
|
const [plugin] = this.pending.splice(index, 1);
|
|
201
|
-
this.
|
|
202
|
-
try {
|
|
203
|
-
plugin.build(this);
|
|
204
|
-
} finally {
|
|
205
|
-
this.building = void 0;
|
|
206
|
-
}
|
|
237
|
+
this.buildPlugin(plugin);
|
|
207
238
|
built.push(plugin);
|
|
208
239
|
builtNames.add(plugin.name);
|
|
209
240
|
}
|
|
210
|
-
for (const plugin of built) await
|
|
241
|
+
for (const plugin of built) await this.readyPlugin(plugin);
|
|
211
242
|
this.initialized = true;
|
|
243
|
+
this.startup_.initEnd = this.now();
|
|
212
244
|
}
|
|
213
245
|
/** Initializes, then hands control to the runner (manual if none was set). */
|
|
214
246
|
async run() {
|
|
215
247
|
await this.init();
|
|
216
248
|
await this.runner?.(this);
|
|
217
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Stops the runner, waits for every plugin's work in flight (`beforeDispose`), then disposes
|
|
252
|
+
* plugins in reverse build order, so each releases what it created: GPU objects, surfaces,
|
|
253
|
+
* listeners, workers (0052). Idempotent; afterwards any other
|
|
254
|
+
* call throws `runtime/disposed`. A plugin whose dispose throws is logged and the rest still run.
|
|
255
|
+
*/
|
|
256
|
+
dispose() {
|
|
257
|
+
this.disposing ??= this.disposeAll();
|
|
258
|
+
return this.disposing;
|
|
259
|
+
}
|
|
260
|
+
/** True once `dispose()` was called. */
|
|
261
|
+
get disposed() {
|
|
262
|
+
return this.disposing !== void 0;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Adds a scope around everything the app runs (see `AppScope`). Returns a function that removes
|
|
266
|
+
* it; don't call that from inside the app's own code (a frame, a plugin's dispose): disposal
|
|
267
|
+
* drops every scope once plugins are done. Scopes enter in the order added and exit in reverse.
|
|
268
|
+
*/
|
|
269
|
+
addScope(scope) {
|
|
270
|
+
this.assertLive();
|
|
271
|
+
this.scopes.push(scope);
|
|
272
|
+
return () => {
|
|
273
|
+
const i = this.scopes.indexOf(scope);
|
|
274
|
+
if (i !== -1) this.scopes.splice(i, 1);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Called by a loop runner as it starts. `requestFrame` and `dispose` reach the loop through it.
|
|
279
|
+
*/
|
|
280
|
+
attachDriver(driver) {
|
|
281
|
+
this.assertLive();
|
|
282
|
+
this.driver = driver;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Asks for a frame: an on-demand runner schedules one if none is coming (0052). Host code that
|
|
286
|
+
* changes what's on screen without writing the world (a canvas style, a DOM overlay) calls it.
|
|
287
|
+
*/
|
|
288
|
+
requestFrame() {
|
|
289
|
+
this.assertLive();
|
|
290
|
+
this.driver?.requestFrame();
|
|
291
|
+
}
|
|
292
|
+
// --- presentation (0062) ----------------------------------------------------
|
|
293
|
+
/** Sets how a frame waits to be on screen (see `FramePresenter`). The render plugin calls it. */
|
|
294
|
+
setPresenter(presenter) {
|
|
295
|
+
this.presenter = presenter;
|
|
296
|
+
}
|
|
297
|
+
/** When init started and ended and when the app became usable, in ms on the app's clock. */
|
|
298
|
+
get startup() {
|
|
299
|
+
return this.startup_;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The host says the app is usable (for a VTT, once the scene is interactive). The first complete
|
|
303
|
+
* frame presented after this is the first usable frame (`startup.usable`, `whenUsable()`). Later
|
|
304
|
+
* calls do nothing.
|
|
305
|
+
*/
|
|
306
|
+
markUsable() {
|
|
307
|
+
this.assertLive();
|
|
308
|
+
if (!Number.isNaN(this.startup_.usableMarked)) return;
|
|
309
|
+
this.startup_.usableMarked = this.now();
|
|
310
|
+
this.driver?.requestFrame();
|
|
311
|
+
}
|
|
312
|
+
/** Resolves with `startup.usable` once the first usable frame is presented. */
|
|
313
|
+
whenUsable() {
|
|
314
|
+
if (!Number.isNaN(this.startup_.usable)) return Promise.resolve(this.startup_.usable);
|
|
315
|
+
return new Promise((resolve) => this.usableWaiters.push(resolve));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Stamps a host write (a patch, a token move) for latency: resolves with the ms from now until the
|
|
319
|
+
* frame that carries it is presented. Call it right after the write. The next frame to start
|
|
320
|
+
* carries every trace stamped before it; a frame that ends still loading (`LOADING_DEMAND`: a
|
|
321
|
+
* pipeline compiling, a mesh not uploaded) hands them on, so the latency runs until the write is
|
|
322
|
+
* visible. `onTrace` listeners see each result.
|
|
323
|
+
*/
|
|
324
|
+
trace(label) {
|
|
325
|
+
this.assertLive();
|
|
326
|
+
return new Promise((resolve) => {
|
|
327
|
+
this.traces.push({ label, at: this.now(), resolve });
|
|
328
|
+
this.driver?.requestFrame();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Requests a frame and resolves (with the ms waited) once a complete frame is presented, as
|
|
333
|
+
* `trace` does, but no `onTrace` listener sees it: for tools waiting on the screen, not for
|
|
334
|
+
* latency.
|
|
335
|
+
*/
|
|
336
|
+
whenPresented() {
|
|
337
|
+
this.assertLive();
|
|
338
|
+
return new Promise((resolve) => {
|
|
339
|
+
this.traces.push({ label: void 0, at: this.now(), resolve });
|
|
340
|
+
this.driver?.requestFrame();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/** Called with every resolved trace: its label and latency in ms. Returns an unsubscribe function. */
|
|
344
|
+
onTrace(listener) {
|
|
345
|
+
this.assertLive();
|
|
346
|
+
this.traceListeners.add(listener);
|
|
347
|
+
return () => this.traceListeners.delete(listener);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Drops accumulated fixed-step time, so the next frame runs FixedUpdate for its own delta only.
|
|
351
|
+
* On-demand runners call it on waking, so a scene idle for a minute doesn't simulate the minute.
|
|
352
|
+
*/
|
|
353
|
+
resetFixedTime() {
|
|
354
|
+
this.accumulator = 0;
|
|
355
|
+
}
|
|
218
356
|
// --- running ---------------------------------------------------------------
|
|
219
357
|
/** Runs one frame. `delta` is in seconds. */
|
|
220
358
|
update(delta) {
|
|
359
|
+
this.assertLive();
|
|
221
360
|
if (!this.initialized) {
|
|
222
361
|
throw new ShardError("app/not-initialized", "App.update() was called before init()", {
|
|
223
362
|
hint: "Await app.init() (or app.run()) first."
|
|
224
363
|
});
|
|
225
364
|
}
|
|
365
|
+
const scoped = this.enter();
|
|
366
|
+
try {
|
|
367
|
+
this.frame(delta);
|
|
368
|
+
} finally {
|
|
369
|
+
this.exit(scoped);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
frame(delta) {
|
|
226
373
|
const world = this.world;
|
|
374
|
+
const traces = this.traces.length > 0 ? this.traces.splice(0) : void 0;
|
|
375
|
+
const usable = Number.isNaN(this.startup_.usable) && !Number.isNaN(this.startup_.usableMarked);
|
|
227
376
|
if (!this.startupDone) {
|
|
228
377
|
this.runSchedule(Startup);
|
|
229
378
|
this.startupDone = true;
|
|
@@ -252,9 +401,39 @@ class App {
|
|
|
252
401
|
this.runSchedule(Last);
|
|
253
402
|
time.frame++;
|
|
254
403
|
for (const listener of this.frameListeners) listener(time.frame);
|
|
404
|
+
if (!traces && !usable) return;
|
|
405
|
+
if (world.resource(FrameDemand).isHeld(LOADING_DEMAND)) {
|
|
406
|
+
if (traces) this.traces.unshift(...traces);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this.present(traces, usable);
|
|
410
|
+
}
|
|
411
|
+
/** Resolves traces and the usable mark once this frame is on screen. */
|
|
412
|
+
present(traces, usable) {
|
|
413
|
+
const done = (when) => {
|
|
414
|
+
const at = when ?? this.now();
|
|
415
|
+
if (usable && Number.isNaN(this.startup_.usable)) {
|
|
416
|
+
this.startup_.usable = at;
|
|
417
|
+
for (const resolve of this.usableWaiters.splice(0)) resolve(at);
|
|
418
|
+
}
|
|
419
|
+
if (!traces) return;
|
|
420
|
+
for (const trace of traces) {
|
|
421
|
+
const ms = Math.max(0, at - trace.at);
|
|
422
|
+
trace.resolve(ms);
|
|
423
|
+
const label = trace.label;
|
|
424
|
+
if (label !== void 0) for (const listener of this.traceListeners) listener(label, ms);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const presenter = this.presenter;
|
|
428
|
+
if (!presenter) {
|
|
429
|
+
done();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
presenter().then(done, () => done());
|
|
255
433
|
}
|
|
256
434
|
/** Called after every frame with the number of frames completed. Returns an unsubscribe function. */
|
|
257
435
|
onFrame(listener) {
|
|
436
|
+
this.assertLive();
|
|
258
437
|
this.frameListeners.add(listener);
|
|
259
438
|
return () => this.frameListeners.delete(listener);
|
|
260
439
|
}
|
|
@@ -263,6 +442,7 @@ class App {
|
|
|
263
442
|
* without a frame loop (headless servers); loop-driven runners step one frame per tick instead.
|
|
264
443
|
*/
|
|
265
444
|
pump() {
|
|
445
|
+
this.assertLive();
|
|
266
446
|
const control = this.world.resource(AppControlResource);
|
|
267
447
|
while (control.pendingSteps > 0) {
|
|
268
448
|
try {
|
|
@@ -301,6 +481,113 @@ class App {
|
|
|
301
481
|
};
|
|
302
482
|
}
|
|
303
483
|
// --- internals -------------------------------------------------------------
|
|
484
|
+
assertLive() {
|
|
485
|
+
if (this.disposing === void 0) return;
|
|
486
|
+
throw new ShardError("runtime/disposed", "This app was disposed", {
|
|
487
|
+
hint: "Create a new App; a disposed one has released its GPU objects and listeners."
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Runs `fn` inside the app's scopes, as build and ready hooks run. For the part of an async ready
|
|
492
|
+
* hook after its first await, which runs outside them: what it makes counts against the app.
|
|
493
|
+
*/
|
|
494
|
+
scoped(fn) {
|
|
495
|
+
const n = this.enter();
|
|
496
|
+
try {
|
|
497
|
+
return fn();
|
|
498
|
+
} finally {
|
|
499
|
+
this.exit(n);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** Enters every scope; returns how many, for the matching `exit`. */
|
|
503
|
+
enter() {
|
|
504
|
+
const n = this.scopes.length;
|
|
505
|
+
for (let i = 0; i < n; i++) this.scopes[i].enter();
|
|
506
|
+
return n;
|
|
507
|
+
}
|
|
508
|
+
/** Exits the first `n` scopes, so one added inside the section never sees an unmatched exit. */
|
|
509
|
+
exit(n) {
|
|
510
|
+
for (let i = Math.min(n, this.scopes.length) - 1; i >= 0; i--) this.scopes[i].exit();
|
|
511
|
+
}
|
|
512
|
+
buildPlugin(plugin) {
|
|
513
|
+
this.building = plugin.name;
|
|
514
|
+
const scoped = this.enter();
|
|
515
|
+
try {
|
|
516
|
+
plugin.build(this);
|
|
517
|
+
} finally {
|
|
518
|
+
this.exit(scoped);
|
|
519
|
+
this.building = void 0;
|
|
520
|
+
}
|
|
521
|
+
this.built.push(plugin);
|
|
522
|
+
}
|
|
523
|
+
/** Awaits a ready hook; the part that runs before its first await is inside the app's scopes. */
|
|
524
|
+
async readyPlugin(plugin) {
|
|
525
|
+
if (!plugin.ready) return;
|
|
526
|
+
const scoped = this.enter();
|
|
527
|
+
let result;
|
|
528
|
+
try {
|
|
529
|
+
result = plugin.ready(this);
|
|
530
|
+
} finally {
|
|
531
|
+
this.exit(scoped);
|
|
532
|
+
}
|
|
533
|
+
await result;
|
|
534
|
+
}
|
|
535
|
+
async disposeAll() {
|
|
536
|
+
const driver = this.driver;
|
|
537
|
+
this.driver = void 0;
|
|
538
|
+
driver?.stop();
|
|
539
|
+
this.frameListeners.clear();
|
|
540
|
+
this.traceListeners.clear();
|
|
541
|
+
this.presenter = void 0;
|
|
542
|
+
this.world.onWake = void 0;
|
|
543
|
+
this.world.asleep = false;
|
|
544
|
+
const log = this.world.tryResource(LogResource);
|
|
545
|
+
let first;
|
|
546
|
+
for (const plugin of this.built) {
|
|
547
|
+
if (!plugin.beforeDispose) continue;
|
|
548
|
+
const scoped = this.enter();
|
|
549
|
+
let result;
|
|
550
|
+
try {
|
|
551
|
+
result = plugin.beforeDispose(this);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
first ??= err;
|
|
554
|
+
log?.error(err);
|
|
555
|
+
continue;
|
|
556
|
+
} finally {
|
|
557
|
+
this.exit(scoped);
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
await result;
|
|
561
|
+
} catch (err) {
|
|
562
|
+
first ??= err;
|
|
563
|
+
log?.error(err);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
for (let i = this.built.length - 1; i >= 0; i--) {
|
|
567
|
+
const plugin = this.built[i];
|
|
568
|
+
if (!plugin.dispose) continue;
|
|
569
|
+
const scoped = this.enter();
|
|
570
|
+
let result;
|
|
571
|
+
try {
|
|
572
|
+
result = plugin.dispose(this);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
first ??= err;
|
|
575
|
+
log?.error(err);
|
|
576
|
+
continue;
|
|
577
|
+
} finally {
|
|
578
|
+
this.exit(scoped);
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
await result;
|
|
582
|
+
} catch (err) {
|
|
583
|
+
first ??= err;
|
|
584
|
+
log?.error(err);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
this.built.length = 0;
|
|
588
|
+
this.scopes.length = 0;
|
|
589
|
+
if (first !== void 0) throw first;
|
|
590
|
+
}
|
|
304
591
|
schedule(label) {
|
|
305
592
|
let schedule = this.schedules.get(label);
|
|
306
593
|
if (!schedule) {
|
package/dist/control.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare class AppControl {
|
|
|
6
6
|
paused: boolean;
|
|
7
7
|
/** Set when a failing frame paused the app, so a fix (e.g. a hot reload) can resume it. */
|
|
8
8
|
pausedByError: boolean;
|
|
9
|
+
/** Called when steps are queued, so an idle on-demand runner wakes to run them (0052). */
|
|
10
|
+
onRequest: (() => void) | undefined;
|
|
9
11
|
/** Stepped frames ever requested / ever run. Both only grow; the difference is what's pending. */
|
|
10
12
|
private requested;
|
|
11
13
|
private completed;
|
package/dist/control.js
CHANGED
|
@@ -3,6 +3,8 @@ class AppControl {
|
|
|
3
3
|
paused = false;
|
|
4
4
|
/** Set when a failing frame paused the app, so a fix (e.g. a hot reload) can resume it. */
|
|
5
5
|
pausedByError = false;
|
|
6
|
+
/** Called when steps are queued, so an idle on-demand runner wakes to run them (0052). */
|
|
7
|
+
onRequest = void 0;
|
|
6
8
|
/** Stepped frames ever requested / ever run. Both only grow; the difference is what's pending. */
|
|
7
9
|
requested = 0;
|
|
8
10
|
completed = 0;
|
|
@@ -16,7 +18,11 @@ class AppControl {
|
|
|
16
18
|
if (frames <= 0) return Promise.resolve();
|
|
17
19
|
this.requested += frames;
|
|
18
20
|
const target = this.requested;
|
|
19
|
-
|
|
21
|
+
const done = new Promise(
|
|
22
|
+
(resolve, reject) => this.waiters.push({ target, resolve, reject })
|
|
23
|
+
);
|
|
24
|
+
this.onRequest?.();
|
|
25
|
+
return done;
|
|
20
26
|
}
|
|
21
27
|
/** A stepped frame threw: drops the pending steps and rejects everyone waiting on them. */
|
|
22
28
|
abort(error) {
|
package/dist/demand.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame demand a renderer holds while a frame skipped draws (pipelines compiling, meshes or
|
|
3
|
+
* materials still loading, 0052). Such a frame isn't complete: traces and the first usable frame
|
|
4
|
+
* wait for one that is (0062).
|
|
5
|
+
*/
|
|
6
|
+
export declare const LOADING_DEMAND = "render/loading";
|
|
7
|
+
/** How the app's frames are driven: every display refresh, only when needed, or by hand. */
|
|
8
|
+
export type FrameMode = 'continuous' | 'on-demand' | 'manual';
|
|
9
|
+
/**
|
|
10
|
+
* What keeps an on-demand runner rendering (0052). Holders are named, so `render.describe` can say
|
|
11
|
+
* who is keeping frames running: awake physics bodies, live particles, playing animations, the
|
|
12
|
+
* RenderScale controller while probing, or anything a game holds itself.
|
|
13
|
+
*/
|
|
14
|
+
export declare class FrameDemandState {
|
|
15
|
+
/** Set by the runner. 'manual' until a loop runner starts. */
|
|
16
|
+
mode: FrameMode;
|
|
17
|
+
/** Clock time (ms, the app's clock) of the earliest frame `after` asked for; Infinity if none. */
|
|
18
|
+
dueAt: number;
|
|
19
|
+
/** Set by the on-demand runner: called when a new demand appears, so an idle runner wakes. */
|
|
20
|
+
onDemand: (() => void) | undefined;
|
|
21
|
+
private readonly holders;
|
|
22
|
+
private readonly now;
|
|
23
|
+
constructor(now?: () => number);
|
|
24
|
+
/** Keeps frames running until `release(key)`. Holding a key twice is one hold. */
|
|
25
|
+
hold(key: string): void;
|
|
26
|
+
release(key: string): void;
|
|
27
|
+
/** Holds `key` while `on` is true: one call per frame from a system that tracks a condition. */
|
|
28
|
+
set(key: string, on: boolean): void;
|
|
29
|
+
/** Asks for one frame `ms` from now, for ambient loops at low rates. The earliest ask wins. */
|
|
30
|
+
after(ms: number): void;
|
|
31
|
+
/** Ms until the frame `after` asked for (0 if overdue); Infinity if none was asked for. */
|
|
32
|
+
dueIn(): number;
|
|
33
|
+
/** @internal The runner calls this as a frame starts: a frame that was due is now running. */
|
|
34
|
+
takeDue(): void;
|
|
35
|
+
/** Whether anything holds frames running. */
|
|
36
|
+
get active(): boolean;
|
|
37
|
+
isHeld(key: string): boolean;
|
|
38
|
+
/** The keys holding frames running, sorted. */
|
|
39
|
+
held(): string[];
|
|
40
|
+
/** The runner's mode, what holds frames, and when the next timed frame is due. For agents. */
|
|
41
|
+
describe(): {
|
|
42
|
+
mode: FrameMode;
|
|
43
|
+
demands: string[];
|
|
44
|
+
dueInMs: number | null;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export declare const FrameDemand: import("@aethervtt/shard-core").ResourceDef<FrameDemandState>;
|
package/dist/demand.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineResource } from "@aethervtt/shard-core";
|
|
2
|
+
const LOADING_DEMAND = "render/loading";
|
|
3
|
+
class FrameDemandState {
|
|
4
|
+
/** Set by the runner. 'manual' until a loop runner starts. */
|
|
5
|
+
mode = "manual";
|
|
6
|
+
/** Clock time (ms, the app's clock) of the earliest frame `after` asked for; Infinity if none. */
|
|
7
|
+
dueAt = Number.POSITIVE_INFINITY;
|
|
8
|
+
/** Set by the on-demand runner: called when a new demand appears, so an idle runner wakes. */
|
|
9
|
+
onDemand = void 0;
|
|
10
|
+
holders = /* @__PURE__ */ new Set();
|
|
11
|
+
now;
|
|
12
|
+
constructor(now = () => performance.now()) {
|
|
13
|
+
this.now = now;
|
|
14
|
+
}
|
|
15
|
+
/** Keeps frames running until `release(key)`. Holding a key twice is one hold. */
|
|
16
|
+
hold(key) {
|
|
17
|
+
if (this.holders.has(key)) return;
|
|
18
|
+
this.holders.add(key);
|
|
19
|
+
this.onDemand?.();
|
|
20
|
+
}
|
|
21
|
+
release(key) {
|
|
22
|
+
this.holders.delete(key);
|
|
23
|
+
}
|
|
24
|
+
/** Holds `key` while `on` is true: one call per frame from a system that tracks a condition. */
|
|
25
|
+
set(key, on) {
|
|
26
|
+
if (on) this.hold(key);
|
|
27
|
+
else this.holders.delete(key);
|
|
28
|
+
}
|
|
29
|
+
/** Asks for one frame `ms` from now, for ambient loops at low rates. The earliest ask wins. */
|
|
30
|
+
after(ms) {
|
|
31
|
+
const at = this.now() + Math.max(0, ms);
|
|
32
|
+
if (at >= this.dueAt) return;
|
|
33
|
+
this.dueAt = at;
|
|
34
|
+
this.onDemand?.();
|
|
35
|
+
}
|
|
36
|
+
/** Ms until the frame `after` asked for (0 if overdue); Infinity if none was asked for. */
|
|
37
|
+
dueIn() {
|
|
38
|
+
return this.dueAt === Number.POSITIVE_INFINITY ? this.dueAt : Math.max(0, this.dueAt - this.now());
|
|
39
|
+
}
|
|
40
|
+
/** @internal The runner calls this as a frame starts: a frame that was due is now running. */
|
|
41
|
+
takeDue() {
|
|
42
|
+
if (this.dueAt <= this.now() + 1) this.dueAt = Number.POSITIVE_INFINITY;
|
|
43
|
+
}
|
|
44
|
+
/** Whether anything holds frames running. */
|
|
45
|
+
get active() {
|
|
46
|
+
return this.holders.size > 0;
|
|
47
|
+
}
|
|
48
|
+
isHeld(key) {
|
|
49
|
+
return this.holders.has(key);
|
|
50
|
+
}
|
|
51
|
+
/** The keys holding frames running, sorted. */
|
|
52
|
+
held() {
|
|
53
|
+
return [...this.holders].sort();
|
|
54
|
+
}
|
|
55
|
+
/** The runner's mode, what holds frames, and when the next timed frame is due. For agents. */
|
|
56
|
+
describe() {
|
|
57
|
+
const due = this.dueIn();
|
|
58
|
+
return { mode: this.mode, demands: this.held(), dueInMs: due === Infinity ? null : due };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const FrameDemand = defineResource("core/FrameDemand", {
|
|
62
|
+
description: "What keeps an on-demand runner rendering: hold(key) / release(key) while something must run each frame, after(ms) for one frame later."
|
|
63
|
+
});
|
|
64
|
+
export {
|
|
65
|
+
FrameDemand,
|
|
66
|
+
FrameDemandState,
|
|
67
|
+
LOADING_DEMAND
|
|
68
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { App, type AppDescription, type AppMethod, type AppOptions } from './app.js';
|
|
1
|
+
export { App, type AppDescription, type AppMethod, type AppOptions, type AppScope, type FrameDriver, type FramePresenter, type StartupTimings, } from './app.js';
|
|
2
2
|
export { AppControl, AppControlResource } from './control.js';
|
|
3
|
+
export { FrameDemand, FrameDemandState, type FrameMode, LOADING_DEMAND } from './demand.js';
|
|
3
4
|
export { COMMON_RATES, RefreshMeter, rateFromIntervals, snapRate } from './display.js';
|
|
4
5
|
export { Log, type LogEntry, type LogLevel, LogResource } from './log.js';
|
|
5
6
|
export { definePlugin, type Plugin } from './plugin.js';
|
|
6
7
|
export { type AnimationFrameOptions, animationFrameRunner, type HeadlessOptions, headlessRunner, type Runner, } from './runners.js';
|
|
7
8
|
export { defineState, inState, OnEnter, OnExit, type StateDef, type StateValue, setState, } from './state.js';
|
|
8
|
-
export { DisplayRate, type DisplayRateData, FixedTime, type FixedTimeData, GlobalRng, Time, type TimeData, } from './time.js';
|
|
9
|
+
export { DevMode, DisplayRate, type DisplayRateData, FixedTime, type FixedTimeData, GlobalRng, Time, type TimeData, } from './time.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
App
|
|
3
|
+
} from "./app.js";
|
|
2
4
|
import { AppControl, AppControlResource } from "./control.js";
|
|
5
|
+
import { FrameDemand, FrameDemandState, LOADING_DEMAND } from "./demand.js";
|
|
3
6
|
import { COMMON_RATES, RefreshMeter, rateFromIntervals, snapRate } from "./display.js";
|
|
4
7
|
import { Log, LogResource } from "./log.js";
|
|
5
8
|
import { definePlugin } from "./plugin.js";
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
setState
|
|
16
19
|
} from "./state.js";
|
|
17
20
|
import {
|
|
21
|
+
DevMode,
|
|
18
22
|
DisplayRate,
|
|
19
23
|
FixedTime,
|
|
20
24
|
GlobalRng,
|
|
@@ -25,9 +29,13 @@ export {
|
|
|
25
29
|
AppControl,
|
|
26
30
|
AppControlResource,
|
|
27
31
|
COMMON_RATES,
|
|
32
|
+
DevMode,
|
|
28
33
|
DisplayRate,
|
|
29
34
|
FixedTime,
|
|
35
|
+
FrameDemand,
|
|
36
|
+
FrameDemandState,
|
|
30
37
|
GlobalRng,
|
|
38
|
+
LOADING_DEMAND,
|
|
31
39
|
Log,
|
|
32
40
|
LogResource,
|
|
33
41
|
OnEnter,
|
package/dist/plugin.d.ts
CHANGED
|
@@ -14,5 +14,17 @@ export interface Plugin {
|
|
|
14
14
|
build(app: App): void;
|
|
15
15
|
/** Optional async setup (e.g. loading WASM), awaited before Startup. */
|
|
16
16
|
ready?(app: App): Promise<void> | void;
|
|
17
|
+
/**
|
|
18
|
+
* Releases what `build` and `ready` created that outlives the app: GPU objects, surfaces, DOM
|
|
19
|
+
* listeners, observers, workers. `app.dispose()` calls it in reverse build order, after the
|
|
20
|
+
* runner stopped (0052).
|
|
21
|
+
*/
|
|
22
|
+
dispose?(app: App): Promise<void> | void;
|
|
23
|
+
/**
|
|
24
|
+
* Waits for work the plugin started that is still in flight (GPU commands submitted but not
|
|
25
|
+
* done). `app.dispose()` awaits it on every plugin before any plugin's `dispose`, so nothing is
|
|
26
|
+
* released while still in use: render's dispose runs last, after others freed their buffers.
|
|
27
|
+
*/
|
|
28
|
+
beforeDispose?(app: App): Promise<void> | void;
|
|
17
29
|
}
|
|
18
30
|
export declare function definePlugin(plugin: Plugin): Plugin;
|
package/dist/runners.d.ts
CHANGED
|
@@ -11,13 +11,28 @@ export declare function headlessRunner(options: HeadlessOptions): Runner;
|
|
|
11
11
|
export interface AnimationFrameOptions {
|
|
12
12
|
/** Longest delta passed to update, in seconds, e.g. after the tab was hidden. Default 0.25. */
|
|
13
13
|
maxDelta?: number;
|
|
14
|
-
/** Stops the loop when aborted. */
|
|
14
|
+
/** Stops the loop when aborted. `app.dispose()` stops it too. */
|
|
15
15
|
signal?: AbortSignal;
|
|
16
16
|
/**
|
|
17
17
|
* Time a few idle frames before the first update to measure the display's refresh rate into
|
|
18
18
|
* DisplayRate (about 12 refreshes: 0.1 s at 120 Hz). Default true.
|
|
19
19
|
*/
|
|
20
20
|
measureRefresh?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* continuous (default): a frame every display refresh. on-demand: a frame only when something
|
|
23
|
+
* changed (0052): a world write, a FrameDemand holder or timer, input, a surface resize, queued
|
|
24
|
+
* steps, or `app.requestFrame()`. Idle, it requests no animation frames at all.
|
|
25
|
+
*/
|
|
26
|
+
mode?: 'continuous' | 'on-demand';
|
|
27
|
+
/**
|
|
28
|
+
* On-demand, for dev builds: every second while idle, compare the plain-object resources marked
|
|
29
|
+
* `hostWritable` with their values when the app went idle, and log
|
|
30
|
+
* `runtime/unmarked-resource-write` for one that changed without waking it. Default false.
|
|
31
|
+
*/
|
|
32
|
+
checkResourceWrites?: boolean;
|
|
21
33
|
}
|
|
22
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Runs frames on `requestAnimationFrame`, for the browser and Studio: one per display refresh, or
|
|
36
|
+
* in `mode: 'on-demand'` only while something needs them (0052).
|
|
37
|
+
*/
|
|
23
38
|
export declare function animationFrameRunner(options?: AnimationFrameOptions): Runner;
|
package/dist/runners.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AppControlResource } from "./control.js";
|
|
2
|
+
import { FrameDemand } from "./demand.js";
|
|
2
3
|
import { RefreshMeter, rateFromIntervals } from "./display.js";
|
|
3
4
|
import { LogResource } from "./log.js";
|
|
4
5
|
import { DisplayRate } from "./time.js";
|
|
@@ -9,76 +10,157 @@ function headlessRunner(options) {
|
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
const PROBE_INTERVALS = 12;
|
|
12
|
-
function probeRefresh() {
|
|
13
|
+
function probeRefresh(probe) {
|
|
13
14
|
return new Promise((resolve) => {
|
|
14
15
|
if (document.hidden) return resolve(0);
|
|
15
16
|
const intervals = new Float64Array(PROBE_INTERVALS);
|
|
16
17
|
let last = -1;
|
|
17
18
|
let n = 0;
|
|
18
19
|
const tick = (t) => {
|
|
19
|
-
if (document.hidden) return resolve(0);
|
|
20
|
+
if (probe.stopped || document.hidden) return resolve(0);
|
|
20
21
|
if (last >= 0) intervals[n++] = t - last;
|
|
21
22
|
last = t;
|
|
22
|
-
if (n < PROBE_INTERVALS) requestAnimationFrame(tick);
|
|
23
|
+
if (n < PROBE_INTERVALS) probe.handle = requestAnimationFrame(tick);
|
|
23
24
|
else resolve(rateFromIntervals(intervals));
|
|
24
25
|
};
|
|
25
|
-
requestAnimationFrame(tick);
|
|
26
|
+
probe.handle = requestAnimationFrame(tick);
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
function animationFrameRunner(options = {}) {
|
|
29
30
|
const maxDelta = options.maxDelta ?? 0.25;
|
|
31
|
+
const onDemand = options.mode === "on-demand";
|
|
30
32
|
return async (app) => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const world = app.world;
|
|
34
|
+
const demand = world.resource(FrameDemand);
|
|
35
|
+
const control = world.resource(AppControlResource);
|
|
36
|
+
const display = world.tryResource(DisplayRate);
|
|
37
|
+
const probe = { handle: 0, stopped: false };
|
|
38
|
+
let handle = 0;
|
|
39
|
+
let pending = false;
|
|
40
|
+
let idle = true;
|
|
41
|
+
let last;
|
|
42
|
+
let timer;
|
|
43
|
+
let writes;
|
|
44
|
+
let finish = () => {
|
|
45
|
+
};
|
|
46
|
+
const finished = new Promise((resolve) => {
|
|
47
|
+
finish = resolve;
|
|
48
|
+
});
|
|
49
|
+
const schedule = () => {
|
|
50
|
+
if (pending || probe.stopped) return;
|
|
51
|
+
pending = true;
|
|
52
|
+
handle = requestAnimationFrame(frame);
|
|
53
|
+
};
|
|
54
|
+
const onTimer = () => {
|
|
55
|
+
timer = void 0;
|
|
56
|
+
schedule();
|
|
57
|
+
};
|
|
58
|
+
const armTimer = () => {
|
|
59
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
60
|
+
timer = void 0;
|
|
61
|
+
const due = demand.dueIn();
|
|
62
|
+
if (due !== Number.POSITIVE_INFINITY) timer = setTimeout(onTimer, due);
|
|
63
|
+
};
|
|
64
|
+
const goIdle = () => {
|
|
65
|
+
idle = true;
|
|
66
|
+
world.asleep = true;
|
|
67
|
+
armTimer();
|
|
68
|
+
writes?.start();
|
|
69
|
+
};
|
|
70
|
+
const onVisibility = () => {
|
|
71
|
+
if (document.hidden) last = void 0;
|
|
72
|
+
};
|
|
73
|
+
const stop = () => {
|
|
74
|
+
if (probe.stopped) return;
|
|
75
|
+
probe.stopped = true;
|
|
76
|
+
cancelAnimationFrame(probe.handle);
|
|
77
|
+
if (pending) cancelAnimationFrame(handle);
|
|
78
|
+
pending = false;
|
|
79
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
80
|
+
timer = void 0;
|
|
81
|
+
writes?.stop();
|
|
82
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
83
|
+
options.signal?.removeEventListener("abort", stop);
|
|
84
|
+
if (onDemand) {
|
|
85
|
+
world.onWake = void 0;
|
|
86
|
+
world.asleep = false;
|
|
87
|
+
demand.onDemand = void 0;
|
|
88
|
+
}
|
|
89
|
+
demand.mode = "manual";
|
|
90
|
+
finish();
|
|
91
|
+
};
|
|
92
|
+
const meter = new RefreshMeter(display?.periodMs ?? 1e3 / 60);
|
|
93
|
+
const frame = (timestamp) => {
|
|
94
|
+
pending = false;
|
|
95
|
+
if (probe.stopped) return;
|
|
96
|
+
const woke = idle;
|
|
97
|
+
idle = false;
|
|
98
|
+
writes?.stop();
|
|
99
|
+
let delta = last === void 0 ? 0 : Math.min((timestamp - last) / 1e3, maxDelta);
|
|
100
|
+
if (onDemand) {
|
|
101
|
+
world.asleep = false;
|
|
102
|
+
demand.takeDue();
|
|
103
|
+
if (woke && last !== void 0) {
|
|
104
|
+
delta = 1 / app.fixedHz;
|
|
105
|
+
app.resetFixedTime();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (display && last !== void 0 && !woke) {
|
|
109
|
+
const hz = meter.sample(timestamp - last);
|
|
110
|
+
if (hz > 0) {
|
|
111
|
+
display.hz = hz;
|
|
112
|
+
display.periodMs = 1e3 / hz;
|
|
113
|
+
display.source = "measured";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
last = timestamp;
|
|
117
|
+
try {
|
|
118
|
+
if (!control.paused) app.update(delta);
|
|
119
|
+
else if (control.pendingSteps > 0) {
|
|
120
|
+
app.update(1 / app.fixedHz);
|
|
121
|
+
control.stepped();
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
world.resource(LogResource).error(err);
|
|
125
|
+
control.paused = true;
|
|
126
|
+
control.pausedByError = true;
|
|
127
|
+
control.abort(err);
|
|
128
|
+
console.error(err);
|
|
129
|
+
}
|
|
130
|
+
if (probe.stopped) return;
|
|
131
|
+
if (!onDemand || demand.active || control.pendingSteps > 0) schedule();
|
|
132
|
+
if (!pending) goIdle();
|
|
133
|
+
};
|
|
134
|
+
options.signal?.addEventListener("abort", stop);
|
|
135
|
+
app.attachDriver({ requestFrame: onDemand ? schedule : () => {
|
|
136
|
+
}, stop });
|
|
137
|
+
demand.mode = onDemand ? "on-demand" : "continuous";
|
|
138
|
+
if (options.signal?.aborted) stop();
|
|
139
|
+
if (display && options.measureRefresh !== false && !probe.stopped) {
|
|
140
|
+
const hz = await probeRefresh(probe);
|
|
34
141
|
if (hz > 0) {
|
|
35
142
|
display.hz = hz;
|
|
36
143
|
display.periodMs = 1e3 / hz;
|
|
37
144
|
display.source = "measured";
|
|
145
|
+
meter.periodMs = display.periodMs;
|
|
38
146
|
}
|
|
39
147
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
if (hz > 0) {
|
|
52
|
-
display.hz = hz;
|
|
53
|
-
display.periodMs = 1e3 / hz;
|
|
54
|
-
display.source = "measured";
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
last = timestamp;
|
|
58
|
-
const control = app.world.resource(AppControlResource);
|
|
59
|
-
try {
|
|
60
|
-
if (!control.paused) app.update(delta);
|
|
61
|
-
else if (control.pendingSteps > 0) {
|
|
62
|
-
app.update(1 / app.fixedHz);
|
|
63
|
-
control.stepped();
|
|
64
|
-
}
|
|
65
|
-
} catch (err) {
|
|
66
|
-
app.world.resource(LogResource).error(err);
|
|
67
|
-
control.paused = true;
|
|
68
|
-
control.pausedByError = true;
|
|
69
|
-
control.abort(err);
|
|
70
|
-
console.error(err);
|
|
71
|
-
}
|
|
72
|
-
handle = requestAnimationFrame(frame);
|
|
148
|
+
if (onDemand && options.checkResourceWrites) {
|
|
149
|
+
const { ResourceWriteCheck } = await import("./write-check.js");
|
|
150
|
+
writes = new ResourceWriteCheck(world, world.tryResource(LogResource), () => idle && !pending);
|
|
151
|
+
}
|
|
152
|
+
if (probe.stopped) return;
|
|
153
|
+
if (onDemand) {
|
|
154
|
+
world.onWake = schedule;
|
|
155
|
+
demand.onDemand = () => {
|
|
156
|
+
if (!idle || probe.stopped) return;
|
|
157
|
+
if (demand.active) schedule();
|
|
158
|
+
else armTimer();
|
|
73
159
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
resolve();
|
|
79
|
-
});
|
|
80
|
-
handle = requestAnimationFrame(frame);
|
|
81
|
-
});
|
|
160
|
+
}
|
|
161
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
162
|
+
schedule();
|
|
163
|
+
return finished;
|
|
82
164
|
};
|
|
83
165
|
}
|
|
84
166
|
export {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stand-in for the browser's frame loop, so `animationFrameRunner` runs headless in tests: it
|
|
3
|
+
* installs `requestAnimationFrame`, `cancelAnimationFrame`, and (when there's none) a visible
|
|
4
|
+
* `document` on globalThis. Frames only run when the test ticks.
|
|
5
|
+
*/
|
|
6
|
+
export interface FakeFrames {
|
|
7
|
+
/** Animation frames requested so far. */
|
|
8
|
+
readonly requested: number;
|
|
9
|
+
/** Callbacks waiting for the next frame. */
|
|
10
|
+
readonly pending: number;
|
|
11
|
+
/** The timestamp the last tick passed, in ms. */
|
|
12
|
+
readonly now: number;
|
|
13
|
+
/** `visibilitychange` listeners on the fake document. */
|
|
14
|
+
readonly listeners: number;
|
|
15
|
+
/** Advances the clock by `ms` (default one 60 Hz refresh) and runs what was waiting. */
|
|
16
|
+
tick(ms?: number): number;
|
|
17
|
+
/** Ticks until nothing waits or `max` frames ran; returns the frames run. */
|
|
18
|
+
runUntilIdle(max?: number, ms?: number): number;
|
|
19
|
+
/** Puts the real globals back. */
|
|
20
|
+
restore(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function fakeAnimationFrames(): FakeFrames;
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function fakeAnimationFrames() {
|
|
2
|
+
const g = globalThis;
|
|
3
|
+
const saved = {
|
|
4
|
+
raf: g.requestAnimationFrame,
|
|
5
|
+
caf: g.cancelAnimationFrame,
|
|
6
|
+
document: g.document
|
|
7
|
+
};
|
|
8
|
+
let waiting = /* @__PURE__ */ new Map();
|
|
9
|
+
let nextId = 1;
|
|
10
|
+
let requested = 0;
|
|
11
|
+
let now = 0;
|
|
12
|
+
let listeners = 0;
|
|
13
|
+
g.requestAnimationFrame = (fn) => {
|
|
14
|
+
const id = nextId++;
|
|
15
|
+
requested++;
|
|
16
|
+
waiting.set(id, fn);
|
|
17
|
+
return id;
|
|
18
|
+
};
|
|
19
|
+
g.cancelAnimationFrame = (id) => {
|
|
20
|
+
waiting.delete(id);
|
|
21
|
+
};
|
|
22
|
+
if (saved.document === void 0) {
|
|
23
|
+
g.document = {
|
|
24
|
+
hidden: false,
|
|
25
|
+
addEventListener: () => void listeners++,
|
|
26
|
+
removeEventListener: () => void listeners--
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const tick = (ms = 1e3 / 60) => {
|
|
30
|
+
now += ms;
|
|
31
|
+
const run = waiting;
|
|
32
|
+
waiting = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const fn of run.values()) fn(now);
|
|
34
|
+
return run.size;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
get requested() {
|
|
38
|
+
return requested;
|
|
39
|
+
},
|
|
40
|
+
get pending() {
|
|
41
|
+
return waiting.size;
|
|
42
|
+
},
|
|
43
|
+
get now() {
|
|
44
|
+
return now;
|
|
45
|
+
},
|
|
46
|
+
get listeners() {
|
|
47
|
+
return listeners;
|
|
48
|
+
},
|
|
49
|
+
tick,
|
|
50
|
+
runUntilIdle(max = 1e4, ms) {
|
|
51
|
+
let frames = 0;
|
|
52
|
+
while (waiting.size > 0 && frames < max) frames += tick(ms);
|
|
53
|
+
return frames;
|
|
54
|
+
},
|
|
55
|
+
restore() {
|
|
56
|
+
g.requestAnimationFrame = saved.raf;
|
|
57
|
+
g.cancelAnimationFrame = saved.caf;
|
|
58
|
+
g.document = saved.document;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
fakeAnimationFrames
|
|
64
|
+
};
|
package/dist/time.d.ts
CHANGED
|
@@ -32,3 +32,7 @@ export interface DisplayRateData {
|
|
|
32
32
|
}
|
|
33
33
|
export declare const DisplayRate: import("@aethervtt/shard-core").ResourceDef<DisplayRateData>;
|
|
34
34
|
export declare const GlobalRng: import("@aethervtt/shard-core").ResourceDef<Rng>;
|
|
35
|
+
/** Whether this is a development build (`new App({ dev: true })`). */
|
|
36
|
+
export declare const DevMode: import("@aethervtt/shard-core").ResourceDef<{
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
}>;
|
package/dist/time.js
CHANGED
|
@@ -11,7 +11,12 @@ const DisplayRate = defineResource("core/DisplayRate", {
|
|
|
11
11
|
const GlobalRng = defineResource("core/GlobalRng", {
|
|
12
12
|
description: "The app root random stream, seeded from AppOptions.seed. Fork it per system."
|
|
13
13
|
});
|
|
14
|
+
const DevMode = defineResource("runtime/DevMode", {
|
|
15
|
+
description: "Development build: loud fallbacks and dev-only checks.",
|
|
16
|
+
init: () => ({ enabled: false })
|
|
17
|
+
});
|
|
14
18
|
export {
|
|
19
|
+
DevMode,
|
|
15
20
|
DisplayRate,
|
|
16
21
|
FixedTime,
|
|
17
22
|
GlobalRng,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { Log } from './log.js';
|
|
3
|
+
/**
|
|
4
|
+
* The on-demand runner's dev check (0052): while the app is idle, every second, compare the
|
|
5
|
+
* plain-object resources marked `hostWritable` with their values when it went idle, and log
|
|
6
|
+
* `runtime/unmarked-resource-write` for one that changed without waking the app. Loaded only when
|
|
7
|
+
* `checkResourceWrites` is on, so production builds don't carry it.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ResourceWriteCheck {
|
|
10
|
+
private readonly snapshots;
|
|
11
|
+
private timer;
|
|
12
|
+
private readonly world;
|
|
13
|
+
private readonly log;
|
|
14
|
+
private readonly quiet;
|
|
15
|
+
/** `quiet`: still idle with no frame requested, so a change nobody announced is a bug. */
|
|
16
|
+
constructor(world: World, log: Log | undefined, quiet: () => boolean);
|
|
17
|
+
/** The app went idle: remember what the resources hold, and check them every second. */
|
|
18
|
+
start(): void;
|
|
19
|
+
/** A frame is running (or the loop stopped): writes are fine again. */
|
|
20
|
+
stop(): void;
|
|
21
|
+
private verify;
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { allResources, isPlainObject } from "@aethervtt/shard-core";
|
|
2
|
+
const CHECK_MS = 1e3;
|
|
3
|
+
function hashOf(value) {
|
|
4
|
+
if (!isPlainObject(value)) return void 0;
|
|
5
|
+
try {
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
} catch {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
class ResourceWriteCheck {
|
|
12
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
13
|
+
timer;
|
|
14
|
+
world;
|
|
15
|
+
log;
|
|
16
|
+
quiet;
|
|
17
|
+
/** `quiet`: still idle with no frame requested, so a change nobody announced is a bug. */
|
|
18
|
+
constructor(world, log, quiet) {
|
|
19
|
+
this.world = world;
|
|
20
|
+
this.log = log;
|
|
21
|
+
this.quiet = quiet;
|
|
22
|
+
}
|
|
23
|
+
/** The app went idle: remember what the resources hold, and check them every second. */
|
|
24
|
+
start() {
|
|
25
|
+
this.snapshots.clear();
|
|
26
|
+
for (const def of allResources()) {
|
|
27
|
+
if (!def.hostWritable) continue;
|
|
28
|
+
const hash = hashOf(this.world.tryResource(def));
|
|
29
|
+
if (hash !== void 0) this.snapshots.set(def, hash);
|
|
30
|
+
}
|
|
31
|
+
this.timer ??= setInterval(() => this.verify(), CHECK_MS);
|
|
32
|
+
}
|
|
33
|
+
/** A frame is running (or the loop stopped): writes are fine again. */
|
|
34
|
+
stop() {
|
|
35
|
+
if (this.timer !== void 0) clearInterval(this.timer);
|
|
36
|
+
this.timer = void 0;
|
|
37
|
+
}
|
|
38
|
+
verify() {
|
|
39
|
+
if (!this.quiet()) return;
|
|
40
|
+
for (const [def, before] of this.snapshots) {
|
|
41
|
+
const now = hashOf(this.world.tryResource(def)) ?? "";
|
|
42
|
+
if (now === before) continue;
|
|
43
|
+
this.snapshots.set(def, now);
|
|
44
|
+
this.log?.log("warn", `Resource "${def.name}" changed without waking the app`, {
|
|
45
|
+
code: "runtime/unmarked-resource-write",
|
|
46
|
+
path: def.name,
|
|
47
|
+
hint: "Outside a frame, write resources with world.patchResource or world.touchResource (or call app.requestFrame())."
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
ResourceWriteCheck
|
|
54
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aethervtt/shard-runtime",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./testing": {
|
|
13
|
+
"types": "./dist/testing.d.ts",
|
|
14
|
+
"default": "./dist/testing.js"
|
|
11
15
|
}
|
|
12
16
|
},
|
|
13
17
|
"files": [
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
"README.md"
|
|
17
21
|
],
|
|
18
22
|
"dependencies": {
|
|
19
|
-
"@aethervtt/shard-core": "0.0
|
|
23
|
+
"@aethervtt/shard-core": "0.1.0"
|
|
20
24
|
},
|
|
21
25
|
"repository": {
|
|
22
26
|
"type": "git",
|
|
@@ -28,6 +32,6 @@
|
|
|
28
32
|
"provenance": true
|
|
29
33
|
},
|
|
30
34
|
"shard": {
|
|
31
|
-
"buildSha": "
|
|
35
|
+
"buildSha": "8e14c9896f82a3554b156f1ffdd68f2cc3e7d78a"
|
|
32
36
|
}
|
|
33
37
|
}
|