@jigs.design/runtime 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +29 -2
- package/dist/frame-policy.d.ts +11 -0
- package/dist/frame-policy.js +17 -0
- package/dist/frame.d.ts +42 -0
- package/dist/frame.js +258 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/parse.d.ts +9 -0
- package/dist/parse.js +150 -0
- package/dist/protocol.d.ts +132 -0
- package/dist/protocol.js +4 -0
- package/dist/runtime-core.d.ts +21 -0
- package/dist/runtime-core.js +735 -0
- package/dist/tree-store.d.ts +56 -0
- package/dist/tree-store.js +340 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.js +22 -0
- package/dist/urls.d.ts +14 -0
- package/dist/urls.js +59 -0
- package/dist/validate.js +33 -0
- package/package.json +28 -3
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
//#region src/runtime-core.ts
|
|
2
|
+
/**
|
|
3
|
+
* The runtime that executes compiled programs inside the sandbox worker.
|
|
4
|
+
*
|
|
5
|
+
* It is shipped to the worker with `createRuntime.toString()`, so the body
|
|
6
|
+
* must not reference anything declared outside it (type imports are erased
|
|
7
|
+
* and fine). `FunctionCtor` is passed in because the worker removes dynamic
|
|
8
|
+
* code evaluation from its globals before any program runs.
|
|
9
|
+
*
|
|
10
|
+
* A small React-style reconciler: programs build a tree with `__pickle.jsx`,
|
|
11
|
+
* the runtime diffs it against the previous render by position and type, and
|
|
12
|
+
* posts the differences as `Op`s. Each render also reports the queries it read
|
|
13
|
+
* and the mutations its handlers called; the page runs those through the
|
|
14
|
+
* client and sends the results back.
|
|
15
|
+
*/
|
|
16
|
+
function createRuntime(post, FunctionCtor) {
|
|
17
|
+
/** The items `each` made, so `expand` can tell them from the program's own objects. */
|
|
18
|
+
const keyedItems = /* @__PURE__ */ new WeakSet();
|
|
19
|
+
const keyedItem = (key, value) => {
|
|
20
|
+
const item = {
|
|
21
|
+
key,
|
|
22
|
+
value
|
|
23
|
+
};
|
|
24
|
+
keyedItems.add(item);
|
|
25
|
+
return item;
|
|
26
|
+
};
|
|
27
|
+
const isKeyedItem = (value) => typeof value === "object" && value !== null && keyedItems.has(value);
|
|
28
|
+
const FRAGMENT = "#fragment";
|
|
29
|
+
const TEXT = "#text";
|
|
30
|
+
const ROOT = 0;
|
|
31
|
+
const MAX_INPUT = 1e4;
|
|
32
|
+
/** Distinct `useMutation` declarations per render; list rows reuse theirs. */
|
|
33
|
+
const MAX_MUTATIONS = 50;
|
|
34
|
+
const MAX_MUTATIONS_PER_EVENT = 5;
|
|
35
|
+
const MAX_QUERIES = 20;
|
|
36
|
+
/** State and mutation slots one render may use, list rows included. */
|
|
37
|
+
const MAX_SLOTS = 2e3;
|
|
38
|
+
/** Results in a row whose arrival changed the input of the query that asked for them. */
|
|
39
|
+
const MAX_INPUT_CHURN = 3;
|
|
40
|
+
const MAX_COMPONENT_DEPTH = 16;
|
|
41
|
+
const MAX_EACH = 500;
|
|
42
|
+
const MAX_ROWS = 500;
|
|
43
|
+
const MAX_ROW_KEYS = 20;
|
|
44
|
+
const MAX_NODES = 1e4;
|
|
45
|
+
const MAX_OPS = 3 * MAX_NODES;
|
|
46
|
+
const MAX_TEXT = 1e4;
|
|
47
|
+
let program = null;
|
|
48
|
+
let programCode = null;
|
|
49
|
+
let prose = {};
|
|
50
|
+
let procedures = {};
|
|
51
|
+
let previous = [];
|
|
52
|
+
let rootElement;
|
|
53
|
+
let nextId = 1;
|
|
54
|
+
let autoKey = 0;
|
|
55
|
+
let mutationKey = 0;
|
|
56
|
+
let errors = [];
|
|
57
|
+
let handlers = /* @__PURE__ */ new Map();
|
|
58
|
+
let nextHandlers = /* @__PURE__ */ new Map();
|
|
59
|
+
const slots = /* @__PURE__ */ new Map();
|
|
60
|
+
const setters = /* @__PURE__ */ new Map();
|
|
61
|
+
const queryResults = /* @__PURE__ */ new Map();
|
|
62
|
+
let reads = /* @__PURE__ */ new Map();
|
|
63
|
+
const mutationSlots = /* @__PURE__ */ new Map();
|
|
64
|
+
const mutationById = /* @__PURE__ */ new Map();
|
|
65
|
+
const mutateFns = /* @__PURE__ */ new Map();
|
|
66
|
+
/** Slot keys the current render used, and its mutation declarations. */
|
|
67
|
+
let visited = /* @__PURE__ */ new Set();
|
|
68
|
+
let mutationNames = /* @__PURE__ */ new Set();
|
|
69
|
+
/** Each `useQuery` call site's last key, and how often a result for it was followed by another key. */
|
|
70
|
+
let querySites = /* @__PURE__ */ new Map();
|
|
71
|
+
let nextQuerySites = /* @__PURE__ */ new Map();
|
|
72
|
+
const unstableSites = /* @__PURE__ */ new Set();
|
|
73
|
+
/** The query whose result this render answers, if any. */
|
|
74
|
+
let settledKey = null;
|
|
75
|
+
let nextMutationId = 0;
|
|
76
|
+
let calls = [];
|
|
77
|
+
let inHandler = false;
|
|
78
|
+
let componentDepth = 0;
|
|
79
|
+
let componentWhere = null;
|
|
80
|
+
/** Component and `<each>` segments. Empty means page state. */
|
|
81
|
+
const pathStack = [];
|
|
82
|
+
/** JSON with sorted keys, so `{ a, b }` and `{ b, a }` share a request. */
|
|
83
|
+
const stableKey = (value) => JSON.stringify(value, (_, inner) => typeof inner === "object" && inner !== null && !Array.isArray(inner) ? Object.fromEntries(Object.entries(inner).toSorted(([a], [b]) => a < b ? -1 : 1)) : inner) ?? "null";
|
|
84
|
+
const pending = Object.freeze({
|
|
85
|
+
data: null,
|
|
86
|
+
error: null,
|
|
87
|
+
status: "pending"
|
|
88
|
+
});
|
|
89
|
+
/** Input as plain JSON data, or a thrown error the handler reports. */
|
|
90
|
+
const plainInput = (input, label) => {
|
|
91
|
+
if (input === void 0) return null;
|
|
92
|
+
let raw;
|
|
93
|
+
try {
|
|
94
|
+
raw = JSON.stringify(input);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error(`${label} input must be plain data`);
|
|
97
|
+
}
|
|
98
|
+
if (typeof raw !== "string") throw new Error(`${label} input must be plain data`);
|
|
99
|
+
if (raw.length >= MAX_INPUT) throw new Error(`${label} input must be under ${MAX_INPUT} characters`);
|
|
100
|
+
return JSON.parse(raw);
|
|
101
|
+
};
|
|
102
|
+
const unknownProcedure = (name, kind) => ({
|
|
103
|
+
code: "UNKNOWN_PROCEDURE",
|
|
104
|
+
message: `"${name}" is not a ${kind} on this host`
|
|
105
|
+
});
|
|
106
|
+
const isProcedure = (name, kind) => Object.hasOwn(procedures, name) && (procedures[name]?.kind === kind || kind === "mutation" && procedures[name]?.kind === "action");
|
|
107
|
+
const reportError = (line, error) => {
|
|
108
|
+
const text = String(error);
|
|
109
|
+
errors.push({
|
|
110
|
+
line,
|
|
111
|
+
message: componentWhere === null ? text : `in <${componentWhere}>: ${text}`
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
const pathKey = (base) => pathStack.length === 0 ? base : `${base}@${pathStack.join("/")}`;
|
|
115
|
+
/** Counts `key` against this render's slots; throws past the limit. */
|
|
116
|
+
const visit = (key, hook) => {
|
|
117
|
+
if (visited.has(key)) return;
|
|
118
|
+
if (visited.size >= MAX_SLOTS) throw new Error(`${hook} is limited to ${MAX_SLOTS} slots per render`);
|
|
119
|
+
visited.add(key);
|
|
120
|
+
};
|
|
121
|
+
const useState = (initial, options) => {
|
|
122
|
+
const key = pathKey(options?.key ?? `auto:${autoKey++}`);
|
|
123
|
+
visit(key, "useState");
|
|
124
|
+
if (!slots.has(key)) slots.set(key, initial);
|
|
125
|
+
let setter = setters.get(key);
|
|
126
|
+
if (setter === void 0) {
|
|
127
|
+
setter = (next) => {
|
|
128
|
+
slots.set(key, typeof next === "function" ? next(slots.get(key)) : next);
|
|
129
|
+
};
|
|
130
|
+
setters.set(key, setter);
|
|
131
|
+
}
|
|
132
|
+
return [slots.get(key), setter];
|
|
133
|
+
};
|
|
134
|
+
/** Whether `useQuery`'s options say to wait: `{ enabled }` with a falsy value. */
|
|
135
|
+
const waits = (options) => typeof options === "object" && options !== null && "enabled" in options && !options.enabled;
|
|
136
|
+
const useQuery = (name, input, options) => {
|
|
137
|
+
if (typeof name !== "string" || name === "") throw new Error("useQuery needs a query name, e.g. useQuery(\"orders\")");
|
|
138
|
+
if (!isProcedure(name, "query")) return Object.freeze({
|
|
139
|
+
data: null,
|
|
140
|
+
error: unknownProcedure(name, "query"),
|
|
141
|
+
status: "error"
|
|
142
|
+
});
|
|
143
|
+
const base = pathKey(`query:${name}`);
|
|
144
|
+
if (waits(options)) {
|
|
145
|
+
let waitingSite = base;
|
|
146
|
+
for (let index = 1; nextQuerySites.has(waitingSite); index++) waitingSite = `${base}#${index}`;
|
|
147
|
+
nextQuerySites.set(waitingSite, {
|
|
148
|
+
churn: 0,
|
|
149
|
+
key: `${name}:waiting`
|
|
150
|
+
});
|
|
151
|
+
return pending;
|
|
152
|
+
}
|
|
153
|
+
const plain = plainInput(input, "useQuery");
|
|
154
|
+
const key = `${name}:${stableKey(plain)}`;
|
|
155
|
+
let site = base;
|
|
156
|
+
for (let index = 1; nextQuerySites.has(site); index++) site = `${base}#${index}`;
|
|
157
|
+
const last = querySites.get(site);
|
|
158
|
+
const churn = last !== void 0 && last.key !== key && last.key === settledKey ? last.churn + 1 : 0;
|
|
159
|
+
nextQuerySites.set(site, {
|
|
160
|
+
churn,
|
|
161
|
+
key
|
|
162
|
+
});
|
|
163
|
+
if (churn >= MAX_INPUT_CHURN && !unstableSites.has(site)) {
|
|
164
|
+
unstableSites.add(site);
|
|
165
|
+
reportError(null, `useQuery("${name}") gets a different input every time its data arrives, so it never shows data, and it was stopped. Build the input from state, not from Date.now(), Math.random() or new Date()`);
|
|
166
|
+
}
|
|
167
|
+
if (unstableSites.has(site)) return Object.freeze({
|
|
168
|
+
data: null,
|
|
169
|
+
error: Object.freeze({
|
|
170
|
+
code: "BAD_INPUT",
|
|
171
|
+
message: `useQuery("${name}") was stopped: its input changes on every render`
|
|
172
|
+
}),
|
|
173
|
+
status: "error"
|
|
174
|
+
});
|
|
175
|
+
if (!reads.has(key)) {
|
|
176
|
+
if (reads.size >= MAX_QUERIES) throw new Error(`useQuery is limited to ${MAX_QUERIES} different requests`);
|
|
177
|
+
reads.set(key, {
|
|
178
|
+
input: plain,
|
|
179
|
+
key,
|
|
180
|
+
name
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return queryResults.get(key) ?? pending;
|
|
184
|
+
};
|
|
185
|
+
const useMutation = (name, options) => {
|
|
186
|
+
if (typeof name !== "string" || name === "") throw new Error("useMutation needs a mutation name, e.g. useMutation(\"archiveOrder\")");
|
|
187
|
+
const declaration = `mutation:${options?.key ?? `auto:${mutationKey++}`}`;
|
|
188
|
+
if (!mutationNames.has(declaration)) {
|
|
189
|
+
if (mutationNames.size >= MAX_MUTATIONS) throw new Error(`useMutation is limited to ${MAX_MUTATIONS} declarations`);
|
|
190
|
+
mutationNames.add(declaration);
|
|
191
|
+
}
|
|
192
|
+
const key = pathKey(declaration);
|
|
193
|
+
visit(key, "useMutation");
|
|
194
|
+
let state = mutationSlots.get(key);
|
|
195
|
+
if (state === void 0) {
|
|
196
|
+
state = {
|
|
197
|
+
data: null,
|
|
198
|
+
error: null,
|
|
199
|
+
id: 0,
|
|
200
|
+
input: null,
|
|
201
|
+
name,
|
|
202
|
+
status: "idle"
|
|
203
|
+
};
|
|
204
|
+
mutationSlots.set(key, state);
|
|
205
|
+
}
|
|
206
|
+
state.name = name;
|
|
207
|
+
const known = isProcedure(name, "mutation");
|
|
208
|
+
let mutate = mutateFns.get(key);
|
|
209
|
+
if (mutate === void 0) {
|
|
210
|
+
mutate = (input) => {
|
|
211
|
+
if (!inHandler) throw new Error("mutate() only works inside event handlers such as onClick");
|
|
212
|
+
const current = mutationSlots.get(key);
|
|
213
|
+
if (current === void 0 || current.status === "pending" || !isProcedure(current.name, "mutation")) return;
|
|
214
|
+
const plain = plainInput(input, "mutate()");
|
|
215
|
+
if (calls.length >= MAX_MUTATIONS_PER_EVENT) throw new Error(`mutate() is limited to ${MAX_MUTATIONS_PER_EVENT} calls per event`);
|
|
216
|
+
if (current.id !== 0) mutationById.delete(current.id);
|
|
217
|
+
const id = ++nextMutationId;
|
|
218
|
+
current.data = null;
|
|
219
|
+
current.error = null;
|
|
220
|
+
current.id = id;
|
|
221
|
+
current.input = plain;
|
|
222
|
+
current.status = "pending";
|
|
223
|
+
mutationById.set(id, key);
|
|
224
|
+
calls.push({
|
|
225
|
+
id,
|
|
226
|
+
input: plain,
|
|
227
|
+
name: current.name
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
mutateFns.set(key, mutate);
|
|
231
|
+
}
|
|
232
|
+
if (!known) return Object.freeze({
|
|
233
|
+
data: null,
|
|
234
|
+
error: unknownProcedure(name, "mutation"),
|
|
235
|
+
input: null,
|
|
236
|
+
mutate,
|
|
237
|
+
status: "error"
|
|
238
|
+
});
|
|
239
|
+
return Object.freeze({
|
|
240
|
+
data: state.data,
|
|
241
|
+
error: state.error,
|
|
242
|
+
input: state.input,
|
|
243
|
+
mutate,
|
|
244
|
+
status: state.status
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
const Pickle = Object.freeze({
|
|
248
|
+
render: (element) => {
|
|
249
|
+
rootElement = element;
|
|
250
|
+
},
|
|
251
|
+
reportError,
|
|
252
|
+
useConstants: () => prose,
|
|
253
|
+
useMutation,
|
|
254
|
+
useQuery,
|
|
255
|
+
useState
|
|
256
|
+
});
|
|
257
|
+
/** `#id` stays with the item; a bare index is only the fallback. */
|
|
258
|
+
const eachSegment = (item, index) => {
|
|
259
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
260
|
+
const record = item;
|
|
261
|
+
const id = record.id;
|
|
262
|
+
if (typeof id === "string" || typeof id === "number") return `#${id}`;
|
|
263
|
+
const key = record.key;
|
|
264
|
+
if (typeof key === "string" || typeof key === "number") return `#${key}`;
|
|
265
|
+
}
|
|
266
|
+
return String(index);
|
|
267
|
+
};
|
|
268
|
+
const callSegment = (name, key, ordinal, at) => {
|
|
269
|
+
if (key !== void 0) {
|
|
270
|
+
if (typeof key === "string" || typeof key === "number") return `${name}=${key}`;
|
|
271
|
+
reportError(at, `<${name}> key must be a string or number`);
|
|
272
|
+
}
|
|
273
|
+
return `${name}#${ordinal}`;
|
|
274
|
+
};
|
|
275
|
+
const each = (items, render, line) => {
|
|
276
|
+
let list;
|
|
277
|
+
if (items === null || items === void 0) list = [];
|
|
278
|
+
else if (typeof items === "number") list = Array.from({ length: Math.max(0, Math.min(Math.floor(items), 501)) }, (_, index) => index);
|
|
279
|
+
else if (Array.isArray(items)) list = items;
|
|
280
|
+
else {
|
|
281
|
+
reportError(line, "<each of> must be a list or a count");
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
if (list.length > MAX_EACH) {
|
|
285
|
+
reportError(line, `<each> shows the first ${MAX_EACH} items only`);
|
|
286
|
+
list = list.slice(0, MAX_EACH);
|
|
287
|
+
}
|
|
288
|
+
/** One item drawn under its path segment; a throw is reported and draws nothing. */
|
|
289
|
+
const draw = (item, index, segment) => {
|
|
290
|
+
pathStack.push(segment);
|
|
291
|
+
try {
|
|
292
|
+
return render(item, index);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
reportError(line, error);
|
|
295
|
+
return null;
|
|
296
|
+
} finally {
|
|
297
|
+
pathStack.pop();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
return list.map((item, index) => {
|
|
301
|
+
const segment = eachSegment(item, index);
|
|
302
|
+
const drawnItem = draw(item, index, segment);
|
|
303
|
+
return segment.startsWith("#") ? keyedItem(`${line}${segment}`, drawnItem) : drawnItem;
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
/** One component invocation; see the prototype runtime for the identity rules. */
|
|
307
|
+
const callComponent = (fn, props, children, at, ordinal, name, fromLibrary) => {
|
|
308
|
+
if (componentDepth >= MAX_COMPONENT_DEPTH) throw new Error("component nesting is deeper than 16");
|
|
309
|
+
componentDepth++;
|
|
310
|
+
const previousWhere = componentWhere;
|
|
311
|
+
const label = typeof name === "string" ? name : "component";
|
|
312
|
+
componentWhere = fromLibrary === true ? label : null;
|
|
313
|
+
const source = props ?? {};
|
|
314
|
+
const body = {};
|
|
315
|
+
for (const key of Object.keys(source)) if (key !== "key") body[key] = source[key];
|
|
316
|
+
pathStack.push(callSegment(label, source.key, ordinal ?? 0, at));
|
|
317
|
+
try {
|
|
318
|
+
return fn(body, children, at);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
reportError(at, error);
|
|
321
|
+
return null;
|
|
322
|
+
} finally {
|
|
323
|
+
pathStack.pop();
|
|
324
|
+
componentWhere = previousWhere;
|
|
325
|
+
componentDepth--;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const pickle = Object.freeze({
|
|
329
|
+
call: callComponent,
|
|
330
|
+
each,
|
|
331
|
+
Fragment: FRAGMENT,
|
|
332
|
+
jsx: (type, props, ...children) => ({
|
|
333
|
+
children,
|
|
334
|
+
props: props ?? {},
|
|
335
|
+
type
|
|
336
|
+
})
|
|
337
|
+
});
|
|
338
|
+
const pickleSafe = (evaluate, fallback, line) => {
|
|
339
|
+
try {
|
|
340
|
+
return evaluate();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
reportError(line ?? null, error);
|
|
343
|
+
return fallback;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const isVNode = (value) => typeof value === "object" && value !== null && "type" in value && "props" in value && "children" in value;
|
|
347
|
+
/** Nodes the current render has drawn, against `MAX_NODES`. */
|
|
348
|
+
let drawn = 0;
|
|
349
|
+
const countNode = () => {
|
|
350
|
+
drawn++;
|
|
351
|
+
if (drawn > MAX_NODES) throw new Error(`the page draws more than ${MAX_NODES} elements; show fewer, for example by paging or limiting a list`);
|
|
352
|
+
};
|
|
353
|
+
const expand = (node, out) => {
|
|
354
|
+
if (node === null || node === void 0 || typeof node === "boolean") return;
|
|
355
|
+
if (isKeyedItem(node)) {
|
|
356
|
+
const start = out.length;
|
|
357
|
+
expand(node.value, out);
|
|
358
|
+
out.slice(start).forEach((drawnNode, index) => {
|
|
359
|
+
drawnNode.key = `${node.key}/${drawnNode.key ?? index}`;
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (Array.isArray(node)) {
|
|
364
|
+
for (const child of node) expand(child, out);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
368
|
+
countNode();
|
|
369
|
+
let text = String(node);
|
|
370
|
+
if (text.length > MAX_TEXT) {
|
|
371
|
+
text = text.slice(0, MAX_TEXT);
|
|
372
|
+
reportError(null, `text longer than ${MAX_TEXT} characters was cut`);
|
|
373
|
+
}
|
|
374
|
+
out.push({
|
|
375
|
+
children: [],
|
|
376
|
+
id: 0,
|
|
377
|
+
props: {},
|
|
378
|
+
text,
|
|
379
|
+
type: TEXT,
|
|
380
|
+
wire: {}
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!isVNode(node)) return;
|
|
385
|
+
if (typeof node.type === "function") {
|
|
386
|
+
expand(node.type(node.props), out);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (node.type === FRAGMENT) {
|
|
390
|
+
expand(node.children, out);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (typeof node.type === "string") {
|
|
394
|
+
countNode();
|
|
395
|
+
const children = [];
|
|
396
|
+
expand(node.children, children);
|
|
397
|
+
out.push({
|
|
398
|
+
children,
|
|
399
|
+
id: 0,
|
|
400
|
+
props: node.props,
|
|
401
|
+
text: "",
|
|
402
|
+
type: node.type,
|
|
403
|
+
wire: {}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
/** Copies a list of objects keeping only flat fields; the page checks it again. */
|
|
408
|
+
const toRows = (list) => list.slice(0, MAX_ROWS).map((item) => {
|
|
409
|
+
const row = {};
|
|
410
|
+
if (typeof item === "object" && item !== null) {
|
|
411
|
+
for (const [key, value] of Object.entries(item).slice(0, MAX_ROW_KEYS)) if (value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value)) row[key] = typeof value === "string" ? value.slice(0, 500) : value;
|
|
412
|
+
}
|
|
413
|
+
return row;
|
|
414
|
+
});
|
|
415
|
+
const toWire = (id, props) => {
|
|
416
|
+
const wire = {};
|
|
417
|
+
for (const [name, value] of Object.entries(props)) {
|
|
418
|
+
if (name === "key") continue;
|
|
419
|
+
if (typeof value === "function") {
|
|
420
|
+
const fn = `${id}.${name}`;
|
|
421
|
+
nextHandlers.set(fn, (...args) => value(...args));
|
|
422
|
+
wire[name] = { fn };
|
|
423
|
+
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") wire[name] = value;
|
|
424
|
+
else if (Array.isArray(value)) wire[name] = { rows: toRows(value) };
|
|
425
|
+
}
|
|
426
|
+
return wire;
|
|
427
|
+
};
|
|
428
|
+
const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
429
|
+
const create = (node, ops) => {
|
|
430
|
+
node.id = nextId++;
|
|
431
|
+
if (node.type === TEXT) {
|
|
432
|
+
ops.push({
|
|
433
|
+
id: node.id,
|
|
434
|
+
op: "createText",
|
|
435
|
+
text: node.text
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
node.wire = toWire(node.id, node.props);
|
|
440
|
+
ops.push({
|
|
441
|
+
id: node.id,
|
|
442
|
+
op: "create",
|
|
443
|
+
props: node.wire,
|
|
444
|
+
type: node.type
|
|
445
|
+
});
|
|
446
|
+
node.children.forEach((child, index) => {
|
|
447
|
+
create(child, ops);
|
|
448
|
+
ops.push({
|
|
449
|
+
id: child.id,
|
|
450
|
+
index,
|
|
451
|
+
op: "place",
|
|
452
|
+
parent: node.id
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
};
|
|
456
|
+
/** Brings a node the last render drew up to date with its new version. */
|
|
457
|
+
const update = (old, node, ops) => {
|
|
458
|
+
node.id = old.id;
|
|
459
|
+
if (node.type === TEXT) {
|
|
460
|
+
if (old.text !== node.text) ops.push({
|
|
461
|
+
id: node.id,
|
|
462
|
+
op: "text",
|
|
463
|
+
text: node.text
|
|
464
|
+
});
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
node.wire = toWire(node.id, node.props);
|
|
468
|
+
const changed = {};
|
|
469
|
+
let dirty = false;
|
|
470
|
+
for (const name of /* @__PURE__ */ new Set([...Object.keys(old.wire), ...Object.keys(node.wire)])) if (!sameValue(old.wire[name], node.wire[name])) {
|
|
471
|
+
changed[name] = node.wire[name] ?? null;
|
|
472
|
+
dirty = true;
|
|
473
|
+
}
|
|
474
|
+
if (dirty) ops.push({
|
|
475
|
+
id: node.id,
|
|
476
|
+
op: "set",
|
|
477
|
+
props: changed
|
|
478
|
+
});
|
|
479
|
+
reconcile(node.id, old.children, node.children, ops);
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* Which entries of `sequence` (old positions, -1 for none) form its
|
|
483
|
+
* longest increasing run: the reused nodes that keep their order and
|
|
484
|
+
* need no `place`. Patience sorting, O(n log n).
|
|
485
|
+
*/
|
|
486
|
+
const keptInOrder = (sequence) => {
|
|
487
|
+
const tails = [];
|
|
488
|
+
const previousOf = [];
|
|
489
|
+
for (const [index, value] of sequence.entries()) {
|
|
490
|
+
if (value === -1) continue;
|
|
491
|
+
let low = 0;
|
|
492
|
+
let high = tails.length;
|
|
493
|
+
while (low < high) {
|
|
494
|
+
const middle = low + high >> 1;
|
|
495
|
+
if ((sequence[tails[middle] ?? 0] ?? 0) < value) low = middle + 1;
|
|
496
|
+
else high = middle;
|
|
497
|
+
}
|
|
498
|
+
previousOf[index] = low > 0 ? tails[low - 1] ?? -1 : -1;
|
|
499
|
+
tails[low] = index;
|
|
500
|
+
}
|
|
501
|
+
const kept = /* @__PURE__ */ new Set();
|
|
502
|
+
for (let index = tails.at(-1) ?? -1; index !== -1; index = previousOf[index] ?? -1) kept.add(index);
|
|
503
|
+
return kept;
|
|
504
|
+
};
|
|
505
|
+
/**
|
|
506
|
+
* Makes `before` into `after` under `parent`. A node an `<each>` item
|
|
507
|
+
* drew is matched by its item's key, so a row inserted at the top of a
|
|
508
|
+
* list costs that row's ops rather than one per row below it; other nodes
|
|
509
|
+
* are matched in order among themselves, as by position. Unmatched old
|
|
510
|
+
* nodes are removed, new ones created, and only nodes out of their old
|
|
511
|
+
* order are placed again.
|
|
512
|
+
*/
|
|
513
|
+
const reconcile = (parent, before, after, ops) => {
|
|
514
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
515
|
+
const unkeyed = [];
|
|
516
|
+
for (const [index, old] of before.entries()) if (old.key === void 0) unkeyed.push(index);
|
|
517
|
+
else if (!byKey.has(old.key)) byKey.set(old.key, index);
|
|
518
|
+
const used = /* @__PURE__ */ new Set();
|
|
519
|
+
let nextUnkeyed = 0;
|
|
520
|
+
const matched = after.map((node) => {
|
|
521
|
+
const candidate = node.key === void 0 ? unkeyed[nextUnkeyed++] : byKey.get(node.key);
|
|
522
|
+
const old = candidate === void 0 ? void 0 : before[candidate];
|
|
523
|
+
if (candidate === void 0 || old?.type !== node.type || used.has(candidate)) return -1;
|
|
524
|
+
used.add(candidate);
|
|
525
|
+
return candidate;
|
|
526
|
+
});
|
|
527
|
+
for (const [index, old] of before.entries()) if (!used.has(index)) ops.push({
|
|
528
|
+
id: old.id,
|
|
529
|
+
op: "remove"
|
|
530
|
+
});
|
|
531
|
+
const kept = keptInOrder(matched);
|
|
532
|
+
for (const [index, node] of after.entries()) {
|
|
533
|
+
const from = matched[index] ?? -1;
|
|
534
|
+
const old = from === -1 ? void 0 : before[from];
|
|
535
|
+
if (old === void 0) create(node, ops);
|
|
536
|
+
else {
|
|
537
|
+
update(old, node, ops);
|
|
538
|
+
if (kept.has(index)) continue;
|
|
539
|
+
}
|
|
540
|
+
ops.push({
|
|
541
|
+
id: node.id,
|
|
542
|
+
index,
|
|
543
|
+
op: "place",
|
|
544
|
+
parent
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
549
|
+
* Frees slots this render didn't use, once there are more than one render
|
|
550
|
+
* may hold. Not before: a document streaming in renders without the parts
|
|
551
|
+
* that haven't arrived yet, and their state must survive until they do.
|
|
552
|
+
*/
|
|
553
|
+
const pruneSlots = () => {
|
|
554
|
+
if (slots.size + mutationSlots.size <= MAX_SLOTS) return;
|
|
555
|
+
for (const key of slots.keys()) if (!visited.has(key)) {
|
|
556
|
+
slots.delete(key);
|
|
557
|
+
setters.delete(key);
|
|
558
|
+
}
|
|
559
|
+
for (const [key, state] of mutationSlots) if (!visited.has(key)) {
|
|
560
|
+
mutationSlots.delete(key);
|
|
561
|
+
mutateFns.delete(key);
|
|
562
|
+
mutationById.delete(state.id);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* Drops every state slot and mutation status, for a program that starts a
|
|
567
|
+
* new UI rather than editing the last one. A result still on its way for
|
|
568
|
+
* a dropped mutation finds no slot and is ignored. Query results stay:
|
|
569
|
+
* they are the server's data, not the UI's.
|
|
570
|
+
*/
|
|
571
|
+
const forgetState = () => {
|
|
572
|
+
slots.clear();
|
|
573
|
+
setters.clear();
|
|
574
|
+
mutationSlots.clear();
|
|
575
|
+
mutationById.clear();
|
|
576
|
+
mutateFns.clear();
|
|
577
|
+
};
|
|
578
|
+
const render = (run) => {
|
|
579
|
+
rootElement = void 0;
|
|
580
|
+
autoKey = 0;
|
|
581
|
+
mutationKey = 0;
|
|
582
|
+
componentDepth = 0;
|
|
583
|
+
componentWhere = null;
|
|
584
|
+
pathStack.length = 0;
|
|
585
|
+
nextHandlers = /* @__PURE__ */ new Map();
|
|
586
|
+
drawn = 0;
|
|
587
|
+
visited = /* @__PURE__ */ new Set();
|
|
588
|
+
mutationNames = /* @__PURE__ */ new Set();
|
|
589
|
+
nextQuerySites = /* @__PURE__ */ new Map();
|
|
590
|
+
const previousReads = reads;
|
|
591
|
+
reads = /* @__PURE__ */ new Map();
|
|
592
|
+
const next = [];
|
|
593
|
+
try {
|
|
594
|
+
run(Pickle, pickle, pickleSafe);
|
|
595
|
+
expand(rootElement, next);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
reads = previousReads;
|
|
598
|
+
return {
|
|
599
|
+
message: String(error),
|
|
600
|
+
ok: false
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
querySites = nextQuerySites;
|
|
604
|
+
pruneSlots();
|
|
605
|
+
for (const key of queryResults.keys()) if (!reads.has(key)) queryResults.delete(key);
|
|
606
|
+
const ops = [];
|
|
607
|
+
reconcile(ROOT, previous, next, ops);
|
|
608
|
+
if (ops.length > MAX_OPS) {
|
|
609
|
+
reads = previousReads;
|
|
610
|
+
return {
|
|
611
|
+
message: `Error: the page changed more than ${MAX_OPS} things at once`,
|
|
612
|
+
ok: false
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
previous = next;
|
|
616
|
+
handlers = nextHandlers;
|
|
617
|
+
return {
|
|
618
|
+
ok: true,
|
|
619
|
+
ops
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
const handle = (message) => {
|
|
623
|
+
errors = [];
|
|
624
|
+
calls = [];
|
|
625
|
+
const result = (ops) => post({
|
|
626
|
+
errors,
|
|
627
|
+
mutations: calls,
|
|
628
|
+
ops,
|
|
629
|
+
queries: [...reads.values()],
|
|
630
|
+
seq: message.seq,
|
|
631
|
+
type: "result"
|
|
632
|
+
});
|
|
633
|
+
const settle = (run) => {
|
|
634
|
+
if (run === null) {
|
|
635
|
+
result([]);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const outcome = render(run);
|
|
639
|
+
if (outcome.ok) result(outcome.ops);
|
|
640
|
+
else post({
|
|
641
|
+
message: outcome.message,
|
|
642
|
+
seq: message.seq,
|
|
643
|
+
type: "failed"
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
if (message.type === "query") {
|
|
647
|
+
if (reads.has(message.key)) queryResults.set(message.key, Object.freeze(message.result));
|
|
648
|
+
settledKey = message.key;
|
|
649
|
+
try {
|
|
650
|
+
settle(program);
|
|
651
|
+
} finally {
|
|
652
|
+
settledKey = null;
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (message.type === "mutation") {
|
|
657
|
+
const key = mutationById.get(message.id);
|
|
658
|
+
const state = key === void 0 ? void 0 : mutationSlots.get(key);
|
|
659
|
+
if (state !== void 0 && state.id === message.id) {
|
|
660
|
+
mutationById.delete(message.id);
|
|
661
|
+
if (message.result.status === "success") {
|
|
662
|
+
state.data = message.result.data;
|
|
663
|
+
state.error = null;
|
|
664
|
+
state.status = "success";
|
|
665
|
+
} else {
|
|
666
|
+
state.data = null;
|
|
667
|
+
state.error = message.result.error;
|
|
668
|
+
state.status = "error";
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
settle(program);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (message.type === "update") {
|
|
675
|
+
if (message.fresh) forgetState();
|
|
676
|
+
const previousProse = prose;
|
|
677
|
+
const previousProcedures = procedures;
|
|
678
|
+
prose = message.prose;
|
|
679
|
+
procedures = message.procedures;
|
|
680
|
+
let candidate = program;
|
|
681
|
+
if (message.code !== programCode) {
|
|
682
|
+
unstableSites.clear();
|
|
683
|
+
querySites = /* @__PURE__ */ new Map();
|
|
684
|
+
try {
|
|
685
|
+
candidate = new FunctionCtor("Pickle", "__pickle", "__pickleSafe", message.code);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
prose = previousProse;
|
|
688
|
+
procedures = previousProcedures;
|
|
689
|
+
post({
|
|
690
|
+
message: String(error),
|
|
691
|
+
seq: message.seq,
|
|
692
|
+
type: "failed"
|
|
693
|
+
});
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (candidate === null) {
|
|
698
|
+
result([]);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const outcome = render(candidate);
|
|
702
|
+
if (!outcome.ok) {
|
|
703
|
+
prose = previousProse;
|
|
704
|
+
procedures = previousProcedures;
|
|
705
|
+
post({
|
|
706
|
+
message: outcome.message,
|
|
707
|
+
seq: message.seq,
|
|
708
|
+
type: "failed"
|
|
709
|
+
});
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
program = candidate;
|
|
713
|
+
programCode = message.code;
|
|
714
|
+
result(outcome.ops);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const handler = handlers.get(message.fn);
|
|
718
|
+
if (handler === void 0 || program === null) {
|
|
719
|
+
result([]);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
inHandler = true;
|
|
723
|
+
try {
|
|
724
|
+
handler(...message.args);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
reportError(null, error);
|
|
727
|
+
} finally {
|
|
728
|
+
inHandler = false;
|
|
729
|
+
}
|
|
730
|
+
settle(program);
|
|
731
|
+
};
|
|
732
|
+
return { handle };
|
|
733
|
+
}
|
|
734
|
+
//#endregion
|
|
735
|
+
export { createRuntime };
|