@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/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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) !==
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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.
|
package/dist/plugin/scope.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/plugin/scope.js
CHANGED
|
@@ -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
|
}
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -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
|
package/dist/plugin/testing.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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");
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -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
|
|
569
|
-
*
|
|
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
|
|
574
|
-
* handler and player state change
|
|
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
|
|
1064
|
-
*
|
|
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
|
|
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
|
|
1385
|
-
*
|
|
1386
|
-
* null counts, the value itself included; a
|
|
1387
|
-
*
|
|
1388
|
-
* field
|
|
1389
|
-
* document). A list of rows
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
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,
|
|
1710
|
-
*
|
|
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.
|
|
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
|
|
1868
|
-
*
|
|
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[];
|
package/dist/plugin/ui.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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). */
|