@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,142 @@
1
+ // The store: one keyed (or single-object) cache per resource. It knows nothing
2
+ // about the DOM, fetch or SSE. Each resource holds a base (what the server last
3
+ // confirmed) and ordered optimistic layers; readers see base + layers.
4
+ import { BAD_KEY } from './security.js';
5
+ /** Copies own enumerable fields, dropping keys that could pollute prototypes. */
6
+ export function clean(obj) {
7
+ if (!obj || typeof obj != 'object' || Array.isArray(obj))
8
+ return obj;
9
+ const out = {};
10
+ for (const k of Object.keys(obj))
11
+ if (!BAD_KEY.test(k))
12
+ out[k] = obj[k];
13
+ return out;
14
+ }
15
+ export function createResource(name, o) {
16
+ const meta = { loading: false, error: null, loadedAt: null, pending: 0, total: null };
17
+ const coll = !!o.key, field = o.order?.replace(/^-/, ''), dir = o.order?.[0] == '-' ? -1 : 1;
18
+ const subs = new Set();
19
+ const prep = (x) => (o.prep ? o.prep(x) : x);
20
+ let base = new Map(), single = null, total = null;
21
+ let layers = [], lastLayer = 0;
22
+ let view = null;
23
+ let journal = null, loads = 0;
24
+ const keyOf = (x) => typeof x == 'string' ? x : (o.key ?? []).map((f) => {
25
+ if (x[f] === undefined)
26
+ o.warn?.(14, name, f);
27
+ return x[f];
28
+ }).join('\x1f');
29
+ const merge = (cur, op) => op.op == 'upsert' ? prep(op.item)
30
+ : op.op == 'remove' ? null
31
+ : cur && prep({ ...cur, ...clean(op.fields) });
32
+ /** Applies one op to a keyed map; counts removals of present items. */
33
+ const applyTo = (m, op, removed) => {
34
+ const k = op.op == 'upsert' ? keyOf(op.item) : op.key, v = merge(m.get(k), op);
35
+ if (v)
36
+ m.set(k, v);
37
+ else if (m.delete(k) && removed)
38
+ removed.n++;
39
+ };
40
+ const changed = (what = 'data') => {
41
+ view = null;
42
+ r.onDirty('data');
43
+ if (what == 'meta')
44
+ r.onDirty('meta');
45
+ };
46
+ function visible() {
47
+ if (view)
48
+ return view;
49
+ let t = total, m = base, s = single;
50
+ const removed = { n: 0 };
51
+ for (const l of layers)
52
+ for (const op of l.ops)
53
+ coll ? (m == base && (m = new Map(base)), applyTo(m, op, removed)) : (s = merge(s, op));
54
+ if (t != null)
55
+ t = Math.max(0, t - removed.n);
56
+ const l = [...m.values()];
57
+ if (field) {
58
+ const c = new Intl.Collator(o.locale?.(), { numeric: true });
59
+ l.sort((a, b) => {
60
+ const x = a[field], y = b[field];
61
+ if (x == null || y == null)
62
+ return +(x == null) - +(y == null);
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');
70
+ }
71
+ return view;
72
+ }
73
+ function write(op) {
74
+ if (r.recording)
75
+ return void r.recording.push(op);
76
+ journal?.push(op);
77
+ coll ? applyTo(base, op) : (single = merge(single, op));
78
+ changed();
79
+ for (const f of subs)
80
+ f({ resource: name, ...op });
81
+ }
82
+ const r = {
83
+ name, meta, key: o.key, isCollection: coll, keyOf, recording: null, loader: null, onDirty() { },
84
+ get: (k) => (coll ? visible().m.get(keyOf(k)) : visible().s ?? undefined),
85
+ has: (k) => r.get(k) !== undefined,
86
+ all: () => visible().l,
87
+ value: () => (coll ? visible().l : visible().s),
88
+ replace(items) {
89
+ if (coll) {
90
+ base = new Map();
91
+ for (const x of Array.isArray(items) ? items : []) {
92
+ const k = keyOf(x);
93
+ if (base.has(k))
94
+ o.warn?.(13, name, k);
95
+ base.delete(k);
96
+ base.set(k, prep(x));
97
+ }
98
+ }
99
+ else
100
+ single = items == null ? null : prep(items);
101
+ changed();
102
+ for (const f of subs)
103
+ f({ resource: name, op: 'refetch' });
104
+ },
105
+ upsert: (item) => write({ op: 'upsert', item }),
106
+ patch: (k, fields) => write({ op: 'patch', key: keyOf(k), fields }),
107
+ remove: (k) => write({ op: 'remove', key: keyOf(k) }),
108
+ refetch: () => r.loader?.refetch() ?? Promise.resolve(),
109
+ subscribe: (fn) => (subs.add(fn), () => subs.delete(fn)),
110
+ startLoad() {
111
+ loads++;
112
+ journal ??= [];
113
+ return journal.length;
114
+ },
115
+ endLoad(mark, loaded) {
116
+ const ops = journal.slice(mark);
117
+ if (--loads == 0)
118
+ journal = null;
119
+ if (!loaded || !ops.length)
120
+ return;
121
+ for (const op of ops)
122
+ coll ? applyTo(base, op) : (single = merge(single, op));
123
+ changed();
124
+ },
125
+ addLayer(ops) {
126
+ layers.push({ id: ++lastLayer, ops });
127
+ meta.pending = layers.length;
128
+ changed('meta');
129
+ return lastLayer;
130
+ },
131
+ dropLayer(id) {
132
+ const n = layers.length;
133
+ layers = layers.filter((l) => l.id != id);
134
+ meta.pending = layers.length;
135
+ if (n != layers.length)
136
+ changed('meta');
137
+ },
138
+ setTotal(t) { total = t; changed(); },
139
+ base: () => ({ items: coll ? [...base.values()] : single, total }),
140
+ };
141
+ return r;
142
+ }
package/dist/sse.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { Feature, Transport } from './runtime/app.js';
2
+ /** JSON payloads are parsed; anything else is delivered as the raw string. */
3
+ export declare const parsePayload: (data: string) => unknown;
4
+ /** The default transport: one EventSource. */
5
+ export declare function eventSourceTransport(): Transport;
6
+ declare const sse: Feature;
7
+ export default sse;
package/dist/sse.js ADDED
@@ -0,0 +1,206 @@
1
+ import { config, SinterError } from './runtime/config.js';
2
+ import { clean } from './runtime/store.js';
3
+ const MIN_BACKOFF = 1000;
4
+ const MAX_BACKOFF = 30_000;
5
+ /** JSON payloads are parsed; anything else is delivered as the raw string. */
6
+ export const parsePayload = (data) => { try {
7
+ return JSON.parse(data);
8
+ }
9
+ catch {
10
+ return data;
11
+ } };
12
+ /** The default transport: one EventSource. */
13
+ export function eventSourceTransport() {
14
+ let es = null;
15
+ let deliver = null;
16
+ const names = new Set();
17
+ const add = (name) => {
18
+ if (!es || names.has(name))
19
+ return;
20
+ names.add(name);
21
+ es.addEventListener(name, (e) => deliver?.(name, parsePayload(e.data)));
22
+ };
23
+ return {
24
+ connect(url, onEvent, onState, initial) {
25
+ deliver = onEvent;
26
+ es = new EventSource(url, { withCredentials: true });
27
+ es.onopen = () => onState('open');
28
+ es.onerror = () => onState('error');
29
+ names.clear();
30
+ for (const n of initial)
31
+ add(n);
32
+ },
33
+ listen: add,
34
+ close() { es?.close(); es = null; },
35
+ };
36
+ }
37
+ class Connection {
38
+ app;
39
+ def;
40
+ t = null;
41
+ backoff = MIN_BACKOFF;
42
+ timer = null;
43
+ closed = false;
44
+ hidden = false;
45
+ everOpen = false;
46
+ url;
47
+ fed;
48
+ constructor(app, def) {
49
+ this.app = app;
50
+ this.def = def;
51
+ this.url = app.url(def.src, app.page);
52
+ this.fed = [...new Set(def.events.map((e) => e.resource))];
53
+ }
54
+ start() {
55
+ const env = this.app.env;
56
+ this.app.addCleanup(env.listen('pagehide', () => { this.hidden = true; this.stop(); }));
57
+ this.app.addCleanup(env.listen('pageshow', () => {
58
+ if (!this.hidden)
59
+ return;
60
+ this.hidden = false;
61
+ this.closed = false;
62
+ void this.probe();
63
+ }));
64
+ this.app.onListen.push((name) => this.t?.listen?.(name));
65
+ this.app.addCleanup(() => { this.closed = true; this.stop(); });
66
+ this.open();
67
+ }
68
+ names() {
69
+ return [...new Set(this.def.events.map((e) => e.name))];
70
+ }
71
+ open() {
72
+ if (this.closed && !this.hidden)
73
+ return;
74
+ const custom = this.app.transport(this.url) ?? config.transport?.(this.url);
75
+ const t = custom ?? eventSourceTransport();
76
+ this.t = t;
77
+ const reconnect = this.everOpen;
78
+ t.connect(this.url, (name, data) => this.event(name, data), (state) => {
79
+ if (this.t !== t)
80
+ return;
81
+ if (state === 'open') {
82
+ this.backoff = MIN_BACKOFF;
83
+ this.everOpen = true;
84
+ if (!custom)
85
+ this.app.streamOpened?.(this.url);
86
+ // events sent while disconnected are lost: refetch everything this stream feeds
87
+ if (reconnect)
88
+ for (const name of this.fed)
89
+ void this.app.resource(name).refetch();
90
+ }
91
+ else {
92
+ t.close();
93
+ this.t = null;
94
+ void this.probe();
95
+ }
96
+ }, this.names());
97
+ }
98
+ stop() {
99
+ this.t?.close();
100
+ this.t = null;
101
+ if (this.timer != null) {
102
+ this.app.env.clearTimeout(this.timer);
103
+ this.timer = null;
104
+ }
105
+ }
106
+ /** The session probe gates every reconnect: 2xx reconnects, 401 leads to login, an outage backs off. */
107
+ async probe() {
108
+ if (this.closed || this.hidden)
109
+ return;
110
+ if (!config.sessionProbe) {
111
+ this.wait(() => this.open());
112
+ return;
113
+ }
114
+ let status;
115
+ try {
116
+ status = (await this.app.req.raw(config.sessionProbe)).status;
117
+ }
118
+ catch (e) {
119
+ status = e instanceof SinterError ? e.status : 0;
120
+ }
121
+ if (this.closed || this.hidden)
122
+ return;
123
+ if (status >= 200 && status < 300)
124
+ this.open();
125
+ else if (status !== 401)
126
+ this.wait(() => void this.probe());
127
+ // 401: the request layer's onUnauthorized handles login
128
+ }
129
+ wait(fn) {
130
+ const delay = Math.round(this.backoff * (0.75 + this.app.env.random() * 0.5));
131
+ this.backoff = Math.min(this.backoff * 2, MAX_BACKOFF);
132
+ this.timer = this.app.env.setTimeout(() => { this.timer = null; fn(); }, delay, 'backoff');
133
+ }
134
+ event(name, payload) {
135
+ for (const def of this.def.events)
136
+ if (def.name === name)
137
+ this.apply(def, payload);
138
+ this.app.serverEvent(name, payload);
139
+ }
140
+ apply(def, payload) {
141
+ const app = this.app;
142
+ const scope = Object.create(app.page);
143
+ scope.$p = payload && typeof payload === 'object' ? payload : {};
144
+ const r = app.resource(def.resource);
145
+ try {
146
+ if (def.when != null && !app.fns[def.when](scope))
147
+ return;
148
+ const paginated = !!app.ir.resources.find((x) => x.name === def.resource)?.paginate;
149
+ switch (def.op) {
150
+ case 'refetch':
151
+ void r.refetch();
152
+ return;
153
+ case 'upsert': {
154
+ const item = def.item != null ? app.fns[def.item](scope) : payload;
155
+ if (item == null || typeof item !== 'object') {
156
+ if (def.fallback === 'refetch')
157
+ void r.refetch();
158
+ return;
159
+ }
160
+ if (paginated && !r.has(item)) {
161
+ void r.refetch();
162
+ return;
163
+ }
164
+ r.upsert(clean(item));
165
+ return;
166
+ }
167
+ case 'patch': {
168
+ if (r.isCollection && !this.hasKey(r.key, scope.$p, def))
169
+ return;
170
+ const key = r.keyOf(scope.$p);
171
+ if (r.isCollection && !r.has(key))
172
+ return;
173
+ r.patch(key, clean(app.fns[def.set](scope)));
174
+ return;
175
+ }
176
+ case 'remove': {
177
+ if (r.isCollection && !this.hasKey(r.key, scope.$p, def))
178
+ return;
179
+ const key = r.keyOf(scope.$p);
180
+ r.remove(key);
181
+ return;
182
+ }
183
+ }
184
+ }
185
+ catch (e) {
186
+ app.warn(`event \`${def.name}\` could not be applied: ${e.message}`, payload);
187
+ }
188
+ }
189
+ hasKey(fields, payload, def) {
190
+ const missing = fields.filter((f) => payload[f] === undefined);
191
+ if (missing.length)
192
+ this.app.warn(`event \`${def.name}\` payload is missing key field(s) ${missing.join(', ')} for \`${def.resource}\``, payload);
193
+ return missing.length === 0;
194
+ }
195
+ }
196
+ const sse = {
197
+ name: 'sse',
198
+ install(app) {
199
+ // connect after mount has created resources and loaders
200
+ app.env.queueMicrotask(() => {
201
+ for (const def of app.ir.syncs)
202
+ new Connection(app, def).start();
203
+ });
204
+ },
205
+ };
206
+ export default sse;
@@ -0,0 +1,71 @@
1
+ import { type App, type Transport } from '../runtime/app.js';
2
+ import { type Middleware } from '../runtime/config.js';
3
+ export type RouteResponse = ((call: RecordedCall) => unknown) | {
4
+ status: number;
5
+ body?: unknown;
6
+ headers?: Record<string, string>;
7
+ } | object | string | number | boolean | null;
8
+ export interface RecordedCall {
9
+ method: string;
10
+ url: string;
11
+ path: string;
12
+ body: unknown;
13
+ headers: Record<string, string>;
14
+ }
15
+ export interface MockRequests extends Middleware {
16
+ /** every request that reached the mock, in order */
17
+ readonly calls: RecordedCall[];
18
+ /** requests waiting for settle() */
19
+ readonly pending: number;
20
+ /** answers every waiting request; returns how many */
21
+ deliver(): Promise<number>;
22
+ /** errors for unmatched requests; render() rethrows them */
23
+ readonly errors: Error[];
24
+ }
25
+ /** Request middleware answering `'METHOD /path'` routes. Responses are held until `settle()`. */
26
+ export declare function mockRequests(routes: Record<string, RouteResponse>): MockRequests;
27
+ export interface MockStream {
28
+ readonly connected: boolean;
29
+ /** delivers a server event with a JSON payload */
30
+ emit(name: string, payload?: unknown): void;
31
+ /** drops the connection, as a network error would */
32
+ drop(): void;
33
+ /** @internal */
34
+ transport(): Transport;
35
+ }
36
+ export declare function mockStream(): MockStream;
37
+ export interface RenderOptions {
38
+ requests?: MockRequests;
39
+ streams?: Record<string, MockStream>;
40
+ /** page URL, for `$url` (default `http://localhost/`) */
41
+ url?: string;
42
+ locale?: string;
43
+ /** start time of sinter's clock */
44
+ now?: number | Date;
45
+ /** helper functions, and namespaces as objects of functions */
46
+ helpers?: Record<string, unknown>;
47
+ /** partials for `<sn-include>`, by path relative to the page */
48
+ files?: Record<string, string>;
49
+ /** localStorage and sessionStorage (default: empty ones); pass the previous page's to test what carries over */
50
+ storage?: Storage;
51
+ session?: Storage;
52
+ }
53
+ export interface Rendered {
54
+ root: HTMLElement;
55
+ app: App;
56
+ /** runs the scheduler until no binding is dirty */
57
+ flush(): Promise<void>;
58
+ /** flushes until every request, queue and debounce has finished */
59
+ settle(): Promise<void>;
60
+ /** moves sinter's clock: debounce, backoff, `$now` */
61
+ advance(ms: number): Promise<void>;
62
+ /** URLs sinter tried to navigate to */
63
+ navigations: string[];
64
+ /** current page URL (after `$url` writes) */
65
+ url(): string;
66
+ /** dispatches pagehide/pageshow/popstate to sinter */
67
+ window(type: 'pagehide' | 'pageshow' | 'popstate'): Promise<void>;
68
+ }
69
+ export declare function render(html: string, options?: RenderOptions): Promise<Rendered>;
70
+ /** An in-memory `Storage`, e.g. to share `session` between two render() calls. */
71
+ export declare function memoryStorage(): Storage;