@vestra-player/sdk 1.6.17 → 1.6.19
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 +24 -0
- package/dist/docs-tree.json.gz +0 -0
- package/dist/plugin/appearance.js +1 -1
- package/dist/plugin/bindings.js +1 -1
- package/dist/plugin/commands.js +3 -2
- package/dist/plugin/connection.d.ts +119 -0
- package/dist/plugin/connection.js +402 -0
- package/dist/plugin/drops.d.ts +3 -3
- package/dist/plugin/drops.js +1 -1
- package/dist/plugin/errors.d.ts +9 -0
- package/dist/plugin/errors.js +35 -0
- package/dist/plugin/filters.js +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.js +3 -1
- package/dist/plugin/language.js +1 -1
- package/dist/plugin/latest.d.ts +12 -0
- package/dist/plugin/latest.js +52 -0
- package/dist/plugin/local.js +1 -2
- package/dist/plugin/markdown.d.ts +2 -2
- package/dist/plugin/markdown.js +1 -1
- package/dist/plugin/media.js +1 -1
- package/dist/plugin/osd.js +2 -2
- package/dist/plugin/packages.js +1 -1
- package/dist/plugin/passes.d.ts +111 -0
- package/dist/plugin/passes.js +341 -0
- package/dist/plugin/player.d.ts +2 -3
- package/dist/plugin/player.js +2 -7
- package/dist/plugin/resolvers.js +1 -1
- package/dist/plugin/runtime.d.ts +26 -185
- package/dist/plugin/runtime.js +35 -755
- package/dist/plugin/scope.d.ts +2 -1
- package/dist/plugin/scope.js +5 -2
- package/dist/plugin/services/dialogs.d.ts +3 -0
- package/dist/plugin/services/dialogs.js +62 -0
- package/dist/plugin/services.js +1 -1
- package/dist/plugin/settings.js +2 -2
- package/dist/plugin/shaders.js +2 -1
- package/dist/plugin/stream.js +1 -2
- package/dist/plugin/subtitles.js +2 -2
- package/dist/plugin/testing/commands.d.ts +65 -0
- package/dist/plugin/testing/commands.js +412 -0
- package/dist/plugin/testing/core-actions.d.ts +39 -0
- package/dist/plugin/testing/core-actions.js +371 -0
- package/dist/plugin/testing/declarations.d.ts +37 -0
- package/dist/plugin/testing/declarations.js +80 -0
- package/dist/plugin/testing/i18n.d.ts +28 -0
- package/dist/plugin/testing/i18n.js +107 -0
- package/dist/plugin/testing/index.d.ts +3140 -0
- package/dist/plugin/testing/index.js +2796 -0
- package/dist/plugin/testing/internals.d.ts +76 -0
- package/dist/plugin/testing/internals.js +60 -0
- package/dist/plugin/testing/jobs.d.ts +9 -0
- package/dist/plugin/testing/jobs.js +304 -0
- package/dist/plugin/testing/layout.d.ts +85 -0
- package/dist/plugin/testing/layout.js +622 -0
- package/dist/plugin/testing/media.d.ts +55 -0
- package/dist/plugin/testing/media.js +615 -0
- package/dist/plugin/testing/osd.d.ts +4 -0
- package/dist/plugin/testing/osd.js +57 -0
- package/dist/plugin/testing/processing.d.ts +56 -0
- package/dist/plugin/testing/processing.js +491 -0
- package/dist/plugin/testing/services.d.ts +50 -0
- package/dist/plugin/testing/services.js +466 -0
- package/dist/plugin/testing/transport.d.ts +29 -0
- package/dist/plugin/testing/transport.js +140 -0
- package/dist/plugin/testing.d.ts +1 -3140
- package/dist/plugin/testing.js +1 -6203
- package/dist/plugin/timeline.js +2 -1
- package/dist/plugin/view/base.d.ts +29 -0
- package/dist/plugin/view/base.js +88 -0
- package/dist/plugin/view/binder.d.ts +22 -0
- package/dist/plugin/view/binder.js +57 -0
- package/dist/plugin/view/image.d.ts +21 -0
- package/dist/plugin/view/image.js +60 -0
- package/dist/plugin/view/layout.d.ts +47 -0
- package/dist/plugin/view/layout.js +171 -0
- package/dist/plugin/view/panel.d.ts +3 -0
- package/dist/plugin/view/panel.js +178 -0
- package/dist/plugin/view/window.d.ts +3 -0
- package/dist/plugin/view/window.js +99 -0
- package/dist/plugin/view/writer.d.ts +58 -0
- package/dist/plugin/view/writer.js +634 -0
- package/dist/plugin/view.js +2 -1
- package/dist/plugin/window.js +1 -1
- package/llms.txt +1 -1
- package/package.json +6 -4
- package/dist/plugin/ui.d.ts +0 -74
- package/dist/plugin/ui.js +0 -1234
package/dist/plugin/runtime.js
CHANGED
|
@@ -1,780 +1,60 @@
|
|
|
1
|
-
/** Shared machinery behind every scope: invocation, update passes, events. */
|
|
2
|
-
import { createWire, HostError } from "../wire.js";
|
|
3
|
-
import { LOCALES } from "../i18n.js";
|
|
4
|
-
import { basename, isAbsolute, relative } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
/** The SDK's own files (`src/` or `dist/`): left out of a call site. */
|
|
7
|
-
const SDK_DIRECTORY = fileURLToPath(new URL("..", import.meta.url));
|
|
8
1
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
2
|
+
* Shared machinery behind every scope: the connection to the player
|
|
3
|
+
* (`connection.ts`, L1) with the update passes (`passes.ts`, L2) on top.
|
|
4
|
+
* `Runtime` is the connection plus the pass methods, delegated, so every
|
|
5
|
+
* caller keeps `runtime.host`, `runtime.on`, `runtime.bind` and the rest.
|
|
12
6
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"resolvers.fileTypes",
|
|
22
|
-
];
|
|
23
|
-
/**
|
|
24
|
-
* The state each event keeps current, and whether `plugin.when` can say
|
|
25
|
-
* the same without the plugin running.
|
|
26
|
-
*/
|
|
27
|
-
const STATE = {
|
|
28
|
-
"source.state": { name: "plugin.player.state", when: true },
|
|
29
|
-
"window.state": { name: "plugin.window.state", when: true },
|
|
30
|
-
"appearance.changed": { name: "plugin.appearance", when: true },
|
|
31
|
-
"timeline.changed": { name: "plugin.timeline.marks", when: false },
|
|
32
|
-
"commands.keys": { name: "plugin.commands.keys", when: false },
|
|
33
|
-
"playback.backend": { name: "plugin.subtitles.state", when: false },
|
|
34
|
-
"resolvers.fileTypes": {
|
|
35
|
-
name: "plugin.resolvers.fileTypes",
|
|
36
|
-
when: false,
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
-
export function toError(error) {
|
|
41
|
-
if (error instanceof Error) {
|
|
42
|
-
const coded = error;
|
|
43
|
-
if (typeof coded.code !== "string")
|
|
44
|
-
Object.defineProperty(coded, "code", {
|
|
45
|
-
value: error.name === "AbortError" ? "aborted" : "error",
|
|
46
|
-
enumerable: false,
|
|
47
|
-
writable: true,
|
|
48
|
-
});
|
|
49
|
-
return coded;
|
|
50
|
-
}
|
|
51
|
-
return new HostError("error", String(error));
|
|
52
|
-
}
|
|
53
|
-
export const failure = (code, message = code) => new HostError(code, message);
|
|
54
|
-
/** Host replies are definite; anything else (lost reply, client timeout) is not. */
|
|
55
|
-
export const uncertain = (error) => !(error instanceof HostError) || error.code === "timeout";
|
|
56
|
-
/** Mark a promise as observed so an unused `ready` never crashes the runner. */
|
|
57
|
-
export function quiet(promise) {
|
|
58
|
-
promise.catch(() => { });
|
|
59
|
-
return promise;
|
|
60
|
-
}
|
|
61
|
-
/** Whether something that read `deps` may see a different result after `change`. */
|
|
62
|
-
function affected(deps, change) {
|
|
63
|
-
for (const [key, fields] of change) {
|
|
64
|
-
const read = deps.get(key);
|
|
65
|
-
if (!read)
|
|
66
|
-
continue;
|
|
67
|
-
if (read.size === 0)
|
|
68
|
-
return true;
|
|
69
|
-
for (const field of fields)
|
|
70
|
-
if (read.has(field))
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
/** Add `from` to `into` (an empty set, the whole state, wins). */
|
|
76
|
-
function merge(into, from) {
|
|
77
|
-
for (const [key, fields] of from) {
|
|
78
|
-
const set = into.get(key);
|
|
79
|
-
if (!set)
|
|
80
|
-
into.set(key, new Set(fields));
|
|
81
|
-
else if (set.size && fields.size)
|
|
82
|
-
for (const field of fields)
|
|
83
|
-
set.add(field);
|
|
84
|
-
else
|
|
85
|
-
set.clear();
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* The longest a binding waits for a full pass while precise passes skip
|
|
90
|
-
* it: state a plugin changes outside code the SDK runs (its own timer,
|
|
91
|
-
* without `update()`) still shows within about this long.
|
|
92
|
-
*/
|
|
93
|
-
export const SAFETY_PASS_MS = 1000;
|
|
94
|
-
/**
|
|
95
|
-
* The player's answers to a declaration that refuse that one declaration
|
|
96
|
-
* (a command's path, texts or count): during activation they skip it
|
|
97
|
-
* instead of failing the plugin (`Runtime.declaring`, SDK 1.5.49).
|
|
98
|
-
*/
|
|
99
|
-
const DECLARATION_REFUSALS = new Set([
|
|
100
|
-
"invalid",
|
|
101
|
-
"conflict",
|
|
102
|
-
"limit",
|
|
103
|
-
]);
|
|
104
|
-
/** The reasons `StopReason` names; anything else reads as `undefined`. */
|
|
105
|
-
const STOP_REASONS = new Set([
|
|
106
|
-
"quit",
|
|
107
|
-
"disable",
|
|
108
|
-
"uninstall",
|
|
109
|
-
"reload",
|
|
110
|
-
"upgrade",
|
|
111
|
-
"idle",
|
|
112
|
-
"failed",
|
|
113
|
-
"host-lost",
|
|
114
|
-
]);
|
|
115
|
-
/** A stop reason the runner passed on, if this SDK knows it. */
|
|
116
|
-
export function stopReason(value) {
|
|
117
|
-
return typeof value === "string" && STOP_REASONS.has(value)
|
|
118
|
-
? value
|
|
119
|
-
: undefined;
|
|
120
|
-
}
|
|
121
|
-
export const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
122
|
-
/** One value in flight, only the newest waiting. Each push learns its fate. */
|
|
123
|
-
export class Latest {
|
|
124
|
-
#queued = [];
|
|
125
|
-
#running = false;
|
|
126
|
-
#stopped = false;
|
|
127
|
-
#send;
|
|
128
|
-
constructor(send) {
|
|
129
|
-
this.#send = send;
|
|
130
|
-
}
|
|
131
|
-
push(value) {
|
|
132
|
-
if (this.#stopped)
|
|
133
|
-
return Promise.resolve(false);
|
|
134
|
-
return new Promise((resolve, reject) => {
|
|
135
|
-
for (const older of this.#queued.splice(0))
|
|
136
|
-
older.settle(false);
|
|
137
|
-
this.#queued.push({
|
|
138
|
-
value,
|
|
139
|
-
settle: (ok, error) => error === undefined ? resolve(ok) : reject(error),
|
|
140
|
-
});
|
|
141
|
-
if (!this.#running)
|
|
142
|
-
void this.#drain();
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
stop() {
|
|
146
|
-
this.#stopped = true;
|
|
147
|
-
for (const older of this.#queued.splice(0))
|
|
148
|
-
older.settle(false);
|
|
149
|
-
}
|
|
150
|
-
async #drain() {
|
|
151
|
-
this.#running = true;
|
|
152
|
-
try {
|
|
153
|
-
let next;
|
|
154
|
-
while ((next = this.#queued.shift()) && !this.#stopped) {
|
|
155
|
-
try {
|
|
156
|
-
await this.#send(next.value);
|
|
157
|
-
next.settle(true);
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
next.settle(false, error);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
finally {
|
|
165
|
-
this.#running = false;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Per-plugin services. Scopes are thin views over this object; the flat RPC
|
|
171
|
-
* clients stay private here so protocol fields never reach plugin code.
|
|
172
|
-
*/
|
|
173
|
-
export class Runtime {
|
|
174
|
-
host;
|
|
175
|
-
manifest;
|
|
176
|
-
directory;
|
|
177
|
-
/** Writable per-plugin data directory from the host, if it provided one. */
|
|
178
|
-
dataDirectory;
|
|
179
|
-
/** Test doubles for local processes; the real system when absent. */
|
|
180
|
-
local;
|
|
181
|
-
/** Registrations that activation must wait for before reporting ready. */
|
|
182
|
-
startup = new Set();
|
|
183
|
-
#activating = true;
|
|
184
|
-
/** `RuntimeOptions.strictDeclarations`. */
|
|
185
|
-
#strict;
|
|
186
|
-
/** Declarations skipped during activation: reported once it completes. */
|
|
187
|
-
#refusals = [];
|
|
188
|
-
#transport;
|
|
189
|
-
/** Each binding with the events whose state it read last time (held). */
|
|
190
|
-
#bindings = new Map();
|
|
191
|
-
/** The events read by the binding being evaluated, with who read them. */
|
|
192
|
-
#reads;
|
|
193
|
-
/** Who reads now (a command's field and where it was added). */
|
|
194
|
-
#reader;
|
|
195
|
-
#scheduled = false;
|
|
196
|
-
/** The next pass evaluates every binding (not only a state copy changed). */
|
|
197
|
-
#full = false;
|
|
198
|
-
/** State copy changes since the last pass (a precise pass, unless `#full`). */
|
|
199
|
-
#pending = new Map();
|
|
200
|
-
/** The change the running precise pass is for; `undefined` in a full pass. */
|
|
201
|
-
#pass;
|
|
202
|
-
/** The state copy fields read by what is being evaluated now. */
|
|
203
|
-
#deps;
|
|
204
|
-
/** Each part's reads and fields last time (`part`). */
|
|
205
|
-
#parts = new WeakMap();
|
|
206
|
-
/** A precise pass skipped something: a full pass follows (`SAFETY_PASS_MS`). */
|
|
207
|
-
#safety;
|
|
208
|
-
/** The running pass skipped a binding or a part. */
|
|
209
|
-
#skipped = false;
|
|
210
|
-
/** The plugin was disposed: no safety pass is armed any more. */
|
|
211
|
-
#stopped = false;
|
|
212
|
-
#events = new Map();
|
|
213
|
-
/** The missing `events` warning was given (once per plugin). */
|
|
214
|
-
#warnedEvents = false;
|
|
215
|
-
views = new Map();
|
|
216
|
-
commands = new Map();
|
|
217
|
-
services = new Map();
|
|
218
|
-
/**
|
|
219
|
-
* Set when the player stops the plugin (`PluginInstance.dispose`), before
|
|
220
|
-
* the root scope is disposed: `DisposeInfo` and `plugin.stopReason`.
|
|
221
|
-
*/
|
|
222
|
-
stopping;
|
|
223
|
-
/** `plugin.app`, from the start-up handshake; `system()` asks the player. */
|
|
224
|
-
app;
|
|
225
|
-
/** `plugin.i18n.locale`, from the start-up handshake; English when unknown. */
|
|
226
|
-
locale;
|
|
227
|
-
/**
|
|
228
|
-
* Protocol additions the player advertises at start-up (`features` in the
|
|
229
|
-
* handshake's app data, player 0.6.0): `ui.patch` (row patches of
|
|
230
|
-
* property writes). Not part of the plugin API; empty for older players.
|
|
231
|
-
*/
|
|
232
|
-
features;
|
|
7
|
+
import { Connection } from "./connection.js";
|
|
8
|
+
import { Passes } from "./passes.js";
|
|
9
|
+
/** Per-plugin services: the connection, and the update passes over it. */
|
|
10
|
+
export class Runtime extends Connection {
|
|
11
|
+
/** The update pass scheduler. */
|
|
12
|
+
passes;
|
|
13
|
+
/** Pass counters, read by the SDK's own tests and benchmark ({@link PassCounts}). */
|
|
14
|
+
counts;
|
|
233
15
|
constructor(transport, manifest, directory, dataDirectory, app, options = {}) {
|
|
234
|
-
|
|
235
|
-
this
|
|
236
|
-
this.
|
|
237
|
-
this.local = transport.local;
|
|
238
|
-
this.host = createWire(transport);
|
|
239
|
-
const host = this.host;
|
|
240
|
-
this.app = appInfo(app, () => host.app.system());
|
|
241
|
-
const features = app
|
|
242
|
-
?.features;
|
|
243
|
-
this.features = new Set(Array.isArray(features)
|
|
244
|
-
? features.filter((name) => typeof name === "string")
|
|
245
|
-
: []);
|
|
246
|
-
this.locale = LOCALES.includes(transport.locale ?? "")
|
|
247
|
-
? transport.locale
|
|
248
|
-
: "en";
|
|
249
|
-
this.manifest = manifest;
|
|
250
|
-
this.directory = directory;
|
|
251
|
-
transport.on("ui.callback", (data) => {
|
|
252
|
-
const event = data;
|
|
253
|
-
// A panel's callback names its slot (player 0.7.6: one panel per
|
|
254
|
-
// slot); older players name only `"$mount"`, and the plugin has one.
|
|
255
|
-
const view = event.view === "$mount"
|
|
256
|
-
? event.slot
|
|
257
|
-
? `panel:${event.slot}`
|
|
258
|
-
: [...this.views.keys()].find((key) => key.startsWith("panel:"))
|
|
259
|
-
: event.view;
|
|
260
|
-
if (view)
|
|
261
|
-
this.views.get(view)?.(event.callback, event.args);
|
|
262
|
-
});
|
|
263
|
-
transport.on("commands.execute", (data) => {
|
|
264
|
-
this.commands.get(data.id)?.();
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
can(capability) {
|
|
268
|
-
return this.manifest.capabilities.includes(capability);
|
|
269
|
-
}
|
|
270
|
-
/** Lazily created singletons shared by every scope of this plugin. */
|
|
271
|
-
service(name, create) {
|
|
272
|
-
if (!this.services.has(name))
|
|
273
|
-
this.services.set(name, create());
|
|
274
|
-
return this.services.get(name);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Registrations started while `activate` runs must succeed before the
|
|
278
|
-
* plugin reports ready; later ones report failures to their scope.
|
|
279
|
-
*/
|
|
280
|
-
starting(work, report) {
|
|
281
|
-
if (this.#activating)
|
|
282
|
-
this.startup.add(work);
|
|
283
|
-
else
|
|
284
|
-
work.catch(report);
|
|
285
|
-
work.catch(() => { });
|
|
286
|
-
return work;
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* A declaration the player checks on its own (`what`: "command x"),
|
|
290
|
-
* started like `starting`. In the player's runner a refusal of it during
|
|
291
|
-
* activation (`invalid`, `conflict`, `limit`) does not fail the plugin:
|
|
292
|
-
* it is logged, skipped and reported to the scope once activation
|
|
293
|
-
* completes, so an `onError` added anywhere in `activate` hears it (SDK
|
|
294
|
-
* 1.5.49; VS Code logs a bad contribution and activates the extension).
|
|
295
|
-
* Under `strictDeclarations` (TestHost) it fails activation as before.
|
|
296
|
-
*/
|
|
297
|
-
declaring(work, what, report) {
|
|
298
|
-
if (!this.#activating || this.#strict)
|
|
299
|
-
return this.starting(work, report);
|
|
300
|
-
const settled = work.then(() => { }, (error) => {
|
|
301
|
-
if (!(error instanceof HostError) ||
|
|
302
|
-
!DECLARATION_REFUSALS.has(error.code))
|
|
303
|
-
throw error;
|
|
304
|
-
console.error(`[${this.manifest.id}] ${what} skipped, the player refused it (${error.code}): ${error.message}`);
|
|
305
|
-
this.#refusals.push(() => report(error));
|
|
306
|
-
});
|
|
307
|
-
// Observed now: `activated()` may come after `activate` awaits, and
|
|
308
|
-
// its `allSettled` still sees the rejection.
|
|
309
|
-
settled.catch(() => { });
|
|
310
|
-
this.startup.add(settled);
|
|
311
|
-
work.catch(() => { });
|
|
312
|
-
return work;
|
|
313
|
-
}
|
|
314
|
-
async activated() {
|
|
315
|
-
while (this.startup.size) {
|
|
316
|
-
const pending = [...this.startup];
|
|
317
|
-
this.startup.clear();
|
|
318
|
-
for (const result of await Promise.allSettled(pending))
|
|
319
|
-
if (result.status === "rejected")
|
|
320
|
-
throw result.reason;
|
|
321
|
-
}
|
|
322
|
-
this.#activating = false;
|
|
323
|
-
for (const report of this.#refusals.splice(0))
|
|
324
|
-
report();
|
|
16
|
+
super(transport, manifest, directory, dataDirectory, app, options);
|
|
17
|
+
this.passes = new Passes(this);
|
|
18
|
+
this.counts = this.passes.counts;
|
|
325
19
|
}
|
|
20
|
+
/** {@link Passes.bind} */
|
|
326
21
|
bind(evaluate) {
|
|
327
|
-
|
|
328
|
-
const binding = { held, deps: new Map() };
|
|
329
|
-
this.#bindings.set(evaluate, binding);
|
|
330
|
-
this.invalidate();
|
|
331
|
-
return {
|
|
332
|
-
dispose: async () => {
|
|
333
|
-
if (this.#bindings.get(evaluate) !== binding)
|
|
334
|
-
return;
|
|
335
|
-
this.#bindings.delete(evaluate);
|
|
336
|
-
for (const hold of held.values())
|
|
337
|
-
await hold.dispose();
|
|
338
|
-
held.clear();
|
|
339
|
-
},
|
|
340
|
-
};
|
|
22
|
+
return this.passes.bind(evaluate);
|
|
341
23
|
}
|
|
342
|
-
/**
|
|
343
|
-
* A service's state for `event` is read: a binding evaluated now depends
|
|
344
|
-
* on it, so the event counts as listened to while the binding reads it.
|
|
345
|
-
*/
|
|
24
|
+
/** {@link Passes.reading} */
|
|
346
25
|
reading(event) {
|
|
347
|
-
|
|
348
|
-
if (!reads || reads.has(event))
|
|
349
|
-
return;
|
|
350
|
-
const reader = this.#reader;
|
|
351
|
-
const state = STATE[event];
|
|
352
|
-
reads.set(event, () => {
|
|
353
|
-
const read = state?.name ?? event;
|
|
354
|
-
const site = reader?.where?.();
|
|
355
|
-
const at = site ? ` (${site})` : "";
|
|
356
|
-
if (!reader)
|
|
357
|
-
return `a binding reads ${read}`;
|
|
358
|
-
if (!reader.field)
|
|
359
|
-
return `${reader.label} reads ${read}${at}`;
|
|
360
|
-
const hint = state?.when ? " — use plugin.when" : "";
|
|
361
|
-
return `${reader.label} is a function that reads ${read}${at}${hint}`;
|
|
362
|
-
});
|
|
26
|
+
this.passes.reading(event);
|
|
363
27
|
}
|
|
364
|
-
/**
|
|
365
|
-
* A field of the SDK's copy of a state (`key`) is read; without `field`,
|
|
366
|
-
* the state is taken (`plugin.player.state`). What is being evaluated
|
|
367
|
-
* then depends on it: a precise pass after a change of that field runs
|
|
368
|
-
* it again.
|
|
369
|
-
*/
|
|
28
|
+
/** {@link Passes.depends} */
|
|
370
29
|
depends(key, field) {
|
|
371
|
-
|
|
372
|
-
if (!deps)
|
|
373
|
-
return;
|
|
374
|
-
let fields = deps.get(key);
|
|
375
|
-
if (!fields)
|
|
376
|
-
deps.set(key, (fields = new Set()));
|
|
377
|
-
if (field !== undefined)
|
|
378
|
-
fields.add(field);
|
|
30
|
+
this.passes.depends(key, field);
|
|
379
31
|
}
|
|
380
|
-
/**
|
|
381
|
-
* Evaluate one part of the binding being evaluated (one `view.bind`
|
|
382
|
-
* function), identified by `token`. A precise pass skips it when nothing
|
|
383
|
-
* it read last time changed, and returns `undefined`; what it read then
|
|
384
|
-
* still counts for the binding.
|
|
385
|
-
*/
|
|
32
|
+
/** {@link Passes.part} */
|
|
386
33
|
part(token, read) {
|
|
387
|
-
|
|
388
|
-
const outerReads = this.#reads;
|
|
389
|
-
const outerDeps = this.#deps;
|
|
390
|
-
if (last && this.#pass && !affected(last.deps, this.#pass)) {
|
|
391
|
-
this.#skipped = true;
|
|
392
|
-
if (outerReads)
|
|
393
|
-
for (const [event, why] of last.reads)
|
|
394
|
-
if (!outerReads.has(event))
|
|
395
|
-
outerReads.set(event, why);
|
|
396
|
-
if (outerDeps)
|
|
397
|
-
merge(outerDeps, last.deps);
|
|
398
|
-
return undefined;
|
|
399
|
-
}
|
|
400
|
-
const reads = new Map();
|
|
401
|
-
const deps = new Map();
|
|
402
|
-
this.#reads = outerReads && reads;
|
|
403
|
-
this.#deps = deps;
|
|
404
|
-
try {
|
|
405
|
-
return { value: read() };
|
|
406
|
-
}
|
|
407
|
-
finally {
|
|
408
|
-
this.#reads = outerReads;
|
|
409
|
-
this.#deps = outerDeps;
|
|
410
|
-
this.#parts.set(token, { reads, deps });
|
|
411
|
-
if (outerReads)
|
|
412
|
-
for (const [event, why] of reads)
|
|
413
|
-
if (!outerReads.has(event))
|
|
414
|
-
outerReads.set(event, why);
|
|
415
|
-
if (outerDeps)
|
|
416
|
-
merge(outerDeps, deps);
|
|
417
|
-
}
|
|
34
|
+
return this.passes.part(token, read);
|
|
418
35
|
}
|
|
419
|
-
/**
|
|
420
|
-
* Run plugin code (a state listener) and return the state copy fields it
|
|
421
|
-
* read; they are not counted as a binding's.
|
|
422
|
-
*/
|
|
36
|
+
/** {@link Passes.observe} */
|
|
423
37
|
observe(run) {
|
|
424
|
-
|
|
425
|
-
const outerDeps = this.#deps;
|
|
426
|
-
const deps = new Map();
|
|
427
|
-
this.#reads = undefined;
|
|
428
|
-
this.#deps = deps;
|
|
429
|
-
try {
|
|
430
|
-
return { value: run(), deps };
|
|
431
|
-
}
|
|
432
|
-
finally {
|
|
433
|
-
this.#reads = outerReads;
|
|
434
|
-
this.#deps = outerDeps;
|
|
435
|
-
}
|
|
38
|
+
return this.passes.observe(run);
|
|
436
39
|
}
|
|
437
|
-
/**
|
|
438
|
-
* Whether code that read `deps` while `change` was delivered needs a full
|
|
439
|
-
* pass after it: it read a field that changed, or no field of that state
|
|
440
|
-
* at all (so the SDK cannot tell what it depends on).
|
|
441
|
-
*/
|
|
40
|
+
/** {@link Passes.needsFull} */
|
|
442
41
|
needsFull(deps, change) {
|
|
443
|
-
|
|
444
|
-
const read = deps.get(key);
|
|
445
|
-
if (!read || read.size === 0)
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
return affected(deps, change);
|
|
42
|
+
return this.passes.needsFull(deps, change);
|
|
449
43
|
}
|
|
450
|
-
/**
|
|
451
|
-
* Evaluate `read` as `label`, added at `where`: the state it reads is
|
|
452
|
-
* reported as read by it. `field`: a command's `enabled` or `checked`,
|
|
453
|
-
* which a `plugin.when` condition can replace.
|
|
454
|
-
*/
|
|
44
|
+
/** {@link Passes.reader} */
|
|
455
45
|
reader(label, where, read, field = false) {
|
|
456
|
-
|
|
457
|
-
this.#reader = { label, where, field };
|
|
458
|
-
try {
|
|
459
|
-
return read();
|
|
460
|
-
}
|
|
461
|
-
finally {
|
|
462
|
-
this.#reader = outer;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Where the plugin's code called into the SDK: the first stack frame
|
|
467
|
-
* outside the SDK, as a path relative to the plugin's directory and a
|
|
468
|
-
* line (`main.ts:353`). Captured now, formatted when asked.
|
|
469
|
-
*/
|
|
470
|
-
callSite() {
|
|
471
|
-
const limit = Error.stackTraceLimit;
|
|
472
|
-
Error.stackTraceLimit = 40;
|
|
473
|
-
const error = new Error();
|
|
474
|
-
Error.stackTraceLimit = limit;
|
|
475
|
-
let site = null;
|
|
476
|
-
return () => {
|
|
477
|
-
if (site === null)
|
|
478
|
-
site = this.#site(error.stack ?? "");
|
|
479
|
-
return site;
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
#site(stack) {
|
|
483
|
-
for (const line of stack.split("\n").slice(1)) {
|
|
484
|
-
const match = /\(?((?:file:\/\/)?[^()\s]+?):(\d+):\d+\)?\s*$/.exec(line);
|
|
485
|
-
if (!match)
|
|
486
|
-
continue;
|
|
487
|
-
let file = match[1];
|
|
488
|
-
if (file.startsWith("file:"))
|
|
489
|
-
try {
|
|
490
|
-
file = fileURLToPath(file);
|
|
491
|
-
}
|
|
492
|
-
catch {
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
const sdk = relative(SDK_DIRECTORY, file);
|
|
496
|
-
if (!isAbsolute(file) || !(sdk.startsWith("..") || isAbsolute(sdk)))
|
|
497
|
-
continue;
|
|
498
|
-
const inside = relative(this.directory, file);
|
|
499
|
-
const shown = inside.startsWith("..") || isAbsolute(inside) ? basename(file) : inside;
|
|
500
|
-
return `${shown.replaceAll("\\", "/")}:${match[2]}`;
|
|
501
|
-
}
|
|
502
|
-
return undefined;
|
|
503
|
-
}
|
|
504
|
-
/** Evaluate one binding, holding the events whose state it read. */
|
|
505
|
-
#evaluate(evaluate, binding) {
|
|
506
|
-
const held = binding.held;
|
|
507
|
-
const reads = new Map();
|
|
508
|
-
const deps = new Map();
|
|
509
|
-
const outer = this.#reads;
|
|
510
|
-
const outerDeps = this.#deps;
|
|
511
|
-
const reader = this.#reader;
|
|
512
|
-
this.#reads = reads;
|
|
513
|
-
this.#deps = deps;
|
|
514
|
-
this.#reader = undefined;
|
|
515
|
-
try {
|
|
516
|
-
evaluate();
|
|
517
|
-
}
|
|
518
|
-
finally {
|
|
519
|
-
this.#reads = outer;
|
|
520
|
-
this.#deps = outerDeps;
|
|
521
|
-
this.#reader = reader;
|
|
522
|
-
binding.deps = deps;
|
|
523
|
-
for (const [event, hold] of held)
|
|
524
|
-
if (!reads.has(event)) {
|
|
525
|
-
held.delete(event);
|
|
526
|
-
void hold.dispose();
|
|
527
|
-
}
|
|
528
|
-
for (const [event, why] of reads)
|
|
529
|
-
if (!held.has(event))
|
|
530
|
-
held.set(event, this.hold(event, why));
|
|
531
|
-
}
|
|
46
|
+
return this.passes.reader(label, where, read, field);
|
|
532
47
|
}
|
|
533
|
-
/**
|
|
534
|
-
* A full pass: every binding is evaluated. Requested after plugin code
|
|
535
|
-
* the SDK ran, by `plugin.update()`, and on any change other than a state
|
|
536
|
-
* copy's fields. Coalesced: one pass per microtask turn, however many
|
|
537
|
-
* requests.
|
|
538
|
-
*/
|
|
48
|
+
/** {@link Passes.invalidate} */
|
|
539
49
|
invalidate() {
|
|
540
|
-
this
|
|
541
|
-
this.#schedule();
|
|
50
|
+
this.passes.invalidate();
|
|
542
51
|
}
|
|
543
|
-
/**
|
|
544
|
-
* Fields of the SDK's copy of a state (`key`) changed: a precise pass
|
|
545
|
-
* evaluates only the bindings (and view bind functions) that read one of
|
|
546
|
-
* them, unless a full pass is due anyway.
|
|
547
|
-
*/
|
|
52
|
+
/** {@link Passes.changed} */
|
|
548
53
|
changed(key, fields) {
|
|
549
|
-
|
|
550
|
-
if (!set)
|
|
551
|
-
this.#pending.set(key, (set = new Set()));
|
|
552
|
-
for (const field of fields)
|
|
553
|
-
set.add(field);
|
|
554
|
-
this.#schedule();
|
|
54
|
+
this.passes.changed(key, fields);
|
|
555
55
|
}
|
|
556
|
-
|
|
557
|
-
if (this.#scheduled)
|
|
558
|
-
return;
|
|
559
|
-
this.#scheduled = true;
|
|
560
|
-
queueMicrotask(() => this.#run());
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* One pass. A precise pass that skips anything arms a full pass
|
|
564
|
-
* `SAFETY_PASS_MS` later (unless a full pass comes first), for plugin
|
|
565
|
-
* state changed outside code the SDK ran.
|
|
566
|
-
*/
|
|
567
|
-
#run() {
|
|
568
|
-
this.#scheduled = false;
|
|
569
|
-
const full = this.#full;
|
|
570
|
-
const change = this.#pending;
|
|
571
|
-
this.#full = false;
|
|
572
|
-
this.#pending = new Map();
|
|
573
|
-
if (full) {
|
|
574
|
-
clearTimeout(this.#safety);
|
|
575
|
-
this.#safety = undefined;
|
|
576
|
-
}
|
|
577
|
-
this.#skipped = false;
|
|
578
|
-
for (const [evaluate, binding] of [...this.#bindings]) {
|
|
579
|
-
if (this.#bindings.get(evaluate) !== binding)
|
|
580
|
-
continue;
|
|
581
|
-
if (!full && !affected(binding.deps, change)) {
|
|
582
|
-
this.#skipped = true;
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
this.#pass = full ? undefined : change;
|
|
586
|
-
try {
|
|
587
|
-
this.#evaluate(evaluate, binding);
|
|
588
|
-
}
|
|
589
|
-
catch (error) {
|
|
590
|
-
console.error(`[${this.manifest.id}] binding failed:`, error);
|
|
591
|
-
}
|
|
592
|
-
finally {
|
|
593
|
-
this.#pass = undefined;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
if (this.#skipped && !this.#safety && !this.#stopped) {
|
|
597
|
-
this.#safety = setTimeout(() => {
|
|
598
|
-
this.#safety = undefined;
|
|
599
|
-
this.invalidate();
|
|
600
|
-
}, SAFETY_PASS_MS);
|
|
601
|
-
this.#safety.unref?.();
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
/** The plugin was disposed: cancel the pending safety pass, arm no more. */
|
|
56
|
+
/** {@link Passes.stop} */
|
|
605
57
|
stop() {
|
|
606
|
-
this
|
|
607
|
-
clearTimeout(this.#safety);
|
|
608
|
-
this.#safety = undefined;
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* Host events with one `events.subscribe` per name, however many listeners.
|
|
612
|
-
* Without the `events` capability the listener simply never fires.
|
|
613
|
-
* `mirror`: the SDK's own copy of the state (`plugin.player.state` and
|
|
614
|
-
* the like), not a listener of the plugin's (see {@link EventGroup}).
|
|
615
|
-
*/
|
|
616
|
-
on(event, listener,
|
|
617
|
-
/** The capability the event needs; `null`: none. */
|
|
618
|
-
needs = "events", mirror = false) {
|
|
619
|
-
let entry = this.#events.get(event);
|
|
620
|
-
const created = !entry;
|
|
621
|
-
if (!entry) {
|
|
622
|
-
entry = { listeners: new Set(), active: 0, whys: new Map() };
|
|
623
|
-
this.#events.set(event, entry);
|
|
624
|
-
}
|
|
625
|
-
const group = entry;
|
|
626
|
-
group.listeners.add(listener);
|
|
627
|
-
const token = {};
|
|
628
|
-
if (!mirror) {
|
|
629
|
-
group.active++;
|
|
630
|
-
group.whys.set(token, listenerWhy(this.callSite()));
|
|
631
|
-
}
|
|
632
|
-
if (created && needs === "events" && !this.can(needs))
|
|
633
|
-
group.unheard = true;
|
|
634
|
-
if (!mirror && group.unheard)
|
|
635
|
-
this.#warnUnheard(event);
|
|
636
|
-
if (created && (needs === null || this.can(needs))) {
|
|
637
|
-
group.told = group.active > 0;
|
|
638
|
-
const whys = group.told ? allWhys(group) : undefined;
|
|
639
|
-
group.toldWhy = whys?.join("\n");
|
|
640
|
-
group.off = this.host.events.subscribe(event, (data) => {
|
|
641
|
-
for (const each of [...group.listeners])
|
|
642
|
-
each(data);
|
|
643
|
-
}, !group.told, whys);
|
|
644
|
-
this.starting(group.off, (error) => console.error(`[${this.manifest.id}] subscribe ${event}:`, error));
|
|
645
|
-
}
|
|
646
|
-
else
|
|
647
|
-
this.#tell(event, group);
|
|
648
|
-
let listening = true;
|
|
649
|
-
return {
|
|
650
|
-
dispose: async () => {
|
|
651
|
-
if (!listening)
|
|
652
|
-
return;
|
|
653
|
-
listening = false;
|
|
654
|
-
group.listeners.delete(listener);
|
|
655
|
-
if (!mirror)
|
|
656
|
-
group.active--;
|
|
657
|
-
group.whys.delete(token);
|
|
658
|
-
if (this.#events.get(event) !== group)
|
|
659
|
-
return;
|
|
660
|
-
if (group.listeners.size)
|
|
661
|
-
return this.#tell(event, group);
|
|
662
|
-
this.#events.delete(event);
|
|
663
|
-
const off = await group.off?.catch(() => undefined);
|
|
664
|
-
await off?.().catch(() => { });
|
|
665
|
-
},
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Count `event` as listened to while held: a watcher of a service's
|
|
670
|
-
* state, a binding that reads it. Nothing when nothing subscribed to it
|
|
671
|
-
* (no capability, or the service did not start).
|
|
672
|
-
*/
|
|
673
|
-
hold(event, why) {
|
|
674
|
-
const group = this.#events.get(event);
|
|
675
|
-
if (!group)
|
|
676
|
-
return { dispose: async () => { } };
|
|
677
|
-
if (group.unheard)
|
|
678
|
-
this.#warnUnheard(event);
|
|
679
|
-
group.active++;
|
|
680
|
-
const token = {};
|
|
681
|
-
group.whys.set(token, why ?? listenerWhy(this.callSite()));
|
|
682
|
-
this.#tell(event, group);
|
|
683
|
-
let held = true;
|
|
684
|
-
return {
|
|
685
|
-
dispose: async () => {
|
|
686
|
-
if (!held)
|
|
687
|
-
return;
|
|
688
|
-
held = false;
|
|
689
|
-
group.active--;
|
|
690
|
-
group.whys.delete(token);
|
|
691
|
-
if (this.#events.get(event) === group)
|
|
692
|
-
this.#tell(event, group);
|
|
693
|
-
},
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* A development warning, once per plugin, no behaviour change (SDK
|
|
698
|
-
* 1.6.12): the plugin follows `event` (an `onChange`, a `bind` that reads
|
|
699
|
-
* the state) without the `events` capability, so changes made elsewhere
|
|
700
|
-
* (the user, the player, another plugin) never reach it; the state moves
|
|
701
|
-
* only after its own calls.
|
|
702
|
-
*/
|
|
703
|
-
#warnUnheard(event) {
|
|
704
|
-
if (this.#warnedEvents)
|
|
705
|
-
return;
|
|
706
|
-
this.#warnedEvents = true;
|
|
707
|
-
console.warn(`[${this.manifest.id}] follows ${event} (onChange, or a bind() that reads the state) without the "events" capability: changes made elsewhere will not reach it, only its own calls refresh the state. Declare "events" in manifest.json.`);
|
|
58
|
+
this.passes.stop();
|
|
708
59
|
}
|
|
709
|
-
/**
|
|
710
|
-
* Tell the player when `group` changes between mirror and listener, and
|
|
711
|
-
* why it listens when that changed.
|
|
712
|
-
*/
|
|
713
|
-
#tell(event, group) {
|
|
714
|
-
const off = group.off;
|
|
715
|
-
if (!off)
|
|
716
|
-
return;
|
|
717
|
-
const active = group.active > 0;
|
|
718
|
-
const whys = active ? allWhys(group) : undefined;
|
|
719
|
-
const why = whys?.join("\n");
|
|
720
|
-
if (group.told === active && group.toldWhy === why)
|
|
721
|
-
return;
|
|
722
|
-
group.told = active;
|
|
723
|
-
group.toldWhy = why;
|
|
724
|
-
// Part of activation while it runs: the player classifies the plugin
|
|
725
|
-
// at its ready, which must see this mark. A failure is not one of
|
|
726
|
-
// activation (an older player never refuses it).
|
|
727
|
-
const told = off
|
|
728
|
-
.then(() => this.host.events.listen(event, active, whys))
|
|
729
|
-
.catch(() => { });
|
|
730
|
-
this.starting(told, () => { });
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* Why the plugin listens to each event it really listens to (mirrors
|
|
734
|
-
* left out), as the player's log says it: for {@link TestHost} and
|
|
735
|
-
* `vestra-plugin test`.
|
|
736
|
-
*/
|
|
737
|
-
listening() {
|
|
738
|
-
const result = new Map();
|
|
739
|
-
for (const [event, group] of this.#events)
|
|
740
|
-
if (group.active > 0)
|
|
741
|
-
result.set(event, allWhys(group));
|
|
742
|
-
return result;
|
|
743
|
-
}
|
|
744
|
-
/** Raw transport events the host pushes without a subscription. */
|
|
745
|
-
raw(event, listener) {
|
|
746
|
-
const off = this.#transport.on(event, listener);
|
|
747
|
-
return { dispose: async () => off() };
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
/** A plugin listener's reason: where it was added. */
|
|
751
|
-
const listenerWhy = (site) => () => {
|
|
752
|
-
const at = site();
|
|
753
|
-
return at ? `a listener added at ${at}` : "a listener";
|
|
754
|
-
};
|
|
755
|
-
/** Every active holder's reason in `group`, in the order they came, once each. */
|
|
756
|
-
function allWhys(group) {
|
|
757
|
-
return [...new Set([...group.whys.values()].map((why) => why()))];
|
|
758
|
-
}
|
|
759
|
-
/** `plugin.app` from the start-up handshake (the runner's environment). */
|
|
760
|
-
export function appInfo(raw, system = () => Promise.reject(failure("unavailable", "no player to ask"))) {
|
|
761
|
-
const info = (raw ?? {});
|
|
762
|
-
const parts = (info.components ?? {});
|
|
763
|
-
const text = (value) => (typeof value === "string" ? value : null);
|
|
764
|
-
const libav = {};
|
|
765
|
-
for (const [name, version] of Object.entries(parts.libav ?? {}))
|
|
766
|
-
if (typeof version === "string")
|
|
767
|
-
libav[name] = version;
|
|
768
|
-
const app = {
|
|
769
|
-
version: text(info.version) ?? "0.0.0",
|
|
770
|
-
build: text(info.build),
|
|
771
|
-
components: Object.freeze({
|
|
772
|
-
mpv: text(parts.mpv),
|
|
773
|
-
libav: Object.freeze(libav),
|
|
774
|
-
slint: text(parts.slint) ?? "",
|
|
775
|
-
}),
|
|
776
|
-
};
|
|
777
|
-
// A method, not data: kept out of comparisons and JSON of the facts.
|
|
778
|
-
Object.defineProperty(app, "system", { value: system, enumerable: false });
|
|
779
|
-
return Object.freeze(app);
|
|
780
60
|
}
|