@vestra-player/sdk 1.6.17 → 1.6.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/docs-tree.json.gz +0 -0
  3. package/dist/plugin/appearance.js +1 -1
  4. package/dist/plugin/bindings.js +1 -1
  5. package/dist/plugin/commands.js +3 -2
  6. package/dist/plugin/connection.d.ts +119 -0
  7. package/dist/plugin/connection.js +402 -0
  8. package/dist/plugin/drops.d.ts +3 -3
  9. package/dist/plugin/drops.js +1 -1
  10. package/dist/plugin/errors.d.ts +9 -0
  11. package/dist/plugin/errors.js +35 -0
  12. package/dist/plugin/filters.js +1 -1
  13. package/dist/plugin/index.d.ts +1 -1
  14. package/dist/plugin/index.js +3 -1
  15. package/dist/plugin/language.js +1 -1
  16. package/dist/plugin/latest.d.ts +12 -0
  17. package/dist/plugin/latest.js +52 -0
  18. package/dist/plugin/local.js +1 -2
  19. package/dist/plugin/markdown.d.ts +2 -2
  20. package/dist/plugin/markdown.js +1 -1
  21. package/dist/plugin/media.js +1 -1
  22. package/dist/plugin/osd.js +2 -2
  23. package/dist/plugin/packages.js +1 -1
  24. package/dist/plugin/passes.d.ts +111 -0
  25. package/dist/plugin/passes.js +341 -0
  26. package/dist/plugin/player.d.ts +2 -3
  27. package/dist/plugin/player.js +2 -7
  28. package/dist/plugin/resolvers.js +1 -1
  29. package/dist/plugin/runtime.d.ts +26 -185
  30. package/dist/plugin/runtime.js +35 -755
  31. package/dist/plugin/scope.d.ts +2 -1
  32. package/dist/plugin/scope.js +5 -2
  33. package/dist/plugin/services/dialogs.d.ts +3 -0
  34. package/dist/plugin/services/dialogs.js +62 -0
  35. package/dist/plugin/services.js +1 -1
  36. package/dist/plugin/settings.js +2 -2
  37. package/dist/plugin/shaders.js +2 -1
  38. package/dist/plugin/stream.js +1 -2
  39. package/dist/plugin/subtitles.js +2 -2
  40. package/dist/plugin/testing/commands.d.ts +65 -0
  41. package/dist/plugin/testing/commands.js +412 -0
  42. package/dist/plugin/testing/core-actions.d.ts +39 -0
  43. package/dist/plugin/testing/core-actions.js +371 -0
  44. package/dist/plugin/testing/declarations.d.ts +37 -0
  45. package/dist/plugin/testing/declarations.js +80 -0
  46. package/dist/plugin/testing/i18n.d.ts +28 -0
  47. package/dist/plugin/testing/i18n.js +107 -0
  48. package/dist/plugin/testing/index.d.ts +3140 -0
  49. package/dist/plugin/testing/index.js +2796 -0
  50. package/dist/plugin/testing/internals.d.ts +76 -0
  51. package/dist/plugin/testing/internals.js +60 -0
  52. package/dist/plugin/testing/jobs.d.ts +9 -0
  53. package/dist/plugin/testing/jobs.js +304 -0
  54. package/dist/plugin/testing/layout.d.ts +85 -0
  55. package/dist/plugin/testing/layout.js +622 -0
  56. package/dist/plugin/testing/media.d.ts +55 -0
  57. package/dist/plugin/testing/media.js +615 -0
  58. package/dist/plugin/testing/osd.d.ts +4 -0
  59. package/dist/plugin/testing/osd.js +57 -0
  60. package/dist/plugin/testing/processing.d.ts +56 -0
  61. package/dist/plugin/testing/processing.js +491 -0
  62. package/dist/plugin/testing/services.d.ts +50 -0
  63. package/dist/plugin/testing/services.js +466 -0
  64. package/dist/plugin/testing/transport.d.ts +29 -0
  65. package/dist/plugin/testing/transport.js +140 -0
  66. package/dist/plugin/testing.d.ts +1 -3140
  67. package/dist/plugin/testing.js +1 -6203
  68. package/dist/plugin/timeline.js +2 -1
  69. package/dist/plugin/view/base.d.ts +29 -0
  70. package/dist/plugin/view/base.js +88 -0
  71. package/dist/plugin/view/binder.d.ts +22 -0
  72. package/dist/plugin/view/binder.js +57 -0
  73. package/dist/plugin/view/image.d.ts +21 -0
  74. package/dist/plugin/view/image.js +60 -0
  75. package/dist/plugin/view/layout.d.ts +47 -0
  76. package/dist/plugin/view/layout.js +171 -0
  77. package/dist/plugin/view/panel.d.ts +3 -0
  78. package/dist/plugin/view/panel.js +178 -0
  79. package/dist/plugin/view/window.d.ts +3 -0
  80. package/dist/plugin/view/window.js +99 -0
  81. package/dist/plugin/view/writer.d.ts +58 -0
  82. package/dist/plugin/view/writer.js +634 -0
  83. package/dist/plugin/view.js +2 -1
  84. package/dist/plugin/window.js +1 -1
  85. package/llms.txt +1 -1
  86. package/package.json +6 -4
  87. package/dist/plugin/ui.d.ts +0 -74
  88. package/dist/plugin/ui.js +0 -1234
