@tiaforge-ui/sinter 0.0.0-stage → 0.2.0

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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -2
  3. package/dist/cache.d.ts +5 -0
  4. package/dist/cache.js +95 -0
  5. package/dist/check/index.d.ts +85 -0
  6. package/dist/check/index.js +156 -0
  7. package/dist/check/openapi-types.d.ts +6 -0
  8. package/dist/check/openapi-types.js +84 -0
  9. package/dist/check/shadow.d.ts +49 -0
  10. package/dist/check/shadow.js +279 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +67 -0
  13. package/dist/compiler/check-unit.d.ts +109 -0
  14. package/dist/compiler/check-unit.js +3 -0
  15. package/dist/compiler/compile.d.ts +66 -0
  16. package/dist/compiler/compile.js +1316 -0
  17. package/dist/compiler/diagnostic-docs.d.ts +10 -0
  18. package/dist/compiler/diagnostic-docs.js +142 -0
  19. package/dist/compiler/diagnostics.d.ts +57 -0
  20. package/dist/compiler/diagnostics.js +77 -0
  21. package/dist/compiler/expr.d.ts +97 -0
  22. package/dist/compiler/expr.js +853 -0
  23. package/dist/compiler/index.d.ts +22 -0
  24. package/dist/compiler/index.js +160 -0
  25. package/dist/compiler/openapi.d.ts +40 -0
  26. package/dist/compiler/openapi.js +177 -0
  27. package/dist/compiler/vocabulary.d.ts +12 -0
  28. package/dist/compiler/vocabulary.js +39 -0
  29. package/dist/dev.d.ts +10 -0
  30. package/dist/dev.js +92 -0
  31. package/dist/each.d.ts +3 -0
  32. package/dist/each.js +121 -0
  33. package/dist/index.d.ts +7 -0
  34. package/dist/index.js +4 -0
  35. package/dist/ir.d.ts +152 -0
  36. package/dist/ir.js +121 -0
  37. package/dist/lsp/quickfix.d.ts +15 -0
  38. package/dist/lsp/quickfix.js +58 -0
  39. package/dist/lsp/rpc.d.ts +31 -0
  40. package/dist/lsp/rpc.js +120 -0
  41. package/dist/lsp/server.d.ts +36 -0
  42. package/dist/lsp/server.js +281 -0
  43. package/dist/lsp/spec-location.d.ts +9 -0
  44. package/dist/lsp/spec-location.js +42 -0
  45. package/dist/lsp/typescript.d.ts +77 -0
  46. package/dist/lsp/typescript.js +242 -0
  47. package/dist/lsp/workspace.d.ts +56 -0
  48. package/dist/lsp/workspace.js +228 -0
  49. package/dist/optimistic.d.ts +3 -0
  50. package/dist/optimistic.js +15 -0
  51. package/dist/paginate.d.ts +3 -0
  52. package/dist/paginate.js +198 -0
  53. package/dist/persist.d.ts +3 -0
  54. package/dist/persist.js +55 -0
  55. package/dist/project.d.ts +42 -0
  56. package/dist/project.js +103 -0
  57. package/dist/runtime/app.d.ts +152 -0
  58. package/dist/runtime/app.js +1256 -0
  59. package/dist/runtime/config.d.ts +53 -0
  60. package/dist/runtime/config.js +118 -0
  61. package/dist/runtime/env.d.ts +18 -0
  62. package/dist/runtime/env.js +23 -0
  63. package/dist/runtime/helpers.d.ts +9 -0
  64. package/dist/runtime/helpers.js +55 -0
  65. package/dist/runtime/request.d.ts +23 -0
  66. package/dist/runtime/request.js +93 -0
  67. package/dist/runtime/security.d.ts +12 -0
  68. package/dist/runtime/security.js +34 -0
  69. package/dist/runtime/store.d.ts +98 -0
  70. package/dist/runtime/store.js +255 -0
  71. package/dist/sse.d.ts +7 -0
  72. package/dist/sse.js +209 -0
  73. package/dist/testing/index.d.ts +73 -0
  74. package/dist/testing/index.js +359 -0
  75. package/dist/vite.d.ts +11 -0
  76. package/dist/vite.js +96 -0
  77. package/dist/ws.d.ts +18 -0
  78. package/dist/ws.js +57 -0
  79. package/docs/diagnostics.md +283 -0
  80. package/html-custom-data.json +250 -0
  81. package/llms.txt +632 -0
  82. package/package.json +93 -4
