@vestra-player/sdk 1.5.0 → 1.5.1

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.1
4
+
5
+ Fix; no API or type changes.
6
+
7
+ - **`storage.json` saves no longer collide.** Every save wrote through the same temporary file (`<name>.json.tmp-<pid>`), and only the writes of one store were queued. Two stores on the same file (the same name opened twice, in one scope or two) could therefore rename each other's temporary file away mid-save, and the save failed with `ENOENT` (on `chmod` or `rename`); on Windows two renames onto the file at once also fail with `EPERM`. Seen intermittently on Windows.
8
+ - Writes to one file are now queued together, whichever store makes them, and land in the order they were made.
9
+ - Each write has a temporary file of its own, made owner-only (`0600`) through its file handle before it gets the store's name, and removed again if the write fails.
10
+ - On Windows, replacing the file is retried for up to 2 s while another program (a virus scanner, the search indexer) briefly holds it open.
11
+
3
12
  ## 1.5.0
4
13
 
5
14
  **Own visual identity: the Aurora palettes and the V-Line icons** (player 0.4.0). Additive; `apiVersion` stays `vestra/1`. Plugins that use the additions set `"minPlayerVersion": "0.4.0"`.
package/README.md CHANGED
@@ -33,7 +33,13 @@ npm i -D @vestra-player/sdk # Node.js 26 or newer
33
33
  `manifest.json` and `main.ts`:
34
34
 
35
35
  ```json
36
- { "id": "examples.hello", "version": "0.1.0", "apiVersion": "vestra/1", "entry": "main.ts", "capabilities": ["commands", "ui.osd"] }
36
+ {
37
+ "id": "examples.hello",
38
+ "version": "0.1.0",
39
+ "apiVersion": "vestra/1",
40
+ "entry": "main.ts",
41
+ "capabilities": ["commands", "ui.osd"]
42
+ }
37
43
  ```
38
44
 
39
45
  ```ts
@@ -54,33 +60,34 @@ Run `npx vestra-plugin pack` to check and package it, or start the player with `
54
60
 
55
61
  Complete, tested plugins in [`examples/`](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples). The [examples page](https://vestrasys.github.io/vestra-player-sdk/documents/Examples.html) shows each one with a screenshot and a step-by-step walkthrough.
56
62
 
57
- | Example | What you learn | Walkthrough |
58
- | --- | --- | --- |
59
- | [hello](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/hello) | The smallest plugin: a manifest, one menu command, an on-screen message. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Hello.html) |
60
- | [blur](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/blur) | Video filters, a tool window with live controls, a shortcut, a settings page, remembered state. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Blur.html) |
61
- | [signal-generator](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/signal-generator) | Generating video and audio frames yourself, phase-continuous audio, audio filters, stream lifecycle. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Signal_generator.html) |
62
- | [bookmark](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/bookmark) | Reading and controlling playback from a tool window. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Bookmark.html) |
63
- | [panel-menu](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/panel-menu) | A sidebar panel with its own context menu, panel-only keys and next/previous. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Panel_menu.html) |
64
- | [core-ui](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/core-ui) | Menus (radio items, groups, changing titles), repeating keys, the shared sidebar, theme widgets. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Core_UI.html) |
65
- | [markdown](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/markdown) | The Markdown view with local and remote images; the manifest's display fields. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Markdown.html) |
63
+ | Example | What you learn | Walkthrough |
64
+ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
65
+ | [hello](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/hello) | The smallest plugin: a manifest, one menu command, an on-screen message. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Hello.html) |
66
+ | [blur](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/blur) | A video filter driven by one slider in a tool window. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Blur.html) |
67
+ | [signal-generator](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/signal-generator) | Generating video and audio frames yourself: colour bars and a sine tone. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Signal_generator.html) |
68
+ | [bookmark](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/bookmark) | A tool window bound to the playback state, with Mark and Jump buttons. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Bookmark.html) |
69
+ | [panel-menu](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/panel-menu) | A sidebar panel with its own right-click menu. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Panel_menu.html) |
70
+ | [menus](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/menus) | Radio items, separator groups and a menu item whose title changes. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Menus.html) |
71
+ | [theme-widgets](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/theme-widgets) | A tool window drawn with the player's theme and widgets. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Theme_widgets.html) |
72
+ | [markdown](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/markdown) | A window that shows a Markdown document. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Markdown.html) |
66
73
 
