@vestra-player/sdk 1.6.18 → 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 +11 -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 +24 -209
- package/dist/plugin/runtime.js +34 -770
- 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/transport.js +1 -1
- 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 +2 -2
- package/dist/plugin/ui.d.ts +0 -74
- package/dist/plugin/ui.js +0 -1234
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.19
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** Step 1 of the SDK refactor, batches B and C (`doc/specs/sdk-refactor.md` §4 in the player repository): `src/plugin/runtime.ts` and `src/plugin/ui.ts` are split by layer; no API, behaviour or wire change, `apiVersion` stays `vestra/1`.
|
|
6
|
+
|
|
7
|
+
- **`runtime.ts` is split into four modules.** `connection.ts` (L1) owns the transport and its typed client, the event subscriptions with their `why`/`mirror` marks (`on`, `hold`, `raw`), the call site of a listener, the activation state (`starting`, `declaring`, `activated`) and the handshake facts (`app`, `locale`, `features`); `passes.ts` (L2) holds the update passes (`bind`, `reading`, `depends`, `part`, `observe`, `needsFull`, `reader`, `invalidate`, `changed`, the safety pass) and the pass counters; `errors.ts` holds `failure`, `quiet`, `uncertain`, `toError`, `sleep` and `abortError` (from `player.ts`); `latest.ts` holds the `Latest` single-flight queue. `Runtime` stays, as the connection plus the pass methods delegated to its `Passes`, so `runtime.host`, `runtime.on`, `runtime.bind` and the rest are unchanged for every caller, `TestHost` and the player's runner.
|
|
8
|
+
- **No module imports the pass scheduler for a helper any more**: the SDK's modules take the helpers from `errors.ts` and `latest.ts`, and `player.ts` and `stream.ts` no longer import each other.
|
|
9
|
+
- **A layer check in `npm run check`** (`scripts/check-layers.mjs`): the wire and its clients (L0) and the primitives (L1: `connection`, `errors`, `latest`, `values`, `stream`, `services`, `local`, `osd`, `bindings`, `language`, `packages`, `markdown`, `drops`) must not import `passes.ts` or `runtime.ts`, nor load `passes.ts` through other modules, value imports under `src/` must not form a cycle (beyond one known cycle among the test host's modules), and a relative import the check cannot resolve fails. Three L1 → L2 edges that wait for the `player.ts` split are printed as a note on every run.
|
|
10
|
+
- **`ui.ts` is split into `src/plugin/view/` and a dialogs service** (batch C). `view/writer.ts` (L1) is the write path of one view: values, encoding and comparison with what was sent, row patches, parts of large writes (`more`), the value limits, `stale` fallback and the back-off after a failed write; it knows nothing of passes. `view/binder.ts` (L2) holds `bind`, `snapshot` and `evaluate` with the read tracking of the passes. `view/base.ts` keeps `ViewBase` for windows, panels and settings pages: callbacks plus a writer and a binder over it, and, as before, every view joins the update passes when made. `view/window.ts` (tool windows), `view/panel.ts` (panels), `view/layout.ts` (the panel positions, `LayoutService`) and `view/image.ts` (`plugin.ui` and image values) hold the rest; `plugin.dialogs` moved to `services/dialogs.ts`. Commands take `ImageHandle` from `view/image.ts`, so the command module no longer imports the view module. Every write, its order and its back-off are the code as it was, moved.
|
|
11
|
+
- **The layer check covers the views**: `view/writer.ts`, `view/image.ts` and `services/dialogs.ts` are L1 and must not import the framework, which now includes `view/binder.ts`.
|
|
12
|
+
- Golden wire traces, host scenarios, pass counts and the API surface snapshot are identical; the public declarations (`dist/index.d.ts`, `dist/plugin/types.d.ts`, `dist/plugin/testing/index.d.ts`, `dist/player-runtime.d.ts`) are unchanged.
|
|
13
|
+
|
|
3
14
|
## 1.6.18
|
|
4
15
|
|
|
5
16
|
**Who must change: nobody.** Characterisation tests for the SDK refactor (step 0 of the plan, `doc/specs/sdk-refactor.md` §4.0 in the player repository); no API change, no wire change, `apiVersion` stays `vestra/1`.
|
package/dist/docs-tree.json.gz
CHANGED
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The player's palette (`plugin.appearance`; `ui.theme` to change it). */
|
|
2
|
-
import { failure } from "./
|
|
2
|
+
import { failure } from "./errors.js";
|
|
3
3
|
import { windowOf } from "./window.js";
|
|
4
4
|
import { LOCALES } from "../i18n.js";
|
|
5
5
|
const preference = (value) => value === "system" || LOCALES.includes(value)
|
package/dist/plugin/bindings.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* binding = { modifier "+" } input modifiers in order Ctrl, Alt, Shift, Cmd
|
|
7
7
|
* input = key | "Mouse:" mouse
|
|
8
8
|
*/
|
|
9
|
-
import { failure } from "./
|
|
9
|
+
import { failure } from "./errors.js";
|
|
10
10
|
/**
|
|
11
11
|
* The names a binding (`CommandOptions.bindings`, `Bindings.set`) is made
|
|
12
12
|
* of, as the player's binding parser accepts them. A binding is
|
package/dist/plugin/commands.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Menu commands: declaration, state sync (revisions hidden) and dispatch. */
|
|
2
|
-
import {
|
|
2
|
+
import { Latest } from "./latest.js";
|
|
3
|
+
import { failure, quiet, uncertain } from "./errors.js";
|
|
3
4
|
import { checkBindings } from "./bindings.js";
|
|
4
5
|
import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, MENU_IDS } from "../commands.js";
|
|
5
|
-
import { ImageHandle } from "./
|
|
6
|
+
import { ImageHandle } from "./view/image.js";
|
|
6
7
|
import { isTextValue, textProblem } from "../i18n.js";
|
|
7
8
|
import { conditionJson, isCondition } from "./when.js";
|
|
8
9
|
const value = (item) => typeof item === "function" ? item() : item;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Json, LocalServices, Transport, Wire } from "../wire.ts";
|
|
2
|
+
import type { App, Disposable, Locale, StopReason, SystemInfo } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Why something listens, for the player's log line that says why a plugin
|
|
5
|
+
* keeps running (`events.subscribe { why }`, player 0.5.21): computed
|
|
6
|
+
* only when the player is told.
|
|
7
|
+
*/
|
|
8
|
+
export type Why = () => string;
|
|
9
|
+
/**
|
|
10
|
+
* Events the player wakes a stopped plugin for (player 0.5.21): a listener
|
|
11
|
+
* of one does not keep the plugin running. They follow deliberate changes
|
|
12
|
+
* (an install, a key edit, the palette, a resolver), not playback.
|
|
13
|
+
*/
|
|
14
|
+
export declare const WAKE_EVENTS: readonly string[];
|
|
15
|
+
export interface PluginManifest {
|
|
16
|
+
id: string;
|
|
17
|
+
capabilities: string[];
|
|
18
|
+
}
|
|
19
|
+
/** How the plugin is run: the player's runner, or `TestHost`. */
|
|
20
|
+
export interface RuntimeOptions {
|
|
21
|
+
/**
|
|
22
|
+
* A declaration the player refuses during activation fails activation
|
|
23
|
+
* (`startPlugin`, so a plugin's tests show it) instead of being logged,
|
|
24
|
+
* reported to `onError` and skipped (the player's runner).
|
|
25
|
+
*/
|
|
26
|
+
strictDeclarations?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** A stop reason the runner passed on, if this SDK knows it. */
|
|
29
|
+
export declare function stopReason(value: unknown): StopReason | undefined;
|
|
30
|
+
type Listener = (data: unknown) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Per-plugin connection to the player. Scopes reach it through `Runtime`;
|
|
33
|
+
* the flat RPC clients stay private here so protocol fields never reach
|
|
34
|
+
* plugin code.
|
|
35
|
+
*/
|
|
36
|
+
export declare class Connection {
|
|
37
|
+
#private;
|
|
38
|
+
readonly host: Wire;
|
|
39
|
+
readonly manifest: PluginManifest;
|
|
40
|
+
readonly directory: string;
|
|
41
|
+
/** Writable per-plugin data directory from the host, if it provided one. */
|
|
42
|
+
readonly dataDirectory: string | undefined;
|
|
43
|
+
/** Test doubles for local processes; the real system when absent. */
|
|
44
|
+
readonly local: LocalServices | undefined;
|
|
45
|
+
/** Registrations that activation must wait for before reporting ready. */
|
|
46
|
+
readonly startup: Set<Promise<unknown>>;
|
|
47
|
+
readonly views: Map<string, (callback: string, args: Json[]) => void>;
|
|
48
|
+
readonly commands: Map<string, () => void>;
|
|
49
|
+
readonly services: Map<string, unknown>;
|
|
50
|
+
/**
|
|
51
|
+
* Set when the player stops the plugin (`PluginInstance.dispose`), before
|
|
52
|
+
* the root scope is disposed: `DisposeInfo` and `plugin.stopReason`.
|
|
53
|
+
*/
|
|
54
|
+
stopping: {
|
|
55
|
+
reason: StopReason | undefined;
|
|
56
|
+
} | undefined;
|
|
57
|
+
/** `plugin.app`, from the start-up handshake; `system()` asks the player. */
|
|
58
|
+
readonly app: App;
|
|
59
|
+
/** `plugin.i18n.locale`, from the start-up handshake; English when unknown. */
|
|
60
|
+
readonly locale: Locale;
|
|
61
|
+
/**
|
|
62
|
+
* Protocol additions the player advertises at start-up (`features` in the
|
|
63
|
+
* handshake's app data, player 0.6.0): `ui.patch` (row patches of
|
|
64
|
+
* property writes). Not part of the plugin API; empty for older players.
|
|
65
|
+
*/
|
|
66
|
+
readonly features: ReadonlySet<string>;
|
|
67
|
+
constructor(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string, app?: unknown, options?: RuntimeOptions);
|
|
68
|
+
can(capability: string): boolean;
|
|
69
|
+
/** Lazily created singletons shared by every scope of this plugin. */
|
|
70
|
+
service<T>(name: string, create: () => T): T;
|
|
71
|
+
/**
|
|
72
|
+
* Registrations started while `activate` runs must succeed before the
|
|
73
|
+
* plugin reports ready; later ones report failures to their scope.
|
|
74
|
+
*/
|
|
75
|
+
starting<T>(work: Promise<T>, report: (error: unknown) => void): Promise<T>;
|
|
76
|
+
/**
|
|
77
|
+
* A declaration the player checks on its own (`what`: "command x"),
|
|
78
|
+
* started like `starting`. In the player's runner a refusal of it during
|
|
79
|
+
* activation (`invalid`, `conflict`, `limit`) does not fail the plugin:
|
|
80
|
+
* it is logged, skipped and reported to the scope once activation
|
|
81
|
+
* completes, so an `onError` added anywhere in `activate` hears it (SDK
|
|
82
|
+
* 1.5.49; VS Code logs a bad contribution and activates the extension).
|
|
83
|
+
* Under `strictDeclarations` (TestHost) it fails activation as before.
|
|
84
|
+
*/
|
|
85
|
+
declaring<T>(work: Promise<T>, what: string, report: (error: unknown) => void): Promise<T>;
|
|
86
|
+
activated(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Where the plugin's code called into the SDK: the first stack frame
|
|
89
|
+
* outside the SDK, as a path relative to the plugin's directory and a
|
|
90
|
+
* line (`main.ts:353`). Captured now, formatted when asked.
|
|
91
|
+
*/
|
|
92
|
+
callSite(): () => string | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Host events with one `events.subscribe` per name, however many listeners.
|
|
95
|
+
* Without the `events` capability the listener simply never fires.
|
|
96
|
+
* `mirror`: the SDK's own copy of the state (`plugin.player.state` and
|
|
97
|
+
* the like), not a listener of the plugin's (see {@link EventGroup}).
|
|
98
|
+
*/
|
|
99
|
+
on(event: string, listener: Listener,
|
|
100
|
+
/** The capability the event needs; `null`: none. */
|
|
101
|
+
needs?: string | null, mirror?: boolean): Disposable;
|
|
102
|
+
/**
|
|
103
|
+
* Count `event` as listened to while held: a watcher of a service's
|
|
104
|
+
* state, a binding that reads it. Nothing when nothing subscribed to it
|
|
105
|
+
* (no capability, or the service did not start).
|
|
106
|
+
*/
|
|
107
|
+
hold(event: string, why?: Why): Disposable;
|
|
108
|
+
/**
|
|
109
|
+
* Why the plugin listens to each event it really listens to (mirrors
|
|
110
|
+
* left out), as the player's log says it: for {@link TestHost} and
|
|
111
|
+
* `vestra-plugin test`.
|
|
112
|
+
*/
|
|
113
|
+
listening(): Map<string, string[]>;
|
|
114
|
+
/** Raw transport events the host pushes without a subscription. */
|
|
115
|
+
raw(event: string, listener: Listener): Disposable;
|
|
116
|
+
}
|
|
117
|
+
/** `plugin.app` from the start-up handshake (the runner's environment). */
|
|
118
|
+
export declare function appInfo(raw: unknown, system?: () => Promise<SystemInfo>): App;
|
|
119
|
+
export {};
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's connection to the player (L1, refactor plan §2.2): the
|
|
3
|
+
* transport and its typed client (`host`), event subscriptions counted per
|
|
4
|
+
* name with their `why`/`mirror` marks (`on`, `hold`, `raw`), the
|
|
5
|
+
* activation state machine (`starting`, `declaring`, `activated`) and what
|
|
6
|
+
* the start-up handshake said (`app`, `locale`, `features`). It knows
|
|
7
|
+
* nothing of update passes (`passes.ts`); `Runtime` (`runtime.ts`) adds them.
|
|
8
|
+
*/
|
|
9
|
+
import { createWire, HostError } from "../wire.js";
|
|
10
|
+
import { LOCALES } from "../i18n.js";
|
|
11
|
+
import { failure } from "./errors.js";
|
|
12
|
+
import { basename, isAbsolute, relative } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
/** The SDK's own files (`src/` or `dist/`): left out of a call site. */
|
|
15
|
+
const SDK_DIRECTORY = fileURLToPath(new URL("..", import.meta.url));
|
|
16
|
+
/**
|
|
17
|
+
* Events the player wakes a stopped plugin for (player 0.5.21): a listener
|
|
18
|
+
* of one does not keep the plugin running. They follow deliberate changes
|
|
19
|
+
* (an install, a key edit, the palette, a resolver), not playback.
|
|
20
|
+
*/
|
|
21
|
+
export const WAKE_EVENTS = [
|
|
22
|
+
"packages.changed",
|
|
23
|
+
"bindings.changed",
|
|
24
|
+
// Player 0.7.6: sent to a plugin whose own keys changed and, from SDK
|
|
25
|
+
// 1.6.16, to every listener when a player command (`host/<id>`) is
|
|
26
|
+
// rebound, which wakes each parked listener.
|
|
27
|
+
"commands.keys",
|
|
28
|
+
"appearance.changed",
|
|
29
|
+
"resolvers.fileTypes",
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* The player's answers to a declaration that refuse that one declaration
|
|
33
|
+
* (a command's path, texts or count): during activation they skip it
|
|
34
|
+
* instead of failing the plugin (`Connection.declaring`, SDK 1.5.49).
|
|
35
|
+
*/
|
|
36
|
+
const DECLARATION_REFUSALS = new Set([
|
|
37
|
+
"invalid",
|
|
38
|
+
"conflict",
|
|
39
|
+
"limit",
|
|
40
|
+
]);
|
|
41
|
+
/** The reasons `StopReason` names; anything else reads as `undefined`. */
|
|
42
|
+
const STOP_REASONS = new Set([
|
|
43
|
+
"quit",
|
|
44
|
+
"disable",
|
|
45
|
+
"uninstall",
|
|
46
|
+
"reload",
|
|
47
|
+
"upgrade",
|
|
48
|
+
"idle",
|
|
49
|
+
"failed",
|
|
50
|
+
"host-lost",
|
|
51
|
+
]);
|
|
52
|
+
/** A stop reason the runner passed on, if this SDK knows it. */
|
|
53
|
+
export function stopReason(value) {
|
|
54
|
+
return typeof value === "string" && STOP_REASONS.has(value)
|
|
55
|
+
? value
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Per-plugin connection to the player. Scopes reach it through `Runtime`;
|
|
60
|
+
* the flat RPC clients stay private here so protocol fields never reach
|
|
61
|
+
* plugin code.
|
|
62
|
+
*/
|
|
63
|
+
export class Connection {
|
|
64
|
+
host;
|
|
65
|
+
manifest;
|
|
66
|
+
directory;
|
|
67
|
+
/** Writable per-plugin data directory from the host, if it provided one. */
|
|
68
|
+
dataDirectory;
|
|
69
|
+
/** Test doubles for local processes; the real system when absent. */
|
|
70
|
+
local;
|
|
71
|
+
/** Registrations that activation must wait for before reporting ready. */
|
|
72
|
+
startup = new Set();
|
|
73
|
+
#activating = true;
|
|
74
|
+
/** `RuntimeOptions.strictDeclarations`. */
|
|
75
|
+
#strict;
|
|
76
|
+
/** Declarations skipped during activation: reported once it completes. */
|
|
77
|
+
#refusals = [];
|
|
78
|
+
#transport;
|
|
79
|
+
#events = new Map();
|
|
80
|
+
/** The missing `events` warning was given (once per plugin). */
|
|
81
|
+
#warnedEvents = false;
|
|
82
|
+
views = new Map();
|
|
83
|
+
commands = new Map();
|
|
84
|
+
services = new Map();
|
|
85
|
+
/**
|
|
86
|
+
* Set when the player stops the plugin (`PluginInstance.dispose`), before
|
|
87
|
+
* the root scope is disposed: `DisposeInfo` and `plugin.stopReason`.
|
|
88
|
+
*/
|
|
89
|
+
stopping;
|
|
90
|
+
/** `plugin.app`, from the start-up handshake; `system()` asks the player. */
|
|
91
|
+
app;
|
|
92
|
+
/** `plugin.i18n.locale`, from the start-up handshake; English when unknown. */
|
|
93
|
+
locale;
|
|
94
|
+
/**
|
|
95
|
+
* Protocol additions the player advertises at start-up (`features` in the
|
|
96
|
+
* handshake's app data, player 0.6.0): `ui.patch` (row patches of
|
|
97
|
+
* property writes). Not part of the plugin API; empty for older players.
|
|
98
|
+
*/
|
|
99
|
+
features;
|
|
100
|
+
constructor(transport, manifest, directory, dataDirectory, app, options = {}) {
|
|
101
|
+
this.#transport = transport;
|
|
102
|
+
this.#strict = options.strictDeclarations ?? false;
|
|
103
|
+
this.dataDirectory = dataDirectory;
|
|
104
|
+
this.local = transport.local;
|
|
105
|
+
this.host = createWire(transport);
|
|
106
|
+
const host = this.host;
|
|
107
|
+
this.app = appInfo(app, () => host.app.system());
|
|
108
|
+
const features = app
|
|
109
|
+
?.features;
|
|
110
|
+
this.features = new Set(Array.isArray(features)
|
|
111
|
+
? features.filter((name) => typeof name === "string")
|
|
112
|
+
: []);
|
|
113
|
+
this.locale = LOCALES.includes(transport.locale ?? "")
|
|
114
|
+
? transport.locale
|
|
115
|
+
: "en";
|
|
116
|
+
this.manifest = manifest;
|
|
117
|
+
this.directory = directory;
|
|
118
|
+
transport.on("ui.callback", (data) => {
|
|
119
|
+
const event = data;
|
|
120
|
+
// A panel's callback names its slot (player 0.7.6: one panel per
|
|
121
|
+
// slot); older players name only `"$mount"`, and the plugin has one.
|
|
122
|
+
const view = event.view === "$mount"
|
|
123
|
+
? event.slot
|
|
124
|
+
? `panel:${event.slot}`
|
|
125
|
+
: [...this.views.keys()].find((key) => key.startsWith("panel:"))
|
|
126
|
+
: event.view;
|
|
127
|
+
if (view)
|
|
128
|
+
this.views.get(view)?.(event.callback, event.args);
|
|
129
|
+
});
|
|
130
|
+
transport.on("commands.execute", (data) => {
|
|
131
|
+
this.commands.get(data.id)?.();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
can(capability) {
|
|
135
|
+
return this.manifest.capabilities.includes(capability);
|
|
136
|
+
}
|
|
137
|
+
/** Lazily created singletons shared by every scope of this plugin. */
|
|
138
|
+
service(name, create) {
|
|
139
|
+
if (!this.services.has(name))
|
|
140
|
+
this.services.set(name, create());
|
|
141
|
+
return this.services.get(name);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Registrations started while `activate` runs must succeed before the
|
|
145
|
+
* plugin reports ready; later ones report failures to their scope.
|
|
146
|
+
*/
|
|
147
|
+
starting(work, report) {
|
|
148
|
+
if (this.#activating)
|
|
149
|
+
this.startup.add(work);
|
|
150
|
+
else
|
|
151
|
+
work.catch(report);
|
|
152
|
+
work.catch(() => { });
|
|
153
|
+
return work;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A declaration the player checks on its own (`what`: "command x"),
|
|
157
|
+
* started like `starting`. In the player's runner a refusal of it during
|
|
158
|
+
* activation (`invalid`, `conflict`, `limit`) does not fail the plugin:
|
|
159
|
+
* it is logged, skipped and reported to the scope once activation
|
|
160
|
+
* completes, so an `onError` added anywhere in `activate` hears it (SDK
|
|
161
|
+
* 1.5.49; VS Code logs a bad contribution and activates the extension).
|
|
162
|
+
* Under `strictDeclarations` (TestHost) it fails activation as before.
|
|
163
|
+
*/
|
|
164
|
+
declaring(work, what, report) {
|
|
165
|
+
if (!this.#activating || this.#strict)
|
|
166
|
+
return this.starting(work, report);
|
|
167
|
+
const settled = work.then(() => { }, (error) => {
|
|
168
|
+
if (!(error instanceof HostError) ||
|
|
169
|
+
!DECLARATION_REFUSALS.has(error.code))
|
|
170
|
+
throw error;
|
|
171
|
+
console.error(`[${this.manifest.id}] ${what} skipped, the player refused it (${error.code}): ${error.message}`);
|
|
172
|
+
this.#refusals.push(() => report(error));
|
|
173
|
+
});
|
|
174
|
+
// Observed now: `activated()` may come after `activate` awaits, and
|
|
175
|
+
// its `allSettled` still sees the rejection.
|
|
176
|
+
settled.catch(() => { });
|
|
177
|
+
this.startup.add(settled);
|
|
178
|
+
work.catch(() => { });
|
|
179
|
+
return work;
|
|
180
|
+
}
|
|
181
|
+
async activated() {
|
|
182
|
+
while (this.startup.size) {
|
|
183
|
+
const pending = [...this.startup];
|
|
184
|
+
this.startup.clear();
|
|
185
|
+
for (const result of await Promise.allSettled(pending))
|
|
186
|
+
if (result.status === "rejected")
|
|
187
|
+
throw result.reason;
|
|
188
|
+
}
|
|
189
|
+
this.#activating = false;
|
|
190
|
+
for (const report of this.#refusals.splice(0))
|
|
191
|
+
report();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Where the plugin's code called into the SDK: the first stack frame
|
|
195
|
+
* outside the SDK, as a path relative to the plugin's directory and a
|
|
196
|
+
* line (`main.ts:353`). Captured now, formatted when asked.
|
|
197
|
+
*/
|
|
198
|
+
callSite() {
|
|
199
|
+
const limit = Error.stackTraceLimit;
|
|
200
|
+
Error.stackTraceLimit = 40;
|
|
201
|
+
const error = new Error();
|
|
202
|
+
Error.stackTraceLimit = limit;
|
|
203
|
+
let site = null;
|
|
204
|
+
return () => {
|
|
205
|
+
if (site === null)
|
|
206
|
+
site = this.#site(error.stack ?? "");
|
|
207
|
+
return site;
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
#site(stack) {
|
|
211
|
+
for (const line of stack.split("\n").slice(1)) {
|
|
212
|
+
const match = /\(?((?:file:\/\/)?[^()\s]+?):(\d+):\d+\)?\s*$/.exec(line);
|
|
213
|
+
if (!match)
|
|
214
|
+
continue;
|
|
215
|
+
let file = match[1];
|
|
216
|
+
if (file.startsWith("file:"))
|
|
217
|
+
try {
|
|
218
|
+
file = fileURLToPath(file);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const sdk = relative(SDK_DIRECTORY, file);
|
|
224
|
+
if (!isAbsolute(file) || !(sdk.startsWith("..") || isAbsolute(sdk)))
|
|
225
|
+
continue;
|
|
226
|
+
const inside = relative(this.directory, file);
|
|
227
|
+
const shown = inside.startsWith("..") || isAbsolute(inside) ? basename(file) : inside;
|
|
228
|
+
return `${shown.replaceAll("\\", "/")}:${match[2]}`;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Host events with one `events.subscribe` per name, however many listeners.
|
|
234
|
+
* Without the `events` capability the listener simply never fires.
|
|
235
|
+
* `mirror`: the SDK's own copy of the state (`plugin.player.state` and
|
|
236
|
+
* the like), not a listener of the plugin's (see {@link EventGroup}).
|
|
237
|
+
*/
|
|
238
|
+
on(event, listener,
|
|
239
|
+
/** The capability the event needs; `null`: none. */
|
|
240
|
+
needs = "events", mirror = false) {
|
|
241
|
+
let entry = this.#events.get(event);
|
|
242
|
+
const created = !entry;
|
|
243
|
+
if (!entry) {
|
|
244
|
+
entry = { listeners: new Set(), active: 0, whys: new Map() };
|
|
245
|
+
this.#events.set(event, entry);
|
|
246
|
+
}
|
|
247
|
+
const group = entry;
|
|
248
|
+
group.listeners.add(listener);
|
|
249
|
+
const token = {};
|
|
250
|
+
if (!mirror) {
|
|
251
|
+
group.active++;
|
|
252
|
+
group.whys.set(token, listenerWhy(this.callSite()));
|
|
253
|
+
}
|
|
254
|
+
if (created && needs === "events" && !this.can(needs))
|
|
255
|
+
group.unheard = true;
|
|
256
|
+
if (!mirror && group.unheard)
|
|
257
|
+
this.#warnUnheard(event);
|
|
258
|
+
if (created && (needs === null || this.can(needs))) {
|
|
259
|
+
group.told = group.active > 0;
|
|
260
|
+
const whys = group.told ? allWhys(group) : undefined;
|
|
261
|
+
group.toldWhy = whys?.join("\n");
|
|
262
|
+
group.off = this.host.events.subscribe(event, (data) => {
|
|
263
|
+
for (const each of [...group.listeners])
|
|
264
|
+
each(data);
|
|
265
|
+
}, !group.told, whys);
|
|
266
|
+
this.starting(group.off, (error) => console.error(`[${this.manifest.id}] subscribe ${event}:`, error));
|
|
267
|
+
}
|
|
268
|
+
else
|
|
269
|
+
this.#tell(event, group);
|
|
270
|
+
let listening = true;
|
|
271
|
+
return {
|
|
272
|
+
dispose: async () => {
|
|
273
|
+
if (!listening)
|
|
274
|
+
return;
|
|
275
|
+
listening = false;
|
|
276
|
+
group.listeners.delete(listener);
|
|
277
|
+
if (!mirror)
|
|
278
|
+
group.active--;
|
|
279
|
+
group.whys.delete(token);
|
|
280
|
+
if (this.#events.get(event) !== group)
|
|
281
|
+
return;
|
|
282
|
+
if (group.listeners.size)
|
|
283
|
+
return this.#tell(event, group);
|
|
284
|
+
this.#events.delete(event);
|
|
285
|
+
const off = await group.off?.catch(() => undefined);
|
|
286
|
+
await off?.().catch(() => { });
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Count `event` as listened to while held: a watcher of a service's
|
|
292
|
+
* state, a binding that reads it. Nothing when nothing subscribed to it
|
|
293
|
+
* (no capability, or the service did not start).
|
|
294
|
+
*/
|
|
295
|
+
hold(event, why) {
|
|
296
|
+
const group = this.#events.get(event);
|
|
297
|
+
if (!group)
|
|
298
|
+
return { dispose: async () => { } };
|
|
299
|
+
if (group.unheard)
|
|
300
|
+
this.#warnUnheard(event);
|
|
301
|
+
group.active++;
|
|
302
|
+
const token = {};
|
|
303
|
+
group.whys.set(token, why ?? listenerWhy(this.callSite()));
|
|
304
|
+
this.#tell(event, group);
|
|
305
|
+
let held = true;
|
|
306
|
+
return {
|
|
307
|
+
dispose: async () => {
|
|
308
|
+
if (!held)
|
|
309
|
+
return;
|
|
310
|
+
held = false;
|
|
311
|
+
group.active--;
|
|
312
|
+
group.whys.delete(token);
|
|
313
|
+
if (this.#events.get(event) === group)
|
|
314
|
+
this.#tell(event, group);
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* A development warning, once per plugin, no behaviour change (SDK
|
|
320
|
+
* 1.6.12): the plugin follows `event` (an `onChange`, a `bind` that reads
|
|
321
|
+
* the state) without the `events` capability, so changes made elsewhere
|
|
322
|
+
* (the user, the player, another plugin) never reach it; the state moves
|
|
323
|
+
* only after its own calls.
|
|
324
|
+
*/
|
|
325
|
+
#warnUnheard(event) {
|
|
326
|
+
if (this.#warnedEvents)
|
|
327
|
+
return;
|
|
328
|
+
this.#warnedEvents = true;
|
|
329
|
+
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.`);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Tell the player when `group` changes between mirror and listener, and
|
|
333
|
+
* why it listens when that changed.
|
|
334
|
+
*/
|
|
335
|
+
#tell(event, group) {
|
|
336
|
+
const off = group.off;
|
|
337
|
+
if (!off)
|
|
338
|
+
return;
|
|
339
|
+
const active = group.active > 0;
|
|
340
|
+
const whys = active ? allWhys(group) : undefined;
|
|
341
|
+
const why = whys?.join("\n");
|
|
342
|
+
if (group.told === active && group.toldWhy === why)
|
|
343
|
+
return;
|
|
344
|
+
group.told = active;
|
|
345
|
+
group.toldWhy = why;
|
|
346
|
+
// Part of activation while it runs: the player classifies the plugin
|
|
347
|
+
// at its ready, which must see this mark. A failure is not one of
|
|
348
|
+
// activation (an older player never refuses it).
|
|
349
|
+
const told = off
|
|
350
|
+
.then(() => this.host.events.listen(event, active, whys))
|
|
351
|
+
.catch(() => { });
|
|
352
|
+
this.starting(told, () => { });
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Why the plugin listens to each event it really listens to (mirrors
|
|
356
|
+
* left out), as the player's log says it: for {@link TestHost} and
|
|
357
|
+
* `vestra-plugin test`.
|
|
358
|
+
*/
|
|
359
|
+
listening() {
|
|
360
|
+
const result = new Map();
|
|
361
|
+
for (const [event, group] of this.#events)
|
|
362
|
+
if (group.active > 0)
|
|
363
|
+
result.set(event, allWhys(group));
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
/** Raw transport events the host pushes without a subscription. */
|
|
367
|
+
raw(event, listener) {
|
|
368
|
+
const off = this.#transport.on(event, listener);
|
|
369
|
+
return { dispose: async () => off() };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** A plugin listener's reason: where it was added. */
|
|
373
|
+
const listenerWhy = (site) => () => {
|
|
374
|
+
const at = site();
|
|
375
|
+
return at ? `a listener added at ${at}` : "a listener";
|
|
376
|
+
};
|
|
377
|
+
/** Every active holder's reason in `group`, in the order they came, once each. */
|
|
378
|
+
function allWhys(group) {
|
|
379
|
+
return [...new Set([...group.whys.values()].map((why) => why()))];
|
|
380
|
+
}
|
|
381
|
+
/** `plugin.app` from the start-up handshake (the runner's environment). */
|
|
382
|
+
export function appInfo(raw, system = () => Promise.reject(failure("unavailable", "no player to ask"))) {
|
|
383
|
+
const info = (raw ?? {});
|
|
384
|
+
const parts = (info.components ?? {});
|
|
385
|
+
const text = (value) => (typeof value === "string" ? value : null);
|
|
386
|
+
const libav = {};
|
|
387
|
+
for (const [name, version] of Object.entries(parts.libav ?? {}))
|
|
388
|
+
if (typeof version === "string")
|
|
389
|
+
libav[name] = version;
|
|
390
|
+
const app = {
|
|
391
|
+
version: text(info.version) ?? "0.0.0",
|
|
392
|
+
build: text(info.build),
|
|
393
|
+
components: Object.freeze({
|
|
394
|
+
mpv: text(parts.mpv),
|
|
395
|
+
libav: Object.freeze(libav),
|
|
396
|
+
slint: text(parts.slint) ?? "",
|
|
397
|
+
}),
|
|
398
|
+
};
|
|
399
|
+
// A method, not data: kept out of comparisons and JSON of the facts.
|
|
400
|
+
Object.defineProperty(app, "system", { value: system, enumerable: false });
|
|
401
|
+
return Object.freeze(app);
|
|
402
|
+
}
|
package/dist/plugin/drops.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Connection } from "./connection.ts";
|
|
2
2
|
import type { ScopeImpl } from "./scope.ts";
|
|
3
3
|
import type { Disposable, DropModifiers, Drops } from "./types.ts";
|
|
4
4
|
type PanelDrop = {
|
|
@@ -16,11 +16,11 @@ type PanelDrop = {
|
|
|
16
16
|
declare class DropService {
|
|
17
17
|
#private;
|
|
18
18
|
readonly handlers: Map<string, (paths: string[], modifiers: DropModifiers) => void>;
|
|
19
|
-
constructor(runtime:
|
|
19
|
+
constructor(runtime: Connection);
|
|
20
20
|
id(): string;
|
|
21
21
|
/** The first listener asks the player for panel drops; the last stops them. */
|
|
22
22
|
listen(scope: ScopeImpl, listener: (drop: PanelDrop) => void): Disposable;
|
|
23
23
|
}
|
|
24
|
-
export declare const dropsOf: (runtime:
|
|
24
|
+
export declare const dropsOf: (runtime: Connection) => DropService;
|
|
25
25
|
export declare const drops: (scope: ScopeImpl) => Drops;
|
|
26
26
|
export {};
|
package/dist/plugin/drops.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginError } from "./types.ts";
|
|
2
|
+
export declare const sleep: (ms: number) => Promise<void>;
|
|
3
|
+
export declare function toError(error: unknown): PluginError;
|
|
4
|
+
export declare const failure: (code: string, message?: string) => PluginError;
|
|
5
|
+
/** Host replies are definite; anything else (lost reply, client timeout) is not. */
|
|
6
|
+
export declare const uncertain: (error: unknown) => boolean;
|
|
7
|
+
/** Mark a promise as observed so an unused `ready` never crashes the runner. */
|
|
8
|
+
export declare function quiet<T>(promise: Promise<T>): Promise<T>;
|
|
9
|
+
export declare function abortError(): PluginError;
|
|
@@ -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 };
|