@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 +10 -1
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/patch.d.ts +101 -0
- package/dist/plugin/patch.js +337 -0
- package/dist/plugin/runtime.d.ts +6 -0
- package/dist/plugin/runtime.js +11 -0
- package/dist/plugin/settings.js +2 -2
- package/dist/plugin/testing.js +132 -14
- package/dist/plugin/ui.d.ts +2 -2
- package/dist/plugin/ui.js +217 -17
- package/dist/wire.d.ts +42 -3
- package/dist/wire.js +3 -3
- package/llms-full.txt +102 -3
- package/llms.txt +20 -20
- package/package.json +1 -1
- package/slint/vestra/markdown.slint +17 -11
- package/slint/vestra/widgets.slint +770 -1
package/dist/plugin/testing.js
CHANGED
|
@@ -86,6 +86,7 @@ import { nodeProblem, VIDEO_PROCESSING_LIMITS } from "../processing.js";
|
|
|
86
86
|
import { shaderIdProblem, shaderListProblem } from "./shaders.js";
|
|
87
87
|
import { RENDER_DEFAULTS, renderProblem, VIEW_DEFAULTS, viewProblem, } from "./view.js";
|
|
88
88
|
import { createPlugin } from "./index.js";
|
|
89
|
+
import { applyOps, PATCH_FEATURE, PATCH_OPS } from "./patch.js";
|
|
89
90
|
import { evaluate } from "./when.js";
|
|
90
91
|
import { WAKE_EVENTS } from "./runtime.js";
|
|
91
92
|
import { THEMES } from "./appearance.js";
|
|
@@ -929,6 +930,54 @@ export class TestHost {
|
|
|
929
930
|
if (reason !== null && reason !== undefined)
|
|
930
931
|
this.#declared(reason, "reason");
|
|
931
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Row patches (player 0.6.0): the `seq` of each property's last write, by
|
|
935
|
+
* view and property (none: 0, the initial properties).
|
|
936
|
+
*/
|
|
937
|
+
#writes = new Map();
|
|
938
|
+
#resetWrites(view) {
|
|
939
|
+
for (const slot of [...this.#writes.keys()])
|
|
940
|
+
if (slot.startsWith(`${view}\u0000`))
|
|
941
|
+
this.#writes.delete(slot);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* A property write as the player takes it, returning the value the
|
|
945
|
+
* property shows after it: a whole `value`, or a `patch` of the array
|
|
946
|
+
* `current` applying on the write numbered `base` (code `stale`
|
|
947
|
+
* otherwise). The result is checked against the value limits whole: the
|
|
948
|
+
* player counts the rows a patch keeps too.
|
|
949
|
+
*/
|
|
950
|
+
#write(view, property, p, current) {
|
|
951
|
+
const slot = `${view}\u0000${property}`;
|
|
952
|
+
const patch = p.patch;
|
|
953
|
+
if (patch === undefined) {
|
|
954
|
+
const value = this.#shownValues(p.value);
|
|
955
|
+
this.#checkValues({ [property]: value });
|
|
956
|
+
this.#seeImages([value]);
|
|
957
|
+
this.#writes.set(slot, typeof p.seq === "number" ? p.seq : 0);
|
|
958
|
+
return value;
|
|
959
|
+
}
|
|
960
|
+
const { base, seq } = patch;
|
|
961
|
+
if (!Number.isInteger(base) ||
|
|
962
|
+
!Number.isInteger(seq) ||
|
|
963
|
+
base < 0 ||
|
|
964
|
+
seq < 1)
|
|
965
|
+
throw new HostError("invalid", "patch.base and patch.seq require non-negative integers (seq ≥ 1)");
|
|
966
|
+
const stale = () => new HostError("stale", "the property changed since the write this patch applies on; send the whole value");
|
|
967
|
+
if ((this.#writes.get(slot) ?? 0) !== base || !Array.isArray(current))
|
|
968
|
+
throw stale();
|
|
969
|
+
const ops = patchOps(patch.ops, current.length);
|
|
970
|
+
const resolved = ops.map((op) => op.op === "set"
|
|
971
|
+
? { ...op, row: this.#shownValues(op.row) }
|
|
972
|
+
: op.op === "insert"
|
|
973
|
+
? { ...op, rows: op.rows.map((row) => this.#shownValues(row)) }
|
|
974
|
+
: op);
|
|
975
|
+
const value = applyOps(current, resolved);
|
|
976
|
+
this.#checkValues({ [property]: value });
|
|
977
|
+
this.#seeImages(resolved.flatMap((op) => op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : []));
|
|
978
|
+
this.#writes.set(slot, seq);
|
|
979
|
+
return value;
|
|
980
|
+
}
|
|
932
981
|
/**
|
|
933
982
|
* Refuse property values over the player's limits ({@link UI_VALUE_LIMITS})
|
|
934
983
|
* with `limit`, naming the property, as the player does.
|
|
@@ -3816,6 +3865,7 @@ export class TestHost {
|
|
|
3816
3865
|
const id = String(p.id);
|
|
3817
3866
|
if (this.windows.has(id))
|
|
3818
3867
|
throw new HostError("conflict", "view already exists");
|
|
3868
|
+
this.#resetWrites(`w:${id}`);
|
|
3819
3869
|
const properties = this.#shownValues(p.properties);
|
|
3820
3870
|
this.#checkValues(properties);
|
|
3821
3871
|
this.#seeImages(properties);
|
|
@@ -3841,10 +3891,8 @@ export class TestHost {
|
|
|
3841
3891
|
const window = this.windows.get(String(p.id));
|
|
3842
3892
|
if (!window)
|
|
3843
3893
|
throw new HostError("missing", "view not found");
|
|
3844
|
-
const
|
|
3845
|
-
this.#
|
|
3846
|
-
this.#seeImages([value]);
|
|
3847
|
-
window.props[String(p.property)] = value;
|
|
3894
|
+
const property = String(p.property);
|
|
3895
|
+
window.props[property] = this.#write(`w:${String(p.id)}`, property, p, window.props[property]);
|
|
3848
3896
|
},
|
|
3849
3897
|
"ui.show": (p) => {
|
|
3850
3898
|
const window = this.windows.get(String(p.id));
|
|
@@ -3878,6 +3926,7 @@ export class TestHost {
|
|
|
3878
3926
|
]));
|
|
3879
3927
|
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
3880
3928
|
this.#seeImages(properties);
|
|
3929
|
+
this.#resetWrites("$mount");
|
|
3881
3930
|
const host = this;
|
|
3882
3931
|
this.panel = {
|
|
3883
3932
|
slot: String(p.slot),
|
|
@@ -4202,11 +4251,9 @@ export class TestHost {
|
|
|
4202
4251
|
this.emit("sequence.step", { direction });
|
|
4203
4252
|
},
|
|
4204
4253
|
"ui.setMount": (p) => {
|
|
4205
|
-
this.mounted();
|
|
4206
|
-
const
|
|
4207
|
-
this.#
|
|
4208
|
-
this.#seeImages([value]);
|
|
4209
|
-
this.mounted().props[String(p.property)] = value;
|
|
4254
|
+
const panel = this.mounted();
|
|
4255
|
+
const property = String(p.property);
|
|
4256
|
+
panel.props[property] = this.#write("$mount", property, p, panel.props[property]);
|
|
4210
4257
|
},
|
|
4211
4258
|
"timeline.marks": () => ({ marks: this.timeline.marks }),
|
|
4212
4259
|
"drops.provide": (p) => {
|
|
@@ -4245,6 +4292,7 @@ export class TestHost {
|
|
|
4245
4292
|
throw new HostError("invalid", "a plugin has at most 8 settings pages");
|
|
4246
4293
|
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
4247
4294
|
this.#seeImages(properties);
|
|
4295
|
+
this.#resetWrites(`$settings:${id}`);
|
|
4248
4296
|
const host = this;
|
|
4249
4297
|
this.settings.pages.set(id, {
|
|
4250
4298
|
category: String(p.category),
|
|
@@ -4267,10 +4315,8 @@ export class TestHost {
|
|
|
4267
4315
|
const page = this.settings.pages.get(String(p.id));
|
|
4268
4316
|
if (!page)
|
|
4269
4317
|
throw new HostError("missing", "page not registered");
|
|
4270
|
-
const
|
|
4271
|
-
this.#
|
|
4272
|
-
this.#seeImages([value]);
|
|
4273
|
-
page.props[String(p.property)] = value;
|
|
4318
|
+
const property = String(p.property);
|
|
4319
|
+
page.props[property] = this.#write(`$settings:${String(p.id)}`, property, p, page.props[property]);
|
|
4274
4320
|
},
|
|
4275
4321
|
"settings.unregister": (p) => {
|
|
4276
4322
|
this.settings.pages.delete(String(p.id));
|
|
@@ -5261,7 +5307,10 @@ export async function startPlugin(activate, options = {}) {
|
|
|
5261
5307
|
mkdirSync(data, { recursive: true });
|
|
5262
5308
|
if (host.fileRoots.length === 0)
|
|
5263
5309
|
host.fileRoots = [directory, data];
|
|
5264
|
-
const instance = await createPlugin(host, manifest, directory, data,
|
|
5310
|
+
const instance = await createPlugin(host, manifest, directory, data,
|
|
5311
|
+
// Like the player: protocol additions it takes (`app.features` set on
|
|
5312
|
+
// the host replaces them, e.g. `[]` for an older player's behaviour).
|
|
5313
|
+
{ features: [PATCH_FEATURE], ...host.app });
|
|
5265
5314
|
await activate(instance.plugin);
|
|
5266
5315
|
// Like the player: a position or resolver another plugin holds is not an
|
|
5267
5316
|
// activation failure.
|
|
@@ -5290,3 +5339,72 @@ export async function startPlugin(activate, options = {}) {
|
|
|
5290
5339
|
},
|
|
5291
5340
|
};
|
|
5292
5341
|
}
|
|
5342
|
+
/**
|
|
5343
|
+
* A patch's ops checked against an array of `length` rows, as the player
|
|
5344
|
+
* checks them (`ui_patch.rs`): in order, each on the result of the ones
|
|
5345
|
+
* before; at most {@link PATCH_OPS}; the result at most
|
|
5346
|
+
* `UI_VALUE_LIMITS.arrayRows` rows.
|
|
5347
|
+
*/
|
|
5348
|
+
function patchOps(ops, length) {
|
|
5349
|
+
if (!Array.isArray(ops))
|
|
5350
|
+
throw new HostError("invalid", "patch.ops requires an array");
|
|
5351
|
+
if (ops.length > PATCH_OPS)
|
|
5352
|
+
throw new HostError("limit", `a patch carries at most ${PATCH_OPS} operations`);
|
|
5353
|
+
const index = (op, key) => {
|
|
5354
|
+
const value = op[key];
|
|
5355
|
+
if (!Number.isInteger(value) || value < 0)
|
|
5356
|
+
throw new HostError("invalid", `patch op ${key} requires a non-negative integer`);
|
|
5357
|
+
return value;
|
|
5358
|
+
};
|
|
5359
|
+
const count = (op) => {
|
|
5360
|
+
const value = index(op, "count");
|
|
5361
|
+
if (value === 0)
|
|
5362
|
+
throw new HostError("invalid", "patch op count must be at least 1");
|
|
5363
|
+
return value;
|
|
5364
|
+
};
|
|
5365
|
+
const past = (what) => new HostError("invalid", `${what} is past the end of the list`);
|
|
5366
|
+
let len = length;
|
|
5367
|
+
return ops.map((raw) => {
|
|
5368
|
+
const op = (raw ?? {});
|
|
5369
|
+
switch (op.op) {
|
|
5370
|
+
case "set": {
|
|
5371
|
+
const at = index(op, "at");
|
|
5372
|
+
if (at >= len)
|
|
5373
|
+
throw past(`set at ${at}`);
|
|
5374
|
+
if (!("row" in op))
|
|
5375
|
+
throw new HostError("invalid", "patch op set requires row");
|
|
5376
|
+
return { op: "set", at, row: op.row };
|
|
5377
|
+
}
|
|
5378
|
+
case "insert": {
|
|
5379
|
+
const at = index(op, "at");
|
|
5380
|
+
const rows = op.rows;
|
|
5381
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
5382
|
+
throw new HostError("invalid", "patch op insert requires rows");
|
|
5383
|
+
if (at > len)
|
|
5384
|
+
throw past(`insert at ${at}`);
|
|
5385
|
+
len += rows.length;
|
|
5386
|
+
if (len > UI_VALUE_LIMITS.arrayRows)
|
|
5387
|
+
throw new HostError("limit", `UI array exceeds ${UI_VALUE_LIMITS.arrayRows} rows`);
|
|
5388
|
+
return { op: "insert", at, rows: rows };
|
|
5389
|
+
}
|
|
5390
|
+
case "remove": {
|
|
5391
|
+
const at = index(op, "at");
|
|
5392
|
+
const n = count(op);
|
|
5393
|
+
if (at + n > len)
|
|
5394
|
+
throw past(`remove ${n} at ${at}`);
|
|
5395
|
+
len -= n;
|
|
5396
|
+
return { op: "remove", at, count: n };
|
|
5397
|
+
}
|
|
5398
|
+
case "move": {
|
|
5399
|
+
const from = index(op, "from");
|
|
5400
|
+
const n = count(op);
|
|
5401
|
+
const to = index(op, "to");
|
|
5402
|
+
if (from + n > len || to + n > len)
|
|
5403
|
+
throw past(`move ${n} from ${from} to ${to}`);
|
|
5404
|
+
return { op: "move", from, count: n, to };
|
|
5405
|
+
}
|
|
5406
|
+
default:
|
|
5407
|
+
throw new HostError("invalid", "patch op must be set, insert, remove or move");
|
|
5408
|
+
}
|
|
5409
|
+
});
|
|
5410
|
+
}
|
package/dist/plugin/ui.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Json } from "../wire.ts";
|
|
1
|
+
import type { Json, PropertyWrite } from "../wire.ts";
|
|
2
2
|
import type { ScopeImpl } from "./scope.ts";
|
|
3
3
|
import type { Dialogs, Disposable, ImageOptions, ImageRef, Panels, PropValue, Props, Ui, View, Windows } from "./types.ts";
|
|
4
4
|
/** Slint names are kebab-case; plugin code may use camelCase. */
|
|
@@ -29,7 +29,7 @@ export declare abstract class ViewBase implements View {
|
|
|
29
29
|
protected closed: boolean;
|
|
30
30
|
constructor(scope: ScopeImpl);
|
|
31
31
|
protected abstract live(): boolean;
|
|
32
|
-
protected abstract write(property: string,
|
|
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
35
|
* property keeps its default) and reported, like a failed write.
|
package/dist/plugin/ui.js
CHANGED
|
@@ -6,6 +6,8 @@ import { basename, extname, resolve } from "node:path";
|
|
|
6
6
|
import { failure, Latest, quiet } from "./runtime.js";
|
|
7
7
|
import { dropsOf } from "./drops.js";
|
|
8
8
|
import { MarkdownHandle, markdown, remoteImageFile } from "./markdown.js";
|
|
9
|
+
import { HostError } from "../wire.js";
|
|
10
|
+
import { countRow, diff, keyOf, PATCH_FEATURE, replay, withinLimits, } from "./patch.js";
|
|
9
11
|
/** Slint names are kebab-case; plugin code may use camelCase. */
|
|
10
12
|
export const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
11
13
|
const encode = (props) => Object.fromEntries(Object.entries(props).map(([k, v]) => [kebab(k), v]));
|
|
@@ -62,8 +64,9 @@ function wireValue(value) {
|
|
|
62
64
|
throw failure("invalid", `image ${value.path} was released with the scope that created it`);
|
|
63
65
|
return value.toJSON();
|
|
64
66
|
}
|
|
67
|
+
// Array.from: a hole is `null`, as `JSON.stringify` writes it.
|
|
65
68
|
if (Array.isArray(value))
|
|
66
|
-
return
|
|
69
|
+
return Array.from(value, (item) => wireValue(item ?? null));
|
|
67
70
|
if (value !== null && typeof value === "object") {
|
|
68
71
|
const out = {};
|
|
69
72
|
for (const [key, item] of Object.entries(value))
|
|
@@ -72,6 +75,89 @@ function wireValue(value) {
|
|
|
72
75
|
}
|
|
73
76
|
return value;
|
|
74
77
|
}
|
|
78
|
+
/** Scalars compared by value. Non-finite numbers are JSON `null`: compared as JSON. */
|
|
79
|
+
const isPrimitive = (value) => value === null ||
|
|
80
|
+
typeof value === "string" ||
|
|
81
|
+
typeof value === "boolean" ||
|
|
82
|
+
(typeof value === "number" && Number.isFinite(value));
|
|
83
|
+
/** A row as `JSON.stringify` writes it inside an array. */
|
|
84
|
+
const encodeRow = (row) => JSON.stringify(row) ?? "null";
|
|
85
|
+
/** Each row of an array as `JSON.stringify` writes it; a hole is `null`. */
|
|
86
|
+
const encodeRows = (rows) => Array.from(rows, encodeRow);
|
|
87
|
+
function sameRows(a, b) {
|
|
88
|
+
if (a.length !== b.length)
|
|
89
|
+
return false;
|
|
90
|
+
for (let i = 0; i < a.length; i++)
|
|
91
|
+
if (a[i] !== b[i])
|
|
92
|
+
return false;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/** An initial property (wire form) as sent. */
|
|
96
|
+
function sentOf(value, patches) {
|
|
97
|
+
if (isPrimitive(value))
|
|
98
|
+
return { kind: "value", value };
|
|
99
|
+
if (Array.isArray(value) && patches) {
|
|
100
|
+
const keyed = keyOf(value);
|
|
101
|
+
return {
|
|
102
|
+
kind: "rows",
|
|
103
|
+
rows: {
|
|
104
|
+
enc: encodeRows(value),
|
|
105
|
+
field: keyed?.field ?? null,
|
|
106
|
+
keys: keyed?.keys ?? null,
|
|
107
|
+
counts: [],
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { kind: "json", enc: JSON.stringify(value) };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The row patch from `sent` to `next` (the bound `rows`), in wire form, and
|
|
115
|
+
* the new rows' limit counts; `null` when the whole value is to be sent:
|
|
116
|
+
* nothing to type the rows by (no rows sent), a Markdown document in a row
|
|
117
|
+
* (the player merges documents in place), too many operations, a value over
|
|
118
|
+
* the limits (the player refuses it whole, as before), or no change (an
|
|
119
|
+
* explicit `set()` of the same value).
|
|
120
|
+
*/
|
|
121
|
+
function rowPatch(sent, next, rows) {
|
|
122
|
+
if (sent.enc.length === 0)
|
|
123
|
+
return null;
|
|
124
|
+
if (next.enc.some((row) => row.includes('"$vestraMarkdown"')))
|
|
125
|
+
return null;
|
|
126
|
+
const nextKeyed = sent.field === null ? null : next.keyed();
|
|
127
|
+
const keyed = nextKeyed !== null && nextKeyed.field === sent.field;
|
|
128
|
+
const plan = diff({ enc: sent.enc, keys: keyed ? sent.keys : null }, { enc: next.enc, keys: keyed ? nextKeyed.keys : null });
|
|
129
|
+
if (!plan || plan.ops.length === 0)
|
|
130
|
+
return null;
|
|
131
|
+
// A released image is refused wherever it is, as in a whole write: the
|
|
132
|
+
// rows the patch does not carry are checked too (those holding images).
|
|
133
|
+
for (let j = 0; j < rows.length; j++)
|
|
134
|
+
if (plan.from[j] >= 0 && next.enc[j].includes('"$vestraImage"'))
|
|
135
|
+
wireValue(rows[j] ?? null);
|
|
136
|
+
const counts = next.enc.map((_, j) => {
|
|
137
|
+
const from = plan.from[j];
|
|
138
|
+
return (from >= 0 ? sent.counts[from] : undefined) ?? countRow(rows[j]);
|
|
139
|
+
});
|
|
140
|
+
if (!withinLimits(counts))
|
|
141
|
+
return null;
|
|
142
|
+
// The ops must give exactly the new rows (a guard against this diff's bugs).
|
|
143
|
+
if (!sameRows(replay(sent.enc, plan.ops, next.enc), next.enc))
|
|
144
|
+
return null;
|
|
145
|
+
const ops = plan.ops.map((op) => {
|
|
146
|
+
switch (op.op) {
|
|
147
|
+
case "set":
|
|
148
|
+
return { op: "set", at: op.at, row: wireValue(rows[op.row] ?? null) };
|
|
149
|
+
case "insert":
|
|
150
|
+
return {
|
|
151
|
+
op: "insert",
|
|
152
|
+
at: op.at,
|
|
153
|
+
rows: op.rows.map((j) => wireValue(rows[j] ?? null)),
|
|
154
|
+
};
|
|
155
|
+
default:
|
|
156
|
+
return op;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return { ops, counts };
|
|
160
|
+
}
|
|
75
161
|
export const ui = (scope) => ({
|
|
76
162
|
image(path, options) {
|
|
77
163
|
if (typeof path !== "string" || !path)
|
|
@@ -96,6 +182,18 @@ export class ViewBase {
|
|
|
96
182
|
#binders = new Map();
|
|
97
183
|
/** What the host currently shows, as far as this side knows. */
|
|
98
184
|
#sent = new Map();
|
|
185
|
+
/**
|
|
186
|
+
* Row patches (player feature `ui.patch`): each property's last write the
|
|
187
|
+
* player confirmed, by `seq` (0: the initial properties). A patch applies
|
|
188
|
+
* on it; a property without one is sent whole.
|
|
189
|
+
*/
|
|
190
|
+
#bases = new Map();
|
|
191
|
+
/**
|
|
192
|
+
* Patches of a property answered `stale` in a row: after two, the
|
|
193
|
+
* property is sent whole until the view shows its initial properties again.
|
|
194
|
+
*/
|
|
195
|
+
#stales = new Map();
|
|
196
|
+
#seq = 0;
|
|
99
197
|
/** Written by `set()`: sent even if equal to what was sent before. */
|
|
100
198
|
#forced = new Set();
|
|
101
199
|
/** The last value of each property whose write failed; not retried until it changes. */
|
|
@@ -111,6 +209,10 @@ export class ViewBase {
|
|
|
111
209
|
this.scope = scope;
|
|
112
210
|
this.#binding = scope.runtime.bind(() => this.evaluate());
|
|
113
211
|
}
|
|
212
|
+
/** The player takes row patches (and `seq` on whole writes). */
|
|
213
|
+
get #patches() {
|
|
214
|
+
return this.scope.runtime.features.has(PATCH_FEATURE);
|
|
215
|
+
}
|
|
114
216
|
/**
|
|
115
217
|
* Initial properties in wire form. A released image is left out (the
|
|
116
218
|
* property keeps its default) and reported, like a failed write.
|
|
@@ -173,11 +275,107 @@ export class ViewBase {
|
|
|
173
275
|
}
|
|
174
276
|
/** The host already shows these values (e.g. sent as initial properties). */
|
|
175
277
|
shown(values) {
|
|
176
|
-
|
|
278
|
+
const patches = this.#patches;
|
|
279
|
+
this.#sent = new Map(Object.entries(values).map(([k, v]) => [k, sentOf(v, patches)]));
|
|
280
|
+
this.#bases = new Map(patches ? Object.keys(values).map((key) => [key, 0]) : []);
|
|
281
|
+
this.#stales.clear();
|
|
177
282
|
this.#forced.clear();
|
|
178
283
|
for (const key of Object.keys(values))
|
|
179
284
|
this.#failed.delete(key);
|
|
180
285
|
}
|
|
286
|
+
/** `value` encoded, and whether it differs from what was sent. */
|
|
287
|
+
#encode(key, value) {
|
|
288
|
+
const sent = this.#sent.get(key);
|
|
289
|
+
if (isPrimitive(value)) {
|
|
290
|
+
// Compared as it is: no `JSON.stringify` for the scalars of a pass.
|
|
291
|
+
const changed = sent?.kind === "value"
|
|
292
|
+
? sent.value !== value
|
|
293
|
+
: sent?.kind !== "json" || sent.enc !== JSON.stringify(value);
|
|
294
|
+
return {
|
|
295
|
+
key,
|
|
296
|
+
value,
|
|
297
|
+
changed,
|
|
298
|
+
signature: () => JSON.stringify(value),
|
|
299
|
+
sent: () => ({ kind: "value", value }),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (Array.isArray(value) && this.#patches) {
|
|
303
|
+
// Row by row: the same work as one `JSON.stringify`, and the rows to
|
|
304
|
+
// diff against those last sent.
|
|
305
|
+
const enc = encodeRows(value);
|
|
306
|
+
const signature = () => `[${enc.join(",")}]`;
|
|
307
|
+
const changed = sent?.kind === "rows"
|
|
308
|
+
? !sameRows(sent.rows.enc, enc)
|
|
309
|
+
: sent?.kind !== "json" || sent.enc !== signature();
|
|
310
|
+
// Keys only for a write: an unchanged array costs its encoding alone.
|
|
311
|
+
let keys;
|
|
312
|
+
const keyed = () => (keys === undefined ? (keys = keyOf(value)) : keys);
|
|
313
|
+
return {
|
|
314
|
+
key,
|
|
315
|
+
value,
|
|
316
|
+
changed,
|
|
317
|
+
signature,
|
|
318
|
+
rows: { enc, keyed },
|
|
319
|
+
sent: (counts = []) => ({
|
|
320
|
+
kind: "rows",
|
|
321
|
+
rows: {
|
|
322
|
+
enc,
|
|
323
|
+
field: keyed()?.field ?? null,
|
|
324
|
+
keys: keyed()?.keys ?? null,
|
|
325
|
+
counts,
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const enc = JSON.stringify(value);
|
|
331
|
+
return {
|
|
332
|
+
key,
|
|
333
|
+
value,
|
|
334
|
+
changed: sent?.kind !== "json" || sent.enc !== enc,
|
|
335
|
+
signature: () => enc,
|
|
336
|
+
sent: () => ({ kind: "json", enc }),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Write one property: a row patch when the player takes them and one
|
|
341
|
+
* applies (else the whole value); a `stale` patch is followed by the whole
|
|
342
|
+
* value at once. The base is forgotten on any failure, so the next write
|
|
343
|
+
* of the property is whole.
|
|
344
|
+
*/
|
|
345
|
+
async #send(pending) {
|
|
346
|
+
const { key, value } = pending;
|
|
347
|
+
if (!this.#patches) {
|
|
348
|
+
await this.write(key, { value: wireValue(value) });
|
|
349
|
+
return pending.sent();
|
|
350
|
+
}
|
|
351
|
+
const base = this.#bases.get(key);
|
|
352
|
+
this.#bases.delete(key);
|
|
353
|
+
const sent = this.#sent.get(key);
|
|
354
|
+
const plan = base !== undefined &&
|
|
355
|
+
sent?.kind === "rows" &&
|
|
356
|
+
pending.rows &&
|
|
357
|
+
(this.#stales.get(key) ?? 0) < 2
|
|
358
|
+
? rowPatch(sent.rows, pending.rows, value)
|
|
359
|
+
: null;
|
|
360
|
+
if (plan) {
|
|
361
|
+
const seq = ++this.#seq;
|
|
362
|
+
try {
|
|
363
|
+
await this.write(key, { patch: { base: base, seq, ops: plan.ops } });
|
|
364
|
+
this.#bases.set(key, seq);
|
|
365
|
+
this.#stales.delete(key);
|
|
366
|
+
return pending.sent(plan.counts);
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
if (!(error instanceof HostError && error.code === "stale"))
|
|
370
|
+
throw error;
|
|
371
|
+
this.#stales.set(key, (this.#stales.get(key) ?? 0) + 1);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const seq = ++this.#seq;
|
|
375
|
+
await this.write(key, { value: wireValue(value), seq });
|
|
376
|
+
this.#bases.set(key, seq);
|
|
377
|
+
return pending.sent();
|
|
378
|
+
}
|
|
181
379
|
/**
|
|
182
380
|
* Send what changed. A bound value is sent when it differs from the last one
|
|
183
381
|
* sent; a `set()` value is always sent once. Nothing is resent merely
|
|
@@ -198,20 +396,22 @@ export class ViewBase {
|
|
|
198
396
|
try {
|
|
199
397
|
let pending;
|
|
200
398
|
do {
|
|
201
|
-
pending = [
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
399
|
+
pending = [];
|
|
400
|
+
for (const [key, value] of this.#values) {
|
|
401
|
+
const encoded = this.#encode(key, value);
|
|
402
|
+
const failedAs = this.#failed.get(key);
|
|
403
|
+
if (failedAs !== undefined && failedAs === encoded.signature())
|
|
404
|
+
continue;
|
|
405
|
+
if (this.#forced.has(key) || encoded.changed)
|
|
406
|
+
pending.push(encoded);
|
|
407
|
+
}
|
|
408
|
+
for (const encoded of pending) {
|
|
208
409
|
if (!this.live())
|
|
209
410
|
return;
|
|
210
|
-
const
|
|
411
|
+
const key = encoded.key;
|
|
211
412
|
this.#forced.delete(key);
|
|
212
413
|
try {
|
|
213
|
-
|
|
214
|
-
this.#sent.set(key, encoded);
|
|
414
|
+
this.#sent.set(key, await this.#send(encoded));
|
|
215
415
|
this.#failed.delete(key);
|
|
216
416
|
this.#backoff = 0;
|
|
217
417
|
this.#broken = false;
|
|
@@ -222,7 +422,7 @@ export class ViewBase {
|
|
|
222
422
|
if (!this.#broken)
|
|
223
423
|
failure ??= error;
|
|
224
424
|
this.#broken = true;
|
|
225
|
-
this.#failed.set(key, encoded);
|
|
425
|
+
this.#failed.set(key, encoded.signature());
|
|
226
426
|
}
|
|
227
427
|
}
|
|
228
428
|
} while (pending.length && !failed && this.live());
|
|
@@ -294,8 +494,8 @@ class WindowHandle extends ViewBase {
|
|
|
294
494
|
live() {
|
|
295
495
|
return this.#opened && !this.closed;
|
|
296
496
|
}
|
|
297
|
-
write(property,
|
|
298
|
-
return this.scope.runtime.host.ui.set(this.#id, property,
|
|
497
|
+
write(property, write) {
|
|
498
|
+
return this.scope.runtime.host.ui.set(this.#id, property, write);
|
|
299
499
|
}
|
|
300
500
|
on(callback, handler) {
|
|
301
501
|
// The host forwards only callbacks named when the window opens.
|
|
@@ -400,8 +600,8 @@ class PanelHandle extends ViewBase {
|
|
|
400
600
|
live() {
|
|
401
601
|
return this.#mounted && !this.closed;
|
|
402
602
|
}
|
|
403
|
-
write(property,
|
|
404
|
-
return this.scope.runtime.host.ui.setMount(property,
|
|
603
|
+
write(property, write) {
|
|
604
|
+
return this.scope.runtime.host.ui.setMount(property, write);
|
|
405
605
|
}
|
|
406
606
|
get state() {
|
|
407
607
|
const service = layoutOf(this.scope.runtime);
|
package/dist/wire.d.ts
CHANGED
|
@@ -56,6 +56,45 @@ export interface UiCallback {
|
|
|
56
56
|
callback: string;
|
|
57
57
|
args: Json[];
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* One row operation of a patch, by index, applied to the result of the
|
|
61
|
+
* ones before (player 0.6.0, feature `ui.patch`). `move` takes `count` rows
|
|
62
|
+
* out at `from` and puts them back at `to`, an index of the list without
|
|
63
|
+
* them.
|
|
64
|
+
*/
|
|
65
|
+
export type RowOp = {
|
|
66
|
+
op: "set";
|
|
67
|
+
at: number;
|
|
68
|
+
row: Json;
|
|
69
|
+
} | {
|
|
70
|
+
op: "insert";
|
|
71
|
+
at: number;
|
|
72
|
+
rows: Json[];
|
|
73
|
+
} | {
|
|
74
|
+
op: "remove";
|
|
75
|
+
at: number;
|
|
76
|
+
count: number;
|
|
77
|
+
} | {
|
|
78
|
+
op: "move";
|
|
79
|
+
from: number;
|
|
80
|
+
count: number;
|
|
81
|
+
to: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* A property write: a whole value (with its `seq` to players that take
|
|
85
|
+
* patches), or a row patch of an array applying on the write numbered
|
|
86
|
+
* `base` (0: the initial properties).
|
|
87
|
+
*/
|
|
88
|
+
export type PropertyWrite = {
|
|
89
|
+
value: Json;
|
|
90
|
+
seq?: number;
|
|
91
|
+
} | {
|
|
92
|
+
patch: {
|
|
93
|
+
base: number;
|
|
94
|
+
seq: number;
|
|
95
|
+
ops: RowOp[];
|
|
96
|
+
};
|
|
97
|
+
};
|
|
59
98
|
/**
|
|
60
99
|
* A definite refusal from the player (or the plugin runner), with a stable
|
|
61
100
|
* `code`. Plugin code sees it as a `PluginError` (an `Error` with `code`);
|
|
@@ -481,7 +520,7 @@ export declare function createWire(transport: Transport): {
|
|
|
481
520
|
global: string;
|
|
482
521
|
properties: Record<string, Json>;
|
|
483
522
|
}) => Promise<void>;
|
|
484
|
-
set: (id: string, property: string,
|
|
523
|
+
set: (id: string, property: string, write: PropertyWrite) => Promise<void>;
|
|
485
524
|
unregister: (id: string) => Promise<void>;
|
|
486
525
|
show: (id?: string) => Promise<void>;
|
|
487
526
|
};
|
|
@@ -668,7 +707,7 @@ export declare function createWire(transport: Transport): {
|
|
|
668
707
|
prepareMs: number;
|
|
669
708
|
shellGeneration: number;
|
|
670
709
|
}>;
|
|
671
|
-
setMount: (property: string,
|
|
710
|
+
setMount: (property: string, write: PropertyWrite) => Promise<void>;
|
|
672
711
|
interaction: (locks: {
|
|
673
712
|
focused: boolean;
|
|
674
713
|
dragging: boolean;
|
|
@@ -677,7 +716,7 @@ export declare function createWire(transport: Transport): {
|
|
|
677
716
|
unmount: () => Promise<void>;
|
|
678
717
|
menu: (items: (string | null)[], x: number, y: number) => Promise<void>;
|
|
679
718
|
open: (id: string, file: string, properties?: Record<string, Json>, callbacks?: string[]) => Promise<void>;
|
|
680
|
-
set: (id: string, property: string,
|
|
719
|
+
set: (id: string, property: string, write: PropertyWrite) => Promise<void>;
|
|
681
720
|
show: (id: string, visible: boolean) => Promise<void>;
|
|
682
721
|
close: (id: string) => Promise<void>;
|
|
683
722
|
};
|
package/dist/wire.js
CHANGED
|
@@ -125,7 +125,7 @@ export function createWire(transport) {
|
|
|
125
125
|
/** Settings pages in the player's options window (`ui.settings`). */
|
|
126
126
|
settings: {
|
|
127
127
|
register: (options) => transport.request("settings.register", options),
|
|
128
|
-
set: (id, property,
|
|
128
|
+
set: (id, property, write) => transport.request("settings.set", { id, property, ...write }),
|
|
129
129
|
unregister: (id) => transport.request("settings.unregister", { id }),
|
|
130
130
|
show: (id) => transport.request("settings.show", id === undefined ? {} : { id }),
|
|
131
131
|
},
|
|
@@ -321,12 +321,12 @@ export function createWire(transport) {
|
|
|
321
321
|
base,
|
|
322
322
|
}),
|
|
323
323
|
mount: (options) => transport.request("ui.mount", options),
|
|
324
|
-
setMount: (property,
|
|
324
|
+
setMount: (property, write) => transport.request("ui.setMount", { property, ...write }),
|
|
325
325
|
interaction: (locks) => transport.request("ui.interaction", locks),
|
|
326
326
|
unmount: () => transport.request("ui.unmount"),
|
|
327
327
|
menu: (items, x, y) => transport.request("ui.menu", { items, x, y }),
|
|
328
328
|
open: (id, file, properties = {}, callbacks = []) => transport.request("ui.open", { id, file, properties, callbacks }),
|
|
329
|
-
set: (id, property,
|
|
329
|
+
set: (id, property, write) => transport.request("ui.set", { id, property, ...write }),
|
|
330
330
|
show: (id, visible) => transport.request("ui.show", { id, visible }),
|
|
331
331
|
close: (id) => transport.request("ui.close", { id }),
|
|
332
332
|
},
|