@vestra-player/sdk 1.5.43 → 1.5.44

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.44
4
+
5
+ **Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. How the SDK sends array properties changes, on players that take it (player 0.6.0); with older players it sends whole values as before. The read-only copy of the widget library gains components that need player 0.6.0 (`"minPlayerVersion": "0.6.0"` for a plugin whose Slint uses them; an older player fails to compile such a file).
6
+
7
+ - **Row patches.** When a bound or set array changes, the SDK sends the rows that changed instead of the whole array: `set`, `insert`, `remove` and `move` operations by index (wire: `ui.setMount`, `ui.set` and `settings.set` with `patch: { base, seq, ops }` instead of `value`; whole writes carry `seq`). Rows are matched by a `key`, `id`, `rowId` or `row-id` field when every row has one, unique, as a string or number (moves are kept to a minimum); otherwise by index, with the unchanged rows at both ends left out. The player writes the rows into its live model in place, without comparing the whole array: selecting one row of a 200-row list costs one row. A player that cannot apply a patch answers `stale` and the SDK sends the whole value at once. Whole values are sent as before when the player does not advertise the feature (`features: ["ui.patch"]` in its start-up data), after a failed write, for rows written into an empty array, for rows holding a Markdown document, and for a value over the limits (refused with `limit`, as before).
8
+ After two `stale` answers in a row for a property, it is sent whole until the view shows its initial properties again. The limit counts see what is sent: an image or document handle is one value, any other object with `toJSON` (a `Date`) counts as what it returns, and a hole in a sparse array is `null`. A released image is refused in any row of the array, also in rows a patch keeps, as in a whole write. Your Slint code must not write into the rows of an array property: a patch does not overwrite rows whose value did not change ([Arrays: only the changed rows are sent](docs/explanation/ui.md#arrays-only-the-changed-rows-are-sent)). `set()` with an unchanged array sends it whole.
9
+ - **Cheaper update passes.** A scalar property is compared with what was sent without `JSON.stringify`; an array is encoded row by row, the same work as before.
10
+ - **`TestHost`** takes patches as the player does (`base`, `stale`, the checks on operations and the value limits on the result) and keeps whole values in `props`, so tests see no difference. It advertises the feature; `host.app = { ...host.app, features: [] }` before `startPlugin` gives an older player's behaviour.
11
+ - **`@vestra/widgets.slint`** (the read-only copy of the player's widget library, player 0.6.0): the widget library copy gains `Check`, `Radio`, `RadioMark`, `RadioGroup`, `Tab`, `Tabs` and `Choice` (a drop-down), drawn in the player's theme with a keyboard focus ring; `Slider` takes the keyboard (arrow keys by `step`, a twentieth of the range by default) and reports `has-focus`. A Slint file that imports them needs player 0.6.0. `Check` takes `self-toggle: false` for a state the plugin decides (a confirmation first); `Tabs` takes `emphasis` per tab; `Slider` takes `focusable: false` to stay out of the Tab order; `Choice` takes `window-height` so its list fits a short window. `@vestra/markdown.slint`: `MarkdownView` builds only the blocks on screen (a `ListView`), so long documents cost what is visible.
12
+
3
13
  ## 1.5.43
4
14
 
5
15
  **Who must change: nobody.** Additions only, `apiVersion` stays `vestra/1`, no new capability. The new API needs player 0.5.25 (`"minPlayerVersion": "0.5.25"`); older players leave the new fields out and refuse the new event (the listener never fires). The `decoding` reason code stays as it is: the cause is a new optional field beside it.
@@ -10,7 +20,6 @@
10
20
  - **`TestHost`**: `upscalingDecoding(cause, plugins?)` fakes each cause with the player's message (and a matching `upscaling.hwdecCurrent`); `upscaling.video`, `area`, `gpu`, `hwdecCurrent` (defaults: 1080p on a 4K area, an RTX 4070, `d3d11va`; `inChain` follows `requested`); `upscalingChanged()` sends the event to a listening plugin.
11
21
  - The player's capability table copy (`video.upscaling.changed` under `video.upscaling`) and `CAPABILITY_INFO` (`VideoUpscaling.onChange`) follow.
12
22
 
13
-
14
23
  ## 1.5.42
15
24
 
16
25
  **Who must change: nobody.** Documentation only: no API changes, `apiVersion` stays `vestra/1`, no player version needed. The package's code is the same as 1.5.41; the doc comments' `@see` links (27 in `src/plugin/types.ts`) now point at `docs/explanation/` instead of `docs/concepts/`. The site's page addresses stay the same, and the old _Concepts_ group address redirects to _Explanation_; `llms.txt` and `llms-full.txt` follow the new pages.
@@ -24,3 +24,4 @@ export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale
24
24
  export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.ts";
25
25
  export { MARKDOWN_LIMITS } from "./plugin/markdown.ts";
26
26
  export { IMAGE_LIMITS, UI_VALUE_LIMITS } from "./plugin/values.ts";
27
+ export { PATCH_FEATURE, PATCH_OPS } from "./plugin/patch.ts";
@@ -22,3 +22,4 @@ export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale
22
22
  export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.js";
23
23
  export { MARKDOWN_LIMITS } from "./plugin/markdown.js";
24
24
  export { IMAGE_LIMITS, UI_VALUE_LIMITS } from "./plugin/values.js";
25
+ export { PATCH_FEATURE, PATCH_OPS } from "./plugin/patch.js";
@@ -0,0 +1,101 @@
1
+ /** The player feature that takes row patches. */
2
+ export declare const PATCH_FEATURE = "ui.patch";
3
+ /** The most operations one patch carries (the player's `MAX_PATCH_OPS`). */
4
+ export declare const PATCH_OPS = 1024;
5
+ /**
6
+ * Fields that identify a row, in the order they are tried. A field is used
7
+ * when every row has it, as a string or a number, and no two rows share it.
8
+ */
9
+ export declare const KEY_FIELDS: readonly string[];
10
+ export type RowKey = string | number;
11
+ /** One planned operation; `set` and `insert` name rows of the new array. */
12
+ export type PlannedOp = {
13
+ op: "set";
14
+ at: number;
15
+ row: number;
16
+ } | {
17
+ op: "insert";
18
+ at: number;
19
+ rows: number[];
20
+ } | {
21
+ op: "remove";
22
+ at: number;
23
+ count: number;
24
+ } | {
25
+ op: "move";
26
+ from: number;
27
+ count: number;
28
+ to: number;
29
+ };
30
+ /** What one row counts toward the value limits (as a row of an array). */
31
+ export interface RowCount {
32
+ values: number;
33
+ text: number;
34
+ /** Over a per-row limit (a string, the depth): the whole value is sent. */
35
+ over: boolean;
36
+ }
37
+ /** An array as sent: each row's JSON, its key field and keys, its counts. */
38
+ export interface Rows {
39
+ enc: string[];
40
+ field: string | null;
41
+ keys: RowKey[] | null;
42
+ /** Filled in as patches need them. */
43
+ counts: (RowCount | undefined)[];
44
+ }
45
+ /**
46
+ * The field that identifies `rows`, and each row's key; `null` when no
47
+ * field of {@link KEY_FIELDS} does (rows are then matched by index).
48
+ */
49
+ export declare function keyOf(rows: readonly unknown[]): {
50
+ field: string;
51
+ keys: RowKey[];
52
+ } | null;
53
+ /**
54
+ * What `row` counts toward the value limits as a row of an array (depth
55
+ * 1), like `valueLimitProblem` on the row as sent: every value one, an
56
+ * image one, a document one and its text, strings and field names their
57
+ * UTF-8 bytes. Any other object with `toJSON` counts as what it returns.
58
+ */
59
+ export declare function countRow(row: unknown): RowCount;
60
+ /** The new array is within the value limits (counted per row). */
61
+ export declare function withinLimits(counts: readonly RowCount[]): boolean;
62
+ /**
63
+ * The operations that turn the old rows into the new ones (`null`: more
64
+ * than {@link PATCH_OPS} would be needed), and for each new row the old row
65
+ * whose JSON it keeps (`-1`: a new or changed row). Keyed when both arrays
66
+ * have keys from the same field; otherwise by index.
67
+ */
68
+ export declare function diff(old: {
69
+ enc: readonly string[];
70
+ keys: readonly RowKey[] | null;
71
+ }, next: {
72
+ enc: readonly string[];
73
+ keys: readonly RowKey[] | null;
74
+ }): {
75
+ ops: PlannedOp[];
76
+ from: Int32Array;
77
+ } | null;
78
+ /**
79
+ * Apply planned operations to a copy of `old` (the rows' JSON), taking new
80
+ * rows from `next`: the check that a diff produces exactly the new rows.
81
+ */
82
+ export declare function replay(old: readonly string[], ops: readonly PlannedOp[], next: readonly string[]): string[];
83
+ /** Apply wire operations to an array (the TestHost's copy of a value). */
84
+ export declare function applyOps<T>(rows: T[], ops: readonly ({
85
+ op: "set";
86
+ at: number;
87
+ row: T;
88
+ } | {
89
+ op: "insert";
90
+ at: number;
91
+ rows: T[];
92
+ } | {
93
+ op: "remove";
94
+ at: number;
95
+ count: number;
96
+ } | {
97
+ op: "move";
98
+ from: number;
99
+ count: number;
100
+ to: number;
101
+ })[]): T[];
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Row patches of array properties (player 0.6.0, feature `ui.patch`): the
3
+ * diff between the rows last sent and the rows bound now, as `set`,
4
+ * `insert`, `remove` and `move` operations by index. Internal; plugins see
5
+ * none of it (the player's doc/architecture/large-lists.md §2).
6
+ */
7
+ import { UI_VALUE_LIMITS } from "./values.js";
8
+ /** The player feature that takes row patches. */
9
+ export const PATCH_FEATURE = "ui.patch";
10
+ /** The most operations one patch carries (the player's `MAX_PATCH_OPS`). */
11
+ export const PATCH_OPS = 1024;
12
+ /**
13
+ * Fields that identify a row, in the order they are tried. A field is used
14
+ * when every row has it, as a string or a number, and no two rows share it.
15
+ */
16
+ export const KEY_FIELDS = ["key", "id", "rowId", "row-id"];
17
+ const plain = (value) => {
18
+ if (value === null || typeof value !== "object" || Array.isArray(value))
19
+ return false;
20
+ const proto = Object.getPrototypeOf(value);
21
+ return proto === Object.prototype || proto === null;
22
+ };
23
+ /**
24
+ * The field that identifies `rows`, and each row's key; `null` when no
25
+ * field of {@link KEY_FIELDS} does (rows are then matched by index).
26
+ */
27
+ export function keyOf(rows) {
28
+ const first = rows[0];
29
+ if (!plain(first))
30
+ return null;
31
+ for (const field of KEY_FIELDS) {
32
+ if (!(field in first))
33
+ continue;
34
+ const keys = [];
35
+ const seen = new Set();
36
+ let ok = true;
37
+ for (let i = 0; i < rows.length; i++) {
38
+ const row = rows[i];
39
+ const key = plain(row) ? row[field] : undefined;
40
+ if ((typeof key !== "string" && typeof key !== "number") ||
41
+ seen.has(key)) {
42
+ ok = false;
43
+ break;
44
+ }
45
+ seen.add(key);
46
+ keys.push(key);
47
+ }
48
+ if (ok)
49
+ return { field, keys };
50
+ }
51
+ return null;
52
+ }
53
+ const bytes = (text) => Buffer.byteLength(text, "utf8");
54
+ /**
55
+ * `value` as `JSON.stringify` writes it: an object's `toJSON` result
56
+ * (once), `undefined` for what it leaves out (a function, a symbol).
57
+ */
58
+ function jsonOf(value) {
59
+ if (value !== null &&
60
+ typeof value === "object" &&
61
+ !("__vestraImage" in value) &&
62
+ !("__vestraMarkdown" in value) &&
63
+ typeof value.toJSON === "function")
64
+ return value.toJSON();
65
+ return typeof value === "function" || typeof value === "symbol"
66
+ ? undefined
67
+ : value;
68
+ }
69
+ /**
70
+ * What `row` counts toward the value limits as a row of an array (depth
71
+ * 1), like `valueLimitProblem` on the row as sent: every value one, an
72
+ * image one, a document one and its text, strings and field names their
73
+ * UTF-8 bytes. Any other object with `toJSON` counts as what it returns.
74
+ */
75
+ export function countRow(row) {
76
+ const count = { values: 0, text: 0, over: false };
77
+ const L = UI_VALUE_LIMITS;
78
+ const text = (s) => {
79
+ const n = bytes(s);
80
+ if (n > L.stringBytes)
81
+ count.over = true;
82
+ count.text += n;
83
+ };
84
+ const walk = (value, depth) => {
85
+ if (count.over)
86
+ return;
87
+ if (depth > L.depth) {
88
+ count.over = true;
89
+ return;
90
+ }
91
+ count.values += 1;
92
+ if (value !== null && typeof value === "object") {
93
+ if ("__vestraImage" in value)
94
+ return;
95
+ if ("__vestraMarkdown" in value) {
96
+ count.text += bytes(value.text);
97
+ return;
98
+ }
99
+ }
100
+ if (typeof value === "string")
101
+ return text(value);
102
+ if (Array.isArray(value)) {
103
+ if (value.length > L.arrayRows)
104
+ count.over = true;
105
+ // Array.from: a hole is `null`, as `JSON.stringify` writes it.
106
+ for (const item of Array.from(value))
107
+ walk(jsonOf(item) ?? null, depth + 1);
108
+ return;
109
+ }
110
+ if (value !== null && typeof value === "object")
111
+ for (const [name, raw] of Object.entries(value)) {
112
+ const item = jsonOf(raw);
113
+ if (item === undefined)
114
+ continue;
115
+ text(name);
116
+ walk(item, depth + 1);
117
+ }
118
+ };
119
+ walk(jsonOf(row) ?? null, 1);
120
+ return count;
121
+ }
122
+ /** The new array is within the value limits (counted per row). */
123
+ export function withinLimits(counts) {
124
+ const L = UI_VALUE_LIMITS;
125
+ if (counts.length > L.arrayRows)
126
+ return false;
127
+ let values = 1;
128
+ let text = 0;
129
+ for (const count of counts) {
130
+ if (count.over)
131
+ return false;
132
+ values += count.values;
133
+ text += count.text;
134
+ }
135
+ return values <= L.values && text <= L.textBytes;
136
+ }
137
+ /** The longest increasing subsequence of `values`, as a set of indices. */
138
+ function increasing(values) {
139
+ const tails = [];
140
+ const before = new Int32Array(values.length).fill(-1);
141
+ for (let i = 0; i < values.length; i++) {
142
+ let lo = 0;
143
+ let hi = tails.length;
144
+ while (lo < hi) {
145
+ const mid = (lo + hi) >> 1;
146
+ if (values[tails[mid]] < values[i])
147
+ lo = mid + 1;
148
+ else
149
+ hi = mid;
150
+ }
151
+ if (lo > 0)
152
+ before[i] = tails[lo - 1];
153
+ tails[lo] = i;
154
+ }
155
+ const out = new Set();
156
+ for (let i = tails.at(-1) ?? -1; i >= 0; i = before[i])
157
+ out.add(i);
158
+ return out;
159
+ }
160
+ /**
161
+ * The operations that turn the old rows into the new ones (`null`: more
162
+ * than {@link PATCH_OPS} would be needed), and for each new row the old row
163
+ * whose JSON it keeps (`-1`: a new or changed row). Keyed when both arrays
164
+ * have keys from the same field; otherwise by index.
165
+ */
166
+ export function diff(old, next) {
167
+ const from = new Int32Array(next.enc.length).fill(-1);
168
+ const ops = old.keys && next.keys
169
+ ? keyed(old.enc, old.keys, next.enc, next.keys, from)
170
+ : byIndex(old.enc, next.enc, from);
171
+ return ops && ops.length <= PATCH_OPS ? { ops, from } : null;
172
+ }
173
+ function byIndex(old, next, from) {
174
+ const min = Math.min(old.length, next.length);
175
+ let p = 0;
176
+ while (p < min && old[p] === next[p])
177
+ from[p] = p++;
178
+ let s = 0;
179
+ while (s < min - p && old[old.length - 1 - s] === next[next.length - 1 - s]) {
180
+ from[next.length - 1 - s] = old.length - 1 - s;
181
+ s++;
182
+ }
183
+ const ops = [];
184
+ const middle = Math.min(old.length, next.length) - p - s;
185
+ for (let i = p; i < p + middle; i++)
186
+ if (old[i] === next[i])
187
+ from[i] = i;
188
+ else
189
+ ops.push({ op: "set", at: i, row: i });
190
+ const at = p + middle;
191
+ if (next.length > old.length)
192
+ ops.push({
193
+ op: "insert",
194
+ at,
195
+ rows: Array.from({ length: next.length - old.length }, (_, k) => at + k),
196
+ });
197
+ else if (old.length > next.length)
198
+ ops.push({ op: "remove", at, count: old.length - next.length });
199
+ return ops;
200
+ }
201
+ function keyed(oldEnc, oldKeys, newEnc, newKeys, from) {
202
+ const sets = [];
203
+ const keep = (j, i) => {
204
+ if (oldEnc[i] === newEnc[j])
205
+ from[j] = i;
206
+ else
207
+ sets.push({ op: "set", at: j, row: j });
208
+ };
209
+ const min = Math.min(oldKeys.length, newKeys.length);
210
+ let p = 0;
211
+ while (p < min && oldKeys[p] === newKeys[p])
212
+ keep(p, p++);
213
+ let s = 0;
214
+ while (s < min - p &&
215
+ oldKeys[oldKeys.length - 1 - s] === newKeys[newKeys.length - 1 - s]) {
216
+ keep(newKeys.length - 1 - s, oldKeys.length - 1 - s);
217
+ s++;
218
+ }
219
+ const oldEnd = oldKeys.length - s;
220
+ const newEnd = newKeys.length - s;
221
+ // The new middle's keys, by position (relative to p).
222
+ const target = new Map();
223
+ for (let j = p; j < newEnd; j++)
224
+ target.set(newKeys[j], j - p);
225
+ const ops = [];
226
+ // Removes, from the end so that indices stay valid; runs merged.
227
+ const kept = [];
228
+ const oldAt = new Map();
229
+ for (let i = oldEnd - 1; i >= p; i--) {
230
+ if (target.has(oldKeys[i]))
231
+ continue;
232
+ const last = ops.at(-1);
233
+ if (last?.op === "remove" && last.at === i + 1) {
234
+ last.at = i;
235
+ last.count++;
236
+ }
237
+ else
238
+ ops.push({ op: "remove", at: i, count: 1 });
239
+ if (ops.length > PATCH_OPS)
240
+ return null;
241
+ }
242
+ for (let i = p; i < oldEnd; i++)
243
+ if (target.has(oldKeys[i])) {
244
+ oldAt.set(oldKeys[i], i);
245
+ kept.push(oldKeys[i]);
246
+ }
247
+ // Moves: every kept row outside a longest increasing run of target
248
+ // positions goes right after the kept row before it in the new order.
249
+ const positions = kept.map((key) => target.get(key));
250
+ const stay = increasing(positions);
251
+ if (kept.length - stay.size + ops.length > PATCH_OPS)
252
+ return null;
253
+ const stays = new Set();
254
+ for (const index of stay)
255
+ stays.add(kept[index]);
256
+ const current = kept.slice();
257
+ let previous;
258
+ for (let j = p; j < newEnd; j++) {
259
+ const key = newKeys[j];
260
+ if (!oldAt.has(key))
261
+ continue;
262
+ if (!stays.has(key)) {
263
+ const at = current.indexOf(key);
264
+ current.splice(at, 1);
265
+ const to = previous === undefined ? 0 : current.indexOf(previous) + 1;
266
+ current.splice(to, 0, key);
267
+ if (at !== to)
268
+ ops.push({ op: "move", from: p + at, count: 1, to: p + to });
269
+ }
270
+ previous = key;
271
+ }
272
+ // Inserts, from the top; runs merged.
273
+ for (let j = p; j < newEnd; j++) {
274
+ const key = newKeys[j];
275
+ if (oldAt.has(key)) {
276
+ keep(j, oldAt.get(key));
277
+ continue;
278
+ }
279
+ const last = ops.at(-1);
280
+ if (last?.op === "insert" && last.at + last.rows.length === j)
281
+ last.rows.push(j);
282
+ else
283
+ ops.push({ op: "insert", at: j, rows: [j] });
284
+ if (ops.length > PATCH_OPS)
285
+ return null;
286
+ }
287
+ sets.sort((a, b) => a.at - b.at);
288
+ for (const set of sets) {
289
+ ops.push(set);
290
+ if (ops.length > PATCH_OPS)
291
+ return null;
292
+ }
293
+ return ops;
294
+ }
295
+ /**
296
+ * Apply planned operations to a copy of `old` (the rows' JSON), taking new
297
+ * rows from `next`: the check that a diff produces exactly the new rows.
298
+ */
299
+ export function replay(old, ops, next) {
300
+ const rows = old.slice();
301
+ for (const op of ops)
302
+ switch (op.op) {
303
+ case "set":
304
+ rows[op.at] = next[op.row];
305
+ break;
306
+ case "insert":
307
+ rows.splice(op.at, 0, ...op.rows.map((j) => next[j]));
308
+ break;
309
+ case "remove":
310
+ rows.splice(op.at, op.count);
311
+ break;
312
+ case "move":
313
+ rows.splice(op.to, 0, ...rows.splice(op.from, op.count));
314
+ break;
315
+ }
316
+ return rows;
317
+ }
318
+ /** Apply wire operations to an array (the TestHost's copy of a value). */
319
+ export function applyOps(rows, ops) {
320
+ const out = rows.slice();
321
+ for (const op of ops)
322
+ switch (op.op) {
323
+ case "set":
324
+ out[op.at] = op.row;
325
+ break;
326
+ case "insert":
327
+ out.splice(op.at, 0, ...op.rows);
328
+ break;
329
+ case "remove":
330
+ out.splice(op.at, op.count);
331
+ break;
332
+ case "move":
333
+ out.splice(op.to, 0, ...out.splice(op.from, op.count));
334
+ break;
335
+ }
336
+ return out;
337
+ }
@@ -54,6 +54,12 @@ export declare class Runtime {
54
54
  readonly app: App;
55
55
  /** `plugin.i18n.locale`, from the start-up handshake; English when unknown. */
56
56
  readonly locale: Locale;
57
+ /**
58
+ * Protocol additions the player advertises at start-up (`features` in the
59
+ * handshake's app data, player 0.6.0): `ui.patch` (row patches of
60
+ * property writes). Not part of the plugin API; empty for older players.
61
+ */
62
+ readonly features: ReadonlySet<string>;
57
63
  constructor(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string, app?: unknown);
58
64
  can(capability: string): boolean;
59
65
  /** Lazily created singletons shared by every scope of this plugin. */
@@ -132,6 +132,12 @@ export class Runtime {
132
132
  app;
133
133
  /** `plugin.i18n.locale`, from the start-up handshake; English when unknown. */
134
134
  locale;
135
+ /**
136
+ * Protocol additions the player advertises at start-up (`features` in the
137
+ * handshake's app data, player 0.6.0): `ui.patch` (row patches of
138
+ * property writes). Not part of the plugin API; empty for older players.
139
+ */
140
+ features;
135
141
  constructor(transport, manifest, directory, dataDirectory, app) {
136
142
  this.#transport = transport;
137
143
  this.dataDirectory = dataDirectory;
@@ -139,6 +145,11 @@ export class Runtime {
139
145
  this.host = createWire(transport);
140
146
  const host = this.host;
141
147
  this.app = appInfo(app, () => host.app.system());
148
+ const features = app
149
+ ?.features;
150
+ this.features = new Set(Array.isArray(features)
151
+ ? features.filter((name) => typeof name === "string")
152
+ : []);
142
153
  this.locale = LOCALES.includes(transport.locale ?? "")
143
154
  ? transport.locale
144
155
  : "en";
@@ -61,8 +61,8 @@ class Page extends ViewBase {
61
61
  live() {
62
62
  return this.#registered && !this.closed;
63
63
  }
64
- write(property, value) {
65
- return this.scope.runtime.host.settings.set(this.#id, property, value);
64
+ write(property, write) {
65
+ return this.scope.runtime.host.settings.set(this.#id, property, write);
66
66
  }
67
67
  async dispose() {
68
68
  if (this.closed)