@vestra-player/sdk 1.5.44 → 1.5.46
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 +26 -0
- package/bin/vestra-plugin.mjs +59 -1
- package/dist/plugin/index.js +4 -1
- package/dist/plugin/patch.d.ts +52 -1
- package/dist/plugin/patch.js +104 -6
- package/dist/plugin/player.d.ts +11 -0
- package/dist/plugin/player.js +80 -7
- package/dist/plugin/runtime.d.ts +58 -1
- package/dist/plugin/runtime.js +216 -16
- package/dist/plugin/scope.d.ts +9 -1
- package/dist/plugin/scope.js +24 -0
- package/dist/plugin/testing.d.ts +7 -0
- package/dist/plugin/testing.js +67 -3
- package/dist/plugin/types.d.ts +41 -29
- package/dist/plugin/ui.d.ts +13 -3
- package/dist/plugin/ui.js +184 -16
- package/dist/plugin/values.d.ts +21 -36
- package/dist/plugin/values.js +22 -37
- package/dist/wire.d.ts +3 -0
- package/llms-full.txt +92 -76
- package/llms.txt +23 -23
- package/package.json +1 -1
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]));
|
|
@@ -110,6 +111,26 @@ function sentOf(value, patches) {
|
|
|
110
111
|
}
|
|
111
112
|
return { kind: "json", enc: JSON.stringify(value) };
|
|
112
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* A write sent in parts stopped because its view closed or showed its
|
|
116
|
+
* initial properties again: not a failure.
|
|
117
|
+
*/
|
|
118
|
+
class Abandoned extends Error {
|
|
119
|
+
}
|
|
120
|
+
/** The wire key of a Markdown document: rows holding one are never sent in parts. */ const MARKDOWN_KEY = '"$vestraMarkdown"';
|
|
121
|
+
/** A row larger than one request can carry (`limit`). */
|
|
122
|
+
const rowTooLarge = (key, bytes) => new HostError("limit", `${key}: a row of ${bytes} bytes is larger than one request takes (${CHUNK_BYTES} bytes)`);
|
|
123
|
+
/** Refuse a patch carrying a row larger than one request, before sending it. */
|
|
124
|
+
function tooLarge(key, ops) {
|
|
125
|
+
for (const op of ops) {
|
|
126
|
+
const rows = op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : [];
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
const bytes = rowBytes(row);
|
|
129
|
+
if (bytes > CHUNK_BYTES)
|
|
130
|
+
throw rowTooLarge(key, bytes);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
113
134
|
/**
|
|
114
135
|
* The row patch from `sent` to `next` (the bound `rows`), in wire form, and
|
|
115
136
|
* the new rows' limit counts; `null` when the whole value is to be sent:
|
|
@@ -194,6 +215,8 @@ export class ViewBase {
|
|
|
194
215
|
*/
|
|
195
216
|
#stales = new Map();
|
|
196
217
|
#seq = 0;
|
|
218
|
+
/** Counts the times the view showed its initial properties (`shown`). */
|
|
219
|
+
#generation = 0;
|
|
197
220
|
/** Written by `set()`: sent even if equal to what was sent before. */
|
|
198
221
|
#forced = new Set();
|
|
199
222
|
/** The last value of each property whose write failed; not retried until it changes. */
|
|
@@ -215,18 +238,51 @@ export class ViewBase {
|
|
|
215
238
|
}
|
|
216
239
|
/**
|
|
217
240
|
* Initial properties in wire form. A released image is left out (the
|
|
218
|
-
* property keeps its default) and reported, like a failed write.
|
|
241
|
+
* property keeps its default) and reported, like a failed write. With a
|
|
242
|
+
* player that takes row patches, the arrays of one request carry at most
|
|
243
|
+
* {@link CHUNK_BYTES} of rows together (SDK 1.5.46): an array over that
|
|
244
|
+
* goes with its first rows, and the first flush sends the rest as patches.
|
|
245
|
+
* An array over the value limits is refused here, as the player would
|
|
246
|
+
* refuse it whole: left out and reported (a panel, a settings page), or,
|
|
247
|
+
* with `refuse` (a window's `show`), thrown.
|
|
219
248
|
*/
|
|
220
|
-
wired(values) {
|
|
249
|
+
wired(values, refuse = false) {
|
|
221
250
|
const out = {};
|
|
251
|
+
let room = CHUNK_BYTES;
|
|
222
252
|
for (const [key, value] of Object.entries(values)) {
|
|
253
|
+
let wire;
|
|
223
254
|
try {
|
|
224
|
-
|
|
255
|
+
wire = wireValue(value);
|
|
225
256
|
}
|
|
226
257
|
catch (error) {
|
|
227
258
|
this.#failed.set(key, JSON.stringify(value));
|
|
228
259
|
this.scope.report(error);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (this.#patches && Array.isArray(wire)) {
|
|
263
|
+
const sizes = wire.map(rowBytes);
|
|
264
|
+
const total = sizes.reduce((a, b) => a + b, 0);
|
|
265
|
+
if (total > room && !JSON.stringify(wire).includes(MARKDOWN_KEY)) {
|
|
266
|
+
const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
|
|
267
|
+
const problem = valueLimitProblem(wire);
|
|
268
|
+
if (problem || largest > CHUNK_BYTES) {
|
|
269
|
+
const error = problem
|
|
270
|
+
? new HostError("limit", `${key}: ${problem}`)
|
|
271
|
+
: rowTooLarge(key, largest);
|
|
272
|
+
if (refuse)
|
|
273
|
+
throw error;
|
|
274
|
+
this.#failed.set(key, JSON.stringify(value));
|
|
275
|
+
this.scope.report(error);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const count = room > 0 ? prefixWithin(sizes, room) : 0;
|
|
279
|
+
wire = wire.slice(0, count);
|
|
280
|
+
room -= sizes.slice(0, count).reduce((a, b) => a + b, 0);
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
room -= total;
|
|
229
284
|
}
|
|
285
|
+
out[key] = wire;
|
|
230
286
|
}
|
|
231
287
|
return out;
|
|
232
288
|
}
|
|
@@ -247,18 +303,32 @@ export class ViewBase {
|
|
|
247
303
|
this.flush();
|
|
248
304
|
}
|
|
249
305
|
bind(props) {
|
|
250
|
-
this
|
|
306
|
+
// `token` names this binding of this view in the runtime's read
|
|
307
|
+
// tracking: the same function bound to two views is two parts.
|
|
308
|
+
this.#binders.set(props, {
|
|
309
|
+
site: this.scope.runtime.callSite(),
|
|
310
|
+
token: {},
|
|
311
|
+
});
|
|
251
312
|
this.scope.update();
|
|
252
313
|
return this.scope.onDispose(() => {
|
|
253
314
|
this.#binders.delete(props);
|
|
254
315
|
});
|
|
255
316
|
}
|
|
256
|
-
/**
|
|
317
|
+
/**
|
|
318
|
+
* Current desired properties (bound values win over earlier `set`). A
|
|
319
|
+
* precise pass (`Runtime.part`) skips a bind function that read none of
|
|
320
|
+
* the changed state: its last values stay.
|
|
321
|
+
*/
|
|
257
322
|
snapshot() {
|
|
258
|
-
|
|
323
|
+
const runtime = this.scope.runtime;
|
|
324
|
+
for (const [binder, { site, token }] of this.#binders) {
|
|
259
325
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
326
|
+
// Encoded inside the part: what encoding reads counts as the
|
|
327
|
+
// function's (a state object it returned).
|
|
328
|
+
const result = runtime.part(token, () => encode(runtime.reader("a view's bind", site, binder)));
|
|
329
|
+
if (!result)
|
|
330
|
+
continue;
|
|
331
|
+
for (const [key, value] of Object.entries(result.value))
|
|
262
332
|
this.#values.set(key, value);
|
|
263
333
|
}
|
|
264
334
|
catch (error) {
|
|
@@ -275,6 +345,7 @@ export class ViewBase {
|
|
|
275
345
|
}
|
|
276
346
|
/** The host already shows these values (e.g. sent as initial properties). */
|
|
277
347
|
shown(values) {
|
|
348
|
+
this.#generation++;
|
|
278
349
|
const patches = this.#patches;
|
|
279
350
|
this.#sent = new Map(Object.entries(values).map(([k, v]) => [k, sentOf(v, patches)]));
|
|
280
351
|
this.#bases = new Map(patches ? Object.keys(values).map((key) => [key, 0]) : []);
|
|
@@ -340,7 +411,10 @@ export class ViewBase {
|
|
|
340
411
|
* Write one property: a row patch when the player takes them and one
|
|
341
412
|
* applies (else the whole value); a `stale` patch is followed by the whole
|
|
342
413
|
* value at once. The base is forgotten on any failure, so the next write
|
|
343
|
-
* of the property is whole.
|
|
414
|
+
* of the property is whole. A write larger than one request
|
|
415
|
+
* ({@link CHUNK_BYTES} of rows, SDK 1.5.46) is sent in parts: a whole
|
|
416
|
+
* array as its first rows and `insert` patches of the rest, a patch as
|
|
417
|
+
* several, each applying on the one before.
|
|
344
418
|
*/
|
|
345
419
|
async #send(pending) {
|
|
346
420
|
const { key, value } = pending;
|
|
@@ -348,34 +422,121 @@ export class ViewBase {
|
|
|
348
422
|
await this.write(key, { value: wireValue(value) });
|
|
349
423
|
return pending.sent();
|
|
350
424
|
}
|
|
425
|
+
const generation = this.#generation;
|
|
351
426
|
const base = this.#bases.get(key);
|
|
352
427
|
this.#bases.delete(key);
|
|
353
428
|
const sent = this.#sent.get(key);
|
|
354
|
-
|
|
429
|
+
let plan = base !== undefined &&
|
|
355
430
|
sent?.kind === "rows" &&
|
|
356
431
|
pending.rows &&
|
|
357
432
|
(this.#stales.get(key) ?? 0) < 2
|
|
358
433
|
? rowPatch(sent.rows, pending.rows, value)
|
|
359
434
|
: null;
|
|
435
|
+
// A patch sent in several never passes the row limit midway (the diff
|
|
436
|
+
// removes before it inserts; checked anyway); otherwise whole.
|
|
437
|
+
if (plan && sent?.kind === "rows") {
|
|
438
|
+
let length = sent.rows.enc.length;
|
|
439
|
+
for (const op of plan.ops) {
|
|
440
|
+
if (op.op === "insert")
|
|
441
|
+
length += op.rows.length;
|
|
442
|
+
else if (op.op === "remove")
|
|
443
|
+
length -= op.count;
|
|
444
|
+
if (length > UI_VALUE_LIMITS.arrayRows) {
|
|
445
|
+
plan = null;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
360
450
|
if (plan) {
|
|
451
|
+
tooLarge(key, plan.ops);
|
|
452
|
+
const [first, ...rest] = chunkOps(plan.ops);
|
|
361
453
|
const seq = ++this.#seq;
|
|
454
|
+
let applied = false;
|
|
362
455
|
try {
|
|
363
|
-
await this.write(key, { patch: { base: base, seq, ops:
|
|
364
|
-
|
|
365
|
-
this.#stales.delete(key);
|
|
366
|
-
return pending.sent(plan.counts);
|
|
456
|
+
await this.write(key, { patch: { base: base, seq, ops: first } });
|
|
457
|
+
applied = true;
|
|
367
458
|
}
|
|
368
459
|
catch (error) {
|
|
369
460
|
if (!(error instanceof HostError && error.code === "stale"))
|
|
370
461
|
throw error;
|
|
371
462
|
this.#stales.set(key, (this.#stales.get(key) ?? 0) + 1);
|
|
372
463
|
}
|
|
464
|
+
if (applied) {
|
|
465
|
+
this.#stales.delete(key);
|
|
466
|
+
await this.#parts(key, seq, rest, false, generation);
|
|
467
|
+
return pending.sent(plan.counts);
|
|
468
|
+
}
|
|
373
469
|
}
|
|
374
470
|
const seq = ++this.#seq;
|
|
471
|
+
// Rows holding Markdown documents go whole, as before: the player
|
|
472
|
+
// merges documents in place and takes no parts of them.
|
|
473
|
+
if (Array.isArray(value) &&
|
|
474
|
+
pending.rows &&
|
|
475
|
+
!pending.rows.enc.some((row) => row.includes(MARKDOWN_KEY))) {
|
|
476
|
+
const sizes = pending.rows.enc.map(jsonBytes);
|
|
477
|
+
const count = prefixWithin(sizes, CHUNK_BYTES);
|
|
478
|
+
if (count < value.length) {
|
|
479
|
+
const rows = value;
|
|
480
|
+
// Over the limits, or a row larger than one request: refused here,
|
|
481
|
+
// as the player would refuse the whole value, before any part is
|
|
482
|
+
// sent (a part refused midway would leave the list half sent).
|
|
483
|
+
const problem = valueLimitProblem(wireValue(value));
|
|
484
|
+
if (problem)
|
|
485
|
+
throw new HostError("limit", `${key}: ${problem}`);
|
|
486
|
+
const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
|
|
487
|
+
if (largest > CHUNK_BYTES)
|
|
488
|
+
throw rowTooLarge(key, largest);
|
|
489
|
+
// Every part but the last carries `more`: the player holds them
|
|
490
|
+
// and writes the whole value at the last, so a list it shows is
|
|
491
|
+
// written once (player 0.6.0).
|
|
492
|
+
await this.write(key, {
|
|
493
|
+
value: wireValue(rows.slice(0, count)),
|
|
494
|
+
seq,
|
|
495
|
+
more: true,
|
|
496
|
+
});
|
|
497
|
+
const insert = {
|
|
498
|
+
op: "insert",
|
|
499
|
+
at: count,
|
|
500
|
+
rows: rows.slice(count).map((row) => wireValue(row ?? null)),
|
|
501
|
+
};
|
|
502
|
+
await this.#parts(key, seq, chunkOps([insert]), true, generation);
|
|
503
|
+
return pending.sent();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
375
506
|
await this.write(key, { value: wireValue(value), seq });
|
|
376
507
|
this.#bases.set(key, seq);
|
|
377
508
|
return pending.sent();
|
|
378
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* The rest of a write sent in parts: each patch applies on the one
|
|
512
|
+
* before (`base`), and the property's base is the last one's; with
|
|
513
|
+
* `more`, every part but the last says so. A failure leaves no base: the
|
|
514
|
+
* next write of the property is whole. When the view closed or showed its
|
|
515
|
+
* initial properties again meanwhile (`generation`), the rest is dropped
|
|
516
|
+
* quietly ({@link Abandoned}): the view's next flush sends what it shows.
|
|
517
|
+
*/
|
|
518
|
+
async #parts(key, base, groups, more, generation) {
|
|
519
|
+
let last = base;
|
|
520
|
+
for (const [i, ops] of groups.entries()) {
|
|
521
|
+
if (!this.live() || this.#generation !== generation)
|
|
522
|
+
throw new Abandoned();
|
|
523
|
+
const seq = ++this.#seq;
|
|
524
|
+
try {
|
|
525
|
+
await this.write(key, {
|
|
526
|
+
patch: { base: last, seq, ops },
|
|
527
|
+
...(more && i < groups.length - 1 ? { more: true } : {}),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
// Refused because the view went meanwhile: not a failure.
|
|
532
|
+
if (!this.live() || this.#generation !== generation)
|
|
533
|
+
throw new Abandoned();
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
last = seq;
|
|
537
|
+
}
|
|
538
|
+
this.#bases.set(key, last);
|
|
539
|
+
}
|
|
379
540
|
/**
|
|
380
541
|
* Send what changed. A bound value is sent when it differs from the last one
|
|
381
542
|
* sent; a `set()` value is always sent once. Nothing is resent merely
|
|
@@ -417,6 +578,13 @@ export class ViewBase {
|
|
|
417
578
|
this.#broken = false;
|
|
418
579
|
}
|
|
419
580
|
catch (error) {
|
|
581
|
+
// The view closed or showed its initial properties again while
|
|
582
|
+
// a write was sent in parts: nothing failed (the next flush
|
|
583
|
+
// sends what the view shows).
|
|
584
|
+
if (error instanceof Abandoned) {
|
|
585
|
+
queueMicrotask(() => this.flush());
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
420
588
|
failed = true;
|
|
421
589
|
// Report the start of a failing streak only.
|
|
422
590
|
if (!this.#broken)
|
|
@@ -516,7 +684,7 @@ class WindowHandle extends ViewBase {
|
|
|
516
684
|
const ui = this.scope.runtime.host.ui;
|
|
517
685
|
if (this.#opened)
|
|
518
686
|
return ui.show(this.#id, true);
|
|
519
|
-
const initial = this.wired(this.snapshot());
|
|
687
|
+
const initial = this.wired(this.snapshot(), true);
|
|
520
688
|
await ui.open(this.#id, this.#file, initial, [...this.handlers.keys()]);
|
|
521
689
|
this.#opened = true;
|
|
522
690
|
this.shown(initial);
|
package/dist/plugin/values.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
* (
|
|
47
|
+
* (1,048,576; 16,384 before player 0.6.0).
|
|
63
48
|
*/
|
|
64
|
-
readonly values:
|
|
65
|
-
/** Rows of one array (
|
|
66
|
-
readonly arrayRows:
|
|
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
|
};
|
package/dist/plugin/values.js
CHANGED
|
@@ -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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
* (
|
|
45
|
+
* (1,048,576; 16,384 before player 0.6.0).
|
|
61
46
|
*/
|
|
62
|
-
values:
|
|
63
|
-
/** Rows of one array (
|
|
64
|
-
arrayRows:
|
|
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
|