@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error helpers shared by every part of the SDK (L1, refactor plan §2.2):
|
|
3
|
+
* no state, no update passes. They lived in `runtime.ts` (`abortError` in
|
|
4
|
+
* `player.ts`), so a module that needed one imported the pass scheduler
|
|
5
|
+
* with it, and `stream.ts` and `player.ts` imported each other.
|
|
6
|
+
*/
|
|
7
|
+
import { HostError } from "../wire.js";
|
|
8
|
+
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
export function toError(error) {
|
|
10
|
+
if (error instanceof Error) {
|
|
11
|
+
const coded = error;
|
|
12
|
+
if (typeof coded.code !== "string")
|
|
13
|
+
Object.defineProperty(coded, "code", {
|
|
14
|
+
value: error.name === "AbortError" ? "aborted" : "error",
|
|
15
|
+
enumerable: false,
|
|
16
|
+
writable: true,
|
|
17
|
+
});
|
|
18
|
+
return coded;
|
|
19
|
+
}
|
|
20
|
+
return new HostError("error", String(error));
|
|
21
|
+
}
|
|
22
|
+
export const failure = (code, message = code) => new HostError(code, message);
|
|
23
|
+
/** Host replies are definite; anything else (lost reply, client timeout) is not. */
|
|
24
|
+
export const uncertain = (error) => !(error instanceof HostError) || error.code === "timeout";
|
|
25
|
+
/** Mark a promise as observed so an unused `ready` never crashes the runner. */
|
|
26
|
+
export function quiet(promise) {
|
|
27
|
+
promise.catch(() => { });
|
|
28
|
+
return promise;
|
|
29
|
+
}
|
|
30
|
+
export function abortError() {
|
|
31
|
+
const error = new Error("aborted");
|
|
32
|
+
error.name = "AbortError";
|
|
33
|
+
error.code = "aborted";
|
|
34
|
+
return error;
|
|
35
|
+
}
|
package/dist/plugin/filters.js
CHANGED
package/dist/plugin/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @internal
|
|
7
7
|
*/
|
|
8
8
|
import type { Transport } from "../wire.ts";
|
|
9
|
-
import type { PluginManifest, RuntimeOptions } from "./
|
|
9
|
+
import type { PluginManifest, RuntimeOptions } from "./connection.ts";
|
|
10
10
|
import type { Plugin } from "./types.ts";
|
|
11
11
|
export type * from "./types.ts";
|
|
12
12
|
export type { PluginManifest, RuntimeOptions };
|
package/dist/plugin/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Runtime } from "./runtime.js";
|
|
2
|
+
import { stopReason } from "./connection.js";
|
|
3
|
+
import { failure } from "./errors.js";
|
|
2
4
|
import { ScopeImpl } from "./scope.js";
|
|
3
5
|
import { playerOf } from "./player.js";
|
|
4
6
|
import { windowOf } from "./window.js";
|
package/dist/plugin/language.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** `plugin.i18n`: the interface language of this run and text values (read-only). */
|
|
2
2
|
import { Catalog, isTextValue, LIST_STYLES, loadPackageCatalog, renderText, textKey, textProblem, } from "../i18n.js";
|
|
3
|
-
import { failure } from "./
|
|
3
|
+
import { failure } from "./errors.js";
|
|
4
4
|
/** Copy arguments (arrays and objects only; texts are already frozen). */
|
|
5
5
|
const copyArgs = (args) => Array.isArray(args) ? Object.freeze([...args]) : Object.freeze({ ...args });
|
|
6
6
|
/** `plugin.i18n.text`: a plain, frozen text value (no translation here). */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single-flight write queue (L1, refactor plan §2.2): a view's
|
|
3
|
+
* property writes, a command's state, an OSD panel. It knows nothing of
|
|
4
|
+
* update passes (it lived in `runtime.ts`, beside the pass scheduler).
|
|
5
|
+
*/
|
|
6
|
+
/** One value in flight, only the newest waiting. Each push learns its fate. */
|
|
7
|
+
export declare class Latest<T> {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(send: (value: T) => Promise<void>);
|
|
10
|
+
push(value: T): Promise<boolean>;
|
|
11
|
+
stop(): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single-flight write queue (L1, refactor plan §2.2): a view's
|
|
3
|
+
* property writes, a command's state, an OSD panel. It knows nothing of
|
|
4
|
+
* update passes (it lived in `runtime.ts`, beside the pass scheduler).
|
|
5
|
+
*/
|
|
6
|
+
/** One value in flight, only the newest waiting. Each push learns its fate. */
|
|
7
|
+
export class Latest {
|
|
8
|
+
#queued = [];
|
|
9
|
+
#running = false;
|
|
10
|
+
#stopped = false;
|
|
11
|
+
#send;
|
|
12
|
+
constructor(send) {
|
|
13
|
+
this.#send = send;
|
|
14
|
+
}
|
|
15
|
+
push(value) {
|
|
16
|
+
if (this.#stopped)
|
|
17
|
+
return Promise.resolve(false);
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
for (const older of this.#queued.splice(0))
|
|
20
|
+
older.settle(false);
|
|
21
|
+
this.#queued.push({
|
|
22
|
+
value,
|
|
23
|
+
settle: (ok, error) => error === undefined ? resolve(ok) : reject(error),
|
|
24
|
+
});
|
|
25
|
+
if (!this.#running)
|
|
26
|
+
void this.#drain();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
stop() {
|
|
30
|
+
this.#stopped = true;
|
|
31
|
+
for (const older of this.#queued.splice(0))
|
|
32
|
+
older.settle(false);
|
|
33
|
+
}
|
|
34
|
+
async #drain() {
|
|
35
|
+
this.#running = true;
|
|
36
|
+
try {
|
|
37
|
+
let next;
|
|
38
|
+
while ((next = this.#queued.shift()) && !this.#stopped) {
|
|
39
|
+
try {
|
|
40
|
+
await this.#send(next.value);
|
|
41
|
+
next.settle(true);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
next.settle(false, error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
this.#running = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/plugin/local.js
CHANGED
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { access, constants, stat } from "node:fs/promises";
|
|
11
11
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
12
|
-
import { failure } from "./
|
|
13
|
-
import { abortError } from "./player.js";
|
|
12
|
+
import { abortError, failure } from "./errors.js";
|
|
14
13
|
/** SIGTERM first, SIGKILL if the process is still there after this long. */
|
|
15
14
|
const KILL_AFTER_MS = 300;
|
|
16
15
|
const lastLine = (text) => text
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Json } from "../wire.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Connection } from "./connection.ts";
|
|
3
3
|
import type { ScopeImpl } from "./scope.ts";
|
|
4
4
|
import type { MarkdownOptions, MarkdownRef } from "./types.ts";
|
|
5
5
|
export declare const MARKDOWN_LIMITS: Readonly<{
|
|
@@ -43,5 +43,5 @@ export declare function httpsDownload(url: string, options: {
|
|
|
43
43
|
* `<dataDirectory>/.vestra-cache/images/` (the Markdown limits: 4 MB, 10 s,
|
|
44
44
|
* 4 at a time per plugin, 32 MB of cache, a day). Rejects `unavailable`.
|
|
45
45
|
*/
|
|
46
|
-
export declare function remoteImageFile(runtime:
|
|
46
|
+
export declare function remoteImageFile(runtime: Connection, url: string): Promise<string>;
|
|
47
47
|
export declare function markdown(scope: ScopeImpl, text: string, options?: MarkdownOptions): Promise<MarkdownRef>;
|
package/dist/plugin/markdown.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { mkdir, readdir, rename, stat, unlink, utimes, writeFile, } from "node:fs/promises";
|
|
9
9
|
import { isAbsolute, join } from "node:path";
|
|
10
|
-
import { failure } from "./
|
|
10
|
+
import { failure } from "./errors.js";
|
|
11
11
|
export const MARKDOWN_LIMITS = Object.freeze({
|
|
12
12
|
/** Text bytes (UTF-8). */
|
|
13
13
|
textBytes: 256 * 1024,
|
package/dist/plugin/media.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { failure, sleep, uncertain } from "./
|
|
1
|
+
import { failure, sleep, uncertain } from "./errors.js";
|
|
2
2
|
import { playerOf } from "./player.js";
|
|
3
3
|
const TERMINAL = ["completed", "failed", "cancelled"];
|
|
4
4
|
/** The player's sets for each {@link ExportEncoding} field. */
|
package/dist/plugin/osd.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** On-screen display: a transient line and a persistent information block. */
|
|
2
|
-
import { Latest } from "./
|
|
3
|
-
import { failure } from "./
|
|
2
|
+
import { Latest } from "./latest.js";
|
|
3
|
+
import { failure } from "./errors.js";
|
|
4
4
|
import { isTextValue, textProblem } from "../i18n.js";
|
|
5
5
|
/**
|
|
6
6
|
* The player's limits on the on-screen display ({@link Osd}), counted on
|
package/dist/plugin/packages.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Package validation, install and uninstall (`plugins.manage`). */
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { failure, sleep, uncertain } from "./
|
|
4
|
+
import { failure, sleep, uncertain } from "./errors.js";
|
|
5
5
|
const TERMINAL = [
|
|
6
6
|
"committed",
|
|
7
7
|
"cancelled",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The update passes (L2, refactor plan §2.2): bindings, the state copy
|
|
3
|
+
* fields they read (`reading`, `depends`, `part`, `observe`, `reader`),
|
|
4
|
+
* full and precise passes (`invalidate`, `changed`) and the safety pass.
|
|
5
|
+
* It uses the connection (`hold`) and nothing it builds on imports it
|
|
6
|
+
* (`scripts/check-layers.mjs`).
|
|
7
|
+
*/
|
|
8
|
+
import type { Connection } from "./connection.ts";
|
|
9
|
+
import type { Disposable } from "./types.ts";
|
|
10
|
+
/**
|
|
11
|
+
* The fields of the SDK's state copies that a binding or listener read, by
|
|
12
|
+
* state (`player`: `plugin.player.state`). An empty set: the state was
|
|
13
|
+
* taken whole (no field read was seen), so any change of it counts.
|
|
14
|
+
*/
|
|
15
|
+
export type Deps = Map<string, Set<string>>;
|
|
16
|
+
/** A change of the SDK's state copies: the fields that changed, by state. */
|
|
17
|
+
export type Change = ReadonlyMap<string, ReadonlySet<string>>;
|
|
18
|
+
/**
|
|
19
|
+
* The longest a binding waits for a full pass while precise passes skip
|
|
20
|
+
* it: state a plugin changes outside code the SDK runs (its own timer,
|
|
21
|
+
* without `update()`) still shows within about this long.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SAFETY_PASS_MS = 1000;
|
|
24
|
+
/**
|
|
25
|
+
* Read-only counters of the update passes, for the SDK's own
|
|
26
|
+
* characterisation tests and benchmark (`test/golden-traces.test.ts`,
|
|
27
|
+
* `scripts/bench.mjs`): how many passes ran, how many were full, how many
|
|
28
|
+
* bindings and view bind functions (parts) were evaluated or skipped. Not
|
|
29
|
+
* part of the plugin API; counting changes no behaviour.
|
|
30
|
+
*/
|
|
31
|
+
export interface PassCounts {
|
|
32
|
+
/** Passes run (`Passes.#run`), precise and full. */
|
|
33
|
+
passes: number;
|
|
34
|
+
/** Of them, full passes (every binding evaluated). */
|
|
35
|
+
fullPasses: number;
|
|
36
|
+
/** Bindings evaluated (`Passes.#evaluate`). */
|
|
37
|
+
evaluations: number;
|
|
38
|
+
/** View bind functions evaluated inside a binding (`Passes.part`). */
|
|
39
|
+
parts: number;
|
|
40
|
+
/** View bind functions a precise pass skipped (`Passes.part`). */
|
|
41
|
+
skippedParts: number;
|
|
42
|
+
/**
|
|
43
|
+
* Full passes the safety timer requested (`SAFETY_PASS_MS` after a
|
|
44
|
+
* precise pass skipped something), counted when the timer fires.
|
|
45
|
+
*/
|
|
46
|
+
safetyPasses: number;
|
|
47
|
+
}
|
|
48
|
+
/** The update pass scheduler of one plugin (`Runtime` delegates to it). */
|
|
49
|
+
export declare class Passes {
|
|
50
|
+
#private;
|
|
51
|
+
/** Pass counters, read by the SDK's own tests and benchmark ({@link PassCounts}). */
|
|
52
|
+
readonly counts: PassCounts;
|
|
53
|
+
constructor(connection: Connection);
|
|
54
|
+
bind(evaluate: () => void): Disposable;
|
|
55
|
+
/**
|
|
56
|
+
* A service's state for `event` is read: a binding evaluated now depends
|
|
57
|
+
* on it, so the event counts as listened to while the binding reads it.
|
|
58
|
+
*/
|
|
59
|
+
reading(event: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* A field of the SDK's copy of a state (`key`) is read; without `field`,
|
|
62
|
+
* the state is taken (`plugin.player.state`). What is being evaluated
|
|
63
|
+
* then depends on it: a precise pass after a change of that field runs
|
|
64
|
+
* it again.
|
|
65
|
+
*/
|
|
66
|
+
depends(key: string, field?: string): void;
|
|
67
|
+
/**
|
|
68
|
+
* Evaluate one part of the binding being evaluated (one `view.bind`
|
|
69
|
+
* function), identified by `token`. A precise pass skips it when nothing
|
|
70
|
+
* it read last time changed, and returns `undefined`; what it read then
|
|
71
|
+
* still counts for the binding.
|
|
72
|
+
*/
|
|
73
|
+
part<T>(token: object, read: () => T): {
|
|
74
|
+
value: T;
|
|
75
|
+
} | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Run plugin code (a state listener) and return the state copy fields it
|
|
78
|
+
* read; they are not counted as a binding's.
|
|
79
|
+
*/
|
|
80
|
+
observe<T>(run: () => T): {
|
|
81
|
+
value: T;
|
|
82
|
+
deps: Deps;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Whether code that read `deps` while `change` was delivered needs a full
|
|
86
|
+
* pass after it: it read a field that changed, or no field of that state
|
|
87
|
+
* at all (so the SDK cannot tell what it depends on).
|
|
88
|
+
*/
|
|
89
|
+
needsFull(deps: Deps, change: Change): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Evaluate `read` as `label`, added at `where`: the state it reads is
|
|
92
|
+
* reported as read by it. `field`: a command's `enabled` or `checked`,
|
|
93
|
+
* which a `plugin.when` condition can replace.
|
|
94
|
+
*/
|
|
95
|
+
reader<T>(label: string, where: (() => string | undefined) | undefined, read: () => T, field?: boolean): T;
|
|
96
|
+
/**
|
|
97
|
+
* A full pass: every binding is evaluated. Requested after plugin code
|
|
98
|
+
* the SDK ran, by `plugin.update()`, and on any change other than a state
|
|
99
|
+
* copy's fields. Coalesced: one pass per microtask turn, however many
|
|
100
|
+
* requests.
|
|
101
|
+
*/
|
|
102
|
+
invalidate(): void;
|
|
103
|
+
/**
|
|
104
|
+
* Fields of the SDK's copy of a state (`key`) changed: a precise pass
|
|
105
|
+
* evaluates only the bindings (and view bind functions) that read one of
|
|
106
|
+
* them, unless a full pass is due anyway.
|
|
107
|
+
*/
|
|
108
|
+
changed(key: string, fields: Iterable<string>): void;
|
|
109
|
+
/** The plugin was disposed: cancel the pending safety pass, arm no more. */
|
|
110
|
+
stop(): void;
|
|
111
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state each event keeps current, and whether `plugin.when` can say
|
|
3
|
+
* the same without the plugin running.
|
|
4
|
+
*/
|
|
5
|
+
const STATE = {
|
|
6
|
+
"source.state": { name: "plugin.player.state", when: true },
|
|
7
|
+
"window.state": { name: "plugin.window.state", when: true },
|
|
8
|
+
"appearance.changed": { name: "plugin.appearance", when: true },
|
|
9
|
+
"timeline.changed": { name: "plugin.timeline.marks", when: false },
|
|
10
|
+
"commands.keys": { name: "plugin.commands.keys", when: false },
|
|
11
|
+
"playback.backend": { name: "plugin.subtitles.state", when: false },
|
|
12
|
+
"resolvers.fileTypes": {
|
|
13
|
+
name: "plugin.resolvers.fileTypes",
|
|
14
|
+
when: false,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
/** Whether something that read `deps` may see a different result after `change`. */
|
|
18
|
+
function affected(deps, change) {
|
|
19
|
+
for (const [key, fields] of change) {
|
|
20
|
+
const read = deps.get(key);
|
|
21
|
+
if (!read)
|
|
22
|
+
continue;
|
|
23
|
+
if (read.size === 0)
|
|
24
|
+
return true;
|
|
25
|
+
for (const field of fields)
|
|
26
|
+
if (read.has(field))
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/** Add `from` to `into` (an empty set, the whole state, wins). */
|
|
32
|
+
function merge(into, from) {
|
|
33
|
+
for (const [key, fields] of from) {
|
|
34
|
+
const set = into.get(key);
|
|
35
|
+
if (!set)
|
|
36
|
+
into.set(key, new Set(fields));
|
|
37
|
+
else if (set.size && fields.size)
|
|
38
|
+
for (const field of fields)
|
|
39
|
+
set.add(field);
|
|
40
|
+
else
|
|
41
|
+
set.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The longest a binding waits for a full pass while precise passes skip
|
|
46
|
+
* it: state a plugin changes outside code the SDK runs (its own timer,
|
|
47
|
+
* without `update()`) still shows within about this long.
|
|
48
|
+
*/
|
|
49
|
+
export const SAFETY_PASS_MS = 1000;
|
|
50
|
+
/** The update pass scheduler of one plugin (`Runtime` delegates to it). */
|
|
51
|
+
export class Passes {
|
|
52
|
+
/** The plugin's connection: holds the events a binding reads. */
|
|
53
|
+
#connection;
|
|
54
|
+
/** Each binding with the events whose state it read last time (held). */
|
|
55
|
+
#bindings = new Map();
|
|
56
|
+
/** The events read by the binding being evaluated, with who read them. */
|
|
57
|
+
#reads;
|
|
58
|
+
/** Who reads now (a command's field and where it was added). */
|
|
59
|
+
#reader;
|
|
60
|
+
#scheduled = false;
|
|
61
|
+
/** The next pass evaluates every binding (not only a state copy changed). */
|
|
62
|
+
#full = false;
|
|
63
|
+
/** State copy changes since the last pass (a precise pass, unless `#full`). */
|
|
64
|
+
#pending = new Map();
|
|
65
|
+
/** The change the running precise pass is for; `undefined` in a full pass. */
|
|
66
|
+
#pass;
|
|
67
|
+
/** The state copy fields read by what is being evaluated now. */
|
|
68
|
+
#deps;
|
|
69
|
+
/** Each part's reads and fields last time (`part`). */
|
|
70
|
+
#parts = new WeakMap();
|
|
71
|
+
/** A precise pass skipped something: a full pass follows (`SAFETY_PASS_MS`). */
|
|
72
|
+
#safety;
|
|
73
|
+
/** The running pass skipped a binding or a part. */
|
|
74
|
+
#skipped = false;
|
|
75
|
+
/** The plugin was disposed: no safety pass is armed any more. */
|
|
76
|
+
#stopped = false;
|
|
77
|
+
/** Pass counters, read by the SDK's own tests and benchmark ({@link PassCounts}). */
|
|
78
|
+
counts = {
|
|
79
|
+
passes: 0,
|
|
80
|
+
fullPasses: 0,
|
|
81
|
+
evaluations: 0,
|
|
82
|
+
parts: 0,
|
|
83
|
+
skippedParts: 0,
|
|
84
|
+
safetyPasses: 0,
|
|
85
|
+
};
|
|
86
|
+
constructor(connection) {
|
|
87
|
+
this.#connection = connection;
|
|
88
|
+
}
|
|
89
|
+
bind(evaluate) {
|
|
90
|
+
const held = new Map();
|
|
91
|
+
const binding = { held, deps: new Map() };
|
|
92
|
+
this.#bindings.set(evaluate, binding);
|
|
93
|
+
this.invalidate();
|
|
94
|
+
return {
|
|
95
|
+
dispose: async () => {
|
|
96
|
+
if (this.#bindings.get(evaluate) !== binding)
|
|
97
|
+
return;
|
|
98
|
+
this.#bindings.delete(evaluate);
|
|
99
|
+
for (const hold of held.values())
|
|
100
|
+
await hold.dispose();
|
|
101
|
+
held.clear();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A service's state for `event` is read: a binding evaluated now depends
|
|
107
|
+
* on it, so the event counts as listened to while the binding reads it.
|
|
108
|
+
*/
|
|
109
|
+
reading(event) {
|
|
110
|
+
const reads = this.#reads;
|
|
111
|
+
if (!reads || reads.has(event))
|
|
112
|
+
return;
|
|
113
|
+
const reader = this.#reader;
|
|
114
|
+
const state = STATE[event];
|
|
115
|
+
reads.set(event, () => {
|
|
116
|
+
const read = state?.name ?? event;
|
|
117
|
+
const site = reader?.where?.();
|
|
118
|
+
const at = site ? ` (${site})` : "";
|
|
119
|
+
if (!reader)
|
|
120
|
+
return `a binding reads ${read}`;
|
|
121
|
+
if (!reader.field)
|
|
122
|
+
return `${reader.label} reads ${read}${at}`;
|
|
123
|
+
const hint = state?.when ? " — use plugin.when" : "";
|
|
124
|
+
return `${reader.label} is a function that reads ${read}${at}${hint}`;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A field of the SDK's copy of a state (`key`) is read; without `field`,
|
|
129
|
+
* the state is taken (`plugin.player.state`). What is being evaluated
|
|
130
|
+
* then depends on it: a precise pass after a change of that field runs
|
|
131
|
+
* it again.
|
|
132
|
+
*/
|
|
133
|
+
depends(key, field) {
|
|
134
|
+
const deps = this.#deps;
|
|
135
|
+
if (!deps)
|
|
136
|
+
return;
|
|
137
|
+
let fields = deps.get(key);
|
|
138
|
+
if (!fields)
|
|
139
|
+
deps.set(key, (fields = new Set()));
|
|
140
|
+
if (field !== undefined)
|
|
141
|
+
fields.add(field);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Evaluate one part of the binding being evaluated (one `view.bind`
|
|
145
|
+
* function), identified by `token`. A precise pass skips it when nothing
|
|
146
|
+
* it read last time changed, and returns `undefined`; what it read then
|
|
147
|
+
* still counts for the binding.
|
|
148
|
+
*/
|
|
149
|
+
part(token, read) {
|
|
150
|
+
const last = this.#parts.get(token);
|
|
151
|
+
const outerReads = this.#reads;
|
|
152
|
+
const outerDeps = this.#deps;
|
|
153
|
+
if (last && this.#pass && !affected(last.deps, this.#pass)) {
|
|
154
|
+
this.#skipped = true;
|
|
155
|
+
this.counts.skippedParts++;
|
|
156
|
+
if (outerReads)
|
|
157
|
+
for (const [event, why] of last.reads)
|
|
158
|
+
if (!outerReads.has(event))
|
|
159
|
+
outerReads.set(event, why);
|
|
160
|
+
if (outerDeps)
|
|
161
|
+
merge(outerDeps, last.deps);
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
this.counts.parts++;
|
|
165
|
+
const reads = new Map();
|
|
166
|
+
const deps = new Map();
|
|
167
|
+
this.#reads = outerReads && reads;
|
|
168
|
+
this.#deps = deps;
|
|
169
|
+
try {
|
|
170
|
+
return { value: read() };
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
this.#reads = outerReads;
|
|
174
|
+
this.#deps = outerDeps;
|
|
175
|
+
this.#parts.set(token, { reads, deps });
|
|
176
|
+
if (outerReads)
|
|
177
|
+
for (const [event, why] of reads)
|
|
178
|
+
if (!outerReads.has(event))
|
|
179
|
+
outerReads.set(event, why);
|
|
180
|
+
if (outerDeps)
|
|
181
|
+
merge(outerDeps, deps);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Run plugin code (a state listener) and return the state copy fields it
|
|
186
|
+
* read; they are not counted as a binding's.
|
|
187
|
+
*/
|
|
188
|
+
observe(run) {
|
|
189
|
+
const outerReads = this.#reads;
|
|
190
|
+
const outerDeps = this.#deps;
|
|
191
|
+
const deps = new Map();
|
|
192
|
+
this.#reads = undefined;
|
|
193
|
+
this.#deps = deps;
|
|
194
|
+
try {
|
|
195
|
+
return { value: run(), deps };
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
this.#reads = outerReads;
|
|
199
|
+
this.#deps = outerDeps;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether code that read `deps` while `change` was delivered needs a full
|
|
204
|
+
* pass after it: it read a field that changed, or no field of that state
|
|
205
|
+
* at all (so the SDK cannot tell what it depends on).
|
|
206
|
+
*/
|
|
207
|
+
needsFull(deps, change) {
|
|
208
|
+
for (const key of change.keys()) {
|
|
209
|
+
const read = deps.get(key);
|
|
210
|
+
if (!read || read.size === 0)
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
return affected(deps, change);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Evaluate `read` as `label`, added at `where`: the state it reads is
|
|
217
|
+
* reported as read by it. `field`: a command's `enabled` or `checked`,
|
|
218
|
+
* which a `plugin.when` condition can replace.
|
|
219
|
+
*/
|
|
220
|
+
reader(label, where, read, field = false) {
|
|
221
|
+
const outer = this.#reader;
|
|
222
|
+
this.#reader = { label, where, field };
|
|
223
|
+
try {
|
|
224
|
+
return read();
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
this.#reader = outer;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Evaluate one binding, holding the events whose state it read. */
|
|
231
|
+
#evaluate(evaluate, binding) {
|
|
232
|
+
const held = binding.held;
|
|
233
|
+
const reads = new Map();
|
|
234
|
+
const deps = new Map();
|
|
235
|
+
const outer = this.#reads;
|
|
236
|
+
const outerDeps = this.#deps;
|
|
237
|
+
const reader = this.#reader;
|
|
238
|
+
this.#reads = reads;
|
|
239
|
+
this.#deps = deps;
|
|
240
|
+
this.#reader = undefined;
|
|
241
|
+
this.counts.evaluations++;
|
|
242
|
+
try {
|
|
243
|
+
evaluate();
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
this.#reads = outer;
|
|
247
|
+
this.#deps = outerDeps;
|
|
248
|
+
this.#reader = reader;
|
|
249
|
+
binding.deps = deps;
|
|
250
|
+
for (const [event, hold] of held)
|
|
251
|
+
if (!reads.has(event)) {
|
|
252
|
+
held.delete(event);
|
|
253
|
+
void hold.dispose();
|
|
254
|
+
}
|
|
255
|
+
for (const [event, why] of reads)
|
|
256
|
+
if (!held.has(event))
|
|
257
|
+
held.set(event, this.#connection.hold(event, why));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A full pass: every binding is evaluated. Requested after plugin code
|
|
262
|
+
* the SDK ran, by `plugin.update()`, and on any change other than a state
|
|
263
|
+
* copy's fields. Coalesced: one pass per microtask turn, however many
|
|
264
|
+
* requests.
|
|
265
|
+
*/
|
|
266
|
+
invalidate() {
|
|
267
|
+
this.#full = true;
|
|
268
|
+
this.#schedule();
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Fields of the SDK's copy of a state (`key`) changed: a precise pass
|
|
272
|
+
* evaluates only the bindings (and view bind functions) that read one of
|
|
273
|
+
* them, unless a full pass is due anyway.
|
|
274
|
+
*/
|
|
275
|
+
changed(key, fields) {
|
|
276
|
+
let set = this.#pending.get(key);
|
|
277
|
+
if (!set)
|
|
278
|
+
this.#pending.set(key, (set = new Set()));
|
|
279
|
+
for (const field of fields)
|
|
280
|
+
set.add(field);
|
|
281
|
+
this.#schedule();
|
|
282
|
+
}
|
|
283
|
+
#schedule() {
|
|
284
|
+
if (this.#scheduled)
|
|
285
|
+
return;
|
|
286
|
+
this.#scheduled = true;
|
|
287
|
+
queueMicrotask(() => this.#run());
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* One pass. A precise pass that skips anything arms a full pass
|
|
291
|
+
* `SAFETY_PASS_MS` later (unless a full pass comes first), for plugin
|
|
292
|
+
* state changed outside code the SDK ran.
|
|
293
|
+
*/
|
|
294
|
+
#run() {
|
|
295
|
+
this.#scheduled = false;
|
|
296
|
+
const full = this.#full;
|
|
297
|
+
const change = this.#pending;
|
|
298
|
+
this.#full = false;
|
|
299
|
+
this.#pending = new Map();
|
|
300
|
+
this.counts.passes++;
|
|
301
|
+
if (full)
|
|
302
|
+
this.counts.fullPasses++;
|
|
303
|
+
if (full) {
|
|
304
|
+
clearTimeout(this.#safety);
|
|
305
|
+
this.#safety = undefined;
|
|
306
|
+
}
|
|
307
|
+
this.#skipped = false;
|
|
308
|
+
for (const [evaluate, binding] of [...this.#bindings]) {
|
|
309
|
+
if (this.#bindings.get(evaluate) !== binding)
|
|
310
|
+
continue;
|
|
311
|
+
if (!full && !affected(binding.deps, change)) {
|
|
312
|
+
this.#skipped = true;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
this.#pass = full ? undefined : change;
|
|
316
|
+
try {
|
|
317
|
+
this.#evaluate(evaluate, binding);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
console.error(`[${this.#connection.manifest.id}] binding failed:`, error);
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
this.#pass = undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (this.#skipped && !this.#safety && !this.#stopped) {
|
|
327
|
+
this.#safety = setTimeout(() => {
|
|
328
|
+
this.#safety = undefined;
|
|
329
|
+
this.counts.safetyPasses++;
|
|
330
|
+
this.invalidate();
|
|
331
|
+
}, SAFETY_PASS_MS);
|
|
332
|
+
this.#safety.unref?.();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/** The plugin was disposed: cancel the pending safety pass, arm no more. */
|
|
336
|
+
stop() {
|
|
337
|
+
this.#stopped = true;
|
|
338
|
+
clearTimeout(this.#safety);
|
|
339
|
+
this.#safety = undefined;
|
|
340
|
+
}
|
|
341
|
+
}
|
package/dist/plugin/player.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { SourceIntent, SourceStamp, SourceState, SourceTicket } from "../source.ts";
|
|
2
|
-
import { Latest } from "./
|
|
2
|
+
import { Latest } from "./latest.ts";
|
|
3
3
|
import type { Runtime } from "./runtime.ts";
|
|
4
4
|
import type { ScopeImpl } from "./scope.ts";
|
|
5
|
-
import type { Conditional, Disposable, Player, PlayerState
|
|
5
|
+
import type { Conditional, Disposable, Player, PlayerState } from "./types.ts";
|
|
6
6
|
/**
|
|
7
7
|
* The key of `plugin.player.state` in the runtime's field tracking
|
|
8
8
|
* (`Runtime.depends`). Its fields are the state's own, plus `stamp` for
|
|
@@ -13,7 +13,6 @@ export declare const rawOf: (state: PlayerState) => SourceState | undefined;
|
|
|
13
13
|
/** A step-seek length the player accepts: 0.1–3600 seconds in tenths. */
|
|
14
14
|
export declare const validStep: (seconds: unknown) => seconds is number;
|
|
15
15
|
export declare const sameStamp: (a: SourceStamp, b: SourceStamp) => boolean;
|
|
16
|
-
export declare function abortError(): PluginError;
|
|
17
16
|
/** One per plugin: the state store and the operation book client. */
|
|
18
17
|
export declare class PlayerService {
|
|
19
18
|
#private;
|