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