@vestra-player/sdk 1.5.45 → 1.5.47

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/dist/plugin/ui.js CHANGED
@@ -7,7 +7,8 @@ import { failure, Latest, quiet } from "./runtime.js";
7
7
  import { dropsOf } from "./drops.js";
8
8
  import { MarkdownHandle, markdown, remoteImageFile } from "./markdown.js";
9
9
  import { HostError } from "../wire.js";
10
- import { countRow, diff, keyOf, PATCH_FEATURE, replay, withinLimits, } from "./patch.js";
10
+ import { countRow, diff, keyOf, CHUNK_BYTES, chunkOps, PATCH_FEATURE, prefixWithin, jsonBytes, rowBytes, replay, withinLimits, } from "./patch.js";
11
+ import { UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
11
12
  /** Slint names are kebab-case; plugin code may use camelCase. */
12
13
  export const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
13
14
  const encode = (props) => Object.fromEntries(Object.entries(props).map(([k, v]) => [kebab(k), v]));
@@ -82,8 +83,113 @@ const isPrimitive = (value) => value === null ||
82
83
  (typeof value === "number" && Number.isFinite(value));
83
84
  /** A row as `JSON.stringify` writes it inside an array. */
84
85
  const encodeRow = (row) => JSON.stringify(row) ?? "null";
86
+ /** What a row's encoding holds: a Markdown document, an image. */
87
+ const HAS_MARKDOWN = 1;
88
+ const HAS_IMAGE = 2;
89
+ const marksOf = (enc) => (enc.includes('"$vestraMarkdown"') ? HAS_MARKDOWN : 0) |
90
+ (enc.includes('"$vestraImage"') ? HAS_IMAGE : 0);
91
+ /**
92
+ * Row encodings by row object, shared by every property (a row's JSON does
93
+ * not depend on where it is bound). Only rows that cannot change are kept
94
+ * ({@link settled}): a plugin that mutates its rows in place sees no
95
+ * difference, and one that keeps unchanged rows as the same deep-frozen
96
+ * objects skips encoding them on every pass. Weak: a row the plugin drops
97
+ * goes with its entry.
98
+ */
99
+ const settledRows = new WeakMap();
100
+ /**
101
+ * Frozen rows seen once. A frozen row is verified and kept on its second
102
+ * pass, so rows frozen afresh on every pass cost one set entry, not a
103
+ * verification each.
104
+ */
105
+ const seenRows = new WeakSet();
106
+ /** Frozen rows that can still change (a mutable part, a getter): never kept. */
107
+ const unsettledRows = new WeakSet();
108
+ /**
109
+ * `value` can never change what `JSON.stringify` writes for it: a primitive,
110
+ * an image or document handle (their wire form is fixed when made), or a
111
+ * frozen plain object or array whose own enumerable properties are data
112
+ * properties (no getter, no own `toJSON`) holding such values. Freezing
113
+ * fixes the properties and the prototype for good, so the answer holds as
114
+ * long as the object lives.
115
+ */
116
+ function settled(value, depth) {
117
+ if (value === null || typeof value !== "object")
118
+ return typeof value !== "function";
119
+ if (value instanceof ImageHandle || value instanceof MarkdownHandle)
120
+ return true;
121
+ if (depth > UI_VALUE_LIMITS.depth || !Object.isFrozen(value))
122
+ return false;
123
+ const proto = Object.getPrototypeOf(value);
124
+ if (Array.isArray(value)
125
+ ? proto !== Array.prototype
126
+ : proto !== Object.prototype && proto !== null)
127
+ return false;
128
+ // An own `toJSON`, enumerable or not, is called by `JSON.stringify`.
129
+ if (Object.prototype.hasOwnProperty.call(value, "toJSON"))
130
+ return false;
131
+ for (const key of Object.keys(value)) {
132
+ const property = Object.getOwnPropertyDescriptor(value, key);
133
+ if (!property || !("value" in property))
134
+ return false;
135
+ if (!settled(property.value, depth + 1))
136
+ return false;
137
+ }
138
+ return true;
139
+ }
140
+ /** A row's encoding and marks, from {@link settledRows} when it is kept. */
141
+ function rowEncoding(row, cache) {
142
+ if (cache && row !== null && typeof row === "object") {
143
+ // A kept row is frozen: looked up first, `isFrozen` only on a miss.
144
+ const kept = settledRows.get(row);
145
+ if (kept)
146
+ return kept;
147
+ }
148
+ if (cache &&
149
+ row !== null &&
150
+ typeof row === "object" &&
151
+ Object.isFrozen(row)) {
152
+ const enc = encodeRow(row);
153
+ const entry = { enc, marks: marksOf(enc) };
154
+ if (!seenRows.has(row))
155
+ seenRows.add(row);
156
+ else if (!unsettledRows.has(row)) {
157
+ if (settled(row, 1))
158
+ settledRows.set(row, entry);
159
+ else
160
+ unsettledRows.add(row);
161
+ }
162
+ return entry;
163
+ }
164
+ const enc = encodeRow(row);
165
+ return { enc, marks: marksOf(enc) };
166
+ }
85
167
  /** Each row of an array as `JSON.stringify` writes it; a hole is `null`. */
86
- const encodeRows = (rows) => Array.from(rows, encodeRow);
168
+ function encodeRows(rows) {
169
+ // A `toJSON` added to the shared prototypes would change any row's JSON:
170
+ // then nothing is taken from (or kept in) the cache.
171
+ const cache = !("toJSON" in Object.prototype) && !("toJSON" in Array.prototype);
172
+ const enc = [];
173
+ const marks = new Uint8Array(rows.length);
174
+ let markdown = false;
175
+ for (let i = 0; i < rows.length; i++) {
176
+ const row = rowEncoding(rows[i], cache);
177
+ enc[i] = row.enc;
178
+ marks[i] = row.marks;
179
+ if (row.marks & HAS_MARKDOWN)
180
+ markdown = true;
181
+ }
182
+ return { enc, marks, markdown, cache };
183
+ }
184
+ /** A row's limit counts, kept with its encoding when the row cannot change. */
185
+ function rowCount(row, cache) {
186
+ const kept = cache && row !== null && typeof row === "object"
187
+ ? settledRows.get(row)
188
+ : undefined;
189
+ if (!kept)
190
+ return countRow(row);
191
+ return (kept.count ??= countRow(row));
192
+ }
87
193
  function sameRows(a, b) {
88
194
  if (a.length !== b.length)
89
195
  return false;
@@ -101,7 +207,7 @@ function sentOf(value, patches) {
101
207
  return {
102
208
  kind: "rows",
103
209
  rows: {
104
- enc: encodeRows(value),
210
+ enc: encodeRows(value).enc,
105
211
  field: keyed?.field ?? null,
106
212
  keys: keyed?.keys ?? null,
107
213
  counts: [],
@@ -110,6 +216,26 @@ function sentOf(value, patches) {
110
216
  }
111
217
  return { kind: "json", enc: JSON.stringify(value) };
112
218
  }
219
+ /**
220
+ * A write sent in parts stopped because its view closed or showed its
221
+ * initial properties again: not a failure.
222
+ */
223
+ class Abandoned extends Error {
224
+ }
225
+ /** The wire key of a Markdown document: rows holding one are never sent in parts. */ const MARKDOWN_KEY = '"$vestraMarkdown"';
226
+ /** A row larger than one request can carry (`limit`). */
227
+ const rowTooLarge = (key, bytes) => new HostError("limit", `${key}: a row of ${bytes} bytes is larger than one request takes (${CHUNK_BYTES} bytes)`);
228
+ /** Refuse a patch carrying a row larger than one request, before sending it. */
229
+ function tooLarge(key, ops) {
230
+ for (const op of ops) {
231
+ const rows = op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : [];
232
+ for (const row of rows) {
233
+ const bytes = rowBytes(row);
234
+ if (bytes > CHUNK_BYTES)
235
+ throw rowTooLarge(key, bytes);
236
+ }
237
+ }
238
+ }
113
239
  /**
114
240
  * The row patch from `sent` to `next` (the bound `rows`), in wire form, and
115
241
  * the new rows' limit counts; `null` when the whole value is to be sent:
@@ -121,7 +247,7 @@ function sentOf(value, patches) {
121
247
  function rowPatch(sent, next, rows) {
122
248
  if (sent.enc.length === 0)
123
249
  return null;
124
- if (next.enc.some((row) => row.includes('"$vestraMarkdown"')))
250
+ if (next.markdown)
125
251
  return null;
126
252
  const nextKeyed = sent.field === null ? null : next.keyed();
127
253
  const keyed = nextKeyed !== null && nextKeyed.field === sent.field;
@@ -131,11 +257,12 @@ function rowPatch(sent, next, rows) {
131
257
  // A released image is refused wherever it is, as in a whole write: the
132
258
  // rows the patch does not carry are checked too (those holding images).
133
259
  for (let j = 0; j < rows.length; j++)
134
- if (plan.from[j] >= 0 && next.enc[j].includes('"$vestraImage"'))
260
+ if (plan.from[j] >= 0 && next.marks[j] & HAS_IMAGE)
135
261
  wireValue(rows[j] ?? null);
136
262
  const counts = next.enc.map((_, j) => {
137
263
  const from = plan.from[j];
138
- return (from >= 0 ? sent.counts[from] : undefined) ?? countRow(rows[j]);
264
+ return ((from >= 0 ? sent.counts[from] : undefined) ??
265
+ rowCount(rows[j], next.cache));
139
266
  });
140
267
  if (!withinLimits(counts))
141
268
  return null;
@@ -194,6 +321,8 @@ export class ViewBase {
194
321
  */
195
322
  #stales = new Map();
196
323
  #seq = 0;
324
+ /** Counts the times the view showed its initial properties (`shown`). */
325
+ #generation = 0;
197
326
  /** Written by `set()`: sent even if equal to what was sent before. */
198
327
  #forced = new Set();
199
328
  /** The last value of each property whose write failed; not retried until it changes. */
@@ -215,18 +344,51 @@ export class ViewBase {
215
344
  }
216
345
  /**
217
346
  * Initial properties in wire form. A released image is left out (the
218
- * property keeps its default) and reported, like a failed write.
347
+ * property keeps its default) and reported, like a failed write. With a
348
+ * player that takes row patches, the arrays of one request carry at most
349
+ * {@link CHUNK_BYTES} of rows together (SDK 1.5.46): an array over that
350
+ * goes with its first rows, and the first flush sends the rest as patches.
351
+ * An array over the value limits is refused here, as the player would
352
+ * refuse it whole: left out and reported (a panel, a settings page), or,
353
+ * with `refuse` (a window's `show`), thrown.
219
354
  */
220
- wired(values) {
355
+ wired(values, refuse = false) {
221
356
  const out = {};
357
+ let room = CHUNK_BYTES;
222
358
  for (const [key, value] of Object.entries(values)) {
359
+ let wire;
223
360
  try {
224
- out[key] = wireValue(value);
361
+ wire = wireValue(value);
225
362
  }
226
363
  catch (error) {
227
364
  this.#failed.set(key, JSON.stringify(value));
228
365
  this.scope.report(error);
366
+ continue;
367
+ }
368
+ if (this.#patches && Array.isArray(wire)) {
369
+ const sizes = wire.map(rowBytes);
370
+ const total = sizes.reduce((a, b) => a + b, 0);
371
+ if (total > room && !JSON.stringify(wire).includes(MARKDOWN_KEY)) {
372
+ const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
373
+ const problem = valueLimitProblem(wire);
374
+ if (problem || largest > CHUNK_BYTES) {
375
+ const error = problem
376
+ ? new HostError("limit", `${key}: ${problem}`)
377
+ : rowTooLarge(key, largest);
378
+ if (refuse)
379
+ throw error;
380
+ this.#failed.set(key, JSON.stringify(value));
381
+ this.scope.report(error);
382
+ continue;
383
+ }
384
+ const count = room > 0 ? prefixWithin(sizes, room) : 0;
385
+ wire = wire.slice(0, count);
386
+ room -= sizes.slice(0, count).reduce((a, b) => a + b, 0);
387
+ }
388
+ else
389
+ room -= total;
229
390
  }
391
+ out[key] = wire;
230
392
  }
231
393
  return out;
232
394
  }
@@ -289,6 +451,7 @@ export class ViewBase {
289
451
  }
290
452
  /** The host already shows these values (e.g. sent as initial properties). */
291
453
  shown(values) {
454
+ this.#generation++;
292
455
  const patches = this.#patches;
293
456
  this.#sent = new Map(Object.entries(values).map(([k, v]) => [k, sentOf(v, patches)]));
294
457
  this.#bases = new Map(patches ? Object.keys(values).map((key) => [key, 0]) : []);
@@ -315,8 +478,10 @@ export class ViewBase {
315
478
  }
316
479
  if (Array.isArray(value) && this.#patches) {
317
480
  // Row by row: the same work as one `JSON.stringify`, and the rows to
318
- // diff against those last sent.
319
- const enc = encodeRows(value);
481
+ // diff against those last sent. Rows that cannot change (deep-frozen
482
+ // objects bound again, SDK 1.5.47) are taken from `settledRows`.
483
+ const encoded = encodeRows(value);
484
+ const { enc } = encoded;
320
485
  const signature = () => `[${enc.join(",")}]`;
321
486
  const changed = sent?.kind === "rows"
322
487
  ? !sameRows(sent.rows.enc, enc)
@@ -329,7 +494,7 @@ export class ViewBase {
329
494
  value,
330
495
  changed,
331
496
  signature,
332
- rows: { enc, keyed },
497
+ rows: { ...encoded, keyed },
333
498
  sent: (counts = []) => ({
334
499
  kind: "rows",
335
500
  rows: {
@@ -354,7 +519,10 @@ export class ViewBase {
354
519
  * Write one property: a row patch when the player takes them and one
355
520
  * applies (else the whole value); a `stale` patch is followed by the whole
356
521
  * value at once. The base is forgotten on any failure, so the next write
357
- * of the property is whole.
522
+ * of the property is whole. A write larger than one request
523
+ * ({@link CHUNK_BYTES} of rows, SDK 1.5.46) is sent in parts: a whole
524
+ * array as its first rows and `insert` patches of the rest, a patch as
525
+ * several, each applying on the one before.
358
526
  */
359
527
  async #send(pending) {
360
528
  const { key, value } = pending;
@@ -362,34 +530,119 @@ export class ViewBase {
362
530
  await this.write(key, { value: wireValue(value) });
363
531
  return pending.sent();
364
532
  }
533
+ const generation = this.#generation;
365
534
  const base = this.#bases.get(key);
366
535
  this.#bases.delete(key);
367
536
  const sent = this.#sent.get(key);
368
- const plan = base !== undefined &&
537
+ let plan = base !== undefined &&
369
538
  sent?.kind === "rows" &&
370
539
  pending.rows &&
371
540
  (this.#stales.get(key) ?? 0) < 2
372
541
  ? rowPatch(sent.rows, pending.rows, value)
373
542
  : null;
543
+ // A patch sent in several never passes the row limit midway (the diff
544
+ // removes before it inserts; checked anyway); otherwise whole.
545
+ if (plan && sent?.kind === "rows") {
546
+ let length = sent.rows.enc.length;
547
+ for (const op of plan.ops) {
548
+ if (op.op === "insert")
549
+ length += op.rows.length;
550
+ else if (op.op === "remove")
551
+ length -= op.count;
552
+ if (length > UI_VALUE_LIMITS.arrayRows) {
553
+ plan = null;
554
+ break;
555
+ }
556
+ }
557
+ }
374
558
  if (plan) {
559
+ tooLarge(key, plan.ops);
560
+ const [first, ...rest] = chunkOps(plan.ops);
375
561
  const seq = ++this.#seq;
562
+ let applied = false;
376
563
  try {
377
- await this.write(key, { patch: { base: base, seq, ops: plan.ops } });
378
- this.#bases.set(key, seq);
379
- this.#stales.delete(key);
380
- return pending.sent(plan.counts);
564
+ await this.write(key, { patch: { base: base, seq, ops: first } });
565
+ applied = true;
381
566
  }
382
567
  catch (error) {
383
568
  if (!(error instanceof HostError && error.code === "stale"))
384
569
  throw error;
385
570
  this.#stales.set(key, (this.#stales.get(key) ?? 0) + 1);
386
571
  }
572
+ if (applied) {
573
+ this.#stales.delete(key);
574
+ await this.#parts(key, seq, rest, false, generation);
575
+ return pending.sent(plan.counts);
576
+ }
387
577
  }
388
578
  const seq = ++this.#seq;
579
+ // Rows holding Markdown documents go whole, as before: the player
580
+ // merges documents in place and takes no parts of them.
581
+ if (Array.isArray(value) && pending.rows && !pending.rows.markdown) {
582
+ const sizes = pending.rows.enc.map(jsonBytes);
583
+ const count = prefixWithin(sizes, CHUNK_BYTES);
584
+ if (count < value.length) {
585
+ const rows = value;
586
+ // Over the limits, or a row larger than one request: refused here,
587
+ // as the player would refuse the whole value, before any part is
588
+ // sent (a part refused midway would leave the list half sent).
589
+ const problem = valueLimitProblem(wireValue(value));
590
+ if (problem)
591
+ throw new HostError("limit", `${key}: ${problem}`);
592
+ const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
593
+ if (largest > CHUNK_BYTES)
594
+ throw rowTooLarge(key, largest);
595
+ // Every part but the last carries `more`: the player holds them
596
+ // and writes the whole value at the last, so a list it shows is
597
+ // written once (player 0.6.0).
598
+ await this.write(key, {
599
+ value: wireValue(rows.slice(0, count)),
600
+ seq,
601
+ more: true,
602
+ });
603
+ const insert = {
604
+ op: "insert",
605
+ at: count,
606
+ rows: rows.slice(count).map((row) => wireValue(row ?? null)),
607
+ };
608
+ await this.#parts(key, seq, chunkOps([insert]), true, generation);
609
+ return pending.sent();
610
+ }
611
+ }
389
612
  await this.write(key, { value: wireValue(value), seq });
390
613
  this.#bases.set(key, seq);
391
614
  return pending.sent();
392
615
  }
616
+ /**
617
+ * The rest of a write sent in parts: each patch applies on the one
618
+ * before (`base`), and the property's base is the last one's; with
619
+ * `more`, every part but the last says so. A failure leaves no base: the
620
+ * next write of the property is whole. When the view closed or showed its
621
+ * initial properties again meanwhile (`generation`), the rest is dropped
622
+ * quietly ({@link Abandoned}): the view's next flush sends what it shows.
623
+ */
624
+ async #parts(key, base, groups, more, generation) {
625
+ let last = base;
626
+ for (const [i, ops] of groups.entries()) {
627
+ if (!this.live() || this.#generation !== generation)
628
+ throw new Abandoned();
629
+ const seq = ++this.#seq;
630
+ try {
631
+ await this.write(key, {
632
+ patch: { base: last, seq, ops },
633
+ ...(more && i < groups.length - 1 ? { more: true } : {}),
634
+ });
635
+ }
636
+ catch (error) {
637
+ // Refused because the view went meanwhile: not a failure.
638
+ if (!this.live() || this.#generation !== generation)
639
+ throw new Abandoned();
640
+ throw error;
641
+ }
642
+ last = seq;
643
+ }
644
+ this.#bases.set(key, last);
645
+ }
393
646
  /**
394
647
  * Send what changed. A bound value is sent when it differs from the last one
395
648
  * sent; a `set()` value is always sent once. Nothing is resent merely
@@ -431,6 +684,13 @@ export class ViewBase {
431
684
  this.#broken = false;
432
685
  }
433
686
  catch (error) {
687
+ // The view closed or showed its initial properties again while
688
+ // a write was sent in parts: nothing failed (the next flush
689
+ // sends what the view shows).
690
+ if (error instanceof Abandoned) {
691
+ queueMicrotask(() => this.flush());
692
+ return;
693
+ }
434
694
  failed = true;
435
695
  // Report the start of a failing streak only.
436
696
  if (!this.#broken)
@@ -530,7 +790,7 @@ class WindowHandle extends ViewBase {
530
790
  const ui = this.scope.runtime.host.ui;
531
791
  if (this.#opened)
532
792
  return ui.show(this.#id, true);
533
- const initial = this.wired(this.snapshot());
793
+ const initial = this.wired(this.snapshot(), true);
534
794
  await ui.open(this.#id, this.#file, initial, [...this.handlers.keys()]);
535
795
  this.#opened = true;
536
796
  this.shown(initial);
@@ -12,41 +12,26 @@ import type { Json } from "../wire.ts";
12
12
  * makes `Window.show` reject; a panel or settings page opens without it and
13
13
  * the failure goes to the plugin's `onError`.
14
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.
15
+ * Since player 0.6.0 (SDK 1.5.46) they allow a whole list: a plugin binds
16
+ * all of its rows (a playlist of 50,000 entries fits) and the view draws
17
+ * them with a Slint `ListView`, which builds only the rows on screen. The
18
+ * SDK sends a changed array as row patches, and an array larger than one
19
+ * request (about 512 KiB of rows) as its first rows followed by `insert`
20
+ * patches of the rest; the player converts large writes off its interface
21
+ * thread. Before player 0.6.0 the limits were 4,096 rows, 16,384 values and
22
+ * 1 MiB of text, so a long list had to be sent as a window of rows.
23
+ *
24
+ * In a row, `null` where the view declares an `image` shows no picture
25
+ * (player 0.6.0): rows without a thumbnail need no placeholder image.
26
+ *
27
+ * @example Bind a whole list
28
+ * The view draws every row with a `ListView`; the SDK sends what changed.
19
29
  *
20
- * @example Send a long playlist as a window of rows
21
- * A list view shows any number of entries while each write stays small:
22
- * the plugin sends only the rows around the scroll position plus the total,
23
- * and the view reports where it scrolled to. After the `scrolled` handler
24
- * returns, the SDK evaluates the `bind` function again and sends the new
25
- * window. Each row object and each of its fields count as one value toward
26
- * `values`, and so does the array holding them.
27
30
  * ```ts
28
- * declare const entries: { title: string; duration: number }[];
29
- * import type { View } from "@vestra-player/sdk";
31
+ * declare const entries: { id: string; title: string; duration: string }[];
30
32
  * declare const list: View;
31
33
  * // ---cut---
32
- * import { UI_VALUE_LIMITS } from "@vestra-player/sdk/testing";
33
- *
34
- * // 200 rows, or fewer if the limits were smaller: 2 fields per row make
35
- * // 3 values (the row and its fields), plus 1 for the array.
36
- * const size = Math.min(
37
- * 200,
38
- * UI_VALUE_LIMITS.arrayRows,
39
- * Math.floor((UI_VALUE_LIMITS.values - 1) / 3),
40
- * );
41
- * let first = 0;
42
- * list.bind(() => ({
43
- * total: entries.length,
44
- * rows: entries.slice(first, first + size),
45
- * }));
46
- * // The Slint view reports its first visible row; the window follows it.
47
- * list.on("scrolled", (row: number) => {
48
- * first = Math.max(0, row - Math.floor(size / 2));
49
- * });
34
+ * list.bind(() => ({ rows: entries.map((e) => ({ ...e, selected: false })) }));
50
35
  * ```
51
36
  *
52
37
  * @since 1.5.10
@@ -59,16 +44,16 @@ export declare const UI_VALUE_LIMITS: {
59
44
  /**
60
45
  * Values in all, counting every array, object, string, number, boolean
61
46
  * and null, the value itself included; a Markdown document is one
62
- * (16384).
47
+ * (1,048,576; 16,384 before player 0.6.0).
63
48
  */
64
- readonly values: 16384;
65
- /** Rows of one array (4096). */
66
- readonly arrayRows: 4096;
49
+ readonly values: number;
50
+ /** Rows of one array (100,000; 4,096 before player 0.6.0). */
51
+ readonly arrayRows: 100000;
67
52
  /** UTF-8 bytes of one string or object field name (64 KiB). */
68
53
  readonly stringBytes: number;
69
54
  /**
70
55
  * UTF-8 bytes of all strings and field names together, the text of the
71
- * value's Markdown documents included (1 MiB).
56
+ * value's Markdown documents included (32 MiB; 1 MiB before player 0.6.0).
72
57
  */
73
58
  readonly textBytes: number;
74
59
  };
@@ -10,41 +10,26 @@
10
10
  * makes `Window.show` reject; a panel or settings page opens without it and
11
11
  * the failure goes to the plugin's `onError`.
12
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.
13
+ * Since player 0.6.0 (SDK 1.5.46) they allow a whole list: a plugin binds
14
+ * all of its rows (a playlist of 50,000 entries fits) and the view draws
15
+ * them with a Slint `ListView`, which builds only the rows on screen. The
16
+ * SDK sends a changed array as row patches, and an array larger than one
17
+ * request (about 512 KiB of rows) as its first rows followed by `insert`
18
+ * patches of the rest; the player converts large writes off its interface
19
+ * thread. Before player 0.6.0 the limits were 4,096 rows, 16,384 values and
20
+ * 1 MiB of text, so a long list had to be sent as a window of rows.
21
+ *
22
+ * In a row, `null` where the view declares an `image` shows no picture
23
+ * (player 0.6.0): rows without a thumbnail need no placeholder image.
24
+ *
25
+ * @example Bind a whole list
26
+ * The view draws every row with a `ListView`; the SDK sends what changed.
17
27
  *
18
- * @example Send a long playlist as a window of rows
19
- * A list view shows any number of entries while each write stays small:
20
- * the plugin sends only the rows around the scroll position plus the total,
21
- * and the view reports where it scrolled to. After the `scrolled` handler
22
- * returns, the SDK evaluates the `bind` function again and sends the new
23
- * window. Each row object and each of its fields count as one value toward
24
- * `values`, and so does the array holding them.
25
28
  * ```ts
26
- * declare const entries: { title: string; duration: number }[];
27
- * import type { View } from "@vestra-player/sdk";
29
+ * declare const entries: { id: string; title: string; duration: string }[];
28
30
  * declare const list: View;
29
31
  * // ---cut---
30
- * import { UI_VALUE_LIMITS } from "@vestra-player/sdk/testing";
31
- *
32
- * // 200 rows, or fewer if the limits were smaller: 2 fields per row make
33
- * // 3 values (the row and its fields), plus 1 for the array.
34
- * const size = Math.min(
35
- * 200,
36
- * UI_VALUE_LIMITS.arrayRows,
37
- * Math.floor((UI_VALUE_LIMITS.values - 1) / 3),
38
- * );
39
- * let first = 0;
40
- * list.bind(() => ({
41
- * total: entries.length,
42
- * rows: entries.slice(first, first + size),
43
- * }));
44
- * // The Slint view reports its first visible row; the window follows it.
45
- * list.on("scrolled", (row: number) => {
46
- * first = Math.max(0, row - Math.floor(size / 2));
47
- * });
32
+ * list.bind(() => ({ rows: entries.map((e) => ({ ...e, selected: false })) }));
48
33
  * ```
49
34
  *
50
35
  * @since 1.5.10
@@ -57,18 +42,18 @@ export const UI_VALUE_LIMITS = {
57
42
  /**
58
43
  * Values in all, counting every array, object, string, number, boolean
59
44
  * and null, the value itself included; a Markdown document is one
60
- * (16384).
45
+ * (1,048,576; 16,384 before player 0.6.0).
61
46
  */
62
- values: 16384,
63
- /** Rows of one array (4096). */
64
- arrayRows: 4096,
47
+ values: 1024 * 1024,
48
+ /** Rows of one array (100,000; 4,096 before player 0.6.0). */
49
+ arrayRows: 100_000,
65
50
  /** UTF-8 bytes of one string or object field name (64 KiB). */
66
51
  stringBytes: 64 * 1024,
67
52
  /**
68
53
  * UTF-8 bytes of all strings and field names together, the text of the
69
- * value's Markdown documents included (1 MiB).
54
+ * value's Markdown documents included (32 MiB; 1 MiB before player 0.6.0).
70
55
  */
71
- textBytes: 1024 * 1024,
56
+ textBytes: 32 * 1024 * 1024,
72
57
  };
73
58
  const L = UI_VALUE_LIMITS;
74
59
  const bytes = (text) => Buffer.byteLength(text, "utf8");
package/dist/wire.d.ts CHANGED
@@ -88,12 +88,15 @@ export type RowOp = {
88
88
  export type PropertyWrite = {
89
89
  value: Json;
90
90
  seq?: number;
91
+ more?: boolean;
91
92
  } | {
92
93
  patch: {
93
94
  base: number;
94
95
  seq: number;
95
96
  ops: RowOp[];
96
97
  };
98
+ /** A part of a whole value sent in parts, not the last (player 0.6.0). */
99
+ more?: boolean;
97
100
  };
98
101
  /**
99
102
  * A definite refusal from the player (or the plugin runner), with a stable