@vestra-player/sdk 1.5.9 → 1.5.11

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.
@@ -0,0 +1,55 @@
1
+ /** The player's limits on one property value, for {@link TestHost}. */
2
+ import type { Json } from "../wire.ts";
3
+ /**
4
+ * The player's limits on one property value (a `PropValue` as written
5
+ * by `View.set` or `View.bind`, or an initial property of a
6
+ * window, panel or settings page). The player's tests check that these
7
+ * agree with the player; {@link TestHost} enforces them the same way.
8
+ *
9
+ * A value over a limit is refused whole with code `limit` (the property
10
+ * keeps what it showed) and reported to `Scope.onError`: a write by
11
+ * `View.set` or `View.bind` as described there. As an initial property it
12
+ * makes `Window.show` reject; a panel or settings page opens without it and
13
+ * the failure goes to the plugin's `onError`.
14
+ *
15
+ * They bound the work one write costs the player's interface thread; they
16
+ * are not meant as list sizes to aim for. A list that can grow without
17
+ * bound (a playlist, a history) should send the rows near the view and say
18
+ * how many there are, not the whole list.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { UI_VALUE_LIMITS } from "@vestra-player/sdk/testing";
23
+ *
24
+ * // 8 fields per row: 9 values (the row and its fields).
25
+ * const window = Math.floor(UI_VALUE_LIMITS.values / 9) - 1;
26
+ * ```
27
+ *
28
+ * @since 1.5.10
29
+ */
30
+ export declare const UI_VALUE_LIMITS: {
31
+ /** Nesting depth; the value itself is depth 0 (8). */
32
+ readonly depth: 8;
33
+ /**
34
+ * Values in all, counting every array, object, string, number, boolean
35
+ * and null, the value itself included; a Markdown document is one
36
+ * (16384).
37
+ */
38
+ readonly values: 16384;
39
+ /** Rows of one array (4096). */
40
+ readonly arrayRows: 4096;
41
+ /** UTF-8 bytes of one string or object field name (64 KiB). */
42
+ readonly stringBytes: number;
43
+ /**
44
+ * UTF-8 bytes of all strings and field names together, the text of the
45
+ * value's Markdown documents included (1 MiB).
46
+ */
47
+ readonly textBytes: number;
48
+ };
49
+ /**
50
+ * Why the player would refuse `value` (in wire form) as a property value,
51
+ * or `null`. The same checks, order and messages as the player's
52
+ * conversion (`host/src/ui_values.rs`). An image reference as the whole
53
+ * value is not counted (the player loads it instead).
54
+ */
55
+ export declare function valueLimitProblem(value: Json): string | null;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The player's limits on one property value (a `PropValue` as written
3
+ * by `View.set` or `View.bind`, or an initial property of a
4
+ * window, panel or settings page). The player's tests check that these
5
+ * agree with the player; {@link TestHost} enforces them the same way.
6
+ *
7
+ * A value over a limit is refused whole with code `limit` (the property
8
+ * keeps what it showed) and reported to `Scope.onError`: a write by
9
+ * `View.set` or `View.bind` as described there. As an initial property it
10
+ * makes `Window.show` reject; a panel or settings page opens without it and
11
+ * the failure goes to the plugin's `onError`.
12
+ *
13
+ * They bound the work one write costs the player's interface thread; they
14
+ * are not meant as list sizes to aim for. A list that can grow without
15
+ * bound (a playlist, a history) should send the rows near the view and say
16
+ * how many there are, not the whole list.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { UI_VALUE_LIMITS } from "@vestra-player/sdk/testing";
21
+ *
22
+ * // 8 fields per row: 9 values (the row and its fields).
23
+ * const window = Math.floor(UI_VALUE_LIMITS.values / 9) - 1;
24
+ * ```
25
+ *
26
+ * @since 1.5.10
27
+ */
28
+ export const UI_VALUE_LIMITS = {
29
+ /** Nesting depth; the value itself is depth 0 (8). */
30
+ depth: 8,
31
+ /**
32
+ * Values in all, counting every array, object, string, number, boolean
33
+ * and null, the value itself included; a Markdown document is one
34
+ * (16384).
35
+ */
36
+ values: 16384,
37
+ /** Rows of one array (4096). */
38
+ arrayRows: 4096,
39
+ /** UTF-8 bytes of one string or object field name (64 KiB). */
40
+ stringBytes: 64 * 1024,
41
+ /**
42
+ * UTF-8 bytes of all strings and field names together, the text of the
43
+ * value's Markdown documents included (1 MiB).
44
+ */
45
+ textBytes: 1024 * 1024,
46
+ };
47
+ const L = UI_VALUE_LIMITS;
48
+ const bytes = (text) => Buffer.byteLength(text, "utf8");
49
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
50
+ /** Text of a Markdown document in wire form, or `null` for other JSON. */
51
+ function documentText(value) {
52
+ if (!isObject(value))
53
+ return null;
54
+ const doc = value.$vestraMarkdown;
55
+ return isObject(doc) && typeof doc.text === "string" ? doc.text : null;
56
+ }
57
+ /**
58
+ * Why the player would refuse `value` (in wire form) as a property value,
59
+ * or `null`. The same checks, order and messages as the player's
60
+ * conversion (`host/src/ui_values.rs`). An image reference as the whole
61
+ * value is not counted (the player loads it instead).
62
+ */
63
+ export function valueLimitProblem(value) {
64
+ if (isObject(value) && "$vestraImage" in value)
65
+ return null;
66
+ const budget = { values: 0, text: 0 };
67
+ const text = (s) => {
68
+ const n = bytes(s);
69
+ if (n > L.stringBytes)
70
+ return `UI string or field name exceeds ${L.stringBytes} bytes`;
71
+ if (budget.text + n > L.textBytes)
72
+ return `UI value text exceeds ${L.textBytes} total bytes`;
73
+ budget.text += n;
74
+ return null;
75
+ };
76
+ const walk = (value, depth) => {
77
+ if (depth > L.depth)
78
+ return `UI value depth exceeds ${L.depth} (root is depth 0)`;
79
+ if (budget.values === L.values)
80
+ return `UI value exceeds ${L.values} total nodes`;
81
+ budget.values += 1;
82
+ const doc = documentText(value);
83
+ if (doc !== null) {
84
+ const n = bytes(doc);
85
+ if (budget.text + n > L.textBytes)
86
+ return `UI value text exceeds ${L.textBytes} total bytes`;
87
+ budget.text += n;
88
+ return null;
89
+ }
90
+ if (typeof value === "string")
91
+ return text(value);
92
+ if (Array.isArray(value)) {
93
+ if (value.length > L.arrayRows)
94
+ return `UI array exceeds ${L.arrayRows} rows`;
95
+ if (value.length > L.values - budget.values)
96
+ return "UI array cannot fit in remaining node budget";
97
+ for (const item of value) {
98
+ const problem = walk(item, depth + 1);
99
+ if (problem)
100
+ return problem;
101
+ }
102
+ return null;
103
+ }
104
+ if (isObject(value)) {
105
+ const entries = Object.entries(value);
106
+ if (entries.length > L.values - budget.values)
107
+ return "UI object cannot fit in remaining node budget";
108
+ for (const [name, item] of entries) {
109
+ const problem = text(name) ?? walk(item, depth + 1);
110
+ if (problem)
111
+ return problem;
112
+ }
113
+ }
114
+ return null;
115
+ };
116
+ return walk(value, 0);
117
+ }
@@ -1,4 +1,3 @@
1
- /** Trusted extraction providers. Declarations cross JSON; handler code stays in JS. */
2
1
  import type { Transport } from "./wire.ts";
3
2
  /** One stream of a {@link ResolvedMedia}. */
4
3
  export type ResolvedResource = {
@@ -1,6 +1,14 @@
1
+ /** Trusted extraction providers. Declarations cross JSON; handler code stays in JS. */
2
+ import { isTextValue, textProblem } from "./i18n.js";
1
3
  const isProducer = (result) => result?.kind === "stream";
2
4
  /** The words of a failure, as the user will read them. */
3
- const reason = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 2048);
5
+ const reason = (error) => {
6
+ // `plugin.i18n.error(…)`: the player shows the text translated.
7
+ const text = error?.text;
8
+ if (error instanceof Error && isTextValue(text) && !textProblem(text))
9
+ return text;
10
+ return (error instanceof Error ? error.message : String(error)).slice(0, 2048);
11
+ };
4
12
  export function sourceResolverClient(transport) {
5
13
  return {
6
14
  /** Requires source.resolve. One registered host/scheme pair, and one file
@@ -1,4 +1,5 @@
1
1
  /** Subtitle wire types; source pixels and renderer confirmation remain distinct. */
2
+ import type { TextJson } from "./i18n.ts";
2
3
  import type { Transport } from "./wire.ts";
3
4
  export type SubtitleTrack = {
4
5
  kind: "embedded";
@@ -169,7 +170,8 @@ export declare function subtitleClient(transport: Transport): {
169
170
  expected: SubtitleStamp;
170
171
  inputOrigin?: SubtitleTrack;
171
172
  format?: "srt" | "ass";
172
- title?: string;
173
+ /** Source text is data; a text value is shown translated (player 0.4.3). */
174
+ title?: string | TextJson;
173
175
  select?: "primary" | "secondary" | "none";
174
176
  }) => Promise<SubtitleState>;
175
177
  select: (target: SubtitleTrack | null, expected: SubtitleStamp, operationId: number, role?: "primary" | "secondary") => Promise<SubtitleState>;
package/dist/wire.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SourceAudioTrack, SourceStamp } from "./source.ts";
2
+ import type { ArgJson, ListJson, TextJson } from "./i18n.ts";
2
3
  export type { ResolvedMedia, ResolvedResource, SourceResolverDeclaration, } from "./source-resolvers.ts";
3
4
  /** Working contract. Trusted plugin runtime, not a permissions sandbox. */
4
5
  export declare const API_VERSION: "vestra/1";
@@ -141,6 +142,8 @@ export interface BindingTable {
141
142
  command: string;
142
143
  title: string;
143
144
  path: string[] | null;
145
+ /** The path's keys (player 0.4.3). */
146
+ key?: string[] | null;
144
147
  scope: "global" | "panel";
145
148
  defaults: string[];
146
149
  current: string[];
@@ -287,10 +290,15 @@ export interface WireSystemInfo {
287
290
  primary: boolean;
288
291
  }[];
289
292
  }
290
- /** Template arguments on the wire (`osd.show`, `osd.panel`, `dialogs.confirm`). */
291
- export type TemplateArgsJson = (string | number)[] | {
292
- [name: string]: string | number;
293
+ /**
294
+ * Template arguments on the wire (`osd.show`, `osd.panel`, `dialogs.confirm`,
295
+ * a text's `args`): data, or text values (player 0.4.3).
296
+ */
297
+ export type TemplateArgsJson = ArgJson[] | {
298
+ [name: string]: ArgJson;
293
299
  };
300
+ /** A text the player shows: source text, or a text value (player 0.4.3). */
301
+ export type TextValueJson = string | TextJson | ListJson;
294
302
  export interface Transport {
295
303
  request<T>(method: string, params?: Json): Promise<T>;
296
304
  on(event: string, listener: (data: unknown) => void): () => void;
@@ -320,8 +328,8 @@ export declare function createWire(transport: Transport): {
320
328
  dialogs: {
321
329
  open: (options: {
322
330
  kind: "openFile" | "openFiles" | "openDirectory" | "saveFile";
323
- title?: string;
324
- fileName?: string;
331
+ title?: TextValueJson;
332
+ fileName?: TextValueJson;
325
333
  }) => Promise<{
326
334
  cancelled: boolean;
327
335
  paths: string[];
@@ -423,7 +431,7 @@ export declare function createWire(transport: Transport): {
423
431
  set: (marks: {
424
432
  time: number;
425
433
  kind: string;
426
- title?: string;
434
+ title?: string | TextJson;
427
435
  }[], sourceId?: number) => Promise<{
428
436
  applied: boolean;
429
437
  }>;
@@ -494,8 +502,8 @@ export declare function createWire(transport: Transport): {
494
502
  };
495
503
  /** `text` is a template in the plugin's catalog, formatted with `args` (player 0.4.3). */
496
504
  osd: {
497
- show: (text: string, args?: TemplateArgsJson) => Promise<void>;
498
- panel: (text: string | null, args?: TemplateArgsJson) => Promise<void>;
505
+ show: (text: TextValueJson, args?: TemplateArgsJson) => Promise<void>;
506
+ panel: (text: TextValueJson | null, args?: TemplateArgsJson, maxColumns?: number) => Promise<void>;
499
507
  };
500
508
  layout: {
501
509
  state: () => Promise<{
@@ -666,7 +674,7 @@ export declare function createWire(transport: Transport): {
666
674
  expected: import("./subtitles.ts").SubtitleStamp;
667
675
  inputOrigin?: import("./subtitles.ts").SubtitleTrack;
668
676
  format?: "srt" | "ass";
669
- title?: string;
677
+ title?: string | TextJson;
670
678
  select?: "primary" | "secondary" | "none";
671
679
  }) => Promise<import("./subtitles.ts").SubtitleState>;
672
680
  select: (target: import("./subtitles.ts").SubtitleTrack | null, expected: import("./subtitles.ts").SubtitleStamp, operationId: number, role?: "primary" | "secondary") => Promise<import("./subtitles.ts").SubtitleState>;
package/dist/wire.js CHANGED
@@ -199,9 +199,10 @@ export function createWire(transport) {
199
199
  text,
200
200
  ...(args === undefined ? {} : { args }),
201
201
  }),
202
- panel: (text, args) => transport.request("osd.panel", {
202
+ panel: (text, args, maxColumns) => transport.request("osd.panel", {
203
203
  text,
204
204
  ...(args === undefined ? {} : { args }),
205
+ ...(maxColumns === undefined ? {} : { maxColumns }),
205
206
  }),
206
207
  },
207
208
  layout: {