@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/CHANGELOG.md +23 -0
- package/bin/vestra-plugin.mjs +59 -1
- package/dist/commands.js +5 -4
- package/dist/plugin/bindings.d.ts +5 -2
- package/dist/plugin/bindings.js +5 -2
- package/dist/plugin/patch.d.ts +52 -1
- package/dist/plugin/patch.js +104 -6
- package/dist/plugin/testing.js +63 -6
- package/dist/plugin/types.d.ts +24 -18
- package/dist/plugin/ui.d.ts +8 -2
- package/dist/plugin/ui.js +279 -19
- 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 +72 -70
- package/llms.txt +22 -22
- 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]));
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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) ??
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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:
|
|
378
|
-
|
|
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);
|
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
|