@vestra-player/sdk 1.5.24 → 1.5.25
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 +9 -0
- package/dist/player-runtime.d.ts +1 -1
- package/dist/player-runtime.js +1 -1
- package/dist/plugin/services.d.ts +35 -0
- package/dist/plugin/services.js +87 -2
- package/dist/plugin/testing.d.ts +24 -1
- package/dist/plugin/testing.js +71 -1
- package/dist/plugin/types.d.ts +77 -0
- package/dist/wire.d.ts +12 -0
- package/dist/wire.js +10 -0
- package/llms-full.txt +151 -3
- package/llms.txt +22 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.25
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** Plugins built on earlier SDKs keep working unchanged: `plugin.storage.json` and every other API do what they did. Additive; `apiVersion` stays `vestra/1`. The addition needs player 0.5.5.
|
|
6
|
+
|
|
7
|
+
- **`plugin.storage.session`** (`SessionStorage`, player 0.5.5; user ruling of 2026-10-27, after Chrome's `chrome.storage.session`): `get(key)`, `set(key, value)`, `delete(key)` and `keys()` of JSON values that live as long as the **player**, not the plugin's process. They survive every restart of the plugin within one player session (a reload, also of a development load, an upgrade, a crash, a failed start) and are cleared when the user disables or uninstalls the plugin, and when the player quits. Per plugin, no capability. Keys are 1–1024 bytes of well-formed text; a plugin's keys, values (as compact JSON) and 128 bytes per key are at most 1 MiB together, and a `set` beyond fails with `limit` and changes nothing. One value must also fit in one 1 MiB message. A value `JSON.stringify` cannot write, or one with a lone surrogate in a string, fails with `invalid`; the order of an object's properties is not kept; `get` returns `undefined` for a missing key. Each `set` is one acknowledged request, without debounce. Players before 0.5.5 reject the calls with `unsupported`, so set `"minPlayerVersion": "0.5.5"`.
|
|
8
|
+
- **`SESSION_STORAGE_LIMITS`** (`@vestra-player/sdk/testing` and `/runtime`): `keyBytes` 1024, `bytes` 1 MiB, `entryBytes` 128.
|
|
9
|
+
- **`TestHost.sessionStorage`** (the player's store for the plugin, with the player's key rule and quota, objects stored with sorted properties as the player returns them) and **`startPlugin(..., { sessionStorage })`** to start with the values an earlier run left, to test a restart within one player session. `TestHost` now refuses any request over the 1 MiB message limit with `limit`, as the plugin runner does.
|
|
10
|
+
- Docs: *How a plugin runs* gains *Restarts: activate can run more than once*: when the player restarts a plugin's process, and "keep state that must survive a restart in session storage (or persistent storage)", with a table of what survives what. *Test a plugin* (testing a restart with session values), *Reference: limits* (`SESSION_STORAGE_LIMITS`). `llms.txt` and `llms-full.txt` regenerated.
|
|
11
|
+
|
|
3
12
|
## 1.5.24
|
|
4
13
|
|
|
5
14
|
**Who must change: nobody.** Plugins built on earlier SDKs keep working unchanged: `when: "interlaced"` and `when: "all"` do what they did. Additive; `apiVersion` stays `vestra/1`. The addition needs player 0.5.4.
|
package/dist/player-runtime.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./ca
|
|
|
15
15
|
export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.ts";
|
|
16
16
|
export { BINDING_INPUTS } from "./plugin/bindings.ts";
|
|
17
17
|
export { OSD_LIMITS } from "./plugin/osd.ts";
|
|
18
|
-
export { SECRET_LIMITS } from "./plugin/services.ts";
|
|
18
|
+
export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.ts";
|
|
19
19
|
export { THEME_PALETTES } from "./palettes.ts";
|
|
20
20
|
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.ts";
|
|
21
21
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.ts";
|
package/dist/player-runtime.js
CHANGED
|
@@ -13,7 +13,7 @@ export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./ca
|
|
|
13
13
|
export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.js";
|
|
14
14
|
export { BINDING_INPUTS } from "./plugin/bindings.js";
|
|
15
15
|
export { OSD_LIMITS } from "./plugin/osd.js";
|
|
16
|
-
export { SECRET_LIMITS } from "./plugin/services.js";
|
|
16
|
+
export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.js";
|
|
17
17
|
export { THEME_PALETTES } from "./palettes.js";
|
|
18
18
|
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.js";
|
|
19
19
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.js";
|
|
@@ -4,6 +4,41 @@ export declare const clipboard: (scope: ScopeImpl) => Clipboard;
|
|
|
4
4
|
export declare const host: (scope: ScopeImpl) => Host;
|
|
5
5
|
export declare const power: (scope: ScopeImpl) => Power;
|
|
6
6
|
export declare function tools(scope: ScopeImpl): Tools;
|
|
7
|
+
/**
|
|
8
|
+
* The player's limits on {@link SessionStorage}. A key outside them fails
|
|
9
|
+
* with code `invalid`; a `set` that would make the plugin's entries larger
|
|
10
|
+
* than `bytes` fails with `limit` and changes nothing. A single `set` must
|
|
11
|
+
* also fit in one message to the player
|
|
12
|
+
* ({@link "@vestra-player/sdk/testing"!MESSAGE_LIMIT | MESSAGE_LIMIT}, 1 MiB
|
|
13
|
+
* with the key and the request around the value). The player's tests check
|
|
14
|
+
* that these agree with the player.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import assert from "node:assert/strict";
|
|
19
|
+
* import { SESSION_STORAGE_LIMITS } from "@vestra-player/sdk/testing";
|
|
20
|
+
*
|
|
21
|
+
* const key = "https://example.com/a.mkv";
|
|
22
|
+
* assert.ok(Buffer.byteLength(key) <= SESSION_STORAGE_LIMITS.keyBytes);
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @since 1.5.25
|
|
26
|
+
*/
|
|
27
|
+
export declare const SESSION_STORAGE_LIMITS: Readonly<{
|
|
28
|
+
/** UTF-8 bytes of a key (1024, at least 1; no NUL character). */
|
|
29
|
+
readonly keyBytes: 1024;
|
|
30
|
+
/**
|
|
31
|
+
* Bytes one plugin may hold at the same time: for every key, its UTF-8
|
|
32
|
+
* bytes, its value written as compact JSON (`JSON.stringify(value)`) and
|
|
33
|
+
* `entryBytes`.
|
|
34
|
+
*/
|
|
35
|
+
readonly bytes: number;
|
|
36
|
+
/**
|
|
37
|
+
* What each key costs on top of its text and value (128): the player's
|
|
38
|
+
* own bookkeeping, so that many tiny entries stay within `bytes` of memory.
|
|
39
|
+
*/
|
|
40
|
+
readonly entryBytes: 128;
|
|
41
|
+
}>;
|
|
7
42
|
export declare const storage: (scope: ScopeImpl) => Storage;
|
|
8
43
|
export declare function system(scope: ScopeImpl): System;
|
|
9
44
|
/**
|
package/dist/plugin/services.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared services: the clipboard, sleep prevention, showing and opening files
|
|
3
|
-
* and links, and
|
|
4
|
-
* run in this plugin's own process.
|
|
3
|
+
* and links, secrets and session storage go to the host; external tools and
|
|
4
|
+
* settings files run in this plugin's own process.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
7
7
|
import { mkdir, open, rename, rm, stat } from "node:fs/promises";
|
|
@@ -238,6 +238,90 @@ class Store {
|
|
|
238
238
|
this.#scope.forget(this);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* The player's limits on {@link SessionStorage}. A key outside them fails
|
|
243
|
+
* with code `invalid`; a `set` that would make the plugin's entries larger
|
|
244
|
+
* than `bytes` fails with `limit` and changes nothing. A single `set` must
|
|
245
|
+
* also fit in one message to the player
|
|
246
|
+
* ({@link "@vestra-player/sdk/testing"!MESSAGE_LIMIT | MESSAGE_LIMIT}, 1 MiB
|
|
247
|
+
* with the key and the request around the value). The player's tests check
|
|
248
|
+
* that these agree with the player.
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* import assert from "node:assert/strict";
|
|
253
|
+
* import { SESSION_STORAGE_LIMITS } from "@vestra-player/sdk/testing";
|
|
254
|
+
*
|
|
255
|
+
* const key = "https://example.com/a.mkv";
|
|
256
|
+
* assert.ok(Buffer.byteLength(key) <= SESSION_STORAGE_LIMITS.keyBytes);
|
|
257
|
+
* ```
|
|
258
|
+
*
|
|
259
|
+
* @since 1.5.25
|
|
260
|
+
*/
|
|
261
|
+
export const SESSION_STORAGE_LIMITS = Object.freeze({
|
|
262
|
+
/** UTF-8 bytes of a key (1024, at least 1; no NUL character). */
|
|
263
|
+
keyBytes: 1024,
|
|
264
|
+
/**
|
|
265
|
+
* Bytes one plugin may hold at the same time: for every key, its UTF-8
|
|
266
|
+
* bytes, its value written as compact JSON (`JSON.stringify(value)`) and
|
|
267
|
+
* `entryBytes`.
|
|
268
|
+
*/
|
|
269
|
+
bytes: 1024 * 1024,
|
|
270
|
+
/**
|
|
271
|
+
* What each key costs on top of its text and value (128): the player's
|
|
272
|
+
* own bookkeeping, so that many tiny entries stay within `bytes` of memory.
|
|
273
|
+
*/
|
|
274
|
+
entryBytes: 128,
|
|
275
|
+
});
|
|
276
|
+
/** A lone surrogate: the player's JSON parser refuses the whole message. */
|
|
277
|
+
const LONE_SURROGATE = /[\uD800-\uDFFF]/u;
|
|
278
|
+
/** Whether a parsed JSON value has a string or a property name with a lone surrogate. */
|
|
279
|
+
function hasLoneSurrogate(value) {
|
|
280
|
+
if (typeof value === "string")
|
|
281
|
+
return LONE_SURROGATE.test(value);
|
|
282
|
+
if (Array.isArray(value))
|
|
283
|
+
return value.some(hasLoneSurrogate);
|
|
284
|
+
if (value && typeof value === "object")
|
|
285
|
+
return Object.entries(value).some(([key, item]) => LONE_SURROGATE.test(key) || hasLoneSurrogate(item));
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
function sessionStorage(scope) {
|
|
289
|
+
const keyed = (key) => {
|
|
290
|
+
if (typeof key !== "string" ||
|
|
291
|
+
!key ||
|
|
292
|
+
key.includes("\0") ||
|
|
293
|
+
LONE_SURROGATE.test(key) ||
|
|
294
|
+
Buffer.byteLength(key) > SESSION_STORAGE_LIMITS.keyBytes)
|
|
295
|
+
throw failure("invalid", `session storage keys are 1–${SESSION_STORAGE_LIMITS.keyBytes} bytes of well-formed text without NUL`);
|
|
296
|
+
return key;
|
|
297
|
+
};
|
|
298
|
+
const host = () => scope.runtime.host.storageSession;
|
|
299
|
+
return {
|
|
300
|
+
async get(key) {
|
|
301
|
+
const found = await host().get(keyed(key));
|
|
302
|
+
return "value" in found ? found.value : undefined;
|
|
303
|
+
},
|
|
304
|
+
async set(key, value) {
|
|
305
|
+
const name = keyed(key);
|
|
306
|
+
let text;
|
|
307
|
+
try {
|
|
308
|
+
text = JSON.stringify(value);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
text = undefined; // a cycle or a BigInt
|
|
312
|
+
}
|
|
313
|
+
if (text === undefined)
|
|
314
|
+
throw failure("invalid", "session storage values must be JSON; delete() removes one");
|
|
315
|
+
// What the player stores, and what `get` will return.
|
|
316
|
+
const json = JSON.parse(text);
|
|
317
|
+
if (hasLoneSurrogate(json))
|
|
318
|
+
throw failure("invalid", "session storage values must be well-formed text: a string has a lone surrogate");
|
|
319
|
+
await host().set(name, json);
|
|
320
|
+
},
|
|
321
|
+
delete: async (key) => host().delete(keyed(key)),
|
|
322
|
+
keys: async () => host().keys(),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
241
325
|
export const storage = (scope) => ({
|
|
242
326
|
json(name, options) {
|
|
243
327
|
if (typeof name !== "string" || !NAME.test(name))
|
|
@@ -247,6 +331,7 @@ export const storage = (scope) => ({
|
|
|
247
331
|
throw failure("invalid", "debounceMs must be a non-negative number");
|
|
248
332
|
return new Store(scope, scope.dataDirectory, name, options);
|
|
249
333
|
},
|
|
334
|
+
session: sessionStorage(scope),
|
|
250
335
|
});
|
|
251
336
|
export function system(scope) {
|
|
252
337
|
const exists = async (path) => {
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { BuiltinCommand, BuiltinSubmenu } from "../commands.ts";
|
|
|
7
7
|
import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.ts";
|
|
8
8
|
import type { CapabilityInfo } from "../capabilities.ts";
|
|
9
9
|
import { OSD_LIMITS } from "./osd.ts";
|
|
10
|
-
import { SECRET_LIMITS } from "./services.ts";
|
|
10
|
+
import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.ts";
|
|
11
11
|
import { BINDING_INPUTS } from "./bindings.ts";
|
|
12
12
|
import { THEME_PALETTES } from "../palettes.ts";
|
|
13
13
|
import { IMAGE_LIMITS, UI_VALUE_LIMITS } from "./values.ts";
|
|
@@ -34,6 +34,8 @@ export { MESSAGE_LIMIT };
|
|
|
34
34
|
export { OSD_LIMITS };
|
|
35
35
|
/** The player's limits on stored secrets. */
|
|
36
36
|
export { SECRET_LIMITS };
|
|
37
|
+
/** The player's limits on `plugin.storage.session`. */
|
|
38
|
+
export { SESSION_STORAGE_LIMITS };
|
|
37
39
|
/** The names bindings are made of. */
|
|
38
40
|
export { BINDING_INPUTS };
|
|
39
41
|
/** The colours of the player's palettes, by `Theme` token. */
|
|
@@ -1072,6 +1074,18 @@ export declare class TestHost implements Transport {
|
|
|
1072
1074
|
* new name fails `limit` once 256 names are stored.
|
|
1073
1075
|
*/
|
|
1074
1076
|
readonly secrets: Map<string, string>;
|
|
1077
|
+
/**
|
|
1078
|
+
* `plugin.storage.session`: key → value, as the player keeps them. Preset
|
|
1079
|
+
* entries to simulate values an earlier run of the plugin stored in the
|
|
1080
|
+
* same player session, or pass `startPlugin(..., { sessionStorage })`.
|
|
1081
|
+
* Like the player, a key is 1–1024 bytes, a `set` that would make the
|
|
1082
|
+
* entries larger than 1 MiB (keys, values as compact JSON and
|
|
1083
|
+
* `SESSION_STORAGE_LIMITS.entryBytes` per key) fails `limit`, and an
|
|
1084
|
+
* object's properties are stored sorted, as the player hands them back.
|
|
1085
|
+
*
|
|
1086
|
+
* @since 1.5.25
|
|
1087
|
+
*/
|
|
1088
|
+
readonly sessionStorage: Map<string, Json>;
|
|
1075
1089
|
/** What `plugin.app.system()` reports. */
|
|
1076
1090
|
systemInfo: WireSystemInfo;
|
|
1077
1091
|
/**
|
|
@@ -1594,6 +1608,15 @@ export declare function startPlugin(activate: (plugin: Plugin) => unknown, optio
|
|
|
1594
1608
|
host?: TestHost;
|
|
1595
1609
|
/** Use an existing directory as `plugin.dataDirectory` (to test a restart). */
|
|
1596
1610
|
dataDirectory?: string;
|
|
1611
|
+
/**
|
|
1612
|
+
* Start with these `plugin.storage.session` values (to test a restart
|
|
1613
|
+
* within one player session): pass the previous run's
|
|
1614
|
+
* `host.sessionStorage`. They are copied into the new host's
|
|
1615
|
+
* {@link TestHost.sessionStorage}.
|
|
1616
|
+
*
|
|
1617
|
+
* @since 1.5.25
|
|
1618
|
+
*/
|
|
1619
|
+
sessionStorage?: ReadonlyMap<string, Json>;
|
|
1597
1620
|
/**
|
|
1598
1621
|
* The interface language to play ({@link TestHost.locale}): the plugin's
|
|
1599
1622
|
* `locales/<locale>.po` (or `.json`) in `directory` is loaded and its
|
package/dist/plugin/testing.js
CHANGED
|
@@ -69,7 +69,7 @@ import { localSocketPath } from "./local.js";
|
|
|
69
69
|
import { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "../commands.js";
|
|
70
70
|
import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.js";
|
|
71
71
|
import { OSD_LIMITS } from "./osd.js";
|
|
72
|
-
import { SECRET_LIMITS } from "./services.js";
|
|
72
|
+
import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.js";
|
|
73
73
|
import { BINDING_INPUTS } from "./bindings.js";
|
|
74
74
|
import { THEME_PALETTES } from "../palettes.js";
|
|
75
75
|
import { IMAGE_LIMITS, UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
|
|
@@ -93,6 +93,8 @@ export { MESSAGE_LIMIT };
|
|
|
93
93
|
export { OSD_LIMITS };
|
|
94
94
|
/** The player's limits on stored secrets. */
|
|
95
95
|
export { SECRET_LIMITS };
|
|
96
|
+
/** The player's limits on `plugin.storage.session`. */
|
|
97
|
+
export { SESSION_STORAGE_LIMITS };
|
|
96
98
|
/** The names bindings are made of. */
|
|
97
99
|
export { BINDING_INPUTS };
|
|
98
100
|
/** The colours of the player's palettes, by `Theme` token. */
|
|
@@ -131,6 +133,34 @@ export { LIST_SEPARATORS };
|
|
|
131
133
|
* ```
|
|
132
134
|
*/
|
|
133
135
|
export { LOCALES };
|
|
136
|
+
/** A `plugin.storage.session` key, checked as the player does. */
|
|
137
|
+
function sessionKey(key) {
|
|
138
|
+
if (typeof key !== "string" ||
|
|
139
|
+
!key ||
|
|
140
|
+
key.includes("\0") ||
|
|
141
|
+
Buffer.byteLength(key) > SESSION_STORAGE_LIMITS.keyBytes)
|
|
142
|
+
throw new HostError("invalid", "session storage keys are 1–1024 bytes of text without NUL");
|
|
143
|
+
return key;
|
|
144
|
+
}
|
|
145
|
+
/** Code point (UTF-8 byte) order, as the player sorts session keys. */
|
|
146
|
+
const byCodePoint = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
147
|
+
/**
|
|
148
|
+
* A JSON value as the player hands it back: the player does not keep the
|
|
149
|
+
* order of object properties, it sorts them.
|
|
150
|
+
*/
|
|
151
|
+
function sortedJson(value) {
|
|
152
|
+
if (Array.isArray(value))
|
|
153
|
+
return value.map(sortedJson);
|
|
154
|
+
if (value && typeof value === "object")
|
|
155
|
+
return Object.fromEntries(Object.keys(value)
|
|
156
|
+
.sort(byCodePoint)
|
|
157
|
+
.map((key) => [key, sortedJson(value[key])]));
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
/** What one session entry counts towards the player's quota. */
|
|
161
|
+
const sessionCost = (key, value) => Buffer.byteLength(key) +
|
|
162
|
+
Buffer.byteLength(JSON.stringify(value)) +
|
|
163
|
+
SESSION_STORAGE_LIMITS.entryBytes;
|
|
134
164
|
const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
135
165
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
136
166
|
function initialSource() {
|
|
@@ -1156,6 +1186,18 @@ export class TestHost {
|
|
|
1156
1186
|
* new name fails `limit` once 256 names are stored.
|
|
1157
1187
|
*/
|
|
1158
1188
|
secrets = new Map();
|
|
1189
|
+
/**
|
|
1190
|
+
* `plugin.storage.session`: key → value, as the player keeps them. Preset
|
|
1191
|
+
* entries to simulate values an earlier run of the plugin stored in the
|
|
1192
|
+
* same player session, or pass `startPlugin(..., { sessionStorage })`.
|
|
1193
|
+
* Like the player, a key is 1–1024 bytes, a `set` that would make the
|
|
1194
|
+
* entries larger than 1 MiB (keys, values as compact JSON and
|
|
1195
|
+
* `SESSION_STORAGE_LIMITS.entryBytes` per key) fails `limit`, and an
|
|
1196
|
+
* object's properties are stored sorted, as the player hands them back.
|
|
1197
|
+
*
|
|
1198
|
+
* @since 1.5.25
|
|
1199
|
+
*/
|
|
1200
|
+
sessionStorage = new Map();
|
|
1159
1201
|
/** What `plugin.app.system()` reports. */
|
|
1160
1202
|
systemInfo = {
|
|
1161
1203
|
os: "macos",
|
|
@@ -1622,6 +1664,11 @@ export class TestHost {
|
|
|
1622
1664
|
*/
|
|
1623
1665
|
async request(method, params = {}) {
|
|
1624
1666
|
const record = (params ?? {});
|
|
1667
|
+
// The plugin runner refuses a request over the message limit before
|
|
1668
|
+
// sending it (runtime/runner.ts): the player never sees it.
|
|
1669
|
+
const line = JSON.stringify({ id: this.calls.length + 1, method, params });
|
|
1670
|
+
if (Buffer.byteLength(line, "utf8") + 1 > MESSAGE_LIMIT)
|
|
1671
|
+
throw new HostError("limit", `${method}: request exceeds the player's 1 MiB message limit`);
|
|
1625
1672
|
this.calls.push({ method, params: record });
|
|
1626
1673
|
this.#inFlight++;
|
|
1627
1674
|
try {
|
|
@@ -2709,6 +2756,27 @@ export class TestHost {
|
|
|
2709
2756
|
this.secrets.set(name, String(p.value));
|
|
2710
2757
|
},
|
|
2711
2758
|
"secrets.delete": (p) => void this.secrets.delete(String(p.name)),
|
|
2759
|
+
// As the player (host/src/session_storage.rs).
|
|
2760
|
+
"storage.session.get": (p) => {
|
|
2761
|
+
const key = sessionKey(p.key);
|
|
2762
|
+
return this.sessionStorage.has(key)
|
|
2763
|
+
? { value: structuredClone(this.sessionStorage.get(key)) }
|
|
2764
|
+
: {};
|
|
2765
|
+
},
|
|
2766
|
+
"storage.session.set": (p) => {
|
|
2767
|
+
const key = sessionKey(p.key);
|
|
2768
|
+
if (!("value" in p))
|
|
2769
|
+
throw new HostError("invalid", "set needs a JSON value");
|
|
2770
|
+
let total = sessionCost(key, p.value);
|
|
2771
|
+
for (const [other, value] of this.sessionStorage)
|
|
2772
|
+
if (other !== key)
|
|
2773
|
+
total += sessionCost(other, value);
|
|
2774
|
+
if (total > SESSION_STORAGE_LIMITS.bytes)
|
|
2775
|
+
throw new HostError("limit", `session storage holds at most ${SESSION_STORAGE_LIMITS.bytes} bytes per plugin (keys, JSON values and ${SESSION_STORAGE_LIMITS.entryBytes} per key); this value would make ${total}`);
|
|
2776
|
+
this.sessionStorage.set(key, sortedJson(p.value));
|
|
2777
|
+
},
|
|
2778
|
+
"storage.session.delete": (p) => void this.sessionStorage.delete(sessionKey(p.key)),
|
|
2779
|
+
"storage.session.keys": () => [...this.sessionStorage.keys()].sort(byCodePoint),
|
|
2712
2780
|
"app.system": () => structuredClone(this.systemInfo),
|
|
2713
2781
|
"clipboard.readText": () => this.clipboard.text,
|
|
2714
2782
|
"clipboard.writeText": (p) => {
|
|
@@ -3741,6 +3809,8 @@ export async function startPlugin(activate, options = {}) {
|
|
|
3741
3809
|
host.manifestId = manifest.id;
|
|
3742
3810
|
host.loadCatalog(directory);
|
|
3743
3811
|
const data = options.dataDirectory ?? host.dataDirectory;
|
|
3812
|
+
for (const [key, value] of options.sessionStorage ?? [])
|
|
3813
|
+
host.sessionStorage.set(key, sortedJson(value));
|
|
3744
3814
|
mkdirSync(data, { recursive: true });
|
|
3745
3815
|
const instance = await createPlugin(host, manifest, directory, data, host.app);
|
|
3746
3816
|
await activate(instance.plugin);
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -5232,6 +5232,83 @@ export interface Storage {
|
|
|
5232
5232
|
validate?: (value: unknown) => value is T;
|
|
5233
5233
|
debounceMs?: number;
|
|
5234
5234
|
}): JsonStore<T>;
|
|
5235
|
+
/**
|
|
5236
|
+
* Values the player keeps in its memory for this plugin until the player
|
|
5237
|
+
* quits: they survive a restart of the plugin's process (reload, upgrade,
|
|
5238
|
+
* crash, failed start) but not the player. See {@link SessionStorage}.
|
|
5239
|
+
*
|
|
5240
|
+
* @since 1.5.25
|
|
5241
|
+
*/
|
|
5242
|
+
readonly session: SessionStorage;
|
|
5243
|
+
}
|
|
5244
|
+
/**
|
|
5245
|
+
* `plugin.storage.session`: JSON values that live as long as the **player**,
|
|
5246
|
+
* not as long as the plugin's process (like `chrome.storage.session`). No
|
|
5247
|
+
* capability; needs player 0.5.5 (older players reject the calls with
|
|
5248
|
+
* `unsupported`).
|
|
5249
|
+
*
|
|
5250
|
+
* `activate` can run more than once in one player session: the player stops
|
|
5251
|
+
* and starts a plugin's process again when the plugin is reloaded (also a
|
|
5252
|
+
* development load with `--plugin`) or upgraded, and after a crash or a
|
|
5253
|
+
* failed start. A later player may also restart plugins that share a
|
|
5254
|
+
* process, or stop an idle plugin and start it on its next use. Variables do
|
|
5255
|
+
* not survive that; keep state that must survive a restart here (or in
|
|
5256
|
+
* {@link Storage.json | storage.json} when it must also survive the player
|
|
5257
|
+
* quitting).
|
|
5258
|
+
*
|
|
5259
|
+
* | | Values |
|
|
5260
|
+
* | --- | --- |
|
|
5261
|
+
* | The plugin's process restarts: reload, upgrade, crash | kept |
|
|
5262
|
+
* | The plugin failed to start or failed, and starts again | kept |
|
|
5263
|
+
* | The user disables the plugin, or uninstalls it | cleared |
|
|
5264
|
+
* | The player quits | cleared |
|
|
5265
|
+
*
|
|
5266
|
+
* - **Per plugin.** A plugin sees only its own keys. Like
|
|
5267
|
+
* {@link Plugin.dataDirectory | dataDirectory}, the values belong to the
|
|
5268
|
+
* installation: a development load (`--plugin`) has its own.
|
|
5269
|
+
* - **JSON.** `set` stores what `JSON.stringify(value)` writes, and `get`
|
|
5270
|
+
* returns that, parsed: `undefined` object properties are dropped, a `Date`
|
|
5271
|
+
* comes back as a string, `NaN` as `null`. **The order of an object's
|
|
5272
|
+
* properties is not kept**: they may come back in another order. A value
|
|
5273
|
+
* `JSON.stringify` cannot write (`undefined`, a function, a cycle, a
|
|
5274
|
+
* `BigInt`) or a string with a lone surrogate (`"\uD800"`) fails with
|
|
5275
|
+
* `invalid`. The value is not checked when it comes back: after an upgrade
|
|
5276
|
+
* an older version's value may be read, so check its shape when it matters.
|
|
5277
|
+
* - **Bounded.** Keys are 1–1024 bytes of text. A plugin holds at most 1 MiB,
|
|
5278
|
+
* counting every key, its value as compact JSON and 128 bytes per key
|
|
5279
|
+
* ({@link "@vestra-player/sdk/testing"!SESSION_STORAGE_LIMITS | SESSION_STORAGE_LIMITS}); a
|
|
5280
|
+
* `set` beyond it fails with `limit` and changes nothing. One `set` is
|
|
5281
|
+
* also one message to the player, at most 1 MiB with its key, so a single
|
|
5282
|
+
* value is a little smaller than that; a larger one fails with `limit`.
|
|
5283
|
+
* Split large state over several keys.
|
|
5284
|
+
* - **Each call is one request.** A `set` is kept once its promise
|
|
5285
|
+
* resolves, so a crash right after loses nothing; there is no debounce.
|
|
5286
|
+
* Calls answer in order.
|
|
5287
|
+
*
|
|
5288
|
+
* @example
|
|
5289
|
+
* ```ts
|
|
5290
|
+
* // A queue the plugin is working through: after a reload or a crash it
|
|
5291
|
+
* // goes on where it stopped, after the player quits it starts afresh.
|
|
5292
|
+
* const pending = (await plugin.storage.session.get<string[]>("queue")) ?? [];
|
|
5293
|
+
* pending.push("https://example.com/a.mkv");
|
|
5294
|
+
* await plugin.storage.session.set("queue", pending);
|
|
5295
|
+
* ```
|
|
5296
|
+
*
|
|
5297
|
+
* @since 1.5.25
|
|
5298
|
+
*/
|
|
5299
|
+
export interface SessionStorage {
|
|
5300
|
+
/** The value stored under `key`, or `undefined` when there is none. */
|
|
5301
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
5302
|
+
/**
|
|
5303
|
+
* Store or replace `value` under `key`. `invalid` for a bad key or a value
|
|
5304
|
+
* that is not JSON, `limit` when the plugin's values would be over 1 MiB
|
|
5305
|
+
* or the request over the 1 MiB message limit.
|
|
5306
|
+
*/
|
|
5307
|
+
set(key: string, value: unknown): Promise<void>;
|
|
5308
|
+
/** Remove a key; removing a missing one is fine. */
|
|
5309
|
+
delete(key: string): Promise<void>;
|
|
5310
|
+
/** The keys stored, in code point order. */
|
|
5311
|
+
keys(): Promise<string[]>;
|
|
5235
5312
|
}
|
|
5236
5313
|
/**
|
|
5237
5314
|
* `plugin.system`: files and links, executed by the player. No capability.
|
package/dist/wire.d.ts
CHANGED
|
@@ -488,6 +488,18 @@ export declare function createWire(transport: Transport): {
|
|
|
488
488
|
set: (name: string, value: string) => Promise<void>;
|
|
489
489
|
delete: (name: string) => Promise<void>;
|
|
490
490
|
};
|
|
491
|
+
/**
|
|
492
|
+
* JSON values the player keeps per plugin until it quits (SDK 1.5.25,
|
|
493
|
+
* player 0.5.5). `get` answers `{}` for a missing key.
|
|
494
|
+
*/
|
|
495
|
+
storageSession: {
|
|
496
|
+
get: (key: string) => Promise<{
|
|
497
|
+
value?: Json;
|
|
498
|
+
}>;
|
|
499
|
+
set: (key: string, value: Json) => Promise<void>;
|
|
500
|
+
delete: (key: string) => Promise<void>;
|
|
501
|
+
keys: () => Promise<string[]>;
|
|
502
|
+
};
|
|
491
503
|
app: {
|
|
492
504
|
system: () => Promise<WireSystemInfo>;
|
|
493
505
|
};
|
package/dist/wire.js
CHANGED
|
@@ -182,6 +182,16 @@ export function createWire(transport) {
|
|
|
182
182
|
set: (name, value) => transport.request("secrets.set", { name, value }),
|
|
183
183
|
delete: (name) => transport.request("secrets.delete", { name }),
|
|
184
184
|
},
|
|
185
|
+
/**
|
|
186
|
+
* JSON values the player keeps per plugin until it quits (SDK 1.5.25,
|
|
187
|
+
* player 0.5.5). `get` answers `{}` for a missing key.
|
|
188
|
+
*/
|
|
189
|
+
storageSession: {
|
|
190
|
+
get: (key) => transport.request("storage.session.get", { key }),
|
|
191
|
+
set: (key, value) => transport.request("storage.session.set", { key, value }),
|
|
192
|
+
delete: (key) => transport.request("storage.session.delete", { key }),
|
|
193
|
+
keys: () => transport.request("storage.session.keys"),
|
|
194
|
+
},
|
|
185
195
|
app: {
|
|
186
196
|
system: () => transport.request("app.system"),
|
|
187
197
|
},
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.25: full reference
|
|
2
2
|
|
|
3
3
|
> Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
|
|
4
4
|
|
|
@@ -221,7 +221,7 @@ It assumes you have written a first plugin (Getting started). For each type's me
|
|
|
221
221
|
#### The plugin process
|
|
222
222
|
|
|
223
223
|
- **One Node.js process per plugin.** The player starts each plugin in a Node.js 26 process of its own (the installed player ships that Node.js). The plugin can use the Node standard library, the file system, the network and npm packages. A crash or a busy loop in one plugin does not stop the player or other plugins.
|
|
224
|
-
- **No web storage.** The player starts Node with `--no-experimental-webstorage`, so `localStorage` and `sessionStorage` do not exist. Keep settings with plugin.storage or in your own files in the data directory.
|
|
224
|
+
- **No web storage.** The player starts Node with `--no-experimental-webstorage`, so `localStorage` and `sessionStorage` do not exist. Keep settings with plugin.storage or in your own files in the data directory, and state that must outlive a restart of the plugin's process, but not the player, in plugin.storage.session (Restarts).
|
|
225
225
|
- **Trusted code.** Manifest capabilities are a contract the player checks on every call (a missing one fails with `code: "capability"`), not a sandbox: the process has the user's file and network access. The full list of capabilities and the API each one opens is in Reference: capabilities.
|
|
226
226
|
- **One API on every system.** The same plugin runs on macOS and Windows. Something a system cannot do fails with `unsupported`; the differences are collected in Reference: platforms.
|
|
227
227
|
- **Types only.** The plugin imports `@vestra-player/sdk` with `import type`. The `plugin` object it receives is built by the player's copy of the SDK, so a built plugin contains only its own code and keeps working with the SDK version the player ships.
|
|
@@ -314,6 +314,27 @@ The player stops a plugin when the user disables or uninstalls it, when it is re
|
|
|
314
314
|
|
|
315
315
|
So `onDispose` cleanup must be quick and must not rely on the player. Media the plugin opened with player.open **keeps playing** after it stops.
|
|
316
316
|
|
|
317
|
+
#### Restarts: activate can run more than once
|
|
318
|
+
|
|
319
|
+
A plugin's process does not live as long as the player. Within one player session the player stops a plugin and starts it again, and `activate` runs again in a new process, when:
|
|
320
|
+
|
|
321
|
+
- the plugin is **reloaded** (from the plugin manager, or a development load with `--plugin`) or **upgraded**;
|
|
322
|
+
- it **crashed**, or failed to start, and is started again.
|
|
323
|
+
|
|
324
|
+
A later player may restart a plugin in more cases: restarting plugins that share one process together when one of them faults or hangs, or stopping a plugin nobody uses and starting it on its next use. Write the plugin for those too.
|
|
325
|
+
|
|
326
|
+
Everything in the process is gone after such a restart: variables, timers, caches, open sockets. The menus, windows and panels come back because `activate` creates them again. **Keep state that must survive a restart in session storage (or persistent storage):**
|
|
327
|
+
|
|
328
|
+
| Where | Survives a restart of the plugin | Survives the player quitting | Cleared when |
|
|
329
|
+
| --- | --- | --- | --- |
|
|
330
|
+
| Variables in the process | no | no | the process stops |
|
|
331
|
+
| plugin.storage.session (player 0.5.5) | yes | no | the user disables or uninstalls the plugin; the player quits |
|
|
332
|
+
| plugin.storage.json, files in the data directory | yes | yes | uninstalling with `removeData` |
|
|
333
|
+
|
|
334
|
+
Session storage is for state that belongs to this run of the player: a queue the plugin is working through, the item a panel was showing, a token that expires anyway. Settings and history the user expects to find tomorrow go in `storage.json`. Session storage keeps JSON values in the player's memory, at most 1 MiB per plugin (one value a little less, since it travels in one 1 MiB message; split large state over several keys), and does not keep the order of an object's properties. Each `set` is kept as soon as its promise resolves, so a crash right after loses nothing. Disabling a plugin is the user switching it off, so its session values go; a reload, a crash or a failed start keeps them.
|
|
335
|
+
|
|
336
|
+
Write `activate` so that running it twice in one session is normal: read what you kept, then build the interface from it.
|
|
337
|
+
|
|
317
338
|
#### The data directory
|
|
318
339
|
|
|
319
340
|
plugin.dataDirectory is a writable directory for the plugin's own files (settings, caches, remembered folders). It needs no capability; the player creates it before it starts the plugin.
|
|
@@ -1660,7 +1681,7 @@ Every test follows the same five steps:
|
|
|
1660
1681
|
4. **Assert** on what the host recorded.
|
|
1661
1682
|
5. **Stop** with `await stop()`: the plugin is disposed as when the player stops it (scopes released, stores flushed) and the host's temporary files are removed. Each test creates its own host.
|
|
1662
1683
|
|
|
1663
|
-
`startPlugin` also returns `plugin`, the object `activate` received, to call the API directly (`plugin.player.state`). Its options: `host` (a host you prepared before activation, as above), `dataDirectory` (an existing folder as `plugin.dataDirectory`, to test a restart), `locale` (play another interface language, see Translate a plugin), and `id` with `capabilities` to start without a `manifest.json`.
|
|
1684
|
+
`startPlugin` also returns `plugin`, the object `activate` received, to call the API directly (`plugin.player.state`). Its options: `host` (a host you prepared before activation, as above), `dataDirectory` (an existing folder as `plugin.dataDirectory`, to test a restart), `sessionStorage` (the values of `plugin.storage.session` an earlier run left, to test a restart within one player session), `locale` (play another interface language, see Translate a plugin), and `id` with `capabilities` to start without a `manifest.json`.
|
|
1664
1685
|
|
|
1665
1686
|
#### Act like the user
|
|
1666
1687
|
|
|
@@ -1775,6 +1796,29 @@ The host checks what the player checks: the command limits, the property value l
|
|
|
1775
1796
|
|
|
1776
1797
|
Copy `host.dataDirectory` after the first run and pass it as `dataDirectory` to the second, to check what the plugin stored and restores; the example is on startPlugin, and Remember a window's state between runs is a plugin that needs it.
|
|
1777
1798
|
|
|
1799
|
+
A restart within one player session (a reload, an upgrade, a crash) keeps plugin.storage.session too. Pass the first run's `host.sessionStorage` as `sessionStorage` to the second run; leave it out to test a fresh player session, where those values are gone:
|
|
1800
|
+
|
|
1801
|
+
```ts
|
|
1802
|
+
import assert from "node:assert/strict";
|
|
1803
|
+
import { test } from "node:test";
|
|
1804
|
+
import { startPlugin } from "@vestra-player/sdk/testing";
|
|
1805
|
+
import activate from "../main.ts";
|
|
1806
|
+
|
|
1807
|
+
test("goes on with the queue after a restart", async () => {
|
|
1808
|
+
const first = await startPlugin(activate);
|
|
1809
|
+
await first.plugin.storage.session.set("queue", ["a.mkv", "b.mkv"]);
|
|
1810
|
+
const kept = first.host.sessionStorage;
|
|
1811
|
+
await first.stop();
|
|
1812
|
+
|
|
1813
|
+
const second = await startPlugin(activate, { sessionStorage: kept });
|
|
1814
|
+
assert.deepEqual(await second.plugin.storage.session.get("queue"), [
|
|
1815
|
+
"a.mkv",
|
|
1816
|
+
"b.mkv",
|
|
1817
|
+
]);
|
|
1818
|
+
await second.stop();
|
|
1819
|
+
});
|
|
1820
|
+
```
|
|
1821
|
+
|
|
1778
1822
|
Every member of `TestHost` is in the @vestra-player/sdk/testing reference.
|
|
1779
1823
|
|
|
1780
1824
|
## How-to: Publish a plugin
|
|
@@ -2186,6 +2230,7 @@ The table lists the API whose documentation names a player version, newest last:
|
|
|
2186
2230
|
| 0.5.1 | ImageRef, ImageOptions.maxSide, PropValue, Ui.image, Ui.remoteImage, SlotState.reason, Panel.setInteraction, PlaybackStats.presentedFps, Player.setStepSeek, Host.run, Packages.reload |
|
|
2187
2231
|
| 0.5.2 | HostAction |
|
|
2188
2232
|
| 0.5.3 | Scrub.seek |
|
|
2233
|
+
| 0.5.5 | SessionStorage |
|
|
2189
2234
|
|
|
2190
2235
|
The Slint library carries no version tags: `Theme.danger` needs player 0.3.0; `@vestra/icons.slint` and the palette `vestra-light` need 0.4.0 (Theme).
|
|
2191
2236
|
|
|
@@ -2307,6 +2352,16 @@ plugin.secrets. A name or value out of its rule fails with code `invalid`; a new
|
|
|
2307
2352
|
| `SECRET_LIMITS.valueBytes` | 1024 (1 KiB) | UTF-8 bytes of a value. |
|
|
2308
2353
|
| `SECRET_LIMITS.names` | 256 | Different names one plugin may have stored at the same time. |
|
|
2309
2354
|
|
|
2355
|
+
#### Session storage
|
|
2356
|
+
|
|
2357
|
+
plugin.storage.session (player 0.5.5). A key out of its rule, or a value that is not JSON (or has a string with a lone surrogate), fails with code `invalid`; a `set` that would take the plugin over `bytes` fails with `limit` and changes nothing. Each key counts its UTF-8 bytes, its value as compact JSON and `entryBytes`. One `set` is also one message, so a single value must fit in `MESSAGE_LIMIT` with its key and the request around it.
|
|
2358
|
+
|
|
2359
|
+
| Limit | Value | Meaning |
|
|
2360
|
+
| --- | --- | --- |
|
|
2361
|
+
| `SESSION_STORAGE_LIMITS.keyBytes` | 1024 (1 KiB) | UTF-8 bytes of a key. |
|
|
2362
|
+
| `SESSION_STORAGE_LIMITS.bytes` | 1048576 (1 MiB) | Bytes one plugin may hold at the same time: for every key, its UTF-8 bytes, its value written as compact JSON (`JSON.stringify(value)`) and `entryBytes`. |
|
|
2363
|
+
| `SESSION_STORAGE_LIMITS.entryBytes` | 128 | What each key costs on top of its text and value (128): the player's own bookkeeping, so that many tiny entries stay within `bytes` of memory. |
|
|
2364
|
+
|
|
2310
2365
|
#### Messages
|
|
2311
2366
|
|
|
2312
2367
|
Every request a plugin sends travels as one protocol line. The plugin's side refuses a longer one with code `limit` before sending it; property values meet their own limits first.
|
|
@@ -6427,6 +6482,70 @@ interface Storage {
|
|
|
6427
6482
|
validate?: (value: unknown) => value is T;
|
|
6428
6483
|
debounceMs?: number;
|
|
6429
6484
|
}): JsonStore<T>;
|
|
6485
|
+
/**
|
|
6486
|
+
* Values the player keeps in its memory for this plugin until the player quits: they survive a restart of the plugin's process (reload, upgrade, crash, failed start) but not the player. See SessionStorage.
|
|
6487
|
+
*/
|
|
6488
|
+
readonly session: SessionStorage;
|
|
6489
|
+
}
|
|
6490
|
+
```
|
|
6491
|
+
|
|
6492
|
+
### SessionStorage
|
|
6493
|
+
|
|
6494
|
+
`plugin.storage.session`: JSON values that live as long as the **player**,
|
|
6495
|
+
not as long as the plugin's process (like `chrome.storage.session`). No
|
|
6496
|
+
capability; needs player 0.5.5 (older players reject the calls with
|
|
6497
|
+
`unsupported`).
|
|
6498
|
+
|
|
6499
|
+
`activate` can run more than once in one player session: the player stops
|
|
6500
|
+
and starts a plugin's process again when the plugin is reloaded (also a
|
|
6501
|
+
development load with `--plugin`) or upgraded, and after a crash or a
|
|
6502
|
+
failed start. A later player may also restart plugins that share a
|
|
6503
|
+
process, or stop an idle plugin and start it on its next use. Variables do
|
|
6504
|
+
not survive that; keep state that must survive a restart here (or in
|
|
6505
|
+
storage.json when it must also survive the player
|
|
6506
|
+
quitting).
|
|
6507
|
+
|
|
6508
|
+
| | Values |
|
|
6509
|
+
| --- | --- |
|
|
6510
|
+
| The plugin's process restarts: reload, upgrade, crash | kept |
|
|
6511
|
+
| The plugin failed to start or failed, and starts again | kept |
|
|
6512
|
+
| The user disables the plugin, or uninstalls it | cleared |
|
|
6513
|
+
| The player quits | cleared |
|
|
6514
|
+
|
|
6515
|
+
- **Per plugin.** A plugin sees only its own keys. Like
|
|
6516
|
+
dataDirectory, the values belong to the
|
|
6517
|
+
installation: a development load (`--plugin`) has its own.
|
|
6518
|
+
- **JSON.** `set` stores what `JSON.stringify(value)` writes, and `get`
|
|
6519
|
+
returns that, parsed: `undefined` object properties are dropped, a `Date`
|
|
6520
|
+
comes back as a string, `NaN` as `null`. **The order of an object's
|
|
6521
|
+
properties is not kept**: they may come back in another order. A value
|
|
6522
|
+
`JSON.stringify` cannot write (`undefined`, a function, a cycle, a
|
|
6523
|
+
`BigInt`) or a string with a lone surrogate (`"\uD800"`) fails with
|
|
6524
|
+
`invalid`. The value is not checked when it comes back: after an upgrade
|
|
6525
|
+
an older version's value may be read, so check its shape when it matters.
|
|
6526
|
+
- **Bounded.** Keys are 1–1024 bytes of text. A plugin holds at most 1 MiB,
|
|
6527
|
+
counting every key, its value as compact JSON and 128 bytes per key
|
|
6528
|
+
(SESSION_STORAGE_LIMITS); a
|
|
6529
|
+
`set` beyond it fails with `limit` and changes nothing. One `set` is
|
|
6530
|
+
also one message to the player, at most 1 MiB with its key, so a single
|
|
6531
|
+
value is a little smaller than that; a larger one fails with `limit`.
|
|
6532
|
+
Split large state over several keys.
|
|
6533
|
+
- **Each call is one request.** A `set` is kept once its promise
|
|
6534
|
+
resolves, so a crash right after loses nothing; there is no debounce.
|
|
6535
|
+
Calls answer in order.
|
|
6536
|
+
|
|
6537
|
+
```ts
|
|
6538
|
+
interface SessionStorage {
|
|
6539
|
+
/** The value stored under `key`, or `undefined` when there is none. */
|
|
6540
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
6541
|
+
/**
|
|
6542
|
+
* Store or replace `value` under `key`. `invalid` for a bad key or a value that is not JSON, `limit` when the plugin's values would be over 1 MiB or the request over the 1 MiB message limit.
|
|
6543
|
+
*/
|
|
6544
|
+
set(key: string, value: unknown): Promise<void>;
|
|
6545
|
+
/** Remove a key; removing a missing one is fine. */
|
|
6546
|
+
delete(key: string): Promise<void>;
|
|
6547
|
+
/** The keys stored, in code point order. */
|
|
6548
|
+
keys(): Promise<string[]>;
|
|
6430
6549
|
}
|
|
6431
6550
|
```
|
|
6432
6551
|
|
|
@@ -7436,6 +7555,10 @@ class TestHost implements Transport {
|
|
|
7436
7555
|
* The player's secret store for this plugin: name → value. Preset entries to simulate stored secrets; inject failures with `host.handle("secrets.get", ...)`. Like the player, `secrets.set` of a new name fails `limit` once 256 names are stored.
|
|
7437
7556
|
*/
|
|
7438
7557
|
readonly secrets: Map<string, string>;
|
|
7558
|
+
/**
|
|
7559
|
+
* `plugin.storage.session`: key → value, as the player keeps them. Preset entries to simulate values an earlier run of the plugin stored in the same player session, or pass `startPlugin(..., { sessionStorage })`. Like the player, a key is 1–1024 bytes, a `set` that would make the entries larger than 1 MiB (keys, values as compact JSON and `SESSION_STORAGE_LIMITS.entryBytes` per key) fails `limit`, and an object's properties are stored sorted, as the player hands them back.
|
|
7560
|
+
*/
|
|
7561
|
+
readonly sessionStorage: Map<string, Json>;
|
|
7439
7562
|
/** What `plugin.app.system()` reports. */
|
|
7440
7563
|
systemInfo: WireSystemInfo;
|
|
7441
7564
|
/** What `plugin.app` reports. Set it before startPlugin: the plugin receives it once at start-up. */
|
|
@@ -7720,6 +7843,10 @@ function startPlugin(activate: (plugin: Plugin) => unknown, options?: {
|
|
|
7720
7843
|
host?: TestHost;
|
|
7721
7844
|
/** Use an existing directory as `plugin.dataDirectory` (to test a restart). */
|
|
7722
7845
|
dataDirectory?: string;
|
|
7846
|
+
/**
|
|
7847
|
+
* Start with these `plugin.storage.session` values (to test a restart within one player session): pass the previous run's `host.sessionStorage`. They are copied into the new host's TestHost.sessionStorage.
|
|
7848
|
+
*/
|
|
7849
|
+
sessionStorage?: ReadonlyMap<string, Json>;
|
|
7723
7850
|
/**
|
|
7724
7851
|
* The interface language to play (TestHost.locale): the plugin's `locales/<locale>.po` (or `.json`) in `directory` is loaded and its menus, messages and titles show translated. Default: the host's.
|
|
7725
7852
|
*/
|
|
@@ -8007,6 +8134,27 @@ const OSD_LIMITS: Readonly<{
|
|
|
8007
8134
|
}>;
|
|
8008
8135
|
```
|
|
8009
8136
|
|
|
8137
|
+
### SESSION_STORAGE_LIMITS
|
|
8138
|
+
|
|
8139
|
+
The player's limits on SessionStorage. A key outside them fails
|
|
8140
|
+
with code `invalid`; a `set` that would make the plugin's entries larger
|
|
8141
|
+
than `bytes` fails with `limit` and changes nothing. A single `set` must
|
|
8142
|
+
also fit in one message to the player
|
|
8143
|
+
(MESSAGE_LIMIT, 1 MiB
|
|
8144
|
+
with the key and the request around the value). The player's tests check
|
|
8145
|
+
that these agree with the player.
|
|
8146
|
+
|
|
8147
|
+
```ts
|
|
8148
|
+
const SESSION_STORAGE_LIMITS: Readonly<{
|
|
8149
|
+
/** UTF-8 bytes of a key (1024, at least 1; no NUL character). */
|
|
8150
|
+
readonly keyBytes: 1024;
|
|
8151
|
+
/** Bytes one plugin may hold at the same time: for every key, its UTF-8 bytes, its value written as compact JSON (`JSON.stringify(value)`) and `entryBytes`. */
|
|
8152
|
+
readonly bytes: number;
|
|
8153
|
+
/** What each key costs on top of its text and value (128): the player's own bookkeeping, so that many tiny entries stay within `bytes` of memory. */
|
|
8154
|
+
readonly entryBytes: 128;
|
|
8155
|
+
}>;
|
|
8156
|
+
```
|
|
8157
|
+
|
|
8010
8158
|
### SECRET_LIMITS
|
|
8011
8159
|
|
|
8012
8160
|
The player's limits on Secrets. A name or value outside them
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vestra Player SDK
|
|
2
2
|
|
|
3
|
-
> @vestra-player/sdk 1.5.
|
|
3
|
+
> @vestra-player/sdk 1.5.25: 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; it runs in its own Node.js process. Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n> are line numbers in llms-full.txt, next to this file: every page, declaration and example.
|
|
4
4
|
|
|
5
5
|
**Rules**
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ my-plugin/
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits:
|
|
36
|
+
Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2172–2238.
|
|
37
37
|
|
|
38
38
|
**main.ts**
|
|
39
39
|
|
|
@@ -76,9 +76,9 @@ export default async function activate(plugin: Plugin) {
|
|
|
76
76
|
- `ui.settings`: Settings.page/show
|
|
77
77
|
- `ui.theme`: Appearance.set/setLanguage
|
|
78
78
|
- `secrets`: Secrets.get/set/delete
|
|
79
|
-
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details:
|
|
79
|
+
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details: L2239–2281.
|
|
80
80
|
|
|
81
|
-
**Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`; `host` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide:
|
|
81
|
+
**Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`; `host` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide: L1635–1823; TestHost: L7019.
|
|
82
82
|
|
|
83
83
|
**Command line**
|
|
84
84
|
|
|
@@ -106,39 +106,39 @@ export default async function activate(plugin: Plugin) {
|
|
|
106
106
|
- `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
|
|
107
107
|
- `reserved`: The id is `vestra` or starts with `vestra.`
|
|
108
108
|
- `versionMismatch`: The package needs a newer player or another `apiVersion`
|
|
109
|
-
- Others: PluginError, llms-full.txt
|
|
109
|
+
- Others: PluginError, llms-full.txt L3190.
|
|
110
110
|
|
|
111
111
|
## Reference
|
|
112
112
|
|
|
113
113
|
- [llms-full.txt](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt), also node_modules/@vestra-player/sdk/llms-full.txt; the pages are on the [site](https://vestrasys.github.io/vestra-player-sdk/).
|
|
114
114
|
- Tutorials: Getting started L9–214
|
|
115
|
-
- Concepts: How a plugin runs L215–
|
|
116
|
-
- How-to: Keep a slider responsive while dragging
|
|
117
|
-
- Guidelines: Interface guidelines
|
|
118
|
-
- Reference: Manifest
|
|
119
|
-
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html):
|
|
120
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html):
|
|
115
|
+
- Concepts: How a plugin runs L215–385; Commands, menus and key bindings L386–483; Playback state, controls and sources L484–629; Video and audio processing L630–708; Windows, panels, settings pages, dialogs and the OSD L709–997
|
|
116
|
+
- How-to: Keep a slider responsive while dragging L998–1172; Remember a window's state between runs L1173–1302; Translate a plugin L1303–1486; Migrate a plugin to English source text L1487–1634; Test a plugin L1635–1823; Publish a plugin L1824–1912; Migrating off package rollback (1.5.16, removed in 1.5.17) L1913–1988
|
|
117
|
+
- Guidelines: Interface guidelines L1989–2073; Menu guidelines L2074–2171
|
|
118
|
+
- Reference: Manifest L2172–2238; Capabilities L2239–2281; Limits L2282–2386; Commands, menus and bindings L2387–2855; Platforms L2856–2884; Theme L2885–3091; Command line L3092–3147
|
|
119
|
+
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3148–6854
|
|
120
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L6855–8257
|
|
121
121
|
|
|
122
122
|
## API
|
|
123
123
|
|
|
124
124
|
Where each module of `plugin` (`plugin.<module>`) is declared:
|
|
125
125
|
|
|
126
|
-
- Plugin
|
|
127
|
-
- Commands
|
|
126
|
+
- Plugin L3382, Scope L3251, PluginError L3190, Disposable L3163
|
|
127
|
+
- Commands L3594; Windows L3824; Panels L4467; Dialogs L4753; Player L5220; video: VideoFilter L5527, VideoView L5590, RenderOptions L5632; audio: AudioFilter L5551; Subtitles L5730; media: MediaCapture L5890; Resolvers L5995; Packages L6237; Osd L5463; Host L5394; Clipboard L6369; Power L6389; Tools L6422; Storage L6465; System L6552; Secrets L6570; window: MainWindow L4590; App L4671; Bindings L6339; Ui L3699; Timeline L4547; Drops L4123; Appearance L4152; I18n L4294; Settings L4400.
|
|
128
128
|
|
|
129
129
|
## Examples
|
|
130
130
|
|
|
131
131
|
Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
|
|
132
132
|
|
|
133
|
-
- hello: The README Quick start: one menu command and an on-screen message.
|
|
134
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
135
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
136
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
137
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
138
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
139
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
140
|
-
- markdown: A window that shows a Markdown document.
|
|
141
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
133
|
+
- hello: The README Quick start: one menu command and an on-screen message. L8681–8770
|
|
134
|
+
- blur: A tool window with one slider that blurs the playing video. L8771–9032
|
|
135
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L9033–9365
|
|
136
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L9366–9550
|
|
137
|
+
- panel-menu: A sidebar panel with its own right-click menu. L9551–9756
|
|
138
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L9757–9916
|
|
139
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L9917–10080
|
|
140
|
+
- markdown: A window that shows a Markdown document. L10081–10275
|
|
141
|
+
- i18n: A plugin in the player's language: English source text, a Chinese translation. L10276–10443
|
|
142
142
|
|
|
143
143
|
## Optional
|
|
144
144
|
|