@vestra-player/sdk 1.6.18 → 1.6.19
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 +11 -0
- package/dist/docs-tree.json.gz +0 -0
- package/dist/plugin/appearance.js +1 -1
- package/dist/plugin/bindings.js +1 -1
- package/dist/plugin/commands.js +3 -2
- package/dist/plugin/connection.d.ts +119 -0
- package/dist/plugin/connection.js +402 -0
- package/dist/plugin/drops.d.ts +3 -3
- package/dist/plugin/drops.js +1 -1
- package/dist/plugin/errors.d.ts +9 -0
- package/dist/plugin/errors.js +35 -0
- package/dist/plugin/filters.js +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.js +3 -1
- package/dist/plugin/language.js +1 -1
- package/dist/plugin/latest.d.ts +12 -0
- package/dist/plugin/latest.js +52 -0
- package/dist/plugin/local.js +1 -2
- package/dist/plugin/markdown.d.ts +2 -2
- package/dist/plugin/markdown.js +1 -1
- package/dist/plugin/media.js +1 -1
- package/dist/plugin/osd.js +2 -2
- package/dist/plugin/packages.js +1 -1
- package/dist/plugin/passes.d.ts +111 -0
- package/dist/plugin/passes.js +341 -0
- package/dist/plugin/player.d.ts +2 -3
- package/dist/plugin/player.js +2 -7
- package/dist/plugin/resolvers.js +1 -1
- package/dist/plugin/runtime.d.ts +24 -209
- package/dist/plugin/runtime.js +34 -770
- package/dist/plugin/scope.d.ts +2 -1
- package/dist/plugin/scope.js +5 -2
- package/dist/plugin/services/dialogs.d.ts +3 -0
- package/dist/plugin/services/dialogs.js +62 -0
- package/dist/plugin/services.js +1 -1
- package/dist/plugin/settings.js +2 -2
- package/dist/plugin/shaders.js +2 -1
- package/dist/plugin/stream.js +1 -2
- package/dist/plugin/subtitles.js +2 -2
- package/dist/plugin/testing/transport.js +1 -1
- package/dist/plugin/timeline.js +2 -1
- package/dist/plugin/view/base.d.ts +29 -0
- package/dist/plugin/view/base.js +88 -0
- package/dist/plugin/view/binder.d.ts +22 -0
- package/dist/plugin/view/binder.js +57 -0
- package/dist/plugin/view/image.d.ts +21 -0
- package/dist/plugin/view/image.js +60 -0
- package/dist/plugin/view/layout.d.ts +47 -0
- package/dist/plugin/view/layout.js +171 -0
- package/dist/plugin/view/panel.d.ts +3 -0
- package/dist/plugin/view/panel.js +178 -0
- package/dist/plugin/view/window.d.ts +3 -0
- package/dist/plugin/view/window.js +99 -0
- package/dist/plugin/view/writer.d.ts +58 -0
- package/dist/plugin/view/writer.js +634 -0
- package/dist/plugin/view.js +2 -1
- package/dist/plugin/window.js +1 -1
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/dist/plugin/ui.d.ts +0 -74
- package/dist/plugin/ui.js +0 -1234
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The view writer (L1, a primitive; doc/specs/sdk-refactor.md §2.2 in the
|
|
3
|
+
* player repository): a view's properties as last sent, and the write path
|
|
4
|
+
* from a value to `ui.set` / `ui.setMount` / `settings.set` — encoding,
|
|
5
|
+
* comparison with what was sent, row patches (planned by `patch.ts`),
|
|
6
|
+
* parts of large writes (`more`), the value limits, `stale` fallback and
|
|
7
|
+
* the back-off after a failed write. It knows nothing of update passes or
|
|
8
|
+
* bindings: the binder (`binder.ts`, L2) puts values in, the view
|
|
9
|
+
* (`base.ts`) says where a write goes.
|
|
10
|
+
*/
|
|
11
|
+
import { HostError } from "../../wire.js";
|
|
12
|
+
import { MarkdownHandle } from "../markdown.js";
|
|
13
|
+
import { countRow, diff, keyOf, CHUNK_BYTES, chunkOps, prefixWithin, jsonBytes, rowBytes, replay, withinLimits, } from "../patch.js";
|
|
14
|
+
import { UI_VALUE_LIMITS, valueLimitProblem } from "../values.js";
|
|
15
|
+
import { failure } from "../errors.js";
|
|
16
|
+
import { ImageHandle } from "./image.js";
|
|
17
|
+
/** Slint names are kebab-case; plugin code may use camelCase. */
|
|
18
|
+
export const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
19
|
+
export const encode = (props) => Object.fromEntries(Object.entries(props).map(([k, v]) => [kebab(k), v]));
|
|
20
|
+
/**
|
|
21
|
+
* A property value as sent: images and documents in wire form, at any depth
|
|
22
|
+
* (released ones refused wherever they are), like the player.
|
|
23
|
+
*/
|
|
24
|
+
function wireValue(value) {
|
|
25
|
+
if (value instanceof MarkdownHandle) {
|
|
26
|
+
if (value.released)
|
|
27
|
+
throw failure("invalid", "Markdown document was released with the scope that created it");
|
|
28
|
+
return value.toJSON();
|
|
29
|
+
}
|
|
30
|
+
if (value instanceof ImageHandle) {
|
|
31
|
+
if (value.released)
|
|
32
|
+
throw failure("invalid", `image ${value.path} was released with the scope that created it`);
|
|
33
|
+
return value.toJSON();
|
|
34
|
+
}
|
|
35
|
+
// Array.from: a hole is `null`, as `JSON.stringify` writes it.
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return Array.from(value, (item) => wireValue(item ?? null));
|
|
38
|
+
if (value !== null && typeof value === "object") {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [key, item] of Object.entries(value))
|
|
41
|
+
out[key] = wireValue(item);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/** Scalars compared by value. Non-finite numbers are JSON `null`: compared as JSON. */
|
|
47
|
+
const isPrimitive = (value) => value === null ||
|
|
48
|
+
typeof value === "string" ||
|
|
49
|
+
typeof value === "boolean" ||
|
|
50
|
+
(typeof value === "number" && Number.isFinite(value));
|
|
51
|
+
/** A row as `JSON.stringify` writes it inside an array. */
|
|
52
|
+
const encodeRow = (row) => JSON.stringify(row) ?? "null";
|
|
53
|
+
/** What a row's encoding holds: a Markdown document, an image. */
|
|
54
|
+
const HAS_MARKDOWN = 1;
|
|
55
|
+
const HAS_IMAGE = 2;
|
|
56
|
+
const marksOf = (enc) => (enc.includes('"$vestraMarkdown"') ? HAS_MARKDOWN : 0) |
|
|
57
|
+
(enc.includes('"$vestraImage"') ? HAS_IMAGE : 0);
|
|
58
|
+
/**
|
|
59
|
+
* Row encodings by row object, shared by every property (a row's JSON does
|
|
60
|
+
* not depend on where it is bound). Only rows that cannot change are kept
|
|
61
|
+
* ({@link settled}): a plugin that mutates its rows in place sees no
|
|
62
|
+
* difference, and one that keeps unchanged rows as the same deep-frozen
|
|
63
|
+
* objects skips encoding them on every pass. Weak: a row the plugin drops
|
|
64
|
+
* goes with its entry.
|
|
65
|
+
*/
|
|
66
|
+
const settledRows = new WeakMap();
|
|
67
|
+
/**
|
|
68
|
+
* Frozen rows seen once. A frozen row is verified and kept on its second
|
|
69
|
+
* pass, so rows frozen afresh on every pass cost one set entry, not a
|
|
70
|
+
* verification each.
|
|
71
|
+
*/
|
|
72
|
+
const seenRows = new WeakSet();
|
|
73
|
+
/** Frozen rows that can still change (a mutable part, a getter): never kept. */
|
|
74
|
+
const unsettledRows = new WeakSet();
|
|
75
|
+
/**
|
|
76
|
+
* `value` can never change what `JSON.stringify` writes for it: a primitive,
|
|
77
|
+
* an image or document handle (their wire form is fixed when made), or a
|
|
78
|
+
* frozen plain object or array whose own enumerable properties are data
|
|
79
|
+
* properties (no getter, no own `toJSON`) holding such values. Freezing
|
|
80
|
+
* fixes the properties and the prototype for good, so the answer holds as
|
|
81
|
+
* long as the object lives.
|
|
82
|
+
*/
|
|
83
|
+
function settled(value, depth) {
|
|
84
|
+
if (value === null || typeof value !== "object")
|
|
85
|
+
return typeof value !== "function";
|
|
86
|
+
if (value instanceof ImageHandle || value instanceof MarkdownHandle)
|
|
87
|
+
return true;
|
|
88
|
+
if (depth > UI_VALUE_LIMITS.depth || !Object.isFrozen(value))
|
|
89
|
+
return false;
|
|
90
|
+
const proto = Object.getPrototypeOf(value);
|
|
91
|
+
if (Array.isArray(value)
|
|
92
|
+
? proto !== Array.prototype
|
|
93
|
+
: proto !== Object.prototype && proto !== null)
|
|
94
|
+
return false;
|
|
95
|
+
// An own `toJSON`, enumerable or not, is called by `JSON.stringify`.
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(value, "toJSON"))
|
|
97
|
+
return false;
|
|
98
|
+
for (const key of Object.keys(value)) {
|
|
99
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
100
|
+
if (!property || !("value" in property))
|
|
101
|
+
return false;
|
|
102
|
+
if (!settled(property.value, depth + 1))
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
/** A row's encoding and marks, from {@link settledRows} when it is kept. */
|
|
108
|
+
function rowEncoding(row, cache) {
|
|
109
|
+
if (cache && row !== null && typeof row === "object") {
|
|
110
|
+
// A kept row is frozen: looked up first, `isFrozen` only on a miss.
|
|
111
|
+
const kept = settledRows.get(row);
|
|
112
|
+
if (kept)
|
|
113
|
+
return kept;
|
|
114
|
+
}
|
|
115
|
+
if (cache &&
|
|
116
|
+
row !== null &&
|
|
117
|
+
typeof row === "object" &&
|
|
118
|
+
Object.isFrozen(row)) {
|
|
119
|
+
const enc = encodeRow(row);
|
|
120
|
+
const entry = { enc, marks: marksOf(enc) };
|
|
121
|
+
if (!seenRows.has(row))
|
|
122
|
+
seenRows.add(row);
|
|
123
|
+
else if (!unsettledRows.has(row)) {
|
|
124
|
+
if (settled(row, 1))
|
|
125
|
+
settledRows.set(row, entry);
|
|
126
|
+
else
|
|
127
|
+
unsettledRows.add(row);
|
|
128
|
+
}
|
|
129
|
+
return entry;
|
|
130
|
+
}
|
|
131
|
+
const enc = encodeRow(row);
|
|
132
|
+
return { enc, marks: marksOf(enc) };
|
|
133
|
+
}
|
|
134
|
+
/** Each row of an array as `JSON.stringify` writes it; a hole is `null`. */
|
|
135
|
+
function encodeRows(rows) {
|
|
136
|
+
// A `toJSON` added to the shared prototypes would change any row's JSON:
|
|
137
|
+
// then nothing is taken from (or kept in) the cache.
|
|
138
|
+
const cache = !("toJSON" in Object.prototype) && !("toJSON" in Array.prototype);
|
|
139
|
+
const enc = [];
|
|
140
|
+
const marks = new Uint8Array(rows.length);
|
|
141
|
+
let markdown = false;
|
|
142
|
+
for (let i = 0; i < rows.length; i++) {
|
|
143
|
+
const row = rowEncoding(rows[i], cache);
|
|
144
|
+
enc[i] = row.enc;
|
|
145
|
+
marks[i] = row.marks;
|
|
146
|
+
if (row.marks & HAS_MARKDOWN)
|
|
147
|
+
markdown = true;
|
|
148
|
+
}
|
|
149
|
+
return { enc, marks, markdown, cache };
|
|
150
|
+
}
|
|
151
|
+
/** A row's limit counts, kept with its encoding when the row cannot change. */
|
|
152
|
+
function rowCount(row, cache) {
|
|
153
|
+
const kept = cache && row !== null && typeof row === "object"
|
|
154
|
+
? settledRows.get(row)
|
|
155
|
+
: undefined;
|
|
156
|
+
if (!kept)
|
|
157
|
+
return countRow(row);
|
|
158
|
+
return (kept.count ??= countRow(row));
|
|
159
|
+
}
|
|
160
|
+
function sameRows(a, b) {
|
|
161
|
+
if (a.length !== b.length)
|
|
162
|
+
return false;
|
|
163
|
+
for (let i = 0; i < a.length; i++)
|
|
164
|
+
if (a[i] !== b[i])
|
|
165
|
+
return false;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
/** An initial property (wire form) as sent. */
|
|
169
|
+
function sentOf(value, patches) {
|
|
170
|
+
if (isPrimitive(value))
|
|
171
|
+
return { kind: "value", value };
|
|
172
|
+
if (Array.isArray(value) && patches) {
|
|
173
|
+
const keyed = keyOf(value);
|
|
174
|
+
return {
|
|
175
|
+
kind: "rows",
|
|
176
|
+
rows: {
|
|
177
|
+
enc: encodeRows(value).enc,
|
|
178
|
+
field: keyed?.field ?? null,
|
|
179
|
+
keys: keyed?.keys ?? null,
|
|
180
|
+
counts: [],
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { kind: "json", enc: JSON.stringify(value) };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A write sent in parts stopped because its view closed or showed its
|
|
188
|
+
* initial properties again: not a failure.
|
|
189
|
+
*/
|
|
190
|
+
class Abandoned extends Error {
|
|
191
|
+
}
|
|
192
|
+
/** The wire key of a Markdown document: rows holding one are never sent in parts. */ const MARKDOWN_KEY = '"$vestraMarkdown"';
|
|
193
|
+
/** A row larger than one request can carry (`limit`). */
|
|
194
|
+
const rowTooLarge = (key, bytes) => new HostError("limit", `${key}: a row of ${bytes} bytes is larger than one request takes (${CHUNK_BYTES} bytes)`);
|
|
195
|
+
/** Refuse a patch carrying a row larger than one request, before sending it. */
|
|
196
|
+
function tooLarge(key, ops) {
|
|
197
|
+
for (const op of ops) {
|
|
198
|
+
const rows = op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : [];
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
const bytes = rowBytes(row);
|
|
201
|
+
if (bytes > CHUNK_BYTES)
|
|
202
|
+
throw rowTooLarge(key, bytes);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The row patch from `sent` to `next` (the bound `rows`), in wire form, and
|
|
208
|
+
* the new rows' limit counts; `null` when the whole value is to be sent:
|
|
209
|
+
* nothing to type the rows by (no rows sent), a Markdown document in a row
|
|
210
|
+
* (the player merges documents in place), too many operations, a value over
|
|
211
|
+
* the limits (the player refuses it whole, as before), or no change (an
|
|
212
|
+
* explicit `set()` of the same value).
|
|
213
|
+
*/
|
|
214
|
+
function rowPatch(sent, next, rows) {
|
|
215
|
+
if (sent.enc.length === 0)
|
|
216
|
+
return null;
|
|
217
|
+
if (next.markdown)
|
|
218
|
+
return null;
|
|
219
|
+
const nextKeyed = sent.field === null ? null : next.keyed();
|
|
220
|
+
const keyed = nextKeyed !== null && nextKeyed.field === sent.field;
|
|
221
|
+
const plan = diff({ enc: sent.enc, keys: keyed ? sent.keys : null }, { enc: next.enc, keys: keyed ? nextKeyed.keys : null });
|
|
222
|
+
if (!plan || plan.ops.length === 0)
|
|
223
|
+
return null;
|
|
224
|
+
// A released image is refused wherever it is, as in a whole write: the
|
|
225
|
+
// rows the patch does not carry are checked too (those holding images).
|
|
226
|
+
for (let j = 0; j < rows.length; j++)
|
|
227
|
+
if (plan.from[j] >= 0 && next.marks[j] & HAS_IMAGE)
|
|
228
|
+
wireValue(rows[j] ?? null);
|
|
229
|
+
const counts = next.enc.map((_, j) => {
|
|
230
|
+
const from = plan.from[j];
|
|
231
|
+
return ((from >= 0 ? sent.counts[from] : undefined) ??
|
|
232
|
+
rowCount(rows[j], next.cache));
|
|
233
|
+
});
|
|
234
|
+
if (!withinLimits(counts))
|
|
235
|
+
return null;
|
|
236
|
+
// The ops must give exactly the new rows (a guard against this diff's bugs).
|
|
237
|
+
if (!sameRows(replay(sent.enc, plan.ops, next.enc), next.enc))
|
|
238
|
+
return null;
|
|
239
|
+
const ops = plan.ops.map((op) => {
|
|
240
|
+
switch (op.op) {
|
|
241
|
+
case "set":
|
|
242
|
+
return { op: "set", at: op.at, row: wireValue(rows[op.row] ?? null) };
|
|
243
|
+
case "insert":
|
|
244
|
+
return {
|
|
245
|
+
op: "insert",
|
|
246
|
+
at: op.at,
|
|
247
|
+
rows: op.rows.map((j) => wireValue(rows[j] ?? null)),
|
|
248
|
+
};
|
|
249
|
+
default:
|
|
250
|
+
return op;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
return { ops, counts };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* One view's properties: their values now, what the player shows as far as
|
|
257
|
+
* this side knows, and the writes that bring the one to the other.
|
|
258
|
+
*/
|
|
259
|
+
export class ViewWriter {
|
|
260
|
+
#target;
|
|
261
|
+
#values = new Map();
|
|
262
|
+
/** What the host currently shows, as far as this side knows. */
|
|
263
|
+
#sent = new Map();
|
|
264
|
+
/**
|
|
265
|
+
* Row patches (player feature `ui.patch`): each property's last write the
|
|
266
|
+
* player confirmed, by `seq` (0: the initial properties). A patch applies
|
|
267
|
+
* on it; a property without one is sent whole.
|
|
268
|
+
*/
|
|
269
|
+
#bases = new Map();
|
|
270
|
+
/**
|
|
271
|
+
* Patches of a property answered `stale` in a row: after two, the
|
|
272
|
+
* property is sent whole until the view shows its initial properties again.
|
|
273
|
+
*/
|
|
274
|
+
#stales = new Map();
|
|
275
|
+
#seq = 0;
|
|
276
|
+
/** Counts the times the view showed its initial properties (`shown`). */
|
|
277
|
+
#generation = 0;
|
|
278
|
+
/** Written by `set()`: sent even if equal to what was sent before. */
|
|
279
|
+
#forced = new Set();
|
|
280
|
+
/** The last value of each property whose write failed; not retried until it changes. */
|
|
281
|
+
#failed = new Map();
|
|
282
|
+
#backoff = 0;
|
|
283
|
+
/** A write failed and none succeeded since: further failures stay quiet. */
|
|
284
|
+
#broken = false;
|
|
285
|
+
#retry;
|
|
286
|
+
#flushing = false;
|
|
287
|
+
constructor(target) {
|
|
288
|
+
this.#target = target;
|
|
289
|
+
}
|
|
290
|
+
/** The player takes row patches (and `seq` on whole writes). */
|
|
291
|
+
get #patches() {
|
|
292
|
+
return this.#target.patches();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Initial properties in wire form. A released image is left out (the
|
|
296
|
+
* property keeps its default) and reported, like a failed write. With a
|
|
297
|
+
* player that takes row patches, the arrays of one request carry at most
|
|
298
|
+
* {@link CHUNK_BYTES} of rows together (SDK 1.5.46): an array over that
|
|
299
|
+
* goes with its first rows, and the first flush sends the rest as patches.
|
|
300
|
+
* An array over the value limits is refused here, as the player would
|
|
301
|
+
* refuse it whole: left out and reported (a panel, a settings page), or,
|
|
302
|
+
* with `refuse` (a window's `show`), thrown.
|
|
303
|
+
*/
|
|
304
|
+
wired(values, refuse = false) {
|
|
305
|
+
const out = {};
|
|
306
|
+
let room = CHUNK_BYTES;
|
|
307
|
+
for (const [key, value] of Object.entries(values)) {
|
|
308
|
+
let wire;
|
|
309
|
+
try {
|
|
310
|
+
wire = wireValue(value);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
this.#failed.set(key, JSON.stringify(value));
|
|
314
|
+
this.#target.report(error);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (this.#patches && Array.isArray(wire)) {
|
|
318
|
+
const sizes = wire.map(rowBytes);
|
|
319
|
+
const total = sizes.reduce((a, b) => a + b, 0);
|
|
320
|
+
if (total > room && !JSON.stringify(wire).includes(MARKDOWN_KEY)) {
|
|
321
|
+
const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
|
|
322
|
+
const problem = valueLimitProblem(wire);
|
|
323
|
+
if (problem || largest > CHUNK_BYTES) {
|
|
324
|
+
const error = problem
|
|
325
|
+
? new HostError("limit", `${key}: ${problem}`)
|
|
326
|
+
: rowTooLarge(key, largest);
|
|
327
|
+
if (refuse)
|
|
328
|
+
throw error;
|
|
329
|
+
this.#failed.set(key, JSON.stringify(value));
|
|
330
|
+
this.#target.report(error);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const count = room > 0 ? prefixWithin(sizes, room) : 0;
|
|
334
|
+
wire = wire.slice(0, count);
|
|
335
|
+
room -= sizes.slice(0, count).reduce((a, b) => a + b, 0);
|
|
336
|
+
}
|
|
337
|
+
else
|
|
338
|
+
room -= total;
|
|
339
|
+
}
|
|
340
|
+
out[key] = wire;
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
/** `View.set`: each value is sent once even if equal to what was sent. */
|
|
345
|
+
set(props) {
|
|
346
|
+
for (const [key, value] of Object.entries(encode(props))) {
|
|
347
|
+
this.#values.set(key, value);
|
|
348
|
+
this.#forced.add(key);
|
|
349
|
+
this.#failed.delete(key); // an explicit write is tried again (after any back-off)
|
|
350
|
+
}
|
|
351
|
+
this.flush();
|
|
352
|
+
}
|
|
353
|
+
/** A bound value (Slint name): sent by the next flush if it changed. */
|
|
354
|
+
assign(key, value) {
|
|
355
|
+
this.#values.set(key, value);
|
|
356
|
+
}
|
|
357
|
+
/** Every property's value now, by Slint name. */
|
|
358
|
+
values() {
|
|
359
|
+
return Object.fromEntries(this.#values);
|
|
360
|
+
}
|
|
361
|
+
/** The host already shows these values (e.g. sent as initial properties). */
|
|
362
|
+
shown(values) {
|
|
363
|
+
this.#generation++;
|
|
364
|
+
const patches = this.#patches;
|
|
365
|
+
this.#sent = new Map(Object.entries(values).map(([k, v]) => [k, sentOf(v, patches)]));
|
|
366
|
+
this.#bases = new Map(patches ? Object.keys(values).map((key) => [key, 0]) : []);
|
|
367
|
+
this.#stales.clear();
|
|
368
|
+
this.#forced.clear();
|
|
369
|
+
for (const key of Object.keys(values))
|
|
370
|
+
this.#failed.delete(key);
|
|
371
|
+
}
|
|
372
|
+
/** `value` encoded, and whether it differs from what was sent. */
|
|
373
|
+
#encode(key, value) {
|
|
374
|
+
const sent = this.#sent.get(key);
|
|
375
|
+
if (isPrimitive(value)) {
|
|
376
|
+
// Compared as it is: no `JSON.stringify` for the scalars of a pass.
|
|
377
|
+
const changed = sent?.kind === "value"
|
|
378
|
+
? sent.value !== value
|
|
379
|
+
: sent?.kind !== "json" || sent.enc !== JSON.stringify(value);
|
|
380
|
+
return {
|
|
381
|
+
key,
|
|
382
|
+
value,
|
|
383
|
+
changed,
|
|
384
|
+
signature: () => JSON.stringify(value),
|
|
385
|
+
sent: () => ({ kind: "value", value }),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
if (Array.isArray(value) && this.#patches) {
|
|
389
|
+
// Row by row: the same work as one `JSON.stringify`, and the rows to
|
|
390
|
+
// diff against those last sent. Rows that cannot change (deep-frozen
|
|
391
|
+
// objects bound again, SDK 1.5.47) are taken from `settledRows`.
|
|
392
|
+
const encoded = encodeRows(value);
|
|
393
|
+
const { enc } = encoded;
|
|
394
|
+
const signature = () => `[${enc.join(",")}]`;
|
|
395
|
+
const changed = sent?.kind === "rows"
|
|
396
|
+
? !sameRows(sent.rows.enc, enc)
|
|
397
|
+
: sent?.kind !== "json" || sent.enc !== signature();
|
|
398
|
+
// Keys only for a write: an unchanged array costs its encoding alone.
|
|
399
|
+
let keys;
|
|
400
|
+
const keyed = () => (keys === undefined ? (keys = keyOf(value)) : keys);
|
|
401
|
+
return {
|
|
402
|
+
key,
|
|
403
|
+
value,
|
|
404
|
+
changed,
|
|
405
|
+
signature,
|
|
406
|
+
rows: { ...encoded, keyed },
|
|
407
|
+
sent: (counts = []) => ({
|
|
408
|
+
kind: "rows",
|
|
409
|
+
rows: {
|
|
410
|
+
enc,
|
|
411
|
+
field: keyed()?.field ?? null,
|
|
412
|
+
keys: keyed()?.keys ?? null,
|
|
413
|
+
counts,
|
|
414
|
+
},
|
|
415
|
+
}),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const enc = JSON.stringify(value);
|
|
419
|
+
return {
|
|
420
|
+
key,
|
|
421
|
+
value,
|
|
422
|
+
changed: sent?.kind !== "json" || sent.enc !== enc,
|
|
423
|
+
signature: () => enc,
|
|
424
|
+
sent: () => ({ kind: "json", enc }),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Write one property: a row patch when the player takes them and one
|
|
429
|
+
* applies (else the whole value); a `stale` patch is followed by the whole
|
|
430
|
+
* value at once. The base is forgotten on any failure, so the next write
|
|
431
|
+
* of the property is whole. A write larger than one request
|
|
432
|
+
* ({@link CHUNK_BYTES} of rows, SDK 1.5.46) is sent in parts: a whole
|
|
433
|
+
* array as its first rows and `insert` patches of the rest, a patch as
|
|
434
|
+
* several, each applying on the one before.
|
|
435
|
+
*/
|
|
436
|
+
async #send(pending) {
|
|
437
|
+
const { key, value } = pending;
|
|
438
|
+
if (!this.#patches) {
|
|
439
|
+
await this.#target.write(key, { value: wireValue(value) });
|
|
440
|
+
return pending.sent();
|
|
441
|
+
}
|
|
442
|
+
const generation = this.#generation;
|
|
443
|
+
const base = this.#bases.get(key);
|
|
444
|
+
this.#bases.delete(key);
|
|
445
|
+
const sent = this.#sent.get(key);
|
|
446
|
+
let plan = base !== undefined &&
|
|
447
|
+
sent?.kind === "rows" &&
|
|
448
|
+
pending.rows &&
|
|
449
|
+
(this.#stales.get(key) ?? 0) < 2
|
|
450
|
+
? rowPatch(sent.rows, pending.rows, value)
|
|
451
|
+
: null;
|
|
452
|
+
// A patch sent in several never passes the row limit midway (the diff
|
|
453
|
+
// removes before it inserts; checked anyway); otherwise whole.
|
|
454
|
+
if (plan && sent?.kind === "rows") {
|
|
455
|
+
let length = sent.rows.enc.length;
|
|
456
|
+
for (const op of plan.ops) {
|
|
457
|
+
if (op.op === "insert")
|
|
458
|
+
length += op.rows.length;
|
|
459
|
+
else if (op.op === "remove")
|
|
460
|
+
length -= op.count;
|
|
461
|
+
if (length > UI_VALUE_LIMITS.arrayRows) {
|
|
462
|
+
plan = null;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (plan) {
|
|
468
|
+
tooLarge(key, plan.ops);
|
|
469
|
+
const [first, ...rest] = chunkOps(plan.ops);
|
|
470
|
+
const seq = ++this.#seq;
|
|
471
|
+
let applied = false;
|
|
472
|
+
try {
|
|
473
|
+
await this.#target.write(key, {
|
|
474
|
+
patch: { base: base, seq, ops: first },
|
|
475
|
+
});
|
|
476
|
+
applied = true;
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (!(error instanceof HostError && error.code === "stale"))
|
|
480
|
+
throw error;
|
|
481
|
+
this.#stales.set(key, (this.#stales.get(key) ?? 0) + 1);
|
|
482
|
+
}
|
|
483
|
+
if (applied) {
|
|
484
|
+
this.#stales.delete(key);
|
|
485
|
+
await this.#parts(key, seq, rest, false, generation);
|
|
486
|
+
return pending.sent(plan.counts);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const seq = ++this.#seq;
|
|
490
|
+
// Rows holding Markdown documents go whole, as before: the player
|
|
491
|
+
// merges documents in place and takes no parts of them.
|
|
492
|
+
if (Array.isArray(value) && pending.rows && !pending.rows.markdown) {
|
|
493
|
+
const sizes = pending.rows.enc.map(jsonBytes);
|
|
494
|
+
const count = prefixWithin(sizes, CHUNK_BYTES);
|
|
495
|
+
if (count < value.length) {
|
|
496
|
+
const rows = value;
|
|
497
|
+
// Over the limits, or a row larger than one request: refused here,
|
|
498
|
+
// as the player would refuse the whole value, before any part is
|
|
499
|
+
// sent (a part refused midway would leave the list half sent).
|
|
500
|
+
const problem = valueLimitProblem(wireValue(value));
|
|
501
|
+
if (problem)
|
|
502
|
+
throw new HostError("limit", `${key}: ${problem}`);
|
|
503
|
+
const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
|
|
504
|
+
if (largest > CHUNK_BYTES)
|
|
505
|
+
throw rowTooLarge(key, largest);
|
|
506
|
+
// Every part but the last carries `more`: the player holds them
|
|
507
|
+
// and writes the whole value at the last, so a list it shows is
|
|
508
|
+
// written once (player 0.6.0).
|
|
509
|
+
await this.#target.write(key, {
|
|
510
|
+
value: wireValue(rows.slice(0, count)),
|
|
511
|
+
seq,
|
|
512
|
+
more: true,
|
|
513
|
+
});
|
|
514
|
+
const insert = {
|
|
515
|
+
op: "insert",
|
|
516
|
+
at: count,
|
|
517
|
+
rows: rows.slice(count).map((row) => wireValue(row ?? null)),
|
|
518
|
+
};
|
|
519
|
+
await this.#parts(key, seq, chunkOps([insert]), true, generation);
|
|
520
|
+
return pending.sent();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
await this.#target.write(key, { value: wireValue(value), seq });
|
|
524
|
+
this.#bases.set(key, seq);
|
|
525
|
+
return pending.sent();
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* The rest of a write sent in parts: each patch applies on the one
|
|
529
|
+
* before (`base`), and the property's base is the last one's; with
|
|
530
|
+
* `more`, every part but the last says so. A failure leaves no base: the
|
|
531
|
+
* next write of the property is whole. When the view closed or showed its
|
|
532
|
+
* initial properties again meanwhile (`generation`), the rest is dropped
|
|
533
|
+
* quietly ({@link Abandoned}): the view's next flush sends what it shows.
|
|
534
|
+
*/
|
|
535
|
+
async #parts(key, base, groups, more, generation) {
|
|
536
|
+
let last = base;
|
|
537
|
+
for (const [i, ops] of groups.entries()) {
|
|
538
|
+
if (!this.#target.live() || this.#generation !== generation)
|
|
539
|
+
throw new Abandoned();
|
|
540
|
+
const seq = ++this.#seq;
|
|
541
|
+
try {
|
|
542
|
+
await this.#target.write(key, {
|
|
543
|
+
patch: { base: last, seq, ops },
|
|
544
|
+
...(more && i < groups.length - 1 ? { more: true } : {}),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
// Refused because the view went meanwhile: not a failure.
|
|
549
|
+
if (!this.#target.live() || this.#generation !== generation)
|
|
550
|
+
throw new Abandoned();
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
last = seq;
|
|
554
|
+
}
|
|
555
|
+
this.#bases.set(key, last);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Send what changed. A bound value is sent when it differs from the last one
|
|
559
|
+
* sent; a `set()` value is always sent once. Nothing is resent merely
|
|
560
|
+
* because a callback arrived, so text the user typed into an in-out
|
|
561
|
+
* property survives other callbacks (call `set()` to pull a control back).
|
|
562
|
+
* A failed bound value is not repeated until it changes; an explicit `set()`
|
|
563
|
+
* is, after a back-off (0.25 s doubling to 30 s). Only the first failure of
|
|
564
|
+
* a failing streak is reported, so an error handler that writes a status to
|
|
565
|
+
* a broken view cannot spin.
|
|
566
|
+
*/
|
|
567
|
+
flush() {
|
|
568
|
+
if (this.#flushing || this.#retry || !this.#target.live())
|
|
569
|
+
return;
|
|
570
|
+
this.#flushing = true;
|
|
571
|
+
void (async () => {
|
|
572
|
+
let failed = false;
|
|
573
|
+
let failure;
|
|
574
|
+
try {
|
|
575
|
+
let pending;
|
|
576
|
+
do {
|
|
577
|
+
pending = [];
|
|
578
|
+
for (const [key, value] of this.#values) {
|
|
579
|
+
const encoded = this.#encode(key, value);
|
|
580
|
+
const failedAs = this.#failed.get(key);
|
|
581
|
+
if (failedAs !== undefined && failedAs === encoded.signature())
|
|
582
|
+
continue;
|
|
583
|
+
if (this.#forced.has(key) || encoded.changed)
|
|
584
|
+
pending.push(encoded);
|
|
585
|
+
}
|
|
586
|
+
for (const encoded of pending) {
|
|
587
|
+
if (!this.#target.live())
|
|
588
|
+
return;
|
|
589
|
+
const key = encoded.key;
|
|
590
|
+
this.#forced.delete(key);
|
|
591
|
+
try {
|
|
592
|
+
this.#sent.set(key, await this.#send(encoded));
|
|
593
|
+
this.#failed.delete(key);
|
|
594
|
+
this.#backoff = 0;
|
|
595
|
+
this.#broken = false;
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
// The view closed or showed its initial properties again while
|
|
599
|
+
// a write was sent in parts: nothing failed (the next flush
|
|
600
|
+
// sends what the view shows).
|
|
601
|
+
if (error instanceof Abandoned) {
|
|
602
|
+
queueMicrotask(() => this.flush());
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
failed = true;
|
|
606
|
+
// Report the start of a failing streak only.
|
|
607
|
+
if (!this.#broken)
|
|
608
|
+
failure ??= error;
|
|
609
|
+
this.#broken = true;
|
|
610
|
+
this.#failed.set(key, encoded.signature());
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} while (pending.length && !failed && this.#target.live());
|
|
614
|
+
}
|
|
615
|
+
finally {
|
|
616
|
+
this.#flushing = false;
|
|
617
|
+
}
|
|
618
|
+
if (!failed)
|
|
619
|
+
return;
|
|
620
|
+
this.#backoff = Math.min(Math.max(250, this.#backoff * 2), 30_000);
|
|
621
|
+
this.#retry = setTimeout(() => {
|
|
622
|
+
this.#retry = undefined;
|
|
623
|
+
this.flush();
|
|
624
|
+
}, this.#backoff);
|
|
625
|
+
this.#retry.unref?.();
|
|
626
|
+
if (failure !== undefined)
|
|
627
|
+
this.#target.report(failure);
|
|
628
|
+
})();
|
|
629
|
+
}
|
|
630
|
+
/** The view closed: no retry after a back-off. */
|
|
631
|
+
stop() {
|
|
632
|
+
clearTimeout(this.#retry);
|
|
633
|
+
}
|
|
634
|
+
}
|
package/dist/plugin/view.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* The first plugin to call one owns it; the owner's `dispose` (or the plugin
|
|
10
10
|
* stopping) puts the defaults back.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { Latest } from "./latest.js";
|
|
13
|
+
import { failure, quiet } from "./errors.js";
|
|
13
14
|
import { playerOf } from "./player.js";
|
|
14
15
|
import { isAbsolute } from "node:path";
|
|
15
16
|
export const VIEW_DEFAULTS = Object.freeze({
|
package/dist/plugin/window.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The main window (`plugin.window`) and the player's own facts (`plugin.app`). */
|
|
2
|
-
import { failure } from "./
|
|
2
|
+
import { failure } from "./errors.js";
|
|
3
3
|
const view = (facts) => Object.freeze({
|
|
4
4
|
fullscreen: facts.fullscreen === true,
|
|
5
5
|
// Players before 0.6.3 do not send it; never maximized in fullscreen.
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vestra Player SDK
|
|
2
2
|
|
|
3
|
-
> @vestra-player/sdk 1.6.
|
|
3
|
+
> @vestra-player/sdk 1.6.19: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
|
|
4
4
|
|
|
5
5
|
## Read the documentation
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vestra-player/sdk",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.19",
|
|
4
4
|
"description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"bench": "node scripts/bench.mjs",
|
|
54
54
|
"build": "node scripts/build.mjs",
|
|
55
55
|
"capabilities:sync": "node scripts/capabilities-sync.mjs",
|
|
56
|
-
"check": "tsc -p tsconfig.check.json",
|
|
56
|
+
"check": "tsc -p tsconfig.check.json && node scripts/check-layers.mjs",
|
|
57
57
|
"doc-lint": "node scripts/doc-lint.mjs",
|
|
58
58
|
"docs": "typedoc",
|
|
59
59
|
"docs:check": "node scripts/check-docs.mjs",
|