@vestra-player/sdk 1.5.54 → 1.5.57

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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.57
4
+
5
+ **Who must change: nobody.** The first npm release with 1.5.56's fix: no change to the TypeScript API or the wire, `apiVersion` stays `vestra/1`, no new capability, no player version needed.
6
+
7
+ - **1.5.56 never reached npm.** Its tag was pushed, but the release run (41502365359) skipped `publish` because GitHub never gave its two macOS jobs a hosted runner (capacity) and they were cancelled. 1.5.57 publishes the same code: `appearance.onChange` and `resolvers.onFileTypesChange` hear a change the player woke the plugin for, and the session-storage clearing docs, as described under [1.5.56](#1556). Install 1.5.57; there is no `@vestra-player/sdk@1.5.56` on npm.
8
+ - _Release process_: runner capacity no longer blocks a release (product-owner ruling, 2026-11-02). `release.yml` publishes once `verify`, `static`, `build`, `docs` and the Ubuntu and Windows tests and examples pass; macOS tests and examples still run on every release as `test-macos`, beside `publish` and without gating it. `_test.yml` takes an `os` input (a JSON array of runner labels, all three by default), so pull requests and `main` still test and gate on Ubuntu, Windows and macOS as before. [Maintaining › Releasing](docs/maintaining.md#releasing) says so.
9
+
10
+ ## 1.5.56
11
+
12
+ **Who must change: nobody.** A fix: no change to the TypeScript API or the wire, `apiVersion` stays `vestra/1`, no new capability, no player version needed.
13
+
14
+ - **`appearance.onChange` and `resolvers.onFileTypesChange` hear a change the player woke the plugin for.** A plugin the player stopped and starts again to deliver a palette or file-type change ([Idle plugins are stopped](docs/explanation/plugins.md#idle-plugins-are-stopped)) read the new value with `window.state` at start; the held `appearance.changed` or `resolvers.fileTypes` that followed compared equal to it, so the listeners never ran. The SDK now compares an event with what the listeners last heard, not with the value read at start: they hear the change once, and the same value again is still not a change. The player sends `resolvers.fileTypes` only when the list changed. It sends `appearance.changed` when the theme or the language changed, both values each time; an event whose language differs from the last one the SDK saw is a language change, and as before does not call `onChange` unless the theme differs too (also for the echo of the plugin's own `setLanguage`). One case remains: a plugin the player woke for a language change alone cannot tell it from a theme change, since the event does not say what changed, and its `onChange` may hear the unchanged theme once. A field naming the change could be added to the event later without breaking anything. `appearance.theme`, `resolvers.fileTypes` and views read the same values as before. `packages.onChange` and `bindings.onChange` (the other two events that wake a plugin) never compared, and are unchanged.
15
+ - _Docs_: when session storage is cleared, corrected against the player: also when a store or another plugin with `plugins.manage` disables the plugin (`packages.setEnabled(id, false)`, or an update installed with `enabled: false`, player 0.6.4), and when an earlier installation kept for a later reinstall (after a `replace`) is dropped because the player keeps only so many; [How a plugin runs › Stopping](docs/explanation/plugins.md#stopping) names other plugins with `plugins.manage` too. `SessionStorage`, [How a plugin runs › Restarts](docs/explanation/plugins.md#restarts-activate-can-run-more-than-once) and [Store a plugin's data](docs/how-to/store-data.md) say so. `Appearance.onChange` and `Resolvers.onFileTypesChange` describe the wake.
16
+
17
+ ## 1.5.55
18
+
19
+ **Who must change: nobody, but check `TestHost` tests that update a disabled plugin.** Additive: `InstallOptions` gains `enabled` and `TestHost` gains `installKeepsDisabled`, `apiVersion` stays `vestra/1`, no new capability, and existing code still compiles. Older players ignore the option, and an install there always enables the plugin. One `TestHost` behaviour change at run time: like player 0.6.4, it now keeps a `disabled` entry disabled when the plugin under test installs an update or reinstall over it without `enabled`, where it used to make it `running`. A test that expects the update to run (the player's Store test did) sets `host.installKeepsDisabled = false` to keep playing players up to 0.6.3, or asserts the new rule; a test that covers both runs once with each.
20
+
21
+ - **Choose whether an installed plugin runs** (`candidate.install({ enabled })`). Planned for player 0.6.4 (not released yet), where an update or reinstall (from any source, `replace` included) keeps the installed plugin's state when `enabled` is left out: a plugin the user or a store disabled stays disabled, and only its files are installed. A first install runs as before. `true` enables and starts it, for a store that disabled a revoked version and installs the update that fixes it (if the new version fails to start, the old one is restored, still disabled); `false` installs it disabled, stopping a running version and dropping its session values as `setEnabled(id, false)` does. Built-in plugins always run. The SDK sends `enabled` only when given and refuses anything but a boolean with `invalid` before sending. On players up to 0.6.3 the option is ignored and every install runs; call `packages.setEnabled` after the install is committed when the state matters there.
22
+ - **`TestHost`** models player 0.6.4 by default: a first install is `running`, an update of a `disabled` entry stays `disabled` unless `enabled: true`, `enabled: false` installs it `disabled`, and a non-boolean `enabled` on the wire is `invalid` (`null` reads as absent, as in the player). `installKeepsDisabled = false` plays players up to 0.6.3 instead: every install is `running` and `enabled` is ignored. `packageCalls` records `install`'s `enabled` when it was given.
23
+ - **Stop reasons, corrected** (SDK 1.5.54 docs, from the player's implementation for 0.6.4, still planned). `upgrade` also covers a new version stopped because its install was cancelled (the previous version comes back). `idle` comes only with lazy activation, which is off by default (`VESTRA_LAZY=1`), and also parks a plugin whose first run it judges lazy right after it is ready, not only after a while without use. `failed` is not sent for a full input queue: an own process is killed, and a member's full queue restarts the shared runner as a whole, as does an unreadable line from the runner itself. In those cases, and when an own process fails to activate or the process has already exited, the plugin gets no stop at all and its `onDispose` cleanups do not run. `host-lost` is not proof of a crash: player 0.6.4 sends `quit` on a normal exit, while an older player could close its pipes first when its window closed. [How a plugin runs › Restarts](docs/explanation/plugins.md#restarts-activate-can-run-more-than-once) now states as fact that plugins sharing one process are restarted together (player 0.5.6 and later). `StopReason`, [The plugin model](docs/explanation/plugins.md#stopping) and [Host and Node](docs/explanation/host-and-node.md#stopping-a-plugin) say so.
24
+ - _Docs_: `InstallOptions.enabled`, `PackageCandidate.install` (with an example; the `replace` example no longer promises a running plugin), `TestHost.candidates` and `TestHost.installKeepsDisabled` describe it.
25
+
3
26
  ## 1.5.54
4
27
 
5
28
  **Who must change: nobody, but check cleanup functions passed directly to `onDispose`.** Additive: `Plugin.stopReason`, the types `StopReason` and `DisposeInfo`, an optional argument for `onDispose` cleanups, `TestHost.previousCrash`, an optional `code` for `TestHost.failVideoShaders` and `stop({ reason })` / `disconnect()` from `startPlugin`; `apiVersion` stays `vestra/1`, no new capability, and existing code still compiles. One runtime difference: with this SDK in the player (or in `TestHost`) an `onDispose` cleanup is called with one argument, the `DisposeInfo`. A function that takes optional parameters of its own and is passed directly now receives it as its first argument: `plugin.onDispose(controller.abort.bind(controller))` aborts with the `DisposeInfo` as the abort reason, `onDispose(save)` with `save(force?: boolean)` gets an object as `force`. Wrap such a function (`() => controller.abort()`). The stop reasons are planned for player 0.6.4; `previous-crash` comes from player 0.6.3.
Binary file
@@ -18,12 +18,34 @@ class AppearanceService {
18
18
  /** The saved interface language (the next start's). */
19
19
  language;
20
20
  #listeners = new Set();
21
+ /**
22
+ * The theme listeners last heard (or the last event's), not the one read
23
+ * at start: a plugin woken from standby reads the new theme from
24
+ * `window.state`, then gets the `appearance.changed` the player held for
25
+ * it, and its listeners still hear that change. Left unset until the
26
+ * first event on purpose: set from the start-up read, it would hide that
27
+ * held change again.
28
+ */
29
+ #notified;
30
+ /**
31
+ * The language as the start-up read and later events carried it (not
32
+ * `language`, which the plugin's own `setLanguage` changes before its
33
+ * echo arrives). The player sends `appearance.changed` once per change
34
+ * of the theme or of the language, each with both values: an event whose
35
+ * language differs from this one is a language change, and its theme is
36
+ * compared with `theme` as before. Only a newest event that repeats the
37
+ * start-up language is taken for a theme change; a plugin woken for a
38
+ * language change gets that too, so its listeners may hear the unchanged
39
+ * theme once (the event does not say what changed).
40
+ */
41
+ #eventLanguage;
21
42
  #started = false;
22
43
  constructor(runtime) {
23
44
  this.#runtime = runtime;
24
45
  this.theme =
25
46
  windowOf(runtime).appearance ?? "vestra";
26
47
  this.language = preference(windowOf(runtime).language) ?? "system";
48
+ this.#eventLanguage = this.language;
27
49
  }
28
50
  start() {
29
51
  if (this.#started)
@@ -33,22 +55,47 @@ class AppearanceService {
33
55
  this.#runtime.on("appearance.changed", (data) => {
34
56
  const event = data;
35
57
  const language = preference(event?.language);
58
+ // Older players send no language: their events are theme changes.
59
+ const languageChanged = language !== undefined && language !== this.#eventLanguage;
60
+ if (language)
61
+ this.#eventLanguage = language;
36
62
  if (language && language !== this.language) {
37
63
  this.language = language;
38
64
  this.#runtime.invalidate();
39
65
  }
40
66
  const theme = event?.theme;
41
- if (theme && THEMES.includes(theme))
67
+ if (!theme || !THEMES.includes(theme))
68
+ return;
69
+ if (languageChanged) {
70
+ // The theme came along unchanged (or this plugin set it already).
42
71
  this.accept(theme);
72
+ this.#notified = theme;
73
+ }
74
+ else
75
+ this.changed(theme);
43
76
  }, "events", true);
44
77
  }
78
+ /** A theme change event: listeners hear it unless they already did. */
79
+ changed(theme) {
80
+ const changed = theme !== this.theme;
81
+ this.theme = theme;
82
+ if (theme !== this.#notified)
83
+ this.#notify(theme);
84
+ if (changed)
85
+ this.#runtime.invalidate();
86
+ }
87
+ /** After this plugin's own `set`: only a change from the current theme. */
45
88
  accept(theme) {
46
89
  if (theme === this.theme)
47
90
  return;
48
91
  this.theme = theme;
92
+ this.#notify(theme);
93
+ this.#runtime.invalidate();
94
+ }
95
+ #notify(theme) {
96
+ this.#notified = theme;
49
97
  for (const listener of [...this.#listeners])
50
98
  listener(theme);
51
- this.#runtime.invalidate();
52
99
  }
53
100
  watch(listener) {
54
101
  this.#listeners.add(listener);
@@ -83,9 +83,11 @@ function installOptions(options) {
83
83
  return {};
84
84
  if (typeof options !== "object" || options === null)
85
85
  throw failure("invalid", "install options must be an object");
86
- const { source, replace } = options;
86
+ const { source, replace, enabled } = options;
87
87
  if (replace !== undefined && typeof replace !== "boolean")
88
88
  throw failure("invalid", "replace must be a boolean");
89
+ if (enabled !== undefined && typeof enabled !== "boolean")
90
+ throw failure("invalid", "enabled must be a boolean");
89
91
  if (source !== undefined) {
90
92
  const kinds = ["repository", "link", "file", "workshop"];
91
93
  if (typeof source !== "object" ||
@@ -99,6 +101,7 @@ function installOptions(options) {
99
101
  return {
100
102
  ...(source === undefined ? {} : { source: { ...source } }),
101
103
  ...(replace ? { replace: true } : {}),
104
+ ...(enabled === undefined ? {} : { enabled }),
102
105
  };
103
106
  }
104
107
  function candidate(scope, directory, checked) {
@@ -17,6 +17,14 @@ class FileTypesService {
17
17
  #runtime;
18
18
  types;
19
19
  #listeners = new Set();
20
+ /**
21
+ * The list listeners last heard, not the one read at start: a plugin
22
+ * woken from standby reads the new list from `window.state`, then gets
23
+ * the `resolvers.fileTypes` the player held for it, and its listeners
24
+ * still hear that change. The player sends the event only when the list
25
+ * changed, so a first event is always news to the listeners.
26
+ */
27
+ #notified;
20
28
  #started = false;
21
29
  constructor(runtime) {
22
30
  this.#runtime = runtime;
@@ -30,7 +38,7 @@ class FileTypesService {
30
38
  this.#runtime.on("resolvers.fileTypes", (data) => {
31
39
  const types = data?.fileTypes;
32
40
  if (Array.isArray(types) && types.every((t) => typeof t === "string"))
33
- this.accept(types);
41
+ this.changed(types);
34
42
  }, "events", true);
35
43
  // What changed between the start-up read and the subscription.
36
44
  const window = windowOf(this.#runtime);
@@ -39,16 +47,33 @@ class FileTypesService {
39
47
  this.accept(window.fileTypes, this.#runtime.can("events"));
40
48
  });
41
49
  }
42
- /** `notify`: false without `events`, whose listeners never fire. */
50
+ /** From `resolvers.fileTypes`: listeners hear it unless they already did. */
51
+ changed(types) {
52
+ const changed = !sameTypes(types, this.types);
53
+ if (changed)
54
+ this.types = Object.freeze([...types]);
55
+ if (!this.#notified || !sameTypes(types, this.#notified))
56
+ this.#notify(types);
57
+ if (changed)
58
+ this.#runtime.invalidate();
59
+ }
60
+ /**
61
+ * From a `window.state` read: only a change from the current list.
62
+ * `notify`: false without `events`, whose listeners never fire.
63
+ */
43
64
  accept(types, notify = true) {
44
65
  if (sameTypes(types, this.types))
45
66
  return;
46
67
  this.types = Object.freeze([...types]);
47
68
  if (notify)
48
- for (const listener of [...this.#listeners])
49
- listener([...types]);
69
+ this.#notify(types);
50
70
  this.#runtime.invalidate();
51
71
  }
72
+ #notify(types) {
73
+ this.#notified = Object.freeze([...types]);
74
+ for (const listener of [...this.#listeners])
75
+ listener([...types]);
76
+ }
52
77
  watch(listener) {
53
78
  this.#listeners.add(listener);
54
79
  const held = this.#runtime.hold("resolvers.fileTypes");
@@ -2436,7 +2436,7 @@ export declare class TestHost implements Transport {
2436
2436
  method: "setEnabled" | "reload" | "uninstall" | "install";
2437
2437
  /** The target plugin id. */
2438
2438
  id: string;
2439
- /** `setEnabled`'s value. */
2439
+ /** `setEnabled`'s value, or `install`'s `enabled` option when given. */
2440
2440
  enabled?: boolean;
2441
2441
  /** `uninstall`'s `removeData` option. */
2442
2442
  removeData?: boolean;
@@ -2450,7 +2450,13 @@ export declare class TestHost implements Transport {
2450
2450
  * `.vxp` file) → its manifest. Other paths fail with `missing`. `install`
2451
2451
  * adds or upgrades the entry in {@link TestHost.packages} following the
2452
2452
  * player's source rules: the same source upgrades, another source needs
2453
- * `replace` (`conflict` otherwise), a built-in id is `unsupported`.
2453
+ * `replace` (`conflict` otherwise), a built-in id is `unsupported`. As
2454
+ * player 0.6.4 (planned), the installed entry's `state` follows
2455
+ * {@link "@vestra-player/sdk"!InstallOptions.enabled}: a first install
2456
+ * runs, an update of a `disabled` entry stays `disabled` unless `enabled`
2457
+ * is `true`, and `enabled: false` installs it `disabled`. Set
2458
+ * {@link TestHost.installKeepsDisabled} to `false` for the older players'
2459
+ * rule (every install runs).
2454
2460
  *
2455
2461
  * @example Offer a package file the plugin can install
2456
2462
  * The plugin can validate and install `/downloads/someone.demo-1.0.0.vxp`
@@ -2467,6 +2473,32 @@ export declare class TestHost implements Transport {
2467
2473
  * ```
2468
2474
  */
2469
2475
  readonly candidates: Map<string, PackageManifest>;
2476
+ /**
2477
+ * Which player's install rule `packages.install` plays. `true` (default,
2478
+ * SDK 1.5.55): player 0.6.4 (planned), where an update or reinstall
2479
+ * keeps a `disabled` plugin disabled unless the plugin passes
2480
+ * {@link "@vestra-player/sdk"!InstallOptions.enabled}. `false`: players
2481
+ * up to 0.6.3, where every install runs the plugin and `enabled` is
2482
+ * ignored (it is still checked and recorded in
2483
+ * {@link TestHost.packageCalls}). A test written for the older rule that
2484
+ * updates a disabled plugin sets it to `false`; one that keeps testing
2485
+ * both runs once with each.
2486
+ *
2487
+ * @example Keep a test written for players up to 0.6.3
2488
+ * The plugin under test installs an update over a plugin the user
2489
+ * disabled and expects it to run afterwards, as players up to 0.6.3 do.
2490
+ * With `false` the host plays that rule; the default plays player 0.6.4,
2491
+ * where the update stays disabled.
2492
+ * ```ts
2493
+ * declare const host: TestHost;
2494
+ * // ---cut---
2495
+ * host.installKeepsDisabled = false; // every install runs, as before 0.6.4
2496
+ * ```
2497
+ *
2498
+ * @capability None.
2499
+ * @since 1.5.55
2500
+ */
2501
+ installKeepsDisabled: boolean;
2470
2502
  /**
2471
2503
  * Remote images (`plugin.ui.markdown`, `plugin.ui.remoteImage`): address
2472
2504
  * → a local PNG or JPEG file served for it, or `null` for a failing
@@ -3099,7 +3099,13 @@ export class TestHost {
3099
3099
  * `.vxp` file) → its manifest. Other paths fail with `missing`. `install`
3100
3100
  * adds or upgrades the entry in {@link TestHost.packages} following the
3101
3101
  * player's source rules: the same source upgrades, another source needs
3102
- * `replace` (`conflict` otherwise), a built-in id is `unsupported`.
3102
+ * `replace` (`conflict` otherwise), a built-in id is `unsupported`. As
3103
+ * player 0.6.4 (planned), the installed entry's `state` follows
3104
+ * {@link "@vestra-player/sdk"!InstallOptions.enabled}: a first install
3105
+ * runs, an update of a `disabled` entry stays `disabled` unless `enabled`
3106
+ * is `true`, and `enabled: false` installs it `disabled`. Set
3107
+ * {@link TestHost.installKeepsDisabled} to `false` for the older players'
3108
+ * rule (every install runs).
3103
3109
  *
3104
3110
  * @example Offer a package file the plugin can install
3105
3111
  * The plugin can validate and install `/downloads/someone.demo-1.0.0.vxp`
@@ -3116,6 +3122,32 @@ export class TestHost {
3116
3122
  * ```
3117
3123
  */
3118
3124
  candidates = new Map();
3125
+ /**
3126
+ * Which player's install rule `packages.install` plays. `true` (default,
3127
+ * SDK 1.5.55): player 0.6.4 (planned), where an update or reinstall
3128
+ * keeps a `disabled` plugin disabled unless the plugin passes
3129
+ * {@link "@vestra-player/sdk"!InstallOptions.enabled}. `false`: players
3130
+ * up to 0.6.3, where every install runs the plugin and `enabled` is
3131
+ * ignored (it is still checked and recorded in
3132
+ * {@link TestHost.packageCalls}). A test written for the older rule that
3133
+ * updates a disabled plugin sets it to `false`; one that keeps testing
3134
+ * both runs once with each.
3135
+ *
3136
+ * @example Keep a test written for players up to 0.6.3
3137
+ * The plugin under test installs an update over a plugin the user
3138
+ * disabled and expects it to run afterwards, as players up to 0.6.3 do.
3139
+ * With `false` the host plays that rule; the default plays player 0.6.4,
3140
+ * where the update stays disabled.
3141
+ * ```ts
3142
+ * declare const host: TestHost;
3143
+ * // ---cut---
3144
+ * host.installKeepsDisabled = false; // every install runs, as before 0.6.4
3145
+ * ```
3146
+ *
3147
+ * @capability None.
3148
+ * @since 1.5.55
3149
+ */
3150
+ installKeepsDisabled = true;
3119
3151
  /**
3120
3152
  * Remote images (`plugin.ui.markdown`, `plugin.ui.remoteImage`): address
3121
3153
  * → a local PNG or JPEG file served for it, or `null` for a failing
@@ -5105,6 +5137,10 @@ export class TestHost {
5105
5137
  if (source.kind === "bundled")
5106
5138
  throw new HostError("invalid", "source bundled is reserved");
5107
5139
  const replace = p.replace === true;
5140
+ // As player 0.6.4: `enabled` is a boolean, or absent (`null` too).
5141
+ const requested = p.enabled ?? undefined;
5142
+ if (requested !== undefined && typeof requested !== "boolean")
5143
+ throw new HostError("invalid", "enabled must be a boolean");
5108
5144
  const entry = this.packages.find((each) => each.id === manifest.id);
5109
5145
  if (entry?.builtin)
5110
5146
  throw new HostError("unsupported", "built-in plugins are updated with the player");
@@ -5120,12 +5156,19 @@ export class TestHost {
5120
5156
  id: manifest.id,
5121
5157
  source,
5122
5158
  replace,
5159
+ ...(requested === undefined ? {} : { enabled: requested }),
5123
5160
  });
5161
+ // As player 0.6.4: a first install runs; an update or reinstall (any
5162
+ // source, `replace` included) keeps a disabled plugin disabled unless
5163
+ // `enabled` is given. Players up to 0.6.3 run every install.
5164
+ const enabled = !this.installKeepsDisabled
5165
+ ? true
5166
+ : (requested ?? entry?.state !== "disabled");
5124
5167
  const next = {
5125
5168
  id: manifest.id,
5126
5169
  version: manifest.version,
5127
5170
  origin: "installed",
5128
- state: "running",
5171
+ state: enabled ? "running" : "disabled",
5129
5172
  error: null,
5130
5173
  capabilities: [...manifest.capabilities],
5131
5174
  directory: `/test-host/packages/${manifest.id}`,
@@ -592,15 +592,33 @@ export interface Plugin extends Scope {
592
592
  * - `uninstall`: the user uninstalls it.
593
593
  * - `reload`: it is reloaded (from the plugin manager, or a development
594
594
  * load with `--plugin`).
595
- * - `upgrade`: a new version replaces it.
596
- * - `idle`: lazy activation stops it after a while without use; its next
597
- * use starts it again ([Idle plugins](../../docs/explanation/plugins.md#idle-plugins-are-stopped)).
598
- * - `failed`: the player stops it for a fault (planned to cover an
599
- * unreadable or oversized line, a malformed request, an input queue that
600
- * overflowed, an uncaught exception in the shared process).
595
+ * - `upgrade`: an install or update replaces the running version; also a
596
+ * new version stopped because its install was cancelled (the previous
597
+ * version comes back).
598
+ * - `idle`: lazy activation parks it; its next use starts it again
599
+ * ([Idle plugins](../../docs/explanation/plugins.md#idle-plugins-are-stopped)).
600
+ * Lazy activation is off by default (`VESTRA_LAZY=1` turns it on); when
601
+ * on, it parks a plugin after a while without use, and parks one whose
602
+ * first run it judges lazy right after it is ready.
603
+ * - `failed`: the player stops it for a fault it can still tell the plugin
604
+ * about (planned to cover a malformed request, an unreadable or
605
+ * oversized line, an activation timeout, an uncaught exception the
606
+ * shared process attributes to this plugin).
601
607
  * - `host-lost`: the plugin's runner saw its pipe to the player close
602
608
  * without a stop: the player crashed or was killed. Requests are refused
603
- * with `disabled`; clean up locally only.
609
+ * with `disabled`; clean up locally only. Player 0.6.4 sends `quit` on a
610
+ * normal exit; read `host-lost` as "the player is gone", not as proof of
611
+ * a crash.
612
+ *
613
+ * Some ends are no stop at all: the plugin gets no reason and its
614
+ * {@link Scope.onDispose} cleanups do not run. That is the case when its
615
+ * own process is killed because its input queue is full or a write to it
616
+ * failed, when its own process fails to activate (the runner exits without
617
+ * cleaning up), when the process has already exited, and when the shared
618
+ * runner is restarted as a whole (a hang, a fault it cannot attribute, an
619
+ * unreadable line of its own, a member's full input queue, the runner
620
+ * exiting): every member is killed with it. Keep state that must survive
621
+ * such an end in storage as it changes, not only in a cleanup.
604
622
  *
605
623
  * Planned for player 0.6.4: the player sends the reason with
606
624
  * `runtime.stop` and its runner reports `host-lost`; the exact list may
@@ -3122,7 +3140,13 @@ export interface Appearance {
3122
3140
  * Needs `events`: without it the listener never runs for other plugins'
3123
3141
  * changes. Views need no help to follow the palette; use this for things
3124
3142
  * the plugin draws itself (a rendered image, colours it computes). Errors
3125
- * go to {@link Scope.onError}.
3143
+ * go to {@link Scope.onError}. When the player starts a stopped plugin
3144
+ * for a palette change (player 0.5.21), {@link Appearance.theme} already
3145
+ * has the new palette at start and the listener still hears the change
3146
+ * (SDK 1.5.56; before, it did not run). A language change does not call
3147
+ * it, except in one case: a plugin started for a language change alone
3148
+ * may hear the unchanged palette once, since the player's event does not
3149
+ * say which of the two changed.
3126
3150
  *
3127
3151
  * @capability events
3128
3152
  *
@@ -8406,6 +8430,10 @@ export interface Resolvers {
8406
8430
  /**
8407
8431
  * {@link Resolvers.fileTypes} changed: a file resolver was added, or its
8408
8432
  * plugin stopped. Needs `events`; without it the listener never fires.
8433
+ * When the player starts a stopped plugin for this change (player
8434
+ * 0.5.21), {@link Resolvers.fileTypes} already has the new list at start
8435
+ * and the listener still hears the change (SDK 1.5.56; before, it did not
8436
+ * run).
8409
8437
  *
8410
8438
  * @param listener - Receives the new list.
8411
8439
  * @returns A handle that removes the listener.
@@ -8516,6 +8544,27 @@ export interface InstallOptions {
8516
8544
  * reconnects to it.
8517
8545
  */
8518
8546
  replace?: boolean;
8547
+ /**
8548
+ * Whether the plugin runs once installed (SDK 1.5.55). Left out, a first
8549
+ * install runs and an update or reinstall (from any source, `replace`
8550
+ * included) keeps the installed plugin's state: one the user or a store
8551
+ * disabled stays disabled, and only its files are installed (nothing
8552
+ * starts it, so the change commits without an `activating` step). `true`
8553
+ * enables and starts it, for example a version the store disabled that
8554
+ * an update fixes; if it fails to start, the old version is restored,
8555
+ * still disabled. `false` installs it disabled: a running version is
8556
+ * stopped and its session values go, as with {@link Packages.setEnabled}.
8557
+ * Built-in plugins always run (and cannot be installed over). A value
8558
+ * that is not a boolean is `invalid`.
8559
+ *
8560
+ * Planned for player 0.6.4: older players ignore it and an install there
8561
+ * always runs the plugin, so after a committed install call
8562
+ * {@link Packages.setEnabled} when the state matters.
8563
+ *
8564
+ * @player 0.6.4 (planned); older players ignore it (an install always
8565
+ * enables the plugin).
8566
+ */
8567
+ enabled?: boolean;
8519
8568
  }
8520
8569
  /**
8521
8570
  * A validated package directory, from {@link Packages.validate}.
@@ -8560,9 +8609,14 @@ export interface PackageCandidate {
8560
8609
  * a built-in id fails with `unsupported` (built-ins update with the
8561
8610
  * player). A reserved id (`vestra` or `vestra.*`, which every built-in
8562
8611
  * uses) fails with `reserved` (player 0.4.1; already when validated), as
8563
- * only the player installs built-in plugins.
8612
+ * only the player installs built-in plugins. An update keeps a disabled
8613
+ * plugin disabled unless `enabled` says otherwise (planned for player
8614
+ * 0.6.4; see {@link InstallOptions.enabled}).
8564
8615
  *
8565
8616
  * @capability plugins.manage
8617
+ * @throws `invalid` (rejects) An option is malformed: a `source` the
8618
+ * player does not accept, or a `replace` or `enabled` that is not a
8619
+ * boolean.
8566
8620
  * @throws `conflict` (rejects) The plugin is installed from another source
8567
8621
  * and `replace` is not set.
8568
8622
  * @throws `unsupported` (rejects) A built-in plugin's id.
@@ -8585,7 +8639,19 @@ export interface PackageCandidate {
8585
8639
  * declare const candidate: PackageCandidate;
8586
8640
  * // ---cut---
8587
8641
  * const change = await candidate.install({ replace: true });
8588
- * await change.done; // "committed" once the new plugin runs
8642
+ * await change.done; // "committed" once installed (running unless it stays disabled)
8643
+ * ```
8644
+ *
8645
+ * @example Enable a fixed version of a plugin the store disabled
8646
+ * A store disabled a version it revoked; the update that fixes it
8647
+ * should run again. Without `enabled` the update keeps the plugin
8648
+ * disabled (player 0.6.4); `true` enables it once installed. Older
8649
+ * players ignore the option and enable every install.
8650
+ * ```ts
8651
+ * declare const candidate: PackageCandidate;
8652
+ * // ---cut---
8653
+ * const change = await candidate.install({ enabled: true });
8654
+ * await change.done; // "committed" once the fixed version runs
8589
8655
  * ```
8590
8656
  *
8591
8657
  * @since 1.0.0
@@ -9665,7 +9731,10 @@ export interface Storage {
9665
9731
  * | --- | --- |
9666
9732
  * | The plugin's process restarts: reload, upgrade, crash | kept |
9667
9733
  * | The plugin failed to start or failed, and starts again | kept |
9668
- * | The user disables the plugin, or uninstalls it | cleared |
9734
+ * | The user, or a plugin with `plugins.manage` (`packages.setEnabled(id, false)`), disables the plugin | cleared |
9735
+ * | An update or reinstall with `enabled: false` switches an enabled plugin off (player 0.6.4) | cleared |
9736
+ * | The plugin is uninstalled | cleared |
9737
+ * | An earlier installation kept for a later reinstall (after a `replace`) is dropped because the player keeps only so many | cleared |
9669
9738
  * | The player quits | cleared |
9670
9739
  *
9671
9740
  * - **Per plugin.** A plugin sees only its own keys. Like
package/dist/wire.d.ts CHANGED
@@ -731,10 +731,15 @@ export declare function createWire(transport: Transport): {
731
731
  /** Player 0.2.0: a `.vxp` file. */
732
732
  validateArchive: (file: string) => Promise<PackageValidation>;
733
733
  installed: (id: string) => Promise<InstalledPackage>;
734
- /** `source` and `replace` are sent only when given (player 0.2.0). */
734
+ /**
735
+ * `source` and `replace` are sent only when given (player 0.2.0);
736
+ * `enabled` only when given (planned for player 0.6.4, older players
737
+ * ignore it).
738
+ */
735
739
  install: (token: string, expectedRevision: number, requestId: string, options?: {
736
740
  source?: WireSource;
737
741
  replace?: boolean;
742
+ enabled?: boolean;
738
743
  }) => Promise<PackageOperation>;
739
744
  operation: (operationId: string) => Promise<PackageOperation>;
740
745
  cancel: (operationId: string) => Promise<PackageOperation>;
package/dist/wire.js CHANGED
@@ -341,13 +341,20 @@ export function createWire(transport) {
341
341
  file,
342
342
  }),
343
343
  installed: (id) => transport.request("packages.installed", { id }),
344
- /** `source` and `replace` are sent only when given (player 0.2.0). */
344
+ /**
345
+ * `source` and `replace` are sent only when given (player 0.2.0);
346
+ * `enabled` only when given (planned for player 0.6.4, older players
347
+ * ignore it).
348
+ */
345
349
  install: (token, expectedRevision, requestId, options = {}) => transport.request("packages.install", {
346
350
  token,
347
351
  expectedRevision,
348
352
  requestId,
349
353
  ...(options.source === undefined ? {} : { source: options.source }),
350
354
  ...(options.replace ? { replace: true } : {}),
355
+ ...(options.enabled === undefined
356
+ ? {}
357
+ : { enabled: options.enabled }),
351
358
  }),
352
359
  operation: (operationId) => transport.request("packages.operation", {
353
360
  operationId,
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.54: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
3
+ > @vestra-player/sdk 1.5.57: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
4
4
 
5
5
  ## Read the documentation
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.54",
3
+ "version": "1.5.57",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {