@tiaforge-ui/sinter 0.1.0 → 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.
- package/README.md +37 -11
- package/dist/check/index.d.ts +4 -1
- package/dist/check/index.js +7 -7
- package/dist/check/shadow.d.ts +3 -1
- package/dist/check/shadow.js +37 -3
- package/dist/cli.js +2 -2
- package/dist/compiler/check-unit.d.ts +12 -0
- package/dist/compiler/compile.d.ts +4 -0
- package/dist/compiler/compile.js +206 -23
- package/dist/compiler/diagnostic-docs.js +10 -5
- package/dist/compiler/diagnostics.d.ts +1 -0
- package/dist/compiler/diagnostics.js +1 -0
- package/dist/compiler/expr.d.ts +8 -0
- package/dist/compiler/expr.js +20 -5
- package/dist/compiler/index.d.ts +3 -2
- package/dist/compiler/index.js +70 -16
- package/dist/compiler/vocabulary.js +3 -2
- package/dist/each.d.ts +3 -0
- package/dist/each.js +121 -0
- package/dist/index.d.ts +1 -1
- package/dist/ir.d.ts +26 -1
- package/dist/lsp/typescript.js +1 -1
- package/dist/lsp/workspace.js +3 -2
- package/dist/paginate.js +18 -4
- package/dist/project.d.ts +6 -0
- package/dist/project.js +22 -19
- package/dist/runtime/app.d.ts +28 -1
- package/dist/runtime/app.js +97 -24
- package/dist/runtime/config.d.ts +3 -3
- package/dist/runtime/config.js +1 -1
- package/dist/runtime/store.d.ts +7 -4
- package/dist/runtime/store.js +154 -41
- package/dist/sse.js +8 -5
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +16 -2
- package/dist/vite.js +4 -2
- package/docs/diagnostics.md +15 -5
- package/html-custom-data.json +17 -1
- package/llms.txt +105 -4
- package/package.json +4 -1
package/dist/runtime/app.js
CHANGED
|
@@ -30,11 +30,10 @@ const after = (el) => {
|
|
|
30
30
|
const ET = EventTarget.prototype;
|
|
31
31
|
/** Per-element registries, kept on the element (cheaper than WeakMaps on the row creation path). */
|
|
32
32
|
const HANDLERS = Symbol('sinter.on'), MODEL = Symbol('sinter.model'), STATE = Symbol('sinter.state');
|
|
33
|
-
const
|
|
33
|
+
const indexReaders = new WeakMap();
|
|
34
34
|
/** row templates: whether each holds one element (see oneElement()) */
|
|
35
35
|
const prepared = new WeakMap();
|
|
36
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
37
|
/** The instance mount() created on `root`; throws if none. */
|
|
39
38
|
export function instance(root) {
|
|
40
39
|
const app = instances.get(root);
|
|
@@ -76,6 +75,22 @@ export function mount(root, o) {
|
|
|
76
75
|
};
|
|
77
76
|
/** Elements written through callbacks (`$.$m(row, …)`) since the last flush. */
|
|
78
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)));
|
|
79
94
|
/** Per flush round: how dirty paths reach a template's rows (0 not, 1 only marked elements, 2 any row). */
|
|
80
95
|
let rowHits = new Map();
|
|
81
96
|
/**
|
|
@@ -170,6 +185,7 @@ export function mount(root, o) {
|
|
|
170
185
|
c();
|
|
171
186
|
instances.delete(root);
|
|
172
187
|
},
|
|
188
|
+
scopeOf: (i) => (i < 0 ? app.page : rootScopes[i] ?? app.page),
|
|
173
189
|
url(tpl, s) {
|
|
174
190
|
let out = '', query = false, dropped = false;
|
|
175
191
|
tpl.forEach((p, i) => {
|
|
@@ -298,6 +314,10 @@ export function mount(root, o) {
|
|
|
298
314
|
}
|
|
299
315
|
for (const def of ir.resources)
|
|
300
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
|
+
}
|
|
301
321
|
// listeners for every event type the page binds, rows included
|
|
302
322
|
listen('toggle');
|
|
303
323
|
for (const nd of [...ir.roots, ...ir.templates.flatMap((t) => t.nodes)])
|
|
@@ -390,10 +410,10 @@ export function mount(root, o) {
|
|
|
390
410
|
cleanups.push(() => ET.removeEventListener.call(root, type, dispatch, true));
|
|
391
411
|
}
|
|
392
412
|
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
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.
|
|
395
415
|
*/
|
|
396
|
-
function bindNodes(defs, els, base, rbs
|
|
416
|
+
function bindNodes(defs, els, base, rbs) {
|
|
397
417
|
// scopes of elements that declare state (and their descendants); most rows have none
|
|
398
418
|
let scopes = null;
|
|
399
419
|
for (let i = 0; i < defs.length; i++) {
|
|
@@ -416,7 +436,7 @@ export function mount(root, o) {
|
|
|
416
436
|
rootScopes[i] = s;
|
|
417
437
|
if (el)
|
|
418
438
|
for (const b of nd.b) {
|
|
419
|
-
const rb = { b, el, s, v: UNSET, c:
|
|
439
|
+
const rb = { b, el, s, v: UNSET, c: !!nd.dl };
|
|
420
440
|
if (b.k == 'on') {
|
|
421
441
|
const h = el[HANDLERS];
|
|
422
442
|
el[HANDLERS] = h == null ? rb : Array.isArray(h) ? (h.push(rb), h) : [h, rb];
|
|
@@ -484,7 +504,7 @@ export function mount(root, o) {
|
|
|
484
504
|
/** Evaluates and writes one binding if needed; false while it waits in a closed dialog. */
|
|
485
505
|
function process(rb, level) {
|
|
486
506
|
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)))
|
|
507
|
+
if (!force && !(deferred.size && deferred.has(rb)) && !(rb.b.d && hit(rb.d ??= depsOf(rb.b, rb.b.d), rb.s)))
|
|
488
508
|
return true;
|
|
489
509
|
if (rb.c && rb.el.closest('dialog:not([open])')) {
|
|
490
510
|
deferred.add(rb);
|
|
@@ -762,7 +782,7 @@ export function mount(root, o) {
|
|
|
762
782
|
els[k] = node && node.localName == defs[k].tag ? node : null;
|
|
763
783
|
}
|
|
764
784
|
}
|
|
765
|
-
if (isSimple(
|
|
785
|
+
if (isSimple(t)) {
|
|
766
786
|
// only value bindings and handlers: bind and write each once, as walk(rbs, 2) would
|
|
767
787
|
for (let k = 0; k < defs.length; k++) {
|
|
768
788
|
const el = els[k];
|
|
@@ -782,7 +802,7 @@ export function mount(root, o) {
|
|
|
782
802
|
}
|
|
783
803
|
}
|
|
784
804
|
else {
|
|
785
|
-
bindNodes(defs, els, s, rbs
|
|
805
|
+
bindNodes(defs, els, s, rbs);
|
|
786
806
|
walk(rbs, 2);
|
|
787
807
|
}
|
|
788
808
|
const nodes = [];
|
|
@@ -804,10 +824,10 @@ export function mount(root, o) {
|
|
|
804
824
|
return one;
|
|
805
825
|
}
|
|
806
826
|
/** A row template whose elements only have value bindings and handlers (no state, model, nested template or dialog). */
|
|
807
|
-
function isSimple(
|
|
827
|
+
function isSimple(t) {
|
|
808
828
|
let v = simple.get(t);
|
|
809
829
|
if (v == null) {
|
|
810
|
-
v =
|
|
830
|
+
v = t.nodes.every((nd) => !nd.dl && nd.state == null && nd.b.every((b) => SIMPLE.has(b.k)));
|
|
811
831
|
simple.set(t, v);
|
|
812
832
|
}
|
|
813
833
|
return v;
|
|
@@ -843,7 +863,7 @@ export function mount(root, o) {
|
|
|
843
863
|
const h = el[HANDLERS], hs = h && (Array.isArray(h) ? h : [h]).filter((x) => x.b.n == e.type);
|
|
844
864
|
if (hs?.length) {
|
|
845
865
|
for (const h of hs)
|
|
846
|
-
h.b.x != null ? trigger(h, e) : handle(h, h.b.f, e, h.b.w);
|
|
866
|
+
h.b.x != null ? trigger(h, e) : h.b.ms ? later(h, e) : handle(h, h.b.f, e, h.b.w);
|
|
847
867
|
return;
|
|
848
868
|
}
|
|
849
869
|
if (el == root)
|
|
@@ -864,6 +884,16 @@ export function mount(root, o) {
|
|
|
864
884
|
if (writes?.some((w) => w.startsWith('$url')))
|
|
865
885
|
writeUrl();
|
|
866
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
|
+
}
|
|
867
897
|
// ---- actions
|
|
868
898
|
function trigger(rb, e) {
|
|
869
899
|
const a = ir.actions[rb.b.x], el = rb.el;
|
|
@@ -889,10 +919,15 @@ export function mount(root, o) {
|
|
|
889
919
|
if (!get && a.concurrency == 'block' && st.n)
|
|
890
920
|
return;
|
|
891
921
|
const s = child(rb.s, { $el: rb.el, $event: e });
|
|
892
|
-
let url, body;
|
|
922
|
+
let url = '', body;
|
|
893
923
|
try {
|
|
894
|
-
|
|
895
|
-
|
|
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
|
+
}
|
|
896
931
|
}
|
|
897
932
|
catch (err) {
|
|
898
933
|
return unhandled(toError(err), rb.el);
|
|
@@ -924,7 +959,9 @@ export function mount(root, o) {
|
|
|
924
959
|
x.disabled = true;
|
|
925
960
|
}
|
|
926
961
|
try {
|
|
927
|
-
const result =
|
|
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 });
|
|
928
965
|
if (c && st.c != c)
|
|
929
966
|
return;
|
|
930
967
|
drop();
|
|
@@ -935,7 +972,7 @@ export function mount(root, o) {
|
|
|
935
972
|
drop();
|
|
936
973
|
if (isAbort(err) || (c && st.c != c))
|
|
937
974
|
return;
|
|
938
|
-
const error = toError(err);
|
|
975
|
+
const error = a.call ? callError(err) : toError(err);
|
|
939
976
|
a.catch != null ? handle(rb, a.catch, null, a.w, { $error: error }) : unhandled(error, el);
|
|
940
977
|
}
|
|
941
978
|
finally {
|
|
@@ -968,14 +1005,34 @@ export function mount(root, o) {
|
|
|
968
1005
|
}
|
|
969
1006
|
// ---- resources
|
|
970
1007
|
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) });
|
|
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 });
|
|
972
1009
|
r.onDirty = (what) => app.mark((what == 'data' ? '$r.' : '$meta.') + def.name);
|
|
973
1010
|
resources.set(def.name, r);
|
|
974
1011
|
Object.defineProperty(app.page.$r, def.name, { get: r.value, enumerable: true });
|
|
975
1012
|
app.page.$meta[def.name] = r.meta;
|
|
976
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;
|
|
977
1017
|
const resolve = () => {
|
|
978
|
-
const s =
|
|
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
|
+
}
|
|
979
1036
|
try {
|
|
980
1037
|
return def.when != null && !fns[def.when](s) ? null : app.url(def.src, s);
|
|
981
1038
|
}
|
|
@@ -1010,8 +1067,23 @@ export function mount(root, o) {
|
|
|
1010
1067
|
};
|
|
1011
1068
|
/** the request URL for the current page of `u` */
|
|
1012
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
|
+
};
|
|
1013
1076
|
const l = {
|
|
1014
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
|
+
},
|
|
1015
1087
|
start() {
|
|
1016
1088
|
const el = def.data != null ? found.get(def.data) : null;
|
|
1017
1089
|
let data;
|
|
@@ -1069,7 +1141,9 @@ export function mount(root, o) {
|
|
|
1069
1141
|
loading(true);
|
|
1070
1142
|
try {
|
|
1071
1143
|
const req = withParams(url, m ? l.pager.params(m) : null);
|
|
1072
|
-
const body =
|
|
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);
|
|
1073
1147
|
if (n != seq)
|
|
1074
1148
|
return;
|
|
1075
1149
|
store(body, req, m);
|
|
@@ -1080,10 +1154,7 @@ export function mount(root, o) {
|
|
|
1080
1154
|
catch (e) {
|
|
1081
1155
|
if (n != seq || isAbort(e))
|
|
1082
1156
|
return;
|
|
1083
|
-
|
|
1084
|
-
r.meta.error = error;
|
|
1085
|
-
r.onDirty('meta');
|
|
1086
|
-
unhandled(error, null, def.name);
|
|
1157
|
+
fail(def.call ? callError(e) : e);
|
|
1087
1158
|
}
|
|
1088
1159
|
finally {
|
|
1089
1160
|
// writes made while this load ran win over its (older) response
|
|
@@ -1095,6 +1166,8 @@ export function mount(root, o) {
|
|
|
1095
1166
|
}
|
|
1096
1167
|
},
|
|
1097
1168
|
};
|
|
1169
|
+
if (def.each)
|
|
1170
|
+
app.ext.each.fanOut(l);
|
|
1098
1171
|
if (def.paginate)
|
|
1099
1172
|
l.pager = app.ext.paginate.pager(l);
|
|
1100
1173
|
r.loader = l;
|
package/dist/runtime/config.d.ts
CHANGED
|
@@ -19,11 +19,11 @@ export declare const config: Config;
|
|
|
19
19
|
export declare function configure(options: Partial<Omit<Config, 'env'>> & {
|
|
20
20
|
env?: Partial<Env>;
|
|
21
21
|
}): void;
|
|
22
|
-
/** `$error`: a non-2xx response, a network error (status 0) or a
|
|
22
|
+
/** `$error`: a non-2xx response, a network error (status 0), a refused request, or a failed action call (status null). */
|
|
23
23
|
export declare class SinterError extends Error {
|
|
24
|
-
readonly status: number;
|
|
24
|
+
readonly status: number | null;
|
|
25
25
|
readonly body: unknown;
|
|
26
|
-
constructor(status: number, body: unknown, message: string);
|
|
26
|
+
constructor(status: number | null, body: unknown, message: string);
|
|
27
27
|
}
|
|
28
28
|
/** page-load cache entries (`cache` on `<sn-resource>`), in sessionStorage */
|
|
29
29
|
export declare const CACHE_PREFIX = "sn:c:";
|
package/dist/runtime/config.js
CHANGED
|
@@ -10,7 +10,7 @@ export function configure(options) {
|
|
|
10
10
|
if (env)
|
|
11
11
|
config.env = { ...config.env, ...env };
|
|
12
12
|
}
|
|
13
|
-
/** `$error`: a non-2xx response, a network error (status 0) or a
|
|
13
|
+
/** `$error`: a non-2xx response, a network error (status 0), a refused request, or a failed action call (status null). */
|
|
14
14
|
export class SinterError extends Error {
|
|
15
15
|
status;
|
|
16
16
|
body;
|
package/dist/runtime/store.d.ts
CHANGED
|
@@ -29,14 +29,14 @@ export type Op = {
|
|
|
29
29
|
export interface Meta {
|
|
30
30
|
loading: boolean;
|
|
31
31
|
error: {
|
|
32
|
-
status: number;
|
|
32
|
+
status: number | null;
|
|
33
33
|
body: unknown;
|
|
34
34
|
message: string;
|
|
35
35
|
} | null;
|
|
36
36
|
loadedAt: number | null;
|
|
37
37
|
pending: number;
|
|
38
38
|
total: number | null;
|
|
39
|
-
page?: number;
|
|
39
|
+
page?: number | null;
|
|
40
40
|
pages?: number | null;
|
|
41
41
|
from?: number | null;
|
|
42
42
|
to?: number | null;
|
|
@@ -50,6 +50,8 @@ export interface ResourceOptions {
|
|
|
50
50
|
warn?: (code: number, ...args: unknown[]) => void;
|
|
51
51
|
/** applied to every stored item (dev builds freeze them) */
|
|
52
52
|
prep?: <T>(item: T) => T;
|
|
53
|
+
/** hand out frozen lists (dev builds) */
|
|
54
|
+
freeze?: boolean;
|
|
53
55
|
}
|
|
54
56
|
/** The public store handle (`app.resource(name)`), plus the hooks loaders and features use. */
|
|
55
57
|
export interface Resource<T extends object = any> {
|
|
@@ -66,7 +68,8 @@ export interface Resource<T extends object = any> {
|
|
|
66
68
|
upsert(item: T): void;
|
|
67
69
|
patch(key: string | T, fields: Partial<T>): void;
|
|
68
70
|
remove(key: string | T): void;
|
|
69
|
-
|
|
71
|
+
/** `hint`: a server event's payload; an `each` resource reloads only the source it names */
|
|
72
|
+
refetch(hint?: object): Promise<void>;
|
|
70
73
|
subscribe(fn: (change: Change<T>) => void): () => void;
|
|
71
74
|
keyOf(item: string | T): string;
|
|
72
75
|
/**
|
|
@@ -86,7 +89,7 @@ export interface Resource<T extends object = any> {
|
|
|
86
89
|
/** while set, upsert/patch/remove are recorded instead of applied */
|
|
87
90
|
recording: Op[] | null;
|
|
88
91
|
loader: {
|
|
89
|
-
refetch(): Promise<void>;
|
|
92
|
+
refetch(hint?: object): Promise<void>;
|
|
90
93
|
} | null;
|
|
91
94
|
onDirty(what: 'data' | 'meta'): void;
|
|
92
95
|
}
|
package/dist/runtime/store.js
CHANGED
|
@@ -12,13 +12,27 @@ export function clean(obj) {
|
|
|
12
12
|
out[k] = obj[k];
|
|
13
13
|
return out;
|
|
14
14
|
}
|
|
15
|
+
/** One collator per locale: constructing one is costly in some engines. */
|
|
16
|
+
const collators = new Map();
|
|
17
|
+
const collator = (l = '') => collators.get(l) ?? (collators.set(l, new Intl.Collator(l || undefined, { numeric: true })), collators.get(l));
|
|
15
18
|
export function createResource(name, o) {
|
|
16
19
|
const meta = { loading: false, error: null, loadedAt: null, pending: 0, total: null };
|
|
17
20
|
const coll = !!o.key, field = o.order?.replace(/^-/, ''), dir = o.order?.[0] == '-' ? -1 : 1;
|
|
18
21
|
const subs = new Set();
|
|
19
22
|
const prep = (x) => (o.prep ? o.prep(x) : x);
|
|
20
|
-
|
|
23
|
+
/** base items in arrival order, and each key's arrival number (ties in `order` keep arrival order) */
|
|
24
|
+
let base = new Map(), seq = new Map(), next = 0;
|
|
25
|
+
let single = null, total = null;
|
|
21
26
|
let layers = [], lastLayer = 0;
|
|
27
|
+
/**
|
|
28
|
+
* The base in display order, the locale it was sorted for, and the keys written since, with
|
|
29
|
+
* their value before the first write. Reads patch it instead of sorting
|
|
30
|
+
* again. Arrays handed out are never changed afterwards.
|
|
31
|
+
*/
|
|
32
|
+
let sorted = null, sortedFor, touched = new Map();
|
|
33
|
+
/** Optimistic layers applied over the base: changed keys (null: removed), keys they add (arrival numbers), removals counted for `total`. */
|
|
34
|
+
let over = null;
|
|
35
|
+
/** what readers see; rebuilt on the first read after a change */
|
|
22
36
|
let view = null;
|
|
23
37
|
let journal = null, loads = 0;
|
|
24
38
|
const keyOf = (x) => typeof x == 'string' ? x : (o.key ?? []).map((f) => {
|
|
@@ -29,71 +43,171 @@ export function createResource(name, o) {
|
|
|
29
43
|
const merge = (cur, op) => op.op == 'upsert' ? prep(op.item)
|
|
30
44
|
: op.op == 'remove' ? null
|
|
31
45
|
: cur && prep({ ...cur, ...clean(op.fields) });
|
|
32
|
-
/** Applies one op to
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
/** Applies one op to the base. */
|
|
47
|
+
const apply = (op) => {
|
|
48
|
+
if (!coll)
|
|
49
|
+
return void (single = merge(single, op));
|
|
50
|
+
const k = op.op == 'upsert' ? keyOf(op.item) : op.key, cur = base.get(k), v = merge(cur, op);
|
|
51
|
+
if (sorted && !touched.has(k))
|
|
52
|
+
touched.set(k, cur);
|
|
53
|
+
if (v) {
|
|
54
|
+
if (!cur)
|
|
55
|
+
seq.set(k, next++);
|
|
56
|
+
base.set(k, v);
|
|
57
|
+
}
|
|
58
|
+
else if (cur) {
|
|
59
|
+
base.delete(k);
|
|
60
|
+
seq.delete(k);
|
|
61
|
+
}
|
|
39
62
|
};
|
|
40
|
-
|
|
63
|
+
/** By `order` (none: all equal, so arrival order decides). */
|
|
64
|
+
const compare = (a, b) => {
|
|
65
|
+
if (!field)
|
|
66
|
+
return 0;
|
|
67
|
+
const x = a[field], y = b[field];
|
|
68
|
+
if (x == null || y == null)
|
|
69
|
+
return +(x == null) - +(y == null);
|
|
70
|
+
return dir * (typeof x == 'string' && typeof y == 'string' ? collator(sortedFor).compare(x, y) : +(x > y) - +(x < y));
|
|
71
|
+
};
|
|
72
|
+
/** A sorted copy of `list` without the `drop` items and with the `add` items (each with its arrival number). */
|
|
73
|
+
function patch(list, drop, add, arrival) {
|
|
74
|
+
const out = list.slice();
|
|
75
|
+
/** the first index whose item does not sort before `v` (with arrival number `n`, after its ties) */
|
|
76
|
+
const find = (v, n) => {
|
|
77
|
+
let lo = 0, hi = out.length;
|
|
78
|
+
while (lo < hi) {
|
|
79
|
+
const mid = (lo + hi) >> 1, c = compare(out[mid], v) || (n == null ? 0 : arrival(keyOf(out[mid])) - n);
|
|
80
|
+
c < 0 ? (lo = mid + 1) : (hi = mid);
|
|
81
|
+
}
|
|
82
|
+
return lo;
|
|
83
|
+
};
|
|
84
|
+
for (const v of drop) {
|
|
85
|
+
// among its ties, by identity; an item whose order field was changed in place is found by a scan
|
|
86
|
+
let i = find(v);
|
|
87
|
+
while (out[i] !== v && i < out.length && !compare(out[i], v))
|
|
88
|
+
i++;
|
|
89
|
+
out.splice(out[i] === v ? i : out.indexOf(v), 1);
|
|
90
|
+
}
|
|
91
|
+
for (const [v, n] of add)
|
|
92
|
+
out.splice(find(v, n), 0, v);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** The base in display order. */
|
|
96
|
+
function baseList() {
|
|
97
|
+
const locale = o.locale?.();
|
|
98
|
+
if (sorted && locale === sortedFor && touched.size <= 64) {
|
|
99
|
+
if (touched.size) {
|
|
100
|
+
const drop = [], add = [];
|
|
101
|
+
for (const [k, old] of touched) {
|
|
102
|
+
if (old)
|
|
103
|
+
drop.push(old);
|
|
104
|
+
const v = base.get(k);
|
|
105
|
+
if (v)
|
|
106
|
+
add.push([v, seq.get(k)]);
|
|
107
|
+
}
|
|
108
|
+
sorted = patch(sorted, drop, add, (k) => seq.get(k));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
sortedFor = locale;
|
|
113
|
+
// a stable sort of the Map's arrival order
|
|
114
|
+
sorted = [...base.values()].sort(compare);
|
|
115
|
+
}
|
|
116
|
+
touched = new Map();
|
|
117
|
+
return sorted;
|
|
118
|
+
}
|
|
119
|
+
/** Recomputes the layers' effect and `meta.total` after any change; marks `meta` if the total changed. */
|
|
120
|
+
function sync() {
|
|
121
|
+
over = null;
|
|
122
|
+
if (layers.length) {
|
|
123
|
+
const m = new Map(), add = new Map();
|
|
124
|
+
let removed = 0, n = next, s = single;
|
|
125
|
+
for (const l of layers)
|
|
126
|
+
for (const op of l.ops) {
|
|
127
|
+
if (!coll) {
|
|
128
|
+
s = merge(s, op);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const k = op.op == 'upsert' ? keyOf(op.item) : op.key, cur = m.has(k) ? m.get(k) : base.get(k), v = merge(cur, op);
|
|
132
|
+
if (v) {
|
|
133
|
+
if (!cur)
|
|
134
|
+
add.set(k, n++);
|
|
135
|
+
m.set(k, v);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
if (cur)
|
|
139
|
+
removed++;
|
|
140
|
+
m.set(k, null);
|
|
141
|
+
add.delete(k);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
over = { m, add, removed, s };
|
|
145
|
+
}
|
|
146
|
+
const t = total == null ? null : Math.max(0, total - (over?.removed ?? 0));
|
|
147
|
+
if (meta.total !== t) {
|
|
148
|
+
meta.total = t;
|
|
149
|
+
r.onDirty('meta');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** A data change: readers rebuild their view; `meta` too when the pending count changed. */
|
|
153
|
+
const changed = (pending) => {
|
|
41
154
|
view = null;
|
|
155
|
+
sync();
|
|
42
156
|
r.onDirty('data');
|
|
43
|
-
if (
|
|
157
|
+
if (pending) {
|
|
158
|
+
meta.pending = layers.length;
|
|
44
159
|
r.onDirty('meta');
|
|
160
|
+
}
|
|
45
161
|
};
|
|
46
162
|
function visible() {
|
|
47
163
|
if (view)
|
|
48
164
|
return view;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return dir * (typeof x == 'string' && typeof y == 'string' ? c.compare(x, y) : +(x > y) - +(x < y));
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
view = { l: Object.freeze(l), m, s };
|
|
67
|
-
if (meta.total !== t) {
|
|
68
|
-
meta.total = t;
|
|
69
|
-
r.onDirty('meta');
|
|
165
|
+
if (!coll)
|
|
166
|
+
return (view = { l: [], get: () => undefined, s: over ? over.s : single });
|
|
167
|
+
let l = baseList(), get = (k) => base.get(k);
|
|
168
|
+
if (over) {
|
|
169
|
+
const { m, add } = over, drop = [], put = [], at = (k) => add.get(k) ?? seq.get(k);
|
|
170
|
+
for (const [k, v] of m) {
|
|
171
|
+
const b = base.get(k);
|
|
172
|
+
if (b)
|
|
173
|
+
drop.push(b);
|
|
174
|
+
if (v)
|
|
175
|
+
put.push([v, at(k)]);
|
|
176
|
+
}
|
|
177
|
+
l = patch(l, drop, put, at);
|
|
178
|
+
get = (k) => (m.has(k) ? m.get(k) ?? undefined : base.get(k));
|
|
70
179
|
}
|
|
71
|
-
|
|
180
|
+
// dev builds freeze what expressions see; the compiler refuses writes to resources (SN205)
|
|
181
|
+
return (view = { l: o.freeze ? Object.freeze(l.slice()) : l, get, s: null });
|
|
72
182
|
}
|
|
73
183
|
function write(op) {
|
|
74
184
|
if (r.recording)
|
|
75
185
|
return void r.recording.push(op);
|
|
76
186
|
journal?.push(op);
|
|
77
|
-
|
|
187
|
+
apply(op);
|
|
78
188
|
changed();
|
|
79
189
|
for (const f of subs)
|
|
80
190
|
f({ resource: name, ...op });
|
|
81
191
|
}
|
|
82
192
|
const r = {
|
|
83
193
|
name, meta, key: o.key, isCollection: coll, keyOf, recording: null, loader: null, onDirty() { },
|
|
84
|
-
get: (k) => (coll ? visible().
|
|
194
|
+
get: (k) => (coll ? visible().get(keyOf(k)) : visible().s ?? undefined),
|
|
85
195
|
has: (k) => r.get(k) !== undefined,
|
|
86
196
|
all: () => visible().l,
|
|
87
197
|
value: () => (coll ? visible().l : visible().s),
|
|
88
198
|
replace(items) {
|
|
89
199
|
if (coll) {
|
|
90
200
|
base = new Map();
|
|
201
|
+
seq = new Map();
|
|
202
|
+
sorted = null;
|
|
203
|
+
touched = new Map();
|
|
91
204
|
for (const x of Array.isArray(items) ? items : []) {
|
|
92
205
|
const k = keyOf(x);
|
|
93
206
|
if (base.has(k))
|
|
94
207
|
o.warn?.(13, name, k);
|
|
95
208
|
base.delete(k);
|
|
96
209
|
base.set(k, prep(x));
|
|
210
|
+
seq.set(k, next++);
|
|
97
211
|
}
|
|
98
212
|
}
|
|
99
213
|
else
|
|
@@ -105,7 +219,7 @@ export function createResource(name, o) {
|
|
|
105
219
|
upsert: (item) => write({ op: 'upsert', item }),
|
|
106
220
|
patch: (k, fields) => write({ op: 'patch', key: keyOf(k), fields }),
|
|
107
221
|
remove: (k) => write({ op: 'remove', key: keyOf(k) }),
|
|
108
|
-
refetch: () => r.loader?.refetch() ?? Promise.resolve(),
|
|
222
|
+
refetch: (hint) => r.loader?.refetch(hint) ?? Promise.resolve(),
|
|
109
223
|
subscribe: (fn) => (subs.add(fn), () => subs.delete(fn)),
|
|
110
224
|
startLoad() {
|
|
111
225
|
loads++;
|
|
@@ -119,23 +233,22 @@ export function createResource(name, o) {
|
|
|
119
233
|
if (!loaded || !ops.length)
|
|
120
234
|
return;
|
|
121
235
|
for (const op of ops)
|
|
122
|
-
|
|
236
|
+
apply(op);
|
|
123
237
|
changed();
|
|
124
238
|
},
|
|
125
239
|
addLayer(ops) {
|
|
126
240
|
layers.push({ id: ++lastLayer, ops });
|
|
127
|
-
|
|
128
|
-
changed('meta');
|
|
241
|
+
changed(true);
|
|
129
242
|
return lastLayer;
|
|
130
243
|
},
|
|
131
244
|
dropLayer(id) {
|
|
132
245
|
const n = layers.length;
|
|
133
246
|
layers = layers.filter((l) => l.id != id);
|
|
134
|
-
meta.pending = layers.length;
|
|
135
247
|
if (n != layers.length)
|
|
136
|
-
changed(
|
|
248
|
+
changed(true);
|
|
137
249
|
},
|
|
138
|
-
|
|
250
|
+
/** the total changes `meta` only: bindings that read the items do not re-run */
|
|
251
|
+
setTotal(t) { total = t; sync(); },
|
|
139
252
|
base: () => ({ items: coll ? [...base.values()] : single, total }),
|
|
140
253
|
};
|
|
141
254
|
return r;
|
package/dist/sse.js
CHANGED
|
@@ -45,10 +45,13 @@ class Connection {
|
|
|
45
45
|
everOpen = false;
|
|
46
46
|
url;
|
|
47
47
|
fed;
|
|
48
|
+
/** where `src` and the events' expressions are evaluated: the enclosing state's scope, or the page */
|
|
49
|
+
base;
|
|
48
50
|
constructor(app, def) {
|
|
49
51
|
this.app = app;
|
|
50
52
|
this.def = def;
|
|
51
|
-
this.
|
|
53
|
+
this.base = app.scopeOf(def.scope ?? -1);
|
|
54
|
+
this.url = app.url(def.src, this.base);
|
|
52
55
|
this.fed = [...new Set(def.events.map((e) => e.resource))];
|
|
53
56
|
}
|
|
54
57
|
start() {
|
|
@@ -116,7 +119,7 @@ class Connection {
|
|
|
116
119
|
status = (await this.app.req.raw(config.sessionProbe)).status;
|
|
117
120
|
}
|
|
118
121
|
catch (e) {
|
|
119
|
-
status = e instanceof SinterError ? e.status : 0;
|
|
122
|
+
status = e instanceof SinterError ? e.status ?? 0 : 0;
|
|
120
123
|
}
|
|
121
124
|
if (this.closed || this.hidden)
|
|
122
125
|
return;
|
|
@@ -139,7 +142,7 @@ class Connection {
|
|
|
139
142
|
}
|
|
140
143
|
apply(def, payload) {
|
|
141
144
|
const app = this.app;
|
|
142
|
-
const scope = Object.create(
|
|
145
|
+
const scope = Object.create(this.base);
|
|
143
146
|
scope.$p = payload && typeof payload === 'object' ? payload : {};
|
|
144
147
|
const r = app.resource(def.resource);
|
|
145
148
|
try {
|
|
@@ -148,13 +151,13 @@ class Connection {
|
|
|
148
151
|
const paginated = !!app.ir.resources.find((x) => x.name === def.resource)?.paginate;
|
|
149
152
|
switch (def.op) {
|
|
150
153
|
case 'refetch':
|
|
151
|
-
void r.refetch();
|
|
154
|
+
void r.refetch(scope.$p);
|
|
152
155
|
return;
|
|
153
156
|
case 'upsert': {
|
|
154
157
|
const item = def.item != null ? app.fns[def.item](scope) : payload;
|
|
155
158
|
if (item == null || typeof item !== 'object') {
|
|
156
159
|
if (def.fallback === 'refetch')
|
|
157
|
-
void r.refetch();
|
|
160
|
+
void r.refetch(scope.$p);
|
|
158
161
|
return;
|
|
159
162
|
}
|
|
160
163
|
if (paginated && !r.has(item)) {
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface RenderOptions {
|
|
|
44
44
|
now?: number | Date;
|
|
45
45
|
/** helper functions, and namespaces as objects of functions */
|
|
46
46
|
helpers?: Record<string, unknown>;
|
|
47
|
+
/** action functions for `data-sn-call` and `<sn-resource call>`, and namespaces as objects of functions */
|
|
48
|
+
actions?: Record<string, unknown>;
|
|
47
49
|
/** partials for `<sn-include>`, by path relative to the page */
|
|
48
50
|
files?: Record<string, string>;
|
|
49
51
|
/** localStorage and sessionStorage (default: empty ones); pass the previous page's to test what carries over */
|