@@ -0,0 +1,634 @@
1
+ /**
2
+ * The view writer (L1, a primitive; doc/specs/sdk-refactor.md §2.2 in the
3
+ * player repository): a view's properties as last sent, and the write path
4
+ * from a value to `ui.set` / `ui.setMount` / `settings.set` — encoding,
5
+ * comparison with what was sent, row patches (planned by `patch.ts`),
6
+ * parts of large writes (`more`), the value limits, `stale` fallback and
7
+ * the back-off after a failed write. It knows nothing of update passes or
8
+ * bindings: the binder (`binder.ts`, L2) puts values in, the view
9
+ * (`base.ts`) says where a write goes.
10
+ */
11
+ import { HostError } from "../../wire.js";
12
+ import { MarkdownHandle } from "../markdown.js";
13
+ import { countRow, diff, keyOf, CHUNK_BYTES, chunkOps, prefixWithin, jsonBytes, rowBytes, replay, withinLimits, } from "../patch.js";
14
+ import { UI_VALUE_LIMITS, valueLimitProblem } from "../values.js";
15
+ import { failure } from "../errors.js";
16
+ import { ImageHandle } from "./image.js";
17
+ /** Slint names are kebab-case; plugin code may use camelCase. */
18
+ export const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
19
+ export const encode = (props) => Object.fromEntries(Object.entries(props).map(([k, v]) => [kebab(k), v]));
20
+ /**
21
+ * A property value as sent: images and documents in wire form, at any depth
22
+ * (released ones refused wherever they are), like the player.
23
+ */
24
+ function wireValue(value) {
25
+ if (value instanceof MarkdownHandle) {
26
+ if (value.released)
27
+ throw failure("invalid", "Markdown document was released with the scope that created it");
28
+ return value.toJSON();
29
+ }
30
+ if (value instanceof ImageHandle) {
31
+ if (value.released)
32
+ throw failure("invalid", `image ${value.path} was released with the scope that created it`);
33
+ return value.toJSON();
34
+ }
35
+ // Array.from: a hole is `null`, as `JSON.stringify` writes it.
36
+ if (Array.isArray(value))
37
+ return Array.from(value, (item) => wireValue(item ?? null));
38
+ if (value !== null && typeof value === "object") {
39
+ const out = {};
40
+ for (const [key, item] of Object.entries(value))
41
+ out[key] = wireValue(item);
42
+ return out;
43
+ }
44
+ return value;
45
+ }
46
+ /** Scalars compared by value. Non-finite numbers are JSON `null`: compared as JSON. */
47
+ const isPrimitive = (value) => value === null ||
48
+ typeof value === "string" ||
49
+ typeof value === "boolean" ||
50
+ (typeof value === "number" && Number.isFinite(value));
51
+ /** A row as `JSON.stringify` writes it inside an array. */
52
+ const encodeRow = (row) => JSON.stringify(row) ?? "null";
53
+ /** What a row's encoding holds: a Markdown document, an image. */
54
+ const HAS_MARKDOWN = 1;
55
+ const HAS_IMAGE = 2;
56
+ const marksOf = (enc) => (enc.includes('"$vestraMarkdown"') ? HAS_MARKDOWN : 0) |
57
+ (enc.includes('"$vestraImage"') ? HAS_IMAGE : 0);
58
+ /**
59
+ * Row encodings by row object, shared by every property (a row's JSON does
60
+ * not depend on where it is bound). Only rows that cannot change are kept
61
+ * ({@link settled}): a plugin that mutates its rows in place sees no
62
+ * difference, and one that keeps unchanged rows as the same deep-frozen
63
+ * objects skips encoding them on every pass. Weak: a row the plugin drops
64
+ * goes with its entry.
65
+ */
66
+ const settledRows = new WeakMap();
67
+ /**
68
+ * Frozen rows seen once. A frozen row is verified and kept on its second
69
+ * pass, so rows frozen afresh on every pass cost one set entry, not a
70
+ * verification each.
71
+ */
72
+ const seenRows = new WeakSet();
73
+ /** Frozen rows that can still change (a mutable part, a getter): never kept. */
74
+ const unsettledRows = new WeakSet();
75
+ /**
76
+ * `value` can never change what `JSON.stringify` writes for it: a primitive,
77
+ * an image or document handle (their wire form is fixed when made), or a
78
+ * frozen plain object or array whose own enumerable properties are data
79
+ * properties (no getter, no own `toJSON`) holding such values. Freezing
80
+ * fixes the properties and the prototype for good, so the answer holds as
81
+ * long as the object lives.
82
+ */
83
+ function settled(value, depth) {
84
+ if (value === null || typeof value !== "object")
85
+ return typeof value !== "function";
86
+ if (value instanceof ImageHandle || value instanceof MarkdownHandle)
87
+ return true;
88
+ if (depth > UI_VALUE_LIMITS.depth || !Object.isFrozen(value))
89
+ return false;
90
+ const proto = Object.getPrototypeOf(value);
91
+ if (Array.isArray(value)
92
+ ? proto !== Array.prototype
93
+ : proto !== Object.prototype && proto !== null)
94
+ return false;
95
+ // An own `toJSON`, enumerable or not, is called by `JSON.stringify`.
96
+ if (Object.prototype.hasOwnProperty.call(value, "toJSON"))
97
+ return false;
98
+ for (const key of Object.keys(value)) {
99
+ const property = Object.getOwnPropertyDescriptor(value, key);
100
+ if (!property || !("value" in property))
101
+ return false;
102
+ if (!settled(property.value, depth + 1))
103
+ return false;
104
+ }
105
+ return true;
106
+ }
107
+ /** A row's encoding and marks, from {@link settledRows} when it is kept. */
108
+ function rowEncoding(row, cache) {
109
+ if (cache && row !== null && typeof row === "object") {
110
+ // A kept row is frozen: looked up first, `isFrozen` only on a miss.
111
+ const kept = settledRows.get(row);
112
+ if (kept)
113
+ return kept;
114
+ }
115
+ if (cache &&
116
+ row !== null &&
117
+ typeof row === "object" &&
118
+ Object.isFrozen(row)) {
119
+ const enc = encodeRow(row);
120
+ const entry = { enc, marks: marksOf(enc) };
121
+ if (!seenRows.has(row))
122
+ seenRows.add(row);
123
+ else if (!unsettledRows.has(row)) {
124
+ if (settled(row, 1))
125
+ settledRows.set(row, entry);
126
+ else
127
+ unsettledRows.add(row);
128
+ }
129
+ return entry;
130
+ }
131
+ const enc = encodeRow(row);
132
+ return { enc, marks: marksOf(enc) };
133
+ }
134
+ /** Each row of an array as `JSON.stringify` writes it; a hole is `null`. */
135
+ function encodeRows(rows) {
136
+ // A `toJSON` added to the shared prototypes would change any row's JSON:
137
+ // then nothing is taken from (or kept in) the cache.
138
+ const cache = !("toJSON" in Object.prototype) && !("toJSON" in Array.prototype);
139
+ const enc = [];
140
+ const marks = new Uint8Array(rows.length);
141
+ let markdown = false;
142
+ for (let i = 0; i < rows.length; i++) {
143
+ const row = rowEncoding(rows[i], cache);
144
+ enc[i] = row.enc;
145
+ marks[i] = row.marks;
146
+ if (row.marks & HAS_MARKDOWN)
147
+ markdown = true;
148
+ }
149
+ return { enc, marks, markdown, cache };
150
+ }
151
+ /** A row's limit counts, kept with its encoding when the row cannot change. */
152
+ function rowCount(row, cache) {
153
+ const kept = cache && row !== null && typeof row === "object"
154
+ ? settledRows.get(row)
155
+ : undefined;
156
+ if (!kept)
157
+ return countRow(row);
158
+ return (kept.count ??= countRow(row));
159
+ }
160
+ function sameRows(a, b) {
161
+ if (a.length !== b.length)
162
+ return false;
163
+ for (let i = 0; i < a.length; i++)
164
+ if (a[i] !== b[i])
165
+ return false;
166
+ return true;
167
+ }
168
+ /** An initial property (wire form) as sent. */
169
+ function sentOf(value, patches) {
170
+ if (isPrimitive(value))
171
+ return { kind: "value", value };
172
+ if (Array.isArray(value) && patches) {
173
+ const keyed = keyOf(value);
174
+ return {
175
+ kind: "rows",
176
+ rows: {
177
+ enc: encodeRows(value).enc,
178
+ field: keyed?.field ?? null,
179
+ keys: keyed?.keys ?? null,
180
+ counts: [],
181
+ },
182
+ };
183
+ }
184
+ return { kind: "json", enc: JSON.stringify(value) };
185
+ }
186
+ /**
187
+ * A write sent in parts stopped because its view closed or showed its
188
+ * initial properties again: not a failure.
189
+ */
190
+ class Abandoned extends Error {
191
+ }
192
+ /** The wire key of a Markdown document: rows holding one are never sent in parts. */ const MARKDOWN_KEY = '"$vestraMarkdown"';
193
+ /** A row larger than one request can carry (`limit`). */
194
+ const rowTooLarge = (key, bytes) => new HostError("limit", `${key}: a row of ${bytes} bytes is larger than one request takes (${CHUNK_BYTES} bytes)`);
195
+ /** Refuse a patch carrying a row larger than one request, before sending it. */
196
+ function tooLarge(key, ops) {
197
+ for (const op of ops) {
198
+ const rows = op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : [];
199
+ for (const row of rows) {
200
+ const bytes = rowBytes(row);
201
+ if (bytes > CHUNK_BYTES)
202
+ throw rowTooLarge(key, bytes);
203
+ }
204
+ }
205
+ }
206
+ /**
207
+ * The row patch from `sent` to `next` (the bound `rows`), in wire form, and
208
+ * the new rows' limit counts; `null` when the whole value is to be sent:
209
+ * nothing to type the rows by (no rows sent), a Markdown document in a row
210
+ * (the player merges documents in place), too many operations, a value over
211
+ * the limits (the player refuses it whole, as before), or no change (an
212
+ * explicit `set()` of the same value).
213
+ */
214
+ function rowPatch(sent, next, rows) {
215
+ if (sent.enc.length === 0)
216
+ return null;
217
+ if (next.markdown)
218
+ return null;
219
+ const nextKeyed = sent.field === null ? null : next.keyed();
220
+ const keyed = nextKeyed !== null && nextKeyed.field === sent.field;
221
+ const plan = diff({ enc: sent.enc, keys: keyed ? sent.keys : null }, { enc: next.enc, keys: keyed ? nextKeyed.keys : null });
222
+ if (!plan || plan.ops.length === 0)
223
+ return null;
224
+ // A released image is refused wherever it is, as in a whole write: the
225
+ // rows the patch does not carry are checked too (those holding images).
226
+ for (let j = 0; j < rows.length; j++)
227
+ if (plan.from[j] >= 0 && next.marks[j] & HAS_IMAGE)
228
+ wireValue(rows[j] ?? null);
229
+ const counts = next.enc.map((_, j) => {
230
+ const from = plan.from[j];
231
+ return ((from >= 0 ? sent.counts[from] : undefined) ??
232
+ rowCount(rows[j], next.cache));
233
+ });
234
+ if (!withinLimits(counts))
235
+ return null;
236
+ // The ops must give exactly the new rows (a guard against this diff's bugs).
237
+ if (!sameRows(replay(sent.enc, plan.ops, next.enc), next.enc))
238
+ return null;
239
+ const ops = plan.ops.map((op) => {
240
+ switch (op.op) {
241
+ case "set":
242
+ return { op: "set", at: op.at, row: wireValue(rows[op.row] ?? null) };
243
+ case "insert":
244
+ return {
245
+ op: "insert",
246
+ at: op.at,
247
+ rows: op.rows.map((j) => wireValue(rows[j] ?? null)),
248
+ };
249
+ default:
250
+ return op;
251
+ }
252
+ });
253
+ return { ops, counts };
254
+ }
255
+ /**
256
+ * One view's properties: their values now, what the player shows as far as
257
+ * this side knows, and the writes that bring the one to the other.
258
+ */
259
+ export class ViewWriter {
260
+ #target;
261
+ #values = new Map();
262
+ /** What the host currently shows, as far as this side knows. */
263
+ #sent = new Map();
264
+ /**
265
+ * Row patches (player feature `ui.patch`): each property's last write the
266
+ * player confirmed, by `seq` (0: the initial properties). A patch applies
267
+ * on it; a property without one is sent whole.
268
+ */
269
+ #bases = new Map();
270
+ /**
271
+ * Patches of a property answered `stale` in a row: after two, the
272
+ * property is sent whole until the view shows its initial properties again.
273
+ */
274
+ #stales = new Map();
275
+ #seq = 0;
276
+ /** Counts the times the view showed its initial properties (`shown`). */
277
+ #generation = 0;
278
+ /** Written by `set()`: sent even if equal to what was sent before. */
279
+ #forced = new Set();
280
+ /** The last value of each property whose write failed; not retried until it changes. */
281
+ #failed = new Map();
282
+ #backoff = 0;
283
+ /** A write failed and none succeeded since: further failures stay quiet. */
284
+ #broken = false;
285
+ #retry;
286
+ #flushing = false;
287
+ constructor(target) {
288
+ this.#target = target;
289
+ }
290
+ /** The player takes row patches (and `seq` on whole writes). */
291
+ get #patches() {
292
+ return this.#target.patches();
293
+ }
294
+ /**
295
+ * Initial properties in wire form. A released image is left out (the
296
+ * property keeps its default) and reported, like a failed write. With a
297
+ * player that takes row patches, the arrays of one request carry at most
298
+ * {@link CHUNK_BYTES} of rows together (SDK 1.5.46): an array over that
299
+ * goes with its first rows, and the first flush sends the rest as patches.
300
+ * An array over the value limits is refused here, as the player would
301
+ * refuse it whole: left out and reported (a panel, a settings page), or,
302
+ * with `refuse` (a window's `show`), thrown.
303
+ */
304
+ wired(values, refuse = false) {
305
+ const out = {};
306
+ let room = CHUNK_BYTES;
307
+ for (const [key, value] of Object.entries(values)) {
308
+ let wire;
309
+ try {
310
+ wire = wireValue(value);
311
+ }
312
+ catch (error) {
313
+ this.#failed.set(key, JSON.stringify(value));
314
+ this.#target.report(error);
315
+ continue;
316
+ }
317
+ if (this.#patches && Array.isArray(wire)) {
318
+ const sizes = wire.map(rowBytes);
319
+ const total = sizes.reduce((a, b) => a + b, 0);
320
+ if (total > room && !JSON.stringify(wire).includes(MARKDOWN_KEY)) {
321
+ const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
322
+ const problem = valueLimitProblem(wire);
323
+ if (problem || largest > CHUNK_BYTES) {
324
+ const error = problem
325
+ ? new HostError("limit", `${key}: ${problem}`)
326
+ : rowTooLarge(key, largest);
327
+ if (refuse)
328
+ throw error;
329
+ this.#failed.set(key, JSON.stringify(value));
330
+ this.#target.report(error);
331
+ continue;
332
+ }
333
+ const count = room > 0 ? prefixWithin(sizes, room) : 0;
334
+ wire = wire.slice(0, count);
335
+ room -= sizes.slice(0, count).reduce((a, b) => a + b, 0);
336
+ }
337
+ else
338
+ room -= total;
339
+ }
340
+ out[key] = wire;
341
+ }
342
+ return out;
343
+ }
344
+ /** `View.set`: each value is sent once even if equal to what was sent. */
345
+ set(props) {
346
+ for (const [key, value] of Object.entries(encode(props))) {
347
+ this.#values.set(key, value);
348
+ this.#forced.add(key);
349
+ this.#failed.delete(key); // an explicit write is tried again (after any back-off)
350
+ }
351
+ this.flush();
352
+ }
353
+ /** A bound value (Slint name): sent by the next flush if it changed. */
354
+ assign(key, value) {
355
+ this.#values.set(key, value);
356
+ }
357
+ /** Every property's value now, by Slint name. */
358
+ values() {
359
+ return Object.fromEntries(this.#values);
360
+ }
361
+ /** The host already shows these values (e.g. sent as initial properties). */
362
+ shown(values) {
363
+ this.#generation++;
364
+ const patches = this.#patches;
365
+ this.#sent = new Map(Object.entries(values).map(([k, v]) => [k, sentOf(v, patches)]));
366
+ this.#bases = new Map(patches ? Object.keys(values).map((key) => [key, 0]) : []);
367
+ this.#stales.clear();
368
+ this.#forced.clear();
369
+ for (const key of Object.keys(values))
370
+ this.#failed.delete(key);
371
+ }
372
+ /** `value` encoded, and whether it differs from what was sent. */
373
+ #encode(key, value) {
374
+ const sent = this.#sent.get(key);
375
+ if (isPrimitive(value)) {
376
+ // Compared as it is: no `JSON.stringify` for the scalars of a pass.
377
+ const changed = sent?.kind === "value"
378
+ ? sent.value !== value
379
+ : sent?.kind !== "json" || sent.enc !== JSON.stringify(value);
380
+ return {
381
+ key,
382
+ value,
383
+ changed,
384
+ signature: () => JSON.stringify(value),
385
+ sent: () => ({ kind: "value", value }),
386
+ };
387
+ }
388
+ if (Array.isArray(value) && this.#patches) {
389
+ // Row by row: the same work as one `JSON.stringify`, and the rows to
390
+ // diff against those last sent. Rows that cannot change (deep-frozen
391
+ // objects bound again, SDK 1.5.47) are taken from `settledRows`.
392
+ const encoded = encodeRows(value);
393
+ const { enc } = encoded;
394
+ const signature = () => `[${enc.join(",")}]`;
395
+ const changed = sent?.kind === "rows"
396
+ ? !sameRows(sent.rows.enc, enc)
397
+ : sent?.kind !== "json" || sent.enc !== signature();
398
+ // Keys only for a write: an unchanged array costs its encoding alone.
399
+ let keys;
400
+ const keyed = () => (keys === undefined ? (keys = keyOf(value)) : keys);
401
+ return {
402
+ key,
403
+ value,
404
+ changed,
405
+ signature,
406
+ rows: { ...encoded, keyed },
407
+ sent: (counts = []) => ({
408
+ kind: "rows",
409
+ rows: {
410
+ enc,
411
+ field: keyed()?.field ?? null,
412
+ keys: keyed()?.keys ?? null,
413
+ counts,
414
+ },
415
+ }),
416
+ };
417
+ }
418
+ const enc = JSON.stringify(value);
419
+ return {
420
+ key,
421
+ value,
422
+ changed: sent?.kind !== "json" || sent.enc !== enc,
423
+ signature: () => enc,
424
+ sent: () => ({ kind: "json", enc }),
425
+ };
426
+ }
427
+ /**
428
+ * Write one property: a row patch when the player takes them and one
429
+ * applies (else the whole value); a `stale` patch is followed by the whole
430
+ * value at once. The base is forgotten on any failure, so the next write
431
+ * of the property is whole. A write larger than one request
432
+ * ({@link CHUNK_BYTES} of rows, SDK 1.5.46) is sent in parts: a whole
433
+ * array as its first rows and `insert` patches of the rest, a patch as
434
+ * several, each applying on the one before.
435
+ */
436
+ async #send(pending) {
437
+ const { key, value } = pending;
438
+ if (!this.#patches) {
439
+ await this.#target.write(key, { value: wireValue(value) });
440
+ return pending.sent();
441
+ }
442
+ const generation = this.#generation;
443
+ const base = this.#bases.get(key);
444
+ this.#bases.delete(key);
445
+ const sent = this.#sent.get(key);
446
+ let plan = base !== undefined &&
447
+ sent?.kind === "rows" &&
448
+ pending.rows &&
449
+ (this.#stales.get(key) ?? 0) < 2
450
+ ? rowPatch(sent.rows, pending.rows, value)
451
+ : null;
452
+ // A patch sent in several never passes the row limit midway (the diff
453
+ // removes before it inserts; checked anyway); otherwise whole.
454
+ if (plan && sent?.kind === "rows") {
455
+ let length = sent.rows.enc.length;
456
+ for (const op of plan.ops) {
457
+ if (op.op === "insert")
458
+ length += op.rows.length;
459
+ else if (op.op === "remove")
460
+ length -= op.count;
461
+ if (length > UI_VALUE_LIMITS.arrayRows) {
462
+ plan = null;
463
+ break;
464
+ }
465
+ }
466
+ }
467
+ if (plan) {
468
+ tooLarge(key, plan.ops);
469
+ const [first, ...rest] = chunkOps(plan.ops);
470
+ const seq = ++this.#seq;
471
+ let applied = false;
472
+ try {
473
+ await this.#target.write(key, {
474
+ patch: { base: base, seq, ops: first },
475
+ });
476
+ applied = true;
477
+ }
478
+ catch (error) {
479
+ if (!(error instanceof HostError && error.code === "stale"))
480
+ throw error;
481
+ this.#stales.set(key, (this.#stales.get(key) ?? 0) + 1);
482
+ }
483
+ if (applied) {
484
+ this.#stales.delete(key);
485
+ await this.#parts(key, seq, rest, false, generation);
486
+ return pending.sent(plan.counts);
487
+ }
488
+ }
489
+ const seq = ++this.#seq;
490
+ // Rows holding Markdown documents go whole, as before: the player
491
+ // merges documents in place and takes no parts of them.
492
+ if (Array.isArray(value) && pending.rows && !pending.rows.markdown) {
493
+ const sizes = pending.rows.enc.map(jsonBytes);
494
+ const count = prefixWithin(sizes, CHUNK_BYTES);
495
+ if (count < value.length) {
496
+ const rows = value;
497
+ // Over the limits, or a row larger than one request: refused here,
498
+ // as the player would refuse the whole value, before any part is
499
+ // sent (a part refused midway would leave the list half sent).
500
+ const problem = valueLimitProblem(wireValue(value));
501
+ if (problem)
502
+ throw new HostError("limit", `${key}: ${problem}`);
503
+ const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
504
+ if (largest > CHUNK_BYTES)
505
+ throw rowTooLarge(key, largest);
506
+ // Every part but the last carries `more`: the player holds them
507
+ // and writes the whole value at the last, so a list it shows is
508
+ // written once (player 0.6.0).
509
+ await this.#target.write(key, {
510
+ value: wireValue(rows.slice(0, count)),
511
+ seq,
512
+ more: true,
513
+ });
514
+ const insert = {
515
+ op: "insert",
516
+ at: count,
517
+ rows: rows.slice(count).map((row) => wireValue(row ?? null)),
518
+ };
519
+ await this.#parts(key, seq, chunkOps([insert]), true, generation);
520
+ return pending.sent();
521
+ }
522
+ }
523
+ await this.#target.write(key, { value: wireValue(value), seq });
524
+ this.#bases.set(key, seq);
525
+ return pending.sent();
526
+ }
527
+ /**
528
+ * The rest of a write sent in parts: each patch applies on the one
529
+ * before (`base`), and the property's base is the last one's; with
530
+ * `more`, every part but the last says so. A failure leaves no base: the
531
+ * next write of the property is whole. When the view closed or showed its
532
+ * initial properties again meanwhile (`generation`), the rest is dropped
533
+ * quietly ({@link Abandoned}): the view's next flush sends what it shows.
534
+ */
535
+ async #parts(key, base, groups, more, generation) {
536
+ let last = base;
537
+ for (const [i, ops] of groups.entries()) {
538
+ if (!this.#target.live() || this.#generation !== generation)
539
+ throw new Abandoned();
540
+ const seq = ++this.#seq;
541
+ try {
542
+ await this.#target.write(key, {
543
+ patch: { base: last, seq, ops },
544
+ ...(more && i < groups.length - 1 ? { more: true } : {}),
545
+ });
546
+ }
547
+ catch (error) {
548
+ // Refused because the view went meanwhile: not a failure.
549
+ if (!this.#target.live() || this.#generation !== generation)
550
+ throw new Abandoned();
551
+ throw error;
552
+ }
553
+ last = seq;
554
+ }
555
+ this.#bases.set(key, last);
556
+ }
557
+ /**
558
+ * Send what changed. A bound value is sent when it differs from the last one
559
+ * sent; a `set()` value is always sent once. Nothing is resent merely
560
+ * because a callback arrived, so text the user typed into an in-out
561
+ * property survives other callbacks (call `set()` to pull a control back).
562
+ * A failed bound value is not repeated until it changes; an explicit `set()`
563
+ * is, after a back-off (0.25 s doubling to 30 s). Only the first failure of
564
+ * a failing streak is reported, so an error handler that writes a status to
565
+ * a broken view cannot spin.
566
+ */
567
+ flush() {
568
+ if (this.#flushing || this.#retry || !this.#target.live())
569
+ return;
570
+ this.#flushing = true;
571
+ void (async () => {
572
+ let failed = false;
573
+ let failure;
574
+ try {
575
+ let pending;
576
+ do {
577
+ pending = [];
578
+ for (const [key, value] of this.#values) {
579
+ const encoded = this.#encode(key, value);
580
+ const failedAs = this.#failed.get(key);
581
+ if (failedAs !== undefined && failedAs === encoded.signature())
582
+ continue;
583
+ if (this.#forced.has(key) || encoded.changed)
584
+ pending.push(encoded);
585
+ }
586
+ for (const encoded of pending) {
587
+ if (!this.#target.live())
588
+ return;
589
+ const key = encoded.key;
590
+ this.#forced.delete(key);
591
+ try {
592
+ this.#sent.set(key, await this.#send(encoded));
593
+ this.#failed.delete(key);
594
+ this.#backoff = 0;
595
+ this.#broken = false;
596
+ }
597
+ catch (error) {
598
+ // The view closed or showed its initial properties again while
599
+ // a write was sent in parts: nothing failed (the next flush
600
+ // sends what the view shows).
601
+ if (error instanceof Abandoned) {
602
+ queueMicrotask(() => this.flush());
603
+ return;
604
+ }
605
+ failed = true;
606
+ // Report the start of a failing streak only.
607
+ if (!this.#broken)
608
+ failure ??= error;
609
+ this.#broken = true;
610
+ this.#failed.set(key, encoded.signature());
611
+ }
612
+ }
613
+ } while (pending.length && !failed && this.#target.live());
614
+ }
615
+ finally {
616
+ this.#flushing = false;
617
+ }
618
+ if (!failed)
619
+ return;
620
+ this.#backoff = Math.min(Math.max(250, this.#backoff * 2), 30_000);
621
+ this.#retry = setTimeout(() => {
622
+ this.#retry = undefined;
623
+ this.flush();
624
+ }, this.#backoff);
625
+ this.#retry.unref?.();
626
+ if (failure !== undefined)
627
+ this.#target.report(failure);
628
+ })();
629
+ }
630
+ /** The view closed: no retry after a back-off. */
631
+ stop() {
632
+ clearTimeout(this.#retry);
633
+ }
634
+ }
@@ -9,7 +9,8 @@
9
9
  * The first plugin to call one owns it; the owner's `dispose` (or the plugin
10
10
  * stopping) puts the defaults back.
11
11
  */
12
- import { failure, Latest, quiet } from "./runtime.js";
12
+ import { Latest } from "./latest.js";
13
+ import { failure, quiet } from "./errors.js";
13
14
  import { playerOf } from "./player.js";
14
15
  import { isAbsolute } from "node:path";
15
16
  export const VIEW_DEFAULTS = Object.freeze({
@@ -1,5 +1,5 @@
1
1
  /** The main window (`plugin.window`) and the player's own facts (`plugin.app`). */
2
- import { failure } from "./runtime.js";
2
+ import { failure } from "./errors.js";
3
3
  const view = (facts) => Object.freeze({
4
4
  fullscreen: facts.fullscreen === true,
5
5
  // Players before 0.6.3 do not send it; never maximized in fullscreen.
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.6.17: 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.6.19: 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