@@ -0,0 +1,1256 @@
1
+ import { config, SinterError } from './config.js';
2
+ import { createRequestLayer, isAbort, toError } from './request.js';
3
+ import { BAD_KEY, encodeSegment, schemesOf, STYLE_BAD, urlAllowed } from './security.js';
4
+ import { clean, createResource } from './store.js';
5
+ const instances = new WeakMap();
6
+ const paths = new Map();
7
+ const split = (p) => paths.get(p) ?? (paths.set(p, p.split('.')), paths.get(p));
8
+ const compiled = new WeakMap();
9
+ const toDeps = (list) => list.map((d) => {
10
+ const p = d.split('.'), a = p.findIndex((x) => x.startsWith('{$'));
11
+ return { p, n: p.map((x) => (x[0] == '{' ? x.slice(1, -1) : undefined)), a, t: a < 0 ? -1 : +p[a].slice(3, -1) };
12
+ });
13
+ const depsOf = (key, list = []) => compiled.get(key) ?? (compiled.set(key, toDeps(list)), compiled.get(key));
14
+ /** Sentinel for "not written yet". */
15
+ const UNSET = {};
16
+ /** Fills `{name}` segments from a scope. */
17
+ const fill = (p, s) => (s && p.includes('{') ? split(p).map((x) => (x[0] == '{' ? '' + s[x.slice(1, -1)] : x)) : split(p));
18
+ const range = (a, stop) => {
19
+ const out = [];
20
+ for (let n = a; n && n != stop; n = n.nextSibling)
21
+ out.push(n);
22
+ return out;
23
+ };
24
+ const child = (s, props) => Object.assign(Object.create(s), props);
25
+ const after = (el) => {
26
+ const c = new Comment();
27
+ el.after(c);
28
+ return c;
29
+ };
30
+ const ET = EventTarget.prototype;
31
+ /** Per-element registries, kept on the element (cheaper than WeakMaps on the row creation path). */
32
+ const HANDLERS = Symbol('sinter.on'), MODEL = Symbol('sinter.model'), STATE = Symbol('sinter.state');
33
+ const indexReaders = new WeakMap();
34
+ /** row templates: whether each holds one element (see oneElement()) */
35
+ const prepared = new WeakMap();
36
+ const simple = new WeakMap(), SIMPLE = new Set(['text', 'attr', 'class', 'style', 'show', 'prop', 'on']);
37
+ /** The instance mount() created on `root`; throws if none. */
38
+ export function instance(root) {
39
+ const app = instances.get(root);
40
+ if (!app)
41
+ throw Error('sinter: not mounted');
42
+ return app;
43
+ }
44
+ export function mount(root, o) {
45
+ if (instances.has(root))
46
+ throw Error('sinter: already mounted');
47
+ const { ir, fns } = o;
48
+ if (ir?.v != 1)
49
+ throw Error('sinter: unsupported IR');
50
+ const env = o.env ?? config.env;
51
+ const req = createRequestLayer(env, o.request ?? config.request);
52
+ const schemes = schemesOf(config.urlSchemes);
53
+ const resources = new Map(), loaders = [];
54
+ const acts = new WeakMap();
55
+ const types = new Set(), selects = new Set(), deferred = new Set();
56
+ const listeners = new Map(), cleanups = [];
57
+ const shared = new Map();
58
+ const rootRbs = [], rootScopes = [], found = new Map();
59
+ let dirty = [], cur = [], scheduled = false, flushing = false, round = 0;
60
+ const res = (name) => {
61
+ const r = resources.get(name);
62
+ if (!r)
63
+ throw Error(`sinter: no resource ${name}`);
64
+ return r;
65
+ };
66
+ const forward = (m) => (n, ...a) => res(n)[m](...a);
67
+ const warn = (code, ...args) => app.warn(code, ...args);
68
+ const evaluate = (rb) => {
69
+ try {
70
+ return fns[rb.b.f](rb.b.e ? (rb.e ??= child(rb.s, { $el: rb.el })) : rb.s);
71
+ }
72
+ catch (e) {
73
+ warn(6, rb.b.k, rb.el.localName, e?.message);
74
+ }
75
+ };
76
+ /** Elements written through callbacks (`$.$m(row, …)`) since the last flush. */
77
+ const marked = new Set();
78
+ /** debounced handlers' pending timers */
79
+ const timers = new WeakMap();
80
+ /** action calls' signal: aborts when the page is hidden */
81
+ let hide = new AbortController();
82
+ /** runs an action function; it counts as busy (for the testing module's settle) while it runs */
83
+ async function invoke(run, args, ctx) {
84
+ app.busy++;
85
+ try {
86
+ return await fns[run](args, ctx);
87
+ }
88
+ finally {
89
+ app.busy--;
90
+ }
91
+ }
92
+ /** a failed call: SinterErrors as they are, anything else with status null */
93
+ const callError = (e) => (e instanceof SinterError ? e : new SinterError(null, e, e?.message || String(e)));
94
+ /** Per flush round: how dirty paths reach a template's rows (0 not, 1 only marked elements, 2 any row). */
95
+ let rowHits = new Map();
96
+ /**
97
+ * Whether a dependency (its `{name}` segments read from scope `s`, or any scope when `s` is
98
+ * absent) and dirty path `d` are prefixes of each other; `*` matches any segment. A change
99
+ * that ends above a row's own element, or only changes the list structure (`#`), does not
100
+ * reach it: reconcile re-evaluates rows whose item or index changed. `~` reaches only rows
101
+ * whose element was marked.
102
+ */
103
+ const overlaps = (dep, d, s) => reach(dep, d, s) > 0;
104
+ /** 0: no overlap; 1: only through a marked element (`~`) of the row; 2: overlap. */
105
+ function reach({ p, n, a, t }, d, s) {
106
+ if (a >= 0 && d.length <= a)
107
+ return 0;
108
+ let via = 2;
109
+ for (let i = 0, len = Math.min(p.length, d.length); i < len; i++) {
110
+ const y = d[i];
111
+ // a list's structure dependency: reached by changes to the list itself or a whole element, not element fields
112
+ if (p[i] == '#')
113
+ return d.length <= i + 1 ? 2 : 0;
114
+ if (y == '#' || y == '~') {
115
+ if (i != a)
116
+ continue;
117
+ if (y == '#' || (s && !marked.has(s[ir.templates[t].item])))
118
+ return 0;
119
+ via = 1;
120
+ continue;
121
+ }
122
+ if (y == '*' || (n[i] !== undefined && !s))
123
+ continue;
124
+ if (n[i] !== undefined ? s[n[i]] != y : p[i] != y && p[i] != '*')
125
+ return 0;
126
+ }
127
+ return s ? 2 : via;
128
+ }
129
+ /** Whether a binding in a template's rows (nested templates included) reads a loop index. */
130
+ const readsIndex = (t) => indexReaders.get(t) ?? (indexReaders.set(t, t.nodes.some((nd) => nd.b.some((b) => b.i || ((b.k == 'for' || b.k == 'if' || b.k == 'else') && readsIndex(ir.templates[b.x]))))), indexReaders.get(t));
131
+ /** Dependencies of every binding in a template's rows, nested templates included. */
132
+ const templateDeps = (t) => compiled.get(t) ?? (compiled.set(t, t.nodes.flatMap((nd) => nd.b.flatMap((b) => [
133
+ ...depsOf(b, b.d),
134
+ ...(b.k == 'for' || b.k == 'if' || b.k == 'else' ? templateDeps(ir.templates[b.x]) : []),
135
+ ]))), compiled.get(t));
136
+ /** Whether any of `deps` overlaps a dirty path of the current flush. */
137
+ const hit = (deps, s) => {
138
+ for (const dep of deps)
139
+ for (const d of cur)
140
+ if (overlaps(dep, d, s))
141
+ return true;
142
+ return false;
143
+ };
144
+ const app = {
145
+ ir, fns, env, req, transport: o.transport ?? (() => undefined), ext: {},
146
+ beforeFlush: [], afterFlush: [], onListen: [], states: [], busy: 0,
147
+ get isDirty() { return dirty.length > 0 || scheduled; },
148
+ dev: false,
149
+ warn() { },
150
+ page: {
151
+ $r: {}, $meta: {}, $url: readUrl(), $now: new Date(env.now()),
152
+ $m: (el, v) => (marked.add(el), v),
153
+ get $locale() { return config.locale?.get(); },
154
+ set $locale(v) { config.locale?.set(v); app.mark('$locale'); },
155
+ },
156
+ resource: res,
157
+ request: (url, init) => req.send(url, init),
158
+ async optimistic(change, run) {
159
+ const drop = app.record(change);
160
+ try {
161
+ return await run();
162
+ }
163
+ finally {
164
+ drop();
165
+ }
166
+ },
167
+ on(event, fn) {
168
+ if (!listeners.has(event))
169
+ listeners.set(event, new Set());
170
+ listeners.get(event).add(fn);
171
+ for (const l of app.onListen)
172
+ l(event);
173
+ return () => listeners.get(event).delete(fn);
174
+ },
175
+ state(el) {
176
+ for (let e = el; e; e = e.parentElement) {
177
+ const s = e[STATE];
178
+ if (s)
179
+ return { get: () => s[1], update: (fn) => { fn(s[1]); app.mark('$s' + s[0]); } };
180
+ }
181
+ throw Error('sinter: no data-sn-state scope');
182
+ },
183
+ destroy() {
184
+ for (const c of cleanups.splice(0))
185
+ c();
186
+ instances.delete(root);
187
+ },
188
+ scopeOf: (i) => (i < 0 ? app.page : rootScopes[i] ?? app.page),
189
+ url(tpl, s) {
190
+ let out = '', query = false, dropped = false;
191
+ tpl.forEach((p, i) => {
192
+ if (typeof p == 'string') {
193
+ // after a dropped parameter, its `&` separator goes too
194
+ out += dropped && p[0] == '&' && /[?&]$/.test(out) ? p.slice(1) : p;
195
+ dropped = false;
196
+ query ||= /[?#]/.test(p);
197
+ return;
198
+ }
199
+ const v = fns[p](s), next = tpl[i + 1];
200
+ // a query parameter whose whole value is null or undefined is left out
201
+ if (v == null && query && /[?&][^?&=#]*=$/.test(out) && (next == null || /^[&#]/.test(next))) {
202
+ out = out.replace(/[^?&=#]*=$/, '');
203
+ dropped = true;
204
+ return;
205
+ }
206
+ // URL parsers turn %2E%2E back into `..`, so a path value cannot be a dot segment at all
207
+ if (!query && (v == '.' || v == '..'))
208
+ throw new SinterError(0, null, `refused \`${v}\` as a URL path value`);
209
+ out += encodeSegment(v);
210
+ });
211
+ return out.replace(/[?&](?=#|$)/, '');
212
+ },
213
+ mark(path, s) {
214
+ dirty.push(fill(path, s));
215
+ if (!scheduled && !flushing) {
216
+ scheduled = true;
217
+ env.queueMicrotask(app.flush);
218
+ }
219
+ },
220
+ flush() {
221
+ scheduled = false;
222
+ if (flushing)
223
+ return;
224
+ flushing = true;
225
+ try {
226
+ for (let pass = 0; dirty.length && pass < 100; pass++) {
227
+ cur = dirty;
228
+ dirty = [];
229
+ for (const h of app.beforeFlush)
230
+ h(cur);
231
+ round++;
232
+ rowHits = new Map();
233
+ for (const l of loaders)
234
+ if (hit(depsOf(l.def, l.def.deps)))
235
+ l.changed();
236
+ walk(rootRbs, 0);
237
+ syncSelects();
238
+ for (const h of app.afterFlush)
239
+ h(cur);
240
+ }
241
+ }
242
+ finally {
243
+ cur = dirty = [];
244
+ marked.clear();
245
+ flushing = false;
246
+ }
247
+ },
248
+ record(change) {
249
+ const rs = [...resources.values()];
250
+ for (const r of rs)
251
+ r.recording = [];
252
+ let recs;
253
+ try {
254
+ change();
255
+ }
256
+ finally {
257
+ recs = rs.map((r) => [r, r.recording]);
258
+ for (const r of rs)
259
+ r.recording = null;
260
+ }
261
+ const ids = recs.filter(([, ops]) => ops.length).map(([r, ops]) => [r, r.addLayer(ops)]);
262
+ let done = false;
263
+ return () => { if (!done) {
264
+ done = true;
265
+ for (const [r, id] of ids)
266
+ r.dropLayer(id);
267
+ } };
268
+ },
269
+ storeFns: (el) => ({
270
+ refresh: (n) => res(n).refetch(),
271
+ upsert: forward('upsert'),
272
+ patch: forward('patch'),
273
+ remove: forward('remove'),
274
+ page: (n, t) => app.ext.paginate.page(n, t),
275
+ close: () => el?.closest('dialog')?.close(),
276
+ reset: () => {
277
+ const form = el?.closest('form');
278
+ if (!form)
279
+ return;
280
+ form.reset();
281
+ for (const c of form.elements) {
282
+ const rb = c[MODEL];
283
+ if (rb)
284
+ setModel(rb, readModel(rb));
285
+ }
286
+ },
287
+ emit: (n, detail) => el?.dispatchEvent(new CustomEvent(n, { detail, bubbles: true })),
288
+ }),
289
+ serverEvent(name, payload) {
290
+ for (const fn of listeners.get(name) ?? [])
291
+ try {
292
+ fn(payload);
293
+ }
294
+ catch (e) {
295
+ console.error(e);
296
+ }
297
+ },
298
+ addCleanup: (fn) => void cleanups.push(fn),
299
+ };
300
+ app.page.$f = app.storeFns(null);
301
+ instances.set(root, app);
302
+ for (const f of o.features ?? [])
303
+ f.install(app);
304
+ // ---- mount: one querySelectorAll, ids checked against the IR's tags
305
+ const qsa = (root instanceof Document ? Document : Element).prototype.querySelectorAll;
306
+ const bad = new Set();
307
+ for (const el of qsa.call(root, '[data-sn]')) {
308
+ const id = +Element.prototype.getAttribute.call(el, 'data-sn');
309
+ found.has(id) ? bad.add(id) : found.set(id, el);
310
+ }
311
+ for (const id of bad) {
312
+ found.delete(id);
313
+ warn(1, id);
314
+ }
315
+ for (const def of ir.resources)
316
+ createLoader(def);
317
+ // action calls see the page leave through ctx.signal
318
+ if (ir.actions.some((x) => x.call) || ir.resources.some((x) => x.call)) {
319
+ app.addCleanup(env.listen('pagehide', () => { hide.abort(); hide = new AbortController(); }));
320
+ }
321
+ // listeners for every event type the page binds, rows included
322
+ listen('toggle');
323
+ for (const nd of [...ir.roots, ...ir.templates.flatMap((t) => t.nodes)])
324
+ for (const b of nd.b) {
325
+ if (b.k == 'on')
326
+ listen(b.n);
327
+ if (b.k == 'model') {
328
+ listen('input');
329
+ listen('change');
330
+ }
331
+ }
332
+ bindNodes(ir.roots, ir.roots.map((nd) => {
333
+ const el = found.get(nd.id);
334
+ if (el?.localName == nd.tag)
335
+ return el;
336
+ warn(el ? 3 : 2, nd.id, el?.localName, nd.tag);
337
+ return null;
338
+ }), app.page, rootRbs);
339
+ cleanups.push(env.listen('popstate', () => {
340
+ const u = app.page.$url;
341
+ for (const k in u)
342
+ delete u[k];
343
+ Object.assign(u, readUrl());
344
+ app.mark('$url');
345
+ }));
346
+ if (ir.now) {
347
+ let t;
348
+ const tick = () => { t = env.setTimeout(() => { app.page.$now = new Date(env.now()); app.mark('$now'); tick(); }, 60_000, 'clock'); };
349
+ tick();
350
+ cleanups.push(() => env.clearTimeout(t));
351
+ }
352
+ for (const l of loaders)
353
+ l.start();
354
+ flushing = true;
355
+ try {
356
+ walk(rootRbs, 2);
357
+ syncSelects();
358
+ for (const h of app.afterFlush)
359
+ h([]);
360
+ }
361
+ finally {
362
+ flushing = false;
363
+ }
364
+ if (dirty.length) {
365
+ scheduled = true;
366
+ env.queueMicrotask(app.flush);
367
+ }
368
+ return app;
369
+ // ---- scopes and bindings
370
+ /** Query parameters, then the `:name` segments of the first `data-sn-path` pattern that matches. */
371
+ function readUrl() {
372
+ const out = Object.create(null), u = new URL(env.href());
373
+ for (const [k, v] of u.searchParams)
374
+ if (!BAD_KEY.test(k))
375
+ out[k] = v;
376
+ const segs = u.pathname.split('/').filter(Boolean);
377
+ for (const p of ir.path ?? []) {
378
+ const want = p.split('/').filter(Boolean);
379
+ if (want.length != segs.length || want.some((w, i) => w[0] != ':' && w != segs[i]))
380
+ continue;
381
+ want.forEach((w, i) => {
382
+ if (w[0] != ':')
383
+ return;
384
+ try {
385
+ out[w.slice(1)] = decodeURIComponent(segs[i]);
386
+ }
387
+ catch {
388
+ out[w.slice(1)] = segs[i];
389
+ }
390
+ });
391
+ break;
392
+ }
393
+ return out;
394
+ }
395
+ function writeUrl() {
396
+ const u = new URL(env.href()), q = new URLSearchParams();
397
+ const fromPath = new Set((ir.path ?? []).flatMap((p) => p.split('/').filter((s) => s[0] == ':').map((s) => s.slice(1))));
398
+ for (const [k, v] of Object.entries(app.page.$url))
399
+ if (v != null && v !== '' && !fromPath.has(k))
400
+ q.set(k, '' + v);
401
+ u.search = '' + q;
402
+ env.replaceState(u.pathname + u.search + u.hash);
403
+ }
404
+ /** One delegated capture listener per used event type, at the mount root. */
405
+ function listen(type) {
406
+ if (types.has(type))
407
+ return;
408
+ types.add(type);
409
+ ET.addEventListener.call(root, type, dispatch, true);
410
+ cleanups.push(() => ET.removeEventListener.call(root, type, dispatch, true));
411
+ }
412
+ /**
413
+ * Only elements the compiler marked as in a dialog of their own block (`dl`) are checked for a
414
+ * closed dialog; dialogs outside a template defer the whole list.
415
+ */
416
+ function bindNodes(defs, els, base, rbs) {
417
+ // scopes of elements that declare state (and their descendants); most rows have none
418
+ let scopes = null;
419
+ for (let i = 0; i < defs.length; i++) {
420
+ const nd = defs[i], el = els[i];
421
+ let s = scopes?.[nd.p] ?? base;
422
+ if (nd.state != null && el) {
423
+ const obj = fns[nd.state](s), sid = nd.sid, key = nd.persist;
424
+ s = child(s, { ['$s' + sid]: obj });
425
+ el[STATE] = [sid, obj];
426
+ if (rbs == rootRbs)
427
+ app.states.push([sid, obj]);
428
+ if (key) {
429
+ app.ext.persist?.state(key, obj);
430
+ app.afterFlush.push((d) => d.some((p) => p[0] == '$s' + sid) && app.ext.persist?.saveState(key, obj));
431
+ }
432
+ }
433
+ if (s !== base)
434
+ (scopes ??= [])[i] = s;
435
+ if (rbs == rootRbs)
436
+ rootScopes[i] = s;
437
+ if (el)
438
+ for (const b of nd.b) {
439
+ const rb = { b, el, s, v: UNSET, c: !!nd.dl };
440
+ if (b.k == 'on') {
441
+ const h = el[HANDLERS];
442
+ el[HANDLERS] = h == null ? rb : Array.isArray(h) ? (h.push(rb), h) : [h, rb];
443
+ continue;
444
+ }
445
+ if (b.k == 'else')
446
+ continue;
447
+ if (b.k == 'model') {
448
+ el[MODEL] = rb;
449
+ if (el.localName == 'select')
450
+ selects.add(rb);
451
+ if (b.p != null)
452
+ app.ext.persist?.model(rb, (v) => setModel(rb, v));
453
+ }
454
+ if (b.k == 'for') {
455
+ rb.rows = [];
456
+ rb.t = ir.templates[b.x];
457
+ }
458
+ if (b.k == 'if') {
459
+ rb.br = 0;
460
+ rb.else = b.y != null && els[b.y] ? [els[b.y], defs[b.y].b.find((x) => x.k == 'else').x] : null;
461
+ }
462
+ rbs.push(rb);
463
+ }
464
+ }
465
+ }
466
+ // ---- rendering
467
+ /** Walks bindings in document order; `force`: 0 dirty ones only, 1 also those reading a loop index, 2 all. */
468
+ function walk(rbs, force) {
469
+ for (const rb of rbs) {
470
+ if (!process(rb, force) || !rb.rows)
471
+ continue;
472
+ const t = rb.t;
473
+ let r = force || !t ? 2 : rowHits.get(t);
474
+ if (r == null) {
475
+ r = 0;
476
+ // a mark counts as "only marked rows" for dependencies on this list's own element; others (outer rows) reach any row
477
+ for (const dep of templateDeps(t))
478
+ for (const d of cur) {
479
+ const x = reach(dep, d);
480
+ r = Math.max(r, x == 1 && dep.t != t.id ? 2 : x);
481
+ }
482
+ rowHits.set(t, r);
483
+ }
484
+ // only marked elements changed and the list did not move: visit their rows directly
485
+ if (r == 1 && rb.rr != round) {
486
+ const keys = rb.keys === undefined ? (rb.keys = t.key != null ? null : new Map(rb.rows.map((x) => [x.key, x]))) : rb.keys;
487
+ if (keys && keys.size == rb.rows.length) {
488
+ for (const item of marked) {
489
+ const x = Object(item) === item ? keys.get(keyOf(t, item, null)) : null;
490
+ if (x && x.item === item)
491
+ walk(x.rbs, 0);
492
+ }
493
+ continue;
494
+ }
495
+ }
496
+ // otherwise skip rows no dirty path can reach (unless they moved or changed), and rows created in this round
497
+ for (const x of rb.rows)
498
+ if (x.r != round && (r || x.f)) {
499
+ walk(x.rbs, Math.max(force, x.f));
500
+ x.f = 0;
501
+ }
502
+ }
503
+ }
504
+ /** Evaluates and writes one binding if needed; false while it waits in a closed dialog. */
505
+ function process(rb, level) {
506
+ const force = level == 2 || (level == 1 && !!rb.b.i);
507
+ if (!force && !(deferred.size && deferred.has(rb)) && !(rb.b.d && hit(rb.d ??= depsOf(rb.b, rb.b.d), rb.s)))
508
+ return true;
509
+ if (rb.c && rb.el.closest('dialog:not([open])')) {
510
+ deferred.add(rb);
511
+ return false;
512
+ }
513
+ if (deferred.size)
514
+ deferred.delete(rb);
515
+ const v = evaluate(rb), k = rb.b.k;
516
+ if (k == 'for')
517
+ reconcile(rb, v);
518
+ else if (k == 'if')
519
+ toggle(rb, v ? 1 : rb.else ? 2 : 0);
520
+ else if (k == 'model')
521
+ writeModel(rb, v);
522
+ else if (!Object.is(v, rb.v)) {
523
+ write(rb, v, rb.v == UNSET ? undefined : rb.v);
524
+ rb.v = v;
525
+ }
526
+ return true;
527
+ }
528
+ function write(rb, v, prev) {
529
+ const el = rb.el, n = rb.b.n;
530
+ switch (rb.b.k) {
531
+ case 'text': {
532
+ const t = el.firstChild;
533
+ v = '' + (v ?? '');
534
+ if (t?.nodeType == 3 && !t.nextSibling)
535
+ t.data = v;
536
+ else
537
+ el.textContent = v;
538
+ break;
539
+ }
540
+ case 'show':
541
+ el.hidden = !v;
542
+ break;
543
+ case 'prop':
544
+ el[n] = v;
545
+ break;
546
+ case 'class':
547
+ // a new element without a class attribute cannot have the class: skip removing it
548
+ if (n) {
549
+ if (v || rb.v !== UNSET || el.hasAttribute('class'))
550
+ el.classList.toggle(n, !!v);
551
+ }
552
+ else {
553
+ if (prev?.[0] && prev[0] != v?.[0])
554
+ el.classList.remove(prev[0]);
555
+ if (v?.[0])
556
+ el.classList.toggle(v[0], !!v[1]);
557
+ }
558
+ break;
559
+ case 'attr':
560
+ if (v == null || v === false)
561
+ el.removeAttribute(n);
562
+ else if (urlAllowed(el, n, (v = v === true ? '' : '' + v), /^https?:/.test(el.baseURI) ? el.baseURI : env.href(), schemes))
563
+ el.setAttribute(n, v);
564
+ else {
565
+ el.removeAttribute(n);
566
+ warn(7, n, el.localName, v);
567
+ }
568
+ break;
569
+ case 'style':
570
+ if (v == null || v === false)
571
+ el.style.removeProperty(n);
572
+ else if (STYLE_BAD.test(v)) {
573
+ el.style.removeProperty(n);
574
+ warn(8, n, v);
575
+ }
576
+ else
577
+ el.style.setProperty(n, v);
578
+ }
579
+ }
580
+ function syncSelects() {
581
+ for (const rb of selects)
582
+ writeModel(rb, evaluate(rb));
583
+ }
584
+ // ---- models
585
+ function writeModel(rb, v) {
586
+ const el = rb.el, k = rb.b.n;
587
+ const has = (x) => Array.isArray(v) && v.map(String).includes(x);
588
+ if (k == 'checkbox')
589
+ el.checked = Array.isArray(v) ? has(el.value) : !!v;
590
+ else if (k == 'radio')
591
+ el.checked = v != null && '' + v == el.value;
592
+ else if (k == 'multi')
593
+ for (const opt of el.options)
594
+ opt.selected = has(opt.value);
595
+ // loose comparison: '1.0' == 1 keeps a number input's text as typed
596
+ else if (el.value != (v ?? ''))
597
+ el.value = v ?? '';
598
+ }
599
+ /** The control's value for its model; undefined for an unchecked radio. */
600
+ function readModel(rb) {
601
+ const el = rb.el, k = rb.b.n;
602
+ if (k == 'number')
603
+ return el.value === '' ? null : +el.value;
604
+ if (k == 'radio')
605
+ return el.checked ? el.value : undefined;
606
+ if (k == 'multi')
607
+ return [...el.selectedOptions].map((x) => x.value);
608
+ if (k != 'checkbox')
609
+ return el.value;
610
+ const cur = evaluate(rb);
611
+ if (!Array.isArray(cur))
612
+ return el.checked;
613
+ const i = cur.map(String).indexOf(el.value);
614
+ if (el.checked && i < 0)
615
+ cur.push(el.value);
616
+ if (!el.checked && i >= 0)
617
+ cur.splice(i, 1);
618
+ return cur;
619
+ }
620
+ function setModel(rb, v) {
621
+ if (v === undefined)
622
+ return;
623
+ try {
624
+ fns[rb.b.a](rb.s, v);
625
+ }
626
+ catch (e) {
627
+ return warn(9, rb.el.localName, e?.message);
628
+ }
629
+ for (const w of rb.b.w ?? [])
630
+ app.mark(w, rb.s);
631
+ }
632
+ // ---- lists and conditionals
633
+ function setRow(s, t, item, i) {
634
+ s[t.item] = item;
635
+ if (t.index)
636
+ s[t.index] = i;
637
+ s[t.ii] = i;
638
+ return s;
639
+ }
640
+ /** A row's identity: the explicit key, the resource key, or the item object itself. */
641
+ function keyOf(t, item, s) {
642
+ if (t.key != null)
643
+ return 'k' + fns[t.key](s);
644
+ if (t.keyFields && item)
645
+ return 'r' + t.keyFields.map((f) => item[f]).join('\x1f');
646
+ if (Object(item) === item)
647
+ return item;
648
+ warn(10);
649
+ return 'p' + typeof item + item;
650
+ }
651
+ function reconcile(rb, v) {
652
+ const t = rb.t, old = rb.rows, end = (rb.end ??= after(rb.el)), tpl = rb.el;
653
+ const items = v == null ? [] : Array.isArray(v) ? v : Symbol.iterator in Object(v) ? [...v] : [];
654
+ const n = items.length, m = old.length, rows = new Array(n), moved = readsIndex(t);
655
+ // a scope is only needed up front for an explicit key expression
656
+ const scopes = t.key != null ? items.map((item, i) => setRow(Object.create(rb.s), t, item, i)) : null;
657
+ const keys = items.map((item, i) => keyOf(t, item, scopes?.[i]));
658
+ if (app.dev && new Set(keys).size < n) {
659
+ const k = keys.find((k, i) => keys.indexOf(k) != i);
660
+ warn(11, typeof k == 'string' ? k.slice(1) : 'the same object twice');
661
+ }
662
+ const reuse = (x, i) => {
663
+ const item = items[i];
664
+ if (x.item !== item) {
665
+ setRow(x.s, t, (x.item = item), (x.i = i));
666
+ x.f = 2;
667
+ }
668
+ else if (x.i != i) {
669
+ setRow(x.s, t, item, (x.i = i));
670
+ if (moved)
671
+ x.f ||= 1;
672
+ }
673
+ rows[i] = x;
674
+ };
675
+ // rows with the same keys at the start and the end stay where they are
676
+ let a = 0, oe = m, ne = n;
677
+ while (a < oe && a < ne && old[a].key === keys[a])
678
+ reuse(old[a], a++);
679
+ while (a < oe && a < ne && old[oe - 1].key === keys[ne - 1])
680
+ reuse(old[--oe], --ne);
681
+ // the middle: match by key, create the rest
682
+ const byKey = new Map(), used = new Uint8Array(oe - a), seq = [];
683
+ for (let o = a; o < oe; o++)
684
+ byKey.set(old[o].key, o);
685
+ for (let i = a; i < ne; i++) {
686
+ const o = byKey.get(keys[i]);
687
+ if (o != null && !used[o - a]) {
688
+ used[o - a] = 1;
689
+ seq.push(o);
690
+ reuse(old[o], i);
691
+ }
692
+ else {
693
+ seq.push(-1);
694
+ rows[i] = instantiate(tpl, t, scopes?.[i] ?? setRow(Object.create(rb.s), t, items[i], i), keys[i], items[i], i);
695
+ }
696
+ }
697
+ // each old row owns the nodes from its first node up to the next old row's first node;
698
+ // collect them for rows that go or move before the DOM changes
699
+ const nodesOf = (o) => range(old[o].first, old[o + 1]?.first ?? end);
700
+ const keep = lis(seq), moving = new Map(), gone = [];
701
+ seq.forEach((o, k) => { if (o >= 0 && !keep.has(k))
702
+ moving.set(rows[a + k], nodesOf(o)); });
703
+ for (let o = a; o < oe; o++)
704
+ if (!used[o - a])
705
+ gone.push(o);
706
+ if (m && gone.length == m) {
707
+ // every old row goes (cleared, or all keys new): when the list owns its parent, drop them in
708
+ // one operation; removing 5,000 rows one by one costs more than building the new ones
709
+ const parent = end.parentNode;
710
+ if (parent.firstChild == tpl && parent.lastChild == end)
711
+ parent.replaceChildren(tpl, end);
712
+ else
713
+ while (tpl.nextSibling != end)
714
+ tpl.nextSibling.remove();
715
+ }
716
+ else
717
+ for (const node of gone.flatMap(nodesOf))
718
+ node.remove();
719
+ if (deferred.size || selects.size)
720
+ for (const o of gone)
721
+ forget(old[o].rbs);
722
+ const ref = ne < n ? rows[ne].first ?? end : end;
723
+ if (!seq.some((o) => o >= 0)) {
724
+ // only new rows in the middle: insert each straight before the reference (going through a
725
+ // fragment first moves every node twice)
726
+ const parent = ref.parentNode;
727
+ for (let i = a; i < ne; i++)
728
+ for (const node of rows[i].nodes)
729
+ parent.insertBefore(node, ref);
730
+ }
731
+ else {
732
+ // move the fewest rows: those outside a longest increasing subsequence of old positions
733
+ let at = ref;
734
+ for (let k = seq.length; k--;) {
735
+ const x = rows[a + k];
736
+ if (!keep.has(k))
737
+ at.before(...(moving.get(x) ?? x.nodes));
738
+ at = x.first ?? at;
739
+ }
740
+ }
741
+ rb.rows = rows;
742
+ rb.rr = round;
743
+ rb.keys = undefined;
744
+ }
745
+ function toggle(rb, br) {
746
+ if (br == rb.br)
747
+ return;
748
+ const end = (rb.end ??= after(rb.el)), [old] = rb.rows ?? [];
749
+ if (old) {
750
+ for (const n of range(old.first, end))
751
+ n.remove();
752
+ forget(old.rbs);
753
+ }
754
+ rb.br = br;
755
+ rb.rows = [];
756
+ if (!br)
757
+ return;
758
+ const [anchor, tid] = br == 1 ? [rb.el, rb.b.x] : rb.else;
759
+ const x = instantiate(anchor, ir.templates[tid], Object.create(rb.s), '', null, 0);
760
+ end.before(...x.nodes);
761
+ rb.rows = [x];
762
+ }
763
+ function instantiate(tpl, t, s, key, item, i) {
764
+ const content = tpl.content, one = prepared.get(tpl) ?? oneElement(tpl);
765
+ const top = one ? content.firstChild.cloneNode(true) : content.cloneNode(true);
766
+ const defs = t.nodes, rbs = [];
767
+ let els;
768
+ if (t.find != null)
769
+ els = fns[t.find](one ? top : top.firstChild);
770
+ else {
771
+ // IR without lookup functions (compiled before they existed)
772
+ els = new Array(defs.length);
773
+ for (let k = 0; k < defs.length; k++) {
774
+ const path = defs[k].path;
775
+ // path[0] picks a child of the content; with one node that child is `top` itself
776
+ let node = one ? (path[0] == 0 ? top : null) : top;
777
+ for (let x = one ? 1 : 0; x < path.length && node; x++) {
778
+ node = node.firstChild;
779
+ for (let c = path[x]; c-- && node;)
780
+ node = node.nextSibling;
781
+ }
782
+ els[k] = node && node.localName == defs[k].tag ? node : null;
783
+ }
784
+ }
785
+ if (isSimple(t)) {
786
+ // only value bindings and handlers: bind and write each once, as walk(rbs, 2) would
787
+ for (let k = 0; k < defs.length; k++) {
788
+ const el = els[k];
789
+ if (el)
790
+ for (const b of defs[k].b) {
791
+ const rb = { b, el, s, v: UNSET, c: false };
792
+ if (b.k == 'on') {
793
+ const h = el[HANDLERS];
794
+ el[HANDLERS] = h == null ? rb : Array.isArray(h) ? (h.push(rb), h) : [h, rb];
795
+ continue;
796
+ }
797
+ rbs.push(rb);
798
+ const v = evaluate(rb);
799
+ write(rb, v, undefined);
800
+ rb.v = v;
801
+ }
802
+ }
803
+ }
804
+ else {
805
+ bindNodes(defs, els, s, rbs);
806
+ walk(rbs, 2);
807
+ }
808
+ const nodes = [];
809
+ if (one)
810
+ nodes.push(top);
811
+ else
812
+ for (let c = top.firstChild; c; c = c.nextSibling)
813
+ nodes.push(c);
814
+ return { first: nodes[0] ?? null, nodes, rbs, s, key, item, i, f: 0, r: round };
815
+ }
816
+ /**
817
+ * Once per row template: whether it holds one element (a <tr>, an <li>), so rows clone just that
818
+ * and no fragment. Not a nested <template>: an anchor needs a parent for its end marker.
819
+ */
820
+ function oneElement(tpl) {
821
+ const content = tpl.content, f = content.firstChild;
822
+ const one = !!f && f == content.lastChild && f.nodeType == 1 && f.localName != 'template';
823
+ prepared.set(tpl, one);
824
+ return one;
825
+ }
826
+ /** A row template whose elements only have value bindings and handlers (no state, model, nested template or dialog). */
827
+ function isSimple(t) {
828
+ let v = simple.get(t);
829
+ if (v == null) {
830
+ v = t.nodes.every((nd) => !nd.dl && nd.state == null && nd.b.every((b) => SIMPLE.has(b.k)));
831
+ simple.set(t, v);
832
+ }
833
+ return v;
834
+ }
835
+ /** Drops removed bindings from the deferred and select sets. */
836
+ function forget(rbs) {
837
+ for (const rb of rbs) {
838
+ deferred.delete(rb);
839
+ selects.delete(rb);
840
+ for (const r of rb.rows ?? [])
841
+ forget(r.rbs);
842
+ }
843
+ }
844
+ // ---- events
845
+ function dispatch(e) {
846
+ let el = e.target;
847
+ if (el?.nodeType != 1)
848
+ el = el?.parentElement ?? null;
849
+ if (!el)
850
+ return;
851
+ if (e.type == 'toggle' && el.localName == 'dialog' && el.hasAttribute('open')) {
852
+ for (const rb of [...deferred])
853
+ if (deferred.has(rb) && el.contains(rb.el)) {
854
+ cur = [];
855
+ walk([rb], 2);
856
+ }
857
+ }
858
+ // models update state before other handlers on the same event
859
+ const m = el[MODEL];
860
+ if (m && /^(input|change)$/.test(e.type) && (e.type == 'input') == (m.b.n == 'text'))
861
+ setModel(m, readModel(m));
862
+ for (; el; el = el.parentElement) {
863
+ const h = el[HANDLERS], hs = h && (Array.isArray(h) ? h : [h]).filter((x) => x.b.n == e.type);
864
+ if (hs?.length) {
865
+ for (const h of hs)
866
+ h.b.x != null ? trigger(h, e) : h.b.ms ? later(h, e) : handle(h, h.b.f, e, h.b.w);
867
+ return;
868
+ }
869
+ if (el == root)
870
+ return;
871
+ }
872
+ }
873
+ /** Runs a handler expression and invalidates what it may have changed. */
874
+ function handle(rb, f, e, writes, extra) {
875
+ const s = child(rb.s, { $el: rb.el, $event: e, $f: app.storeFns(rb.el), ...extra });
876
+ try {
877
+ fns[f](s);
878
+ }
879
+ catch (err) {
880
+ console.error(err);
881
+ }
882
+ for (const w of writes ?? [])
883
+ app.mark(w, s);
884
+ if (writes?.some((w) => w.startsWith('$url')))
885
+ writeUrl();
886
+ }
887
+ /** A debounced handler: runs once `ms` after the last event, with that event. */
888
+ function later(rb, e) {
889
+ const t = timers.get(rb);
890
+ if (t) {
891
+ env.clearTimeout(t);
892
+ app.busy--;
893
+ }
894
+ app.busy++;
895
+ timers.set(rb, env.setTimeout(() => { timers.delete(rb); app.busy--; handle(rb, rb.b.f, e, rb.b.w); }, rb.b.ms, 'debounce'));
896
+ }
897
+ // ---- actions
898
+ function trigger(rb, e) {
899
+ const a = ir.actions[rb.b.x], el = rb.el;
900
+ if (e.type == 'submit' || el.localName == 'a' || (e.type == 'click' && /^(button|input)$/.test(el.localName) && el.closest('form')))
901
+ e.preventDefault();
902
+ if (!a.debounce)
903
+ return fire(rb, a, e);
904
+ const st = act(el);
905
+ if (st.t) {
906
+ env.clearTimeout(st.t);
907
+ app.busy--;
908
+ }
909
+ app.busy++;
910
+ st.t = env.setTimeout(() => { st.t = null; app.busy--; fire(rb, a, e); }, a.debounce, 'debounce');
911
+ }
912
+ function act(el) {
913
+ if (!acts.has(el))
914
+ acts.set(el, { n: 0, q: [], w: null, c: null, t: null, off: null });
915
+ return acts.get(el);
916
+ }
917
+ function fire(rb, a, e) {
918
+ const st = act(rb.el), get = a.method == 'GET';
919
+ if (!get && a.concurrency == 'block' && st.n)
920
+ return;
921
+ const s = child(rb.s, { $el: rb.el, $event: e });
922
+ let url = '', body;
923
+ try {
924
+ // a call's arguments are taken when it is triggered, like a request's URL and body
925
+ if (a.call)
926
+ body = fns[a.call.args](s);
927
+ else {
928
+ url = app.url(a.url, s);
929
+ body = a.body != null ? fns[a.body](s) : a.form && !get && a.method != 'DELETE' ? serialize(rb.el) : undefined;
930
+ }
931
+ }
932
+ catch (err) {
933
+ return unhandled(toError(err), rb.el);
934
+ }
935
+ const drop = a.optimistic != null ? app.ext.optimistic.begin(a.optimistic, s) : () => { };
936
+ if (get) {
937
+ st.c?.abort();
938
+ return void send(rb, a, st, url, body, drop, (st.c = new AbortController()));
939
+ }
940
+ const run = () => void send(rb, a, st, url, body, drop);
941
+ if (!st.n)
942
+ run();
943
+ else if (a.concurrency == 'latest') {
944
+ st.w ? st.w.drop() : app.busy++;
945
+ st.w = { run, drop };
946
+ }
947
+ else {
948
+ app.busy++;
949
+ st.q.push(run);
950
+ }
951
+ }
952
+ async function send(rb, a, st, url, body, drop, c) {
953
+ const el = rb.el;
954
+ st.n++;
955
+ el.setAttribute('aria-busy', 'true');
956
+ if (a.form && a.concurrency == 'block' && !c && !st.off) {
957
+ st.off = [...el.elements].filter((x) => !x.disabled);
958
+ for (const x of st.off)
959
+ x.disabled = true;
960
+ }
961
+ try {
962
+ const result = a.call
963
+ ? await invoke(a.call.run, body, { signal: hide.signal, request: (u, i) => req.send(u, i), el })
964
+ : await req.send(url, { method: a.method, body, signal: c?.signal });
965
+ if (c && st.c != c)
966
+ return;
967
+ drop();
968
+ if (a.then != null)
969
+ handle(rb, a.then, null, a.w, { $result: result });
970
+ }
971
+ catch (err) {
972
+ drop();
973
+ if (isAbort(err) || (c && st.c != c))
974
+ return;
975
+ const error = a.call ? callError(err) : toError(err);
976
+ a.catch != null ? handle(rb, a.catch, null, a.w, { $error: error }) : unhandled(error, el);
977
+ }
978
+ finally {
979
+ if (c && st.c == c)
980
+ st.c = null;
981
+ if (!--st.n) {
982
+ for (const x of st.off ?? [])
983
+ x.disabled = false;
984
+ st.off = null;
985
+ const next = st.q.shift() ?? st.w?.run;
986
+ if (next && next == st.w?.run)
987
+ st.w = null;
988
+ if (next) {
989
+ app.busy--;
990
+ next();
991
+ }
992
+ else
993
+ el.removeAttribute('aria-busy');
994
+ }
995
+ }
996
+ }
997
+ /** The unhandled-error path: `sn-error` event, then `onError`, then the dev console. */
998
+ function unhandled(error, el, resource) {
999
+ const target = el ?? root, ev = new CustomEvent('sn-error', { detail: { error, element: target, resource }, bubbles: true, cancelable: true });
1000
+ ET.dispatchEvent.call(target, ev);
1001
+ if (ev.defaultPrevented)
1002
+ return;
1003
+ config.onError?.(error, target);
1004
+ warn(16, resource ? ` loading \`${resource}\`` : '', error.message, error);
1005
+ }
1006
+ // ---- resources
1007
+ function createLoader(def) {
1008
+ const r = createResource(def.name, { key: def.key, order: def.order, locale: () => config.locale?.get(), warn, prep: (x) => (app.prep ? app.prep(x) : x), freeze: app.dev });
1009
+ r.onDirty = (what) => app.mark((what == 'data' ? '$r.' : '$meta.') + def.name);
1010
+ resources.set(def.name, r);
1011
+ Object.defineProperty(app.page.$r, def.name, { get: r.value, enumerable: true });
1012
+ app.page.$meta[def.name] = r.meta;
1013
+ let url = null, c = null, seq = 0, again = null;
1014
+ const scope = () => app.scopeOf(def.scope);
1015
+ /** `call`: the arguments of the current load; the identity of a load is its arguments' JSON */
1016
+ let args = [], calls = 0;
1017
+ const resolve = () => {
1018
+ const s = scope();
1019
+ if (def.call) {
1020
+ try {
1021
+ if (def.when != null && !fns[def.when](s))
1022
+ return null;
1023
+ args = fns[def.call.args](s);
1024
+ try {
1025
+ return 'call:' + JSON.stringify(args);
1026
+ }
1027
+ catch {
1028
+ return `call:#${++calls}`;
1029
+ }
1030
+ }
1031
+ catch (e) {
1032
+ warn(17, def.name, e.message);
1033
+ return null;
1034
+ }
1035
+ }
1036
+ try {
1037
+ return def.when != null && !fns[def.when](s) ? null : app.url(def.src, s);
1038
+ }
1039
+ catch (e) {
1040
+ warn(17, def.name, e.message);
1041
+ return null;
1042
+ }
1043
+ };
1044
+ const loading = (on) => { if (r.meta.loading != on) {
1045
+ r.meta.loading = on;
1046
+ r.onDirty('meta');
1047
+ } };
1048
+ const pick = (path, body) => (path ? path.split('.').reduce((x, k) => x?.[k], body) : body);
1049
+ const fill = (items, total, m) => {
1050
+ l.pager?.appends(m) && r.isCollection ? items.forEach(r.upsert) : r.replace(items);
1051
+ r.setTotal(total);
1052
+ l.pager?.received(items, total, m);
1053
+ };
1054
+ /** stores a response; `req` is the URL it came from, for the page-load cache */
1055
+ const store = (body, req, m = l.pager?.current()) => {
1056
+ let items = pick(def.items, body);
1057
+ if (r.isCollection && !Array.isArray(items)) {
1058
+ warn(12, def.name, def.items ?? '(response)');
1059
+ items = [];
1060
+ }
1061
+ if (Array.isArray(items))
1062
+ items = items.map(clean);
1063
+ const t = def.total ? pick(def.total, body) : null, total = t == null ? null : +t;
1064
+ fill(items, total, m);
1065
+ if (def.cache != null)
1066
+ app.ext.cache?.loaded(def, r, req, items, total, !!l.pager?.appends(m));
1067
+ };
1068
+ /** the request URL for the current page of `u` */
1069
+ const request = (u, m = l.pager?.current()) => withParams(u, m ? l.pager.params(m) : null);
1070
+ const fail = (e) => {
1071
+ const error = toError(e);
1072
+ r.meta.error = error;
1073
+ r.onDirty('meta');
1074
+ unhandled(error, null, def.name);
1075
+ };
1076
+ const l = {
1077
+ def, res: r, app, pager: null,
1078
+ scope, get: loadShared, failed: fail, loading,
1079
+ items(body) {
1080
+ let items = pick(def.items, body);
1081
+ if (!Array.isArray(items)) {
1082
+ warn(12, def.name, def.items ?? '(response)');
1083
+ items = [];
1084
+ }
1085
+ return items.map(clean);
1086
+ },
1087
+ start() {
1088
+ const el = def.data != null ? found.get(def.data) : null;
1089
+ let data;
1090
+ if (el) {
1091
+ if (el.localName == 'script' && el.parentElement?.localName == 'head')
1092
+ try {
1093
+ data = { v: JSON.parse(el.textContent) };
1094
+ }
1095
+ catch {
1096
+ warn(5, def.name);
1097
+ }
1098
+ else
1099
+ warn(4, def.name);
1100
+ }
1101
+ url = data ? resolve() : null;
1102
+ if (url == null) {
1103
+ // the page-load cache: show the last confirmed data now; changed() revalidates
1104
+ const u = def.cache != null && app.ext.cache ? resolve() : null;
1105
+ if (u != null) {
1106
+ l.pager?.reset();
1107
+ const hit = app.ext.cache.read(def, request(u));
1108
+ // loadedAt stays null: it means the server answered during this page load, and
1109
+ // guards such as `$meta.user.loadedAt && !user` must not act on another page's data
1110
+ if (hit)
1111
+ fill(hit.items, hit.total, l.pager?.current());
1112
+ }
1113
+ return l.changed();
1114
+ }
1115
+ l.pager?.reset();
1116
+ store(data.v, request(url));
1117
+ r.meta.loadedAt = env.now();
1118
+ },
1119
+ /** `src` or `when` may have changed: reload from the first page if the URL differs. */
1120
+ changed() {
1121
+ const u = resolve();
1122
+ if (u === url)
1123
+ return;
1124
+ url = u;
1125
+ l.pager?.reset();
1126
+ if (u != null)
1127
+ return void l.load();
1128
+ c?.abort();
1129
+ r.replace(r.isCollection ? [] : null);
1130
+ loading(false);
1131
+ },
1132
+ /** Repeats the current request; several calls in one task share one request. */
1133
+ refetch: () => (again ??= Promise.resolve().then(() => { again = null; return l.load(); })),
1134
+ async load(move) {
1135
+ if (url == null)
1136
+ return;
1137
+ c?.abort();
1138
+ const ctl = (c = new AbortController()), n = ++seq, m = l.pager && (move ?? l.pager.current());
1139
+ const mark = r.startLoad();
1140
+ let loaded = false;
1141
+ loading(true);
1142
+ try {
1143
+ const req = withParams(url, m ? l.pager.params(m) : null);
1144
+ const body = def.call
1145
+ ? await invoke(def.call.run, args, { signal: ctl.signal, request: (u, i) => app.req.send(u, i), el: null })
1146
+ : await loadShared(req, ctl.signal);
1147
+ if (n != seq)
1148
+ return;
1149
+ store(body, req, m);
1150
+ loaded = true;
1151
+ r.meta.error = null;
1152
+ r.meta.loadedAt = env.now();
1153
+ }
1154
+ catch (e) {
1155
+ if (n != seq || isAbort(e))
1156
+ return;
1157
+ fail(def.call ? callError(e) : e);
1158
+ }
1159
+ finally {
1160
+ // writes made while this load ran win over its (older) response
1161
+ r.endLoad(mark, loaded);
1162
+ if (n == seq) {
1163
+ c = null;
1164
+ loading(false);
1165
+ }
1166
+ }
1167
+ },
1168
+ };
1169
+ if (def.each)
1170
+ app.ext.each.fanOut(l);
1171
+ if (def.paginate)
1172
+ l.pager = app.ext.paginate.pager(l);
1173
+ r.loader = l;
1174
+ loaders.push(l);
1175
+ }
1176
+ /** GETs `url`; concurrent loads of one URL share one request, aborted when no one waits. */
1177
+ function loadShared(url, signal) {
1178
+ let e = shared.get(url);
1179
+ if (!e) {
1180
+ const c = new AbortController(), entry = { n: 0, c, p: req.send(url, { signal: c.signal }) };
1181
+ entry.p.catch(() => { }).finally(() => shared.get(url) == entry && shared.delete(url));
1182
+ shared.set(url, (e = entry));
1183
+ }
1184
+ const entry = e;
1185
+ entry.n++;
1186
+ return new Promise((ok, no) => {
1187
+ let done = false;
1188
+ const leave = () => { done = true; entry.n--; signal.removeEventListener('abort', abort); };
1189
+ const abort = () => {
1190
+ if (done)
1191
+ return;
1192
+ leave();
1193
+ if (!entry.n) {
1194
+ entry.c.abort();
1195
+ shared.delete(url);
1196
+ }
1197
+ no(new DOMException('', 'AbortError'));
1198
+ };
1199
+ signal.aborted ? abort() : signal.addEventListener('abort', abort);
1200
+ entry.p.then((v) => done || (leave(), ok(v)), (err) => done || (leave(), no(err)));
1201
+ });
1202
+ }
1203
+ }
1204
+ export function withParams(url, params) {
1205
+ if (!params)
1206
+ return url;
1207
+ const [path, hash] = url.split('#'), [base, query] = path.split('?'), q = new URLSearchParams(query);
1208
+ for (const k in params)
1209
+ q.set(k, params[k]);
1210
+ return `${base}?${q}${hash ? '#' + hash : ''}`;
1211
+ }
1212
+ /** Indexes (into `seq`) of a longest increasing subsequence, ignoring -1 entries. */
1213
+ export function lis(seq) {
1214
+ const tails = [], prev = [];
1215
+ seq.forEach((v, i) => {
1216
+ if (v < 0)
1217
+ return;
1218
+ let lo = 0, hi = tails.length;
1219
+ while (lo < hi) {
1220
+ const m = (lo + hi) >> 1;
1221
+ seq[tails[m]] < v ? (lo = m + 1) : (hi = m);
1222
+ }
1223
+ prev[i] = tails[lo - 1];
1224
+ tails[lo] = i;
1225
+ });
1226
+ const keep = new Set();
1227
+ for (let k = tails.at(-1); k != null; k = prev[k])
1228
+ keep.add(k);
1229
+ return keep;
1230
+ }
1231
+ /** Serializes a form's named controls to nested JSON, or FormData if it has a file input. */
1232
+ export function serialize(form) {
1233
+ const controls = [...form.elements];
1234
+ if (controls.some((c) => c.type == 'file'))
1235
+ return new FormData(form);
1236
+ const out = Object.create(null), boxes = {};
1237
+ for (const c of controls)
1238
+ if (c.type == 'checkbox')
1239
+ boxes[c.name] = (boxes[c.name] ?? 0) + 1;
1240
+ for (const c of controls) {
1241
+ const { name, type } = c, segs = name?.match(/[^.[\]]+/g);
1242
+ if (!segs || c.disabled || /^(submit|button|reset|image|fieldset|output)$/.test(type) || (type == 'radio' && !c.checked) || segs.some((s) => BAD_KEY.test(s)))
1243
+ continue;
1244
+ const k = segs.pop();
1245
+ let o = out;
1246
+ segs.forEach((s, i) => (o = o[s] ??= /^\d+$/.test(segs[i + 1] ?? k) ? [] : Object.create(null)));
1247
+ if (type == 'checkbox' && boxes[name] > 1)
1248
+ (o[k] ??= []).push(...(c.checked ? [c.value] : []));
1249
+ else
1250
+ o[k] = type == 'checkbox' ? c.checked
1251
+ : /^(number|range)$/.test(type) ? (c.value === '' ? null : +c.value)
1252
+ : type == 'select-multiple' ? [...c.selectedOptions].map((x) => x.value)
1253
+ : c.value;
1254
+ }
1255
+ return out;
1256
+ }