@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.
@@ -53,6 +53,39 @@ export function quiet(promise) {
53
53
  promise.catch(() => { });
54
54
  return promise;
55
55
  }
56
+ /** Whether something that read `deps` may see a different result after `change`. */
57
+ function affected(deps, change) {
58
+ for (const [key, fields] of change) {
59
+ const read = deps.get(key);
60
+ if (!read)
61
+ continue;
62
+ if (read.size === 0)
63
+ return true;
64
+ for (const field of fields)
65
+ if (read.has(field))
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ /** Add `from` to `into` (an empty set, the whole state, wins). */
71
+ function merge(into, from) {
72
+ for (const [key, fields] of from) {
73
+ const set = into.get(key);
74
+ if (!set)
75
+ into.set(key, new Set(fields));
76
+ else if (set.size && fields.size)
77
+ for (const field of fields)
78
+ set.add(field);
79
+ else
80
+ set.clear();
81
+ }
82
+ }
83
+ /**
84
+ * The longest a binding waits for a full pass while precise passes skip
85
+ * it: state a plugin changes outside code the SDK runs (its own timer,
86
+ * without `update()`) still shows within about this long.
87
+ */
88
+ export const SAFETY_PASS_MS = 1000;
56
89
  export const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
57
90
  /** One value in flight, only the newest waiting. Each push learns its fate. */
58
91
  export class Latest {
@@ -124,6 +157,22 @@ export class Runtime {
124
157
  /** Who reads now (a command's field and where it was added). */
125
158
  #reader;
126
159
  #scheduled = false;
160
+ /** The next pass evaluates every binding (not only a state copy changed). */
161
+ #full = false;
162
+ /** State copy changes since the last pass (a precise pass, unless `#full`). */
163
+ #pending = new Map();
164
+ /** The change the running precise pass is for; `undefined` in a full pass. */
165
+ #pass;
166
+ /** The state copy fields read by what is being evaluated now. */
167
+ #deps;
168
+ /** Each part's reads and fields last time (`part`). */
169
+ #parts = new WeakMap();
170
+ /** A precise pass skipped something: a full pass follows (`SAFETY_PASS_MS`). */
171
+ #safety;
172
+ /** The running pass skipped a binding or a part. */
173
+ #skipped = false;
174
+ /** The plugin was disposed: no safety pass is armed any more. */
175
+ #stopped = false;
127
176
  #events = new Map();
128
177
  views = new Map();
129
178
  commands = new Map();
@@ -196,11 +245,12 @@ export class Runtime {
196
245
  }
197
246
  bind(evaluate) {
198
247
  const held = new Map();
199
- this.#bindings.set(evaluate, held);
248
+ const binding = { held, deps: new Map() };
249
+ this.#bindings.set(evaluate, binding);
200
250
  this.invalidate();
201
251
  return {
202
252
  dispose: async () => {
203
- if (this.#bindings.get(evaluate) !== held)
253
+ if (this.#bindings.get(evaluate) !== binding)
204
254
  return;
205
255
  this.#bindings.delete(evaluate);
206
256
  for (const hold of held.values())
@@ -231,6 +281,92 @@ export class Runtime {
231
281
  return `${reader.label} is a function that reads ${read}${at}${hint}`;
232
282
  });
233
283
  }
284
+ /**
285
+ * A field of the SDK's copy of a state (`key`) is read; without `field`,
286
+ * the state is taken (`plugin.player.state`). What is being evaluated
287
+ * then depends on it: a precise pass after a change of that field runs
288
+ * it again.
289
+ */
290
+ depends(key, field) {
291
+ const deps = this.#deps;
292
+ if (!deps)
293
+ return;
294
+ let fields = deps.get(key);
295
+ if (!fields)
296
+ deps.set(key, (fields = new Set()));
297
+ if (field !== undefined)
298
+ fields.add(field);
299
+ }
300
+ /**
301
+ * Evaluate one part of the binding being evaluated (one `view.bind`
302
+ * function), identified by `token`. A precise pass skips it when nothing
303
+ * it read last time changed, and returns `undefined`; what it read then
304
+ * still counts for the binding.
305
+ */
306
+ part(token, read) {
307
+ const last = this.#parts.get(token);
308
+ const outerReads = this.#reads;
309
+ const outerDeps = this.#deps;
310
+ if (last && this.#pass && !affected(last.deps, this.#pass)) {
311
+ this.#skipped = true;
312
+ if (outerReads)
313
+ for (const [event, why] of last.reads)
314
+ if (!outerReads.has(event))
315
+ outerReads.set(event, why);
316
+ if (outerDeps)
317
+ merge(outerDeps, last.deps);
318
+ return undefined;
319
+ }
320
+ const reads = new Map();
321
+ const deps = new Map();
322
+ this.#reads = outerReads && reads;
323
+ this.#deps = deps;
324
+ try {
325
+ return { value: read() };
326
+ }
327
+ finally {
328
+ this.#reads = outerReads;
329
+ this.#deps = outerDeps;
330
+ this.#parts.set(token, { reads, deps });
331
+ if (outerReads)
332
+ for (const [event, why] of reads)
333
+ if (!outerReads.has(event))
334
+ outerReads.set(event, why);
335
+ if (outerDeps)
336
+ merge(outerDeps, deps);
337
+ }
338
+ }
339
+ /**
340
+ * Run plugin code (a state listener) and return the state copy fields it
341
+ * read; they are not counted as a binding's.
342
+ */
343
+ observe(run) {
344
+ const outerReads = this.#reads;
345
+ const outerDeps = this.#deps;
346
+ const deps = new Map();
347
+ this.#reads = undefined;
348
+ this.#deps = deps;
349
+ try {
350
+ return { value: run(), deps };
351
+ }
352
+ finally {
353
+ this.#reads = outerReads;
354
+ this.#deps = outerDeps;
355
+ }
356
+ }
357
+ /**
358
+ * Whether code that read `deps` while `change` was delivered needs a full
359
+ * pass after it: it read a field that changed, or no field of that state
360
+ * at all (so the SDK cannot tell what it depends on).
361
+ */
362
+ needsFull(deps, change) {
363
+ for (const key of change.keys()) {
364
+ const read = deps.get(key);
365
+ if (!read || read.size === 0)
366
+ return true;
367
+ }
368
+ return affected(deps, change);
369
+ }
234
370
  /**
235
371
  * Evaluate `read` as `label`, added at `where`: the state it reads is
236
372
  * reported as read by it. `field`: a command's `enabled` or `checked`,
@@ -286,18 +422,24 @@ export class Runtime {
286
422
  return undefined;
287
423
  }
288
424
  /** Evaluate one binding, holding the events whose state it read. */
289
- #evaluate(evaluate, held) {
425
+ #evaluate(evaluate, binding) {
426
+ const held = binding.held;
290
427
  const reads = new Map();
428
+ const deps = new Map();
291
429
  const outer = this.#reads;
430
+ const outerDeps = this.#deps;
292
431
  const reader = this.#reader;
293
432
  this.#reads = reads;
433
+ this.#deps = deps;
294
434
  this.#reader = undefined;
295
435
  try {
296
436
  evaluate();
297
437
  }
298
438
  finally {
299
439
  this.#reads = outer;
440
+ this.#deps = outerDeps;
300
441
  this.#reader = reader;
442
+ binding.deps = deps;
301
443
  for (const [event, hold] of held)
302
444
  if (!reads.has(event)) {
303
445
  held.delete(event);
@@ -308,24 +450,82 @@ export class Runtime {
308
450
  held.set(event, this.hold(event, why));
309
451
  }
310
452
  }
311
- /** Coalesce: one evaluation pass per microtask turn, however many changes. */
453
+ /**
454
+ * A full pass: every binding is evaluated. Requested after plugin code
455
+ * the SDK ran, by `plugin.update()`, and on any change other than a state
456
+ * copy's fields. Coalesced: one pass per microtask turn, however many
457
+ * requests.
458
+ */
312
459
  invalidate() {
460
+ this.#full = true;
461
+ this.#schedule();
462
+ }
463
+ /**
464
+ * Fields of the SDK's copy of a state (`key`) changed: a precise pass
465
+ * evaluates only the bindings (and view bind functions) that read one of
466
+ * them, unless a full pass is due anyway.
467
+ */
468
+ changed(key, fields) {
469
+ let set = this.#pending.get(key);
470
+ if (!set)
471
+ this.#pending.set(key, (set = new Set()));
472
+ for (const field of fields)
473
+ set.add(field);
474
+ this.#schedule();
475
+ }
476
+ #schedule() {
313
477
  if (this.#scheduled)
314
478
  return;
315
479
  this.#scheduled = true;
316
- queueMicrotask(() => {
317
- this.#scheduled = false;
318
- for (const [evaluate, held] of [...this.#bindings]) {
319
- if (this.#bindings.get(evaluate) !== held)
320
- continue;
321
- try {
322
- this.#evaluate(evaluate, held);
323
- }
324
- catch (error) {
325
- console.error(`[${this.manifest.id}] binding failed:`, error);
326
- }
480
+ queueMicrotask(() => this.#run());
481
+ }
482
+ /**
483
+ * One pass. A precise pass that skips anything arms a full pass
484
+ * `SAFETY_PASS_MS` later (unless a full pass comes first), for plugin
485
+ * state changed outside code the SDK ran.
486
+ */
487
+ #run() {
488
+ this.#scheduled = false;
489
+ const full = this.#full;
490
+ const change = this.#pending;
491
+ this.#full = false;
492
+ this.#pending = new Map();
493
+ if (full) {
494
+ clearTimeout(this.#safety);
495
+ this.#safety = undefined;
496
+ }
497
+ this.#skipped = false;
498
+ for (const [evaluate, binding] of [...this.#bindings]) {
499
+ if (this.#bindings.get(evaluate) !== binding)
500
+ continue;
501
+ if (!full && !affected(binding.deps, change)) {
502
+ this.#skipped = true;
503
+ continue;
327
504
  }
328
- });
505
+ this.#pass = full ? undefined : change;
506
+ try {
507
+ this.#evaluate(evaluate, binding);
508
+ }
509
+ catch (error) {
510
+ console.error(`[${this.manifest.id}] binding failed:`, error);
511
+ }
512
+ finally {
513
+ this.#pass = undefined;
514
+ }
515
+ }
516
+ if (this.#skipped && !this.#safety && !this.#stopped) {
517
+ this.#safety = setTimeout(() => {
518
+ this.#safety = undefined;
519
+ this.invalidate();
520
+ }, SAFETY_PASS_MS);
521
+ this.#safety.unref?.();
522
+ }
523
+ }
524
+ /** The plugin was disposed: cancel the pending safety pass, arm no more. */
525
+ stop() {
526
+ this.#stopped = true;
527
+ clearTimeout(this.#safety);
528
+ this.#safety = undefined;
329
529
  }
330
530
  /**
331
531
  * Host events with one `events.subscribe` per name, however many listeners.
@@ -1,4 +1,4 @@
1
- import type { Runtime } from "./runtime.ts";
1
+ import type { Change, Runtime } from "./runtime.ts";
2
2
  import type { Disposable, Plugin, PluginError, Scope } from "./types.ts";
3
3
  export declare class ScopeImpl implements Plugin {
4
4
  #private;
@@ -54,6 +54,14 @@ export declare class ScopeImpl implements Plugin {
54
54
  report(error: unknown): void;
55
55
  /** Invoke plugin code on its behalf: report failures, then refresh bindings. */
56
56
  run(handler: (...args: never[]) => unknown, ...args: unknown[]): void;
57
+ /**
58
+ * Run a listener of a state change (`player.onChange`) like `run`, except
59
+ * that the full pass after it is left out when it returned synchronously
60
+ * and read only fields of the state that `change` left alone: its result
61
+ * then depends on nothing new. The precise pass for the change runs
62
+ * anyway, and a full pass follows within `SAFETY_PASS_MS`.
63
+ */
64
+ runOn(change: Change, handler: (...args: never[]) => unknown, ...args: unknown[]): void;
57
65
  update(): void;
58
66
  dispose(): Promise<void>;
59
67
  }
@@ -145,6 +145,30 @@ export class ScopeImpl {
145
145
  }
146
146
  this.update();
147
147
  }
148
+ /**
149
+ * Run a listener of a state change (`player.onChange`) like `run`, except
150
+ * that the full pass after it is left out when it returned synchronously
151
+ * and read only fields of the state that `change` left alone: its result
152
+ * then depends on nothing new. The precise pass for the change runs
153
+ * anyway, and a full pass follows within `SAFETY_PASS_MS`.
154
+ */
155
+ runOn(change, handler, ...args) {
156
+ if (this.disposed)
157
+ return;
158
+ let full = true;
159
+ try {
160
+ const { value, deps } = this.runtime.observe(() => handler(...args));
161
+ if (value instanceof Promise)
162
+ value.catch((error) => this.report(error)).finally(() => this.update());
163
+ else
164
+ full = this.runtime.needsFull(deps, change);
165
+ }
166
+ catch (error) {
167
+ this.report(error);
168
+ }
169
+ if (full)
170
+ this.update();
171
+ }
148
172
  update() {
149
173
  this.runtime.invalidate();
150
174
  }
@@ -2621,6 +2621,13 @@ export declare class TestHost implements Transport {
2621
2621
  * for: on a loaded machine it can finish after `settle()` returns. Poll
2622
2622
  * the result you assert on instead (second example).
2623
2623
  *
2624
+ * `settle()` does not wait for the SDK's once-a-second full update pass
2625
+ * (SDK 1.5.45). After `setSource` (or another player state change) only
2626
+ * the bindings that read a changed field of `plugin.player.state` run
2627
+ * again; state the plugin changed outside a handler without
2628
+ * `plugin.update()` shows only in the next full pass. Have the plugin
2629
+ * call `plugin.update()` after such a change, or wait `settle(1000)`.
2630
+ *
2624
2631
  * @param ms - Extra time to wait, for debounces and timers (default 0).
2625
2632
  *
2626
2633
  * @example Wait past a debounced save
@@ -935,10 +935,15 @@ export class TestHost {
935
935
  * view and property (none: 0, the initial properties).
936
936
  */
937
937
  #writes = new Map();
938
+ /** The parts of a whole value held until its last part, by view and property. */
939
+ #parts = new Map();
938
940
  #resetWrites(view) {
939
941
  for (const slot of [...this.#writes.keys()])
940
942
  if (slot.startsWith(`${view}\u0000`))
941
943
  this.#writes.delete(slot);
944
+ for (const slot of [...this.#parts.keys()])
945
+ if (slot.startsWith(`${view}\u0000`))
946
+ this.#parts.delete(slot);
942
947
  }
943
948
  /**
944
949
  * A property write as the player takes it, returning the value the
@@ -950,6 +955,44 @@ export class TestHost {
950
955
  #write(view, property, p, current) {
951
956
  const slot = `${view}\u0000${property}`;
952
957
  const patch = p.patch;
958
+ // A whole array sent in parts (player 0.6.0, SDK 1.5.46): every part but
959
+ // the last carries `more`; the player holds them and writes the whole
960
+ // value at the last, as here (`undefined`: held, nothing shown yet).
961
+ const more = p.more === true;
962
+ const held = this.#parts.get(slot);
963
+ this.#parts.delete(slot);
964
+ if (more && patch === undefined) {
965
+ const rows = this.#shownValues(p.value);
966
+ if (!Array.isArray(rows))
967
+ throw new HostError("invalid", "a value sent in parts is an array");
968
+ noMarkdownParts(rows);
969
+ this.#checkValues({ [property]: rows });
970
+ this.#parts.set(slot, {
971
+ seq: typeof p.seq === "number" ? p.seq : 0,
972
+ rows,
973
+ });
974
+ return undefined;
975
+ }
976
+ if (patch !== undefined && held && patch.base === held.seq) {
977
+ const ops = patchOps(patch.ops, held.rows.length);
978
+ const resolved = ops.map((op) => op.op === "set"
979
+ ? { ...op, row: this.#shownValues(op.row) }
980
+ : op.op === "insert"
981
+ ? { ...op, rows: op.rows.map((row) => this.#shownValues(row)) }
982
+ : op);
983
+ noMarkdownParts(resolved.flatMap((op) => op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : []));
984
+ const rows = applyOps(held.rows, resolved);
985
+ this.#checkValues({ [property]: rows });
986
+ if (more) {
987
+ this.#parts.set(slot, { seq: patch.seq, rows });
988
+ return undefined;
989
+ }
990
+ this.#seeImages([rows]);
991
+ this.#writes.set(slot, patch.seq);
992
+ return rows;
993
+ }
994
+ if (patch !== undefined && more)
995
+ throw new HostError("stale", "the property changed since the write this patch applies on; send the whole value");
953
996
  if (patch === undefined) {
954
997
  const value = this.#shownValues(p.value);
955
998
  this.#checkValues({ [property]: value });
@@ -3213,6 +3256,13 @@ export class TestHost {
3213
3256
  * for: on a loaded machine it can finish after `settle()` returns. Poll
3214
3257
  * the result you assert on instead (second example).
3215
3258
  *
3259
+ * `settle()` does not wait for the SDK's once-a-second full update pass
3260
+ * (SDK 1.5.45). After `setSource` (or another player state change) only
3261
+ * the bindings that read a changed field of `plugin.player.state` run
3262
+ * again; state the plugin changed outside a handler without
3263
+ * `plugin.update()` shows only in the next full pass. Have the plugin
3264
+ * call `plugin.update()` after such a change, or wait `settle(1000)`.
3265
+ *
3216
3266
  * @param ms - Extra time to wait, for debounces and timers (default 0).
3217
3267
  *
3218
3268
  * @example Wait past a debounced save
@@ -3892,7 +3942,10 @@ export class TestHost {
3892
3942
  if (!window)
3893
3943
  throw new HostError("missing", "view not found");
3894
3944
  const property = String(p.property);
3895
- window.props[property] = this.#write(`w:${String(p.id)}`, property, p, window.props[property]);
3945
+ const next = this.#write(`w:${String(p.id)}`, property, p, window.props[property]);
3946
+ // A part of a value sent in parts is held until the last (player 0.6.0).
3947
+ if (next !== undefined)
3948
+ window.props[property] = next;
3896
3949
  },
3897
3950
  "ui.show": (p) => {
3898
3951
  const window = this.windows.get(String(p.id));
@@ -4253,7 +4306,10 @@ export class TestHost {
4253
4306
  "ui.setMount": (p) => {
4254
4307
  const panel = this.mounted();
4255
4308
  const property = String(p.property);
4256
- panel.props[property] = this.#write("$mount", property, p, panel.props[property]);
4309
+ const next = this.#write("$mount", property, p, panel.props[property]);
4310
+ // A part of a value sent in parts is held until the last (player 0.6.0).
4311
+ if (next !== undefined)
4312
+ panel.props[property] = next;
4257
4313
  },
4258
4314
  "timeline.marks": () => ({ marks: this.timeline.marks }),
4259
4315
  "drops.provide": (p) => {
@@ -4316,7 +4372,10 @@ export class TestHost {
4316
4372
  if (!page)
4317
4373
  throw new HostError("missing", "page not registered");
4318
4374
  const property = String(p.property);
4319
- page.props[property] = this.#write(`$settings:${String(p.id)}`, property, p, page.props[property]);
4375
+ const next = this.#write(`$settings:${String(p.id)}`, property, p, page.props[property]);
4376
+ // A part of a value sent in parts is held until the last (player 0.6.0).
4377
+ if (next !== undefined)
4378
+ page.props[property] = next;
4320
4379
  },
4321
4380
  "settings.unregister": (p) => {
4322
4381
  this.settings.pages.delete(String(p.id));
@@ -5345,6 +5404,11 @@ export async function startPlugin(activate, options = {}) {
5345
5404
  * before; at most {@link PATCH_OPS}; the result at most
5346
5405
  * `UI_VALUE_LIMITS.arrayRows` rows.
5347
5406
  */
5407
+ /** The player takes no Markdown documents in parts of a value (`invalid`). */
5408
+ function noMarkdownParts(rows) {
5409
+ if (JSON.stringify(rows).includes('"$vestraMarkdown"'))
5410
+ throw new HostError("invalid", "a value sent in parts holds no Markdown documents");
5411
+ }
5348
5412
  function patchOps(ops, length) {
5349
5413
  if (!Array.isArray(ops))
5350
5414
  throw new HostError("invalid", "patch.ops requires an array");
@@ -565,14 +565,16 @@ export interface CommandState {
565
565
  reason: TextValue | null;
566
566
  }
567
567
  /**
568
- * A value, or a function the SDK re-evaluates on every update pass (see
569
- * {@link Scope.update}); only changes are sent to the player.
568
+ * A value, or a function the SDK re-evaluates on every full update pass,
569
+ * and after a player state change only if it read a field of
570
+ * `plugin.player.state` that changed (see {@link Scope.update}); only
571
+ * changes are sent to the player.
570
572
  *
571
573
  * @example Mix a computed state with a fixed one
572
574
  * The Loop item carries a check mark while a loop is set and is always
573
- * enabled. A function is evaluated again in every update pass (after each
574
- * handler and player state change) and only changes are sent; a plain
575
- * value never changes.
575
+ * enabled. A function is evaluated again in every full update pass (after
576
+ * each handler), and after a player state change only if it read a changed
577
+ * field; only changes are sent, and a plain value never changes.
576
578
  * ```ts
577
579
  * plugin.commands.add({
578
580
  * id: "loop",
@@ -1060,8 +1062,9 @@ export interface CommandOptions {
1060
1062
  * @remarks
1061
1063
  * The handle of one menu item and its keys. Its state (`enabled`, `checked`,
1062
1064
  * `busy`, `reason`) is bound: functions given to {@link Commands.add} or
1063
- * {@link Command.set} are evaluated again in every update pass and only
1064
- * changes reach the player. Its title, path, bindings and button cannot
1065
+ * {@link Command.set} are evaluated again in every full update pass (after
1066
+ * a player state change only if they read a changed field of
1067
+ * `plugin.player.state`) and only changes reach the player. Its title, path, bindings and button cannot
1065
1068
  * change in place; dispose the command (or a child {@link Scope}) and add it
1066
1069
  * again. Disposing it, its scope or stopping the plugin removes the menu
1067
1070
  * item, its keys and its bar button.
@@ -1081,7 +1084,7 @@ export interface CommandOptions {
1081
1084
  * path: ["@video", "Export…"],
1082
1085
  * run: runExport,
1083
1086
  * });
1084
- * // A function: re-evaluated in every update pass.
1087
+ * // A function: re-evaluated in every full pass and when `phase` changes.
1085
1088
  * exportCommand.set({ enabled: () => plugin.player.state.phase === "ready" });
1086
1089
  * ```
1087
1090
  *
@@ -1381,20 +1384,23 @@ export interface MarkdownOptions {
1381
1384
  * thumbnails.
1382
1385
  *
1383
1386
  * Limits per value (`UI_VALUE_LIMITS` in `@vestra-player/sdk/testing`,
1384
- * player 0.4.3; older players: 4096 values and 1024 rows): nesting depth 8,
1385
- * 16384 values in total (every array, object, string, number, boolean and
1386
- * null counts, the value itself included; a document or an image is one), 4096 rows
1387
- * per array, 64 KiB per string or field name and 1 MiB of text in total,
1388
- * field names and the text of its Markdown documents included (256 KB per
1389
- * document). A list of rows with 8 fields is 9 values per row, so about
1390
- * 1800 rows fit. The whole request must also fit the player's 1 MiB
1391
- * message. A value over a limit is refused with code `limit` and the
1392
- * property keeps what it showed (see {@link View.set}); `TestHost` refuses
1393
- * it the same way.
1394
- *
1395
- * The limits bound the work of one write, not the size of your data: for
1396
- * a list that can grow without bound, send the rows near the view and the
1397
- * row count, and move that window as the user scrolls.
1387
+ * player 0.6.0; players 0.4.3 to 0.5: 16384 values, 4096 rows, 1 MiB of
1388
+ * text): nesting depth 8, 1,048,576 values in total (every array, object,
1389
+ * string, number, boolean and null counts, the value itself included; a
1390
+ * document or an image is one), 100,000 rows per array, 64 KiB per string
1391
+ * or field name and 32 MiB of text in total, field names and the text of
1392
+ * its Markdown documents included (256 KB per document). A list of rows
1393
+ * with 13 fields is 14 values per row, so 50,000 such rows fit. An array
1394
+ * property larger than one 1 MiB request is sent in parts (SDK 1.5.46, a
1395
+ * player that takes row patches); only the top-level array is split, so
1396
+ * any other value, and each row, must fit in one request (a row of more
1397
+ * than 512 KiB is refused with `limit`). A value over a limit is refused
1398
+ * with code
1399
+ * `limit` and the property keeps what it showed (see {@link View.set});
1400
+ * `TestHost` refuses it the same way.
1401
+ *
1402
+ * Bind a whole list and draw it with a Slint `ListView`, which builds only
1403
+ * the rows on screen; the SDK sends what changed (row patches).
1398
1404
  *
1399
1405
  * @since 1.0.0
1400
1406
  * @category UI
@@ -1706,8 +1712,9 @@ export interface Ui {
1706
1712
  * callback.
1707
1713
  *
1708
1714
  * `bind` is the usual way: the SDK re-evaluates bound functions in every
1709
- * update pass (after each handler it calls, after player state changes, on
1710
- * {@link Scope.update}) and sends only what changed, at most one write in
1715
+ * full update pass (after each handler it calls, on {@link Scope.update}),
1716
+ * and after a player state change those that read a changed field of
1717
+ * `plugin.player.state`, and sends only what changed, at most one write in
1711
1718
  * flight per view. `set` sends now, even an unchanged value.
1712
1719
  *
1713
1720
  * Writes that fail (a name the Slint file does not declare, a value of the
@@ -1843,7 +1850,11 @@ export interface View {
1843
1850
  *
1844
1851
  * @remarks
1845
1852
  * The SDK calls `props` in every update pass (and right after `bind`) and
1846
- * sends the values that differ from the ones it sent last. Because an
1853
+ * sends the values that differ from the ones it sent last. After a change
1854
+ * of the player state alone it calls `props` only if it read a field of
1855
+ * `plugin.player.state` that changed (SDK 1.5.45); a full pass follows
1856
+ * after each handler, on {@link Scope.update} and at least once a second
1857
+ * while the player state keeps changing. Because an
1847
1858
  * unchanged value is not sent again, text the user typed into an `in-out`
1848
1859
  * property stays until the plugin's value for it changes.
1849
1860
  *
@@ -1864,8 +1875,9 @@ export interface View {
1864
1875
  * @example Show what is playing and whether it can seek
1865
1876
  * The status window always shows the current file and enables its seek
1866
1877
  * controls only for seekable sources. The SDK calls the function right
1867
- * away and again in every update pass (after player state changes and
1868
- * after each handler) and sends only the values that changed.
1878
+ * away, again in every full update pass (after each handler) and after
1879
+ * each change of `source` or `can`, and sends only the values that
1880
+ * changed.
1869
1881
  * ```ts
1870
1882
  * const status = plugin.windows.create("ui/status.slint");
1871
1883
  * status.bind(() => ({
@@ -5697,8 +5709,8 @@ export interface Player {
5697
5709
  * @example Drive the player's Next and Previous from a playlist
5698
5710
  * The player's Next and Previous items and keys play the plugin's list.
5699
5711
  * The player calls `next` and `previous` when the user asks; `canNext`
5700
- * and `canPrevious` are evaluated again after every handler the SDK runs,
5701
- * and while they return false the player disables its items ("No next
5712
+ * and `canPrevious` are evaluated again after every handler the SDK runs
5713
+ * (and after a player state change if they read a changed field), and while they return false the player disables its items ("No next
5702
5714
  * item").
5703
5715
  * ```ts
5704
5716
  * declare const items: string[];
@@ -32,13 +32,23 @@ export declare abstract class ViewBase implements View {
32
32
  protected abstract write(property: string, write: PropertyWrite): Promise<void>;
33
33
  /**
34
34
  * Initial properties in wire form. A released image is left out (the
35
- * property keeps its default) and reported, like a failed write.
35
+ * property keeps its default) and reported, like a failed write. With a
36
+ * player that takes row patches, the arrays of one request carry at most
37
+ * {@link CHUNK_BYTES} of rows together (SDK 1.5.46): an array over that
38
+ * goes with its first rows, and the first flush sends the rest as patches.
39
+ * An array over the value limits is refused here, as the player would
40
+ * refuse it whole: left out and reported (a panel, a settings page), or,
41
+ * with `refuse` (a window's `show`), thrown.
36
42
  */
37
- protected wired(values: Record<string, PropValue>): Record<string, Json>;
43
+ protected wired(values: Record<string, PropValue>, refuse?: boolean): Record<string, Json>;
38
44
  on<A extends Json[]>(callback: string, handler: (...args: A) => unknown): Disposable;
39
45
  set(props: Props): void;
40
46
  bind(props: () => Props): Disposable;
41
- /** Current desired properties (bound values win over earlier `set`). */
47
+ /**
48
+ * Current desired properties (bound values win over earlier `set`). A
49
+ * precise pass (`Runtime.part`) skips a bind function that read none of
50
+ * the changed state: its last values stay.
51
+ */
42
52
  protected snapshot(): Record<string, PropValue>;
43
53
  protected evaluate(): void;
44
54
  /** The host already shows these values (e.g. sent as initial properties). */