67
74
  ## Command line
68
75
 
69
- | Command | Does |
70
- | --- | --- |
71
- | `vestra-plugin check [dir...]` | Checks the manifest, then a strict type check of the plugin and its tests. |
72
- | `vestra-plugin test [dir...]` | Runs `node --test` on `tests/*.test.{ts,mjs}`. |
73
- | `vestra-plugin build [dir...] [--version x.y.z]` | Writes `dist/`: types stripped, files copied, manifest versioned. |
74
- | `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]` | Builds, then packs `dist/` as `<id>-<version>.vxp`. |
76
+ | Command | Does |
77
+ | ----------------------------------------------------------- | -------------------------------------------------------------------------- |
78
+ | `vestra-plugin check [dir...]` | Checks the manifest, then a strict type check of the plugin and its tests. |
79
+ | `vestra-plugin test [dir...]` | Runs `node --test` on `tests/*.test.{ts,mjs}`. |
80
+ | `vestra-plugin build [dir...] [--version x.y.z]` | Writes `dist/`: types stripped, files copied, manifest versioned. |
81
+ | `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]` | Builds, then packs `dist/` as `<id>-<version>.vxp`. |
75
82
 
76
83
  ## Compatibility
77
84
 
78
- | SDK | Plugin API | Vestra Player | Node.js |
79
- | --- | --- | --- | --- |
80
- | 1.0 | `vestra/1` | 0.1 or newer | 22.18 or newer |
81
- | 1.1 | `vestra/1` | 0.2 or newer for the 1.1 additions | 22.18 or newer |
82
- | 1.2 | `vestra/1` | 0.2 or newer | 26 (shipped with the player) |
83
- | 1.3 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions (menu ids, `Theme.danger`, empty video chains) | 26 (shipped with the player) |
85
+ | SDK | Plugin API | Vestra Player | Node.js |
86
+ | --- | ---------- | ----------------------------------------------------------------------------------------- | ---------------------------- |
87
+ | 1.0 | `vestra/1` | 0.1 or newer | 22.18 or newer |
88
+ | 1.1 | `vestra/1` | 0.2 or newer for the 1.1 additions | 22.18 or newer |
89
+ | 1.2 | `vestra/1` | 0.2 or newer | 26 (shipped with the player) |
90
+ | 1.3 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions (menu ids, `Theme.danger`, empty video chains) | 26 (shipped with the player) |
84
91
  | 1.4 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions. `ThemeId` is `"vestra"` only (see the CHANGELOG) | 26 (shipped with the player) |
85
92
  | 1.5 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions; 0.4 for the 1.5 additions (`vestra-light`, `@vestra/icons.slint`) | 26 (shipped with the player) |
86
93
 
@@ -4,7 +4,7 @@
4
4
  * run in this plugin's own process.
5
5
  */
6
6
  import { existsSync, readFileSync, renameSync } from "node:fs";
7
- import { chmod, mkdir, rename, stat, writeFile } from "node:fs/promises";
7
+ import { mkdir, open, rename, rm, stat } from "node:fs/promises";
8
8
  import { join, resolve } from "node:path";
9
9
  import { failure } from "./runtime.js";
10
10
  import { systemServices } from "./local.js";
@@ -101,6 +101,54 @@ export function tools(scope) {
101
101
  };
102
102
  }
103
103
  const NAME = /^[a-z0-9-]{1,64}$/;
104
+ /** Writes so far in this process; numbers each write's temporary file. */
105
+ let written = 0;
106
+ /**
107
+ * The pending writes of each file, whichever store they come from: the same
108
+ * name opened twice must not write or rename into one file at once.
109
+ */
110
+ const writes = new Map();
111
+ /** Write `text` to `path` atomically, owner-only (0600). */
112
+ async function writeAtomic(path, text) {
113
+ // A name of its own per write, so no other write renames it away.
114
+ const temporary = `${path}.tmp-${process.pid}-${++written}`;
115
+ await mkdir(join(path, ".."), { recursive: true });
116
+ try {
117
+ // Settings can hold private paths and history. The mode is set through
118
+ // the handle, before the file has its name.
119
+ const file = await open(temporary, "w", 0o600);
120
+ try {
121
+ await file.chmod(0o600);
122
+ await file.writeFile(text);
123
+ }
124
+ finally {
125
+ await file.close();
126
+ }
127
+ await replace(temporary, path);
128
+ }
129
+ catch (error) {
130
+ await rm(temporary, { force: true }).catch(() => { });
131
+ throw error;
132
+ }
133
+ }
134
+ /**
135
+ * Rename over `to`. Windows refuses while another program (a virus scanner,
136
+ * the search indexer) has `to` open for a moment: retry for up to 2 s there.
137
+ */
138
+ async function replace(from, to) {
139
+ for (let attempt = 0;; attempt++)
140
+ try {
141
+ return await rename(from, to);
142
+ }
143
+ catch (error) {
144
+ const code = error.code;
145
+ if (process.platform !== "win32" ||
146
+ !(code === "EPERM" || code === "EACCES" || code === "EBUSY") ||
147
+ attempt >= 20)
148
+ throw error;
149
+ await new Promise((resolve) => setTimeout(resolve, 10 + attempt * 10));
150
+ }
151
+ }
104
152
  class Store {
105
153
  #scope;
106
154
  #path;
@@ -159,17 +207,19 @@ class Store {
159
207
  return this.#writing;
160
208
  this.#dirty = false;
161
209
  const text = `${JSON.stringify(this.#value, null, 2)}\n`;
162
- // One write at a time, in order; the newest value is written last.
163
- this.#writing = this.#writing
210
+ // One write per file at a time, in order; the newest value is written
211
+ // last. The queue is per file, not per store (see `writes`).
212
+ const path = this.#path;
213
+ const write = (writes.get(path) ?? Promise.resolve())
164
214
  .catch(() => { })
165
- .then(async () => {
166
- const temporary = `${this.#path}.tmp-${process.pid}`;
167
- await mkdir(join(this.#path, ".."), { recursive: true });
168
- // Owner-only: settings can hold private paths and history.
169
- await writeFile(temporary, text, { mode: 0o600 });
170
- await chmod(temporary, 0o600);
171
- await rename(temporary, this.#path);
172
- });
215
+ .then(() => writeAtomic(path, text));
216
+ const done = () => {
217
+ if (writes.get(path) === write)
218
+ writes.delete(path);
219
+ };
220
+ writes.set(path, write);
221
+ write.then(done, done);
222
+ this.#writing = write;
173
223
  return this.#writing;
174
224
  }
175
225
  async dispose() {
@@ -1229,8 +1229,17 @@ export interface SlotState {
1229
1229
  *
1230
1230
  * @capability ui.mount; `events` to follow changes made elsewhere.
1231
1231
  *
1232
- * @example A "Show sidebar" menu item that works with any sidebar panel (examples/core-ui)
1233
- * {@includeCode ../../examples/core-ui/main.ts#sidebar}
1232
+ * @example A "Show sidebar" menu item that works with any sidebar panel
1233
+ * ```ts
1234
+ * const sidebar = plugin.panels.sidebar;
1235
+ * plugin.commands.add({
1236
+ * id: "sidebar",
1237
+ * path: ["Tools", "Show sidebar"],
1238
+ * checked: () => sidebar.state?.preferred ?? false,
1239
+ * enabled: () => sidebar.state !== null,
1240
+ * run: () => sidebar.toggle(),
1241
+ * });
1242
+ * ```
1234
1243
  *
1235
1244
  * @see {@link Panels}, {@link SlotState}
1236
1245
  * @since 1.0.0
@@ -1330,8 +1339,17 @@ export interface PanelSlot {
1330
1339
  *
1331
1340
  * @capability ui.mount
1332
1341
  *
1333
- * @example
1334
- * {@includeCode ../../examples/core-ui/main.ts#sidebar}
1342
+ * @example A "Detach sidebar" menu item
1343
+ * ```ts
1344
+ * const sidebar = plugin.panels.sidebar;
1345
+ * plugin.commands.add({
1346
+ * id: "detach",
1347
+ * path: ["Tools", "Detach sidebar"],
1348
+ * checked: () => sidebar.state?.detached ?? false,
1349
+ * enabled: () => sidebar.state !== null,
1350
+ * run: () => sidebar.setDetached(!sidebar.state?.detached),
1351
+ * });
1352
+ * ```
1335
1353
  *
1336
1354
  * @since 1.0.0
1337
1355
  * @category UI
@@ -1714,12 +1732,59 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
1714
1732
  *
1715
1733
  * @capability ui.settings
1716
1734
  *
1717
- * @example Two preferences on a page (examples/blur)
1718
- * {@includeCode ../../examples/blur/main.ts#settings-page}
1735
+ * @example Two preferences on a page, kept in a store
1736
+ * ```ts
1737
+ * const store = plugin.storage.json("blur", {
1738
+ * defaults: { restoreOnStart: false, messages: true },
1739
+ * });
1740
+ * const page = plugin.settings.page("ui/settings.slint", {
1741
+ * id: "blur",
1742
+ * category: "video",
1743
+ * title: "Blur",
1744
+ * });
1745
+ * page.bind(() => ({
1746
+ * restoreOnStart: store.value.restoreOnStart,
1747
+ * messages: store.value.messages,
1748
+ * }));
1749
+ * page.on("restoreToggled", (on: boolean) =>
1750
+ * store.save({ ...store.value, restoreOnStart: on }),
1751
+ * );
1752
+ * page.on("messagesToggled", (on: boolean) =>
1753
+ * store.save({ ...store.value, messages: on }),
1754
+ * );
1755
+ * ```
1756
+ *
1757
+ * `ui/settings.slint` exports the bridge global and the layout:
1719
1758
  *
1720
- * `ui/settings.slint`:
1759
+ * ```slint
1760
+ * import { CheckBox } from "std-widgets.slint";
1761
+ * import { Theme } from "@vestra/theme.slint";
1721
1762
  *
1722
- * {@includeCode ../../examples/blur/ui/settings.slint}
1763
+ * export global BlurPage {
1764
+ * in-out property <bool> restore-on-start;
1765
+ * in-out property <bool> messages: true;
1766
+ * callback restore-toggled(bool);
1767
+ * callback messages-toggled(bool);
1768
+ * }
1769
+ *
1770
+ * export component BlurSettings inherits Rectangle {
1771
+ * VerticalLayout {
1772
+ * padding: Theme.dialog-padding;
1773
+ * spacing: 6px;
1774
+ * alignment: start;
1775
+ * CheckBox {
1776
+ * text: "Turn blur back on when the player starts";
1777
+ * checked <=> BlurPage.restore-on-start;
1778
+ * toggled => { BlurPage.restore-toggled(self.checked); }
1779
+ * }
1780
+ * CheckBox {
1781
+ * text: "Show on-screen messages";
1782
+ * checked <=> BlurPage.messages;
1783
+ * toggled => { BlurPage.messages-toggled(self.checked); }
1784
+ * }
1785
+ * }
1786
+ * }
1787
+ * ```
1723
1788
  *
1724
1789
  * @see {@link Settings.page}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
1725
1790
  * @since 1.0.0
@@ -1854,7 +1919,7 @@ export interface Settings {
1854
1919
  * @throws `capability` (rejects) The manifest does not declare
1855
1920
  * `ui.settings`.
1856
1921
  *
1857
- * @example An "Options…" button in a tool window (examples/blur)
1922
+ * @example An "Options…" button in a tool window
1858
1923
  * ```ts
1859
1924
  * const controls = plugin.windows.create("ui/window.slint");
1860
1925
  * controls.on("openOptions", () => plugin.settings.show("blur"));
@@ -3051,8 +3116,14 @@ export interface OsdPanel extends Disposable {
3051
3116
  *
3052
3117
  * @capability ui.osd
3053
3118
  *
3054
- * @example On-screen messages that the user can turn off (examples/blur)
3055
- * {@includeCode ../../examples/blur/main.ts#osd}
3119
+ * @example On-screen messages that the user can turn off
3120
+ * ```ts
3121
+ * const store = plugin.storage.json("prefs", { defaults: { messages: true } });
3122
+ * const say = (text: string) => {
3123
+ * if (store.value.messages) void plugin.osd.show(text);
3124
+ * };
3125
+ * say("Blur on");
3126
+ * ```
3056
3127
  *
3057
3128
  * @see {@link OsdPanel}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
3058
3129
  * @since 1.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
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": {