pyweb-stack 0.1.0__py3-none-any.whl

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 (54) hide show
  1. pyweb/__init__.py +71 -0
  2. pyweb/app.py +37 -0
  3. pyweb/app_loader.py +139 -0
  4. pyweb/asgi.py +71 -0
  5. pyweb/auth.py +701 -0
  6. pyweb/bench.py +101 -0
  7. pyweb/browser.py +232 -0
  8. pyweb/build.py +295 -0
  9. pyweb/cache.py +201 -0
  10. pyweb/cli/__init__.py +465 -0
  11. pyweb/cli/__main__.py +3 -0
  12. pyweb/compiler/__init__.py +5 -0
  13. pyweb/compiler/ast.py +45 -0
  14. pyweb/compiler/codegen/__init__.py +9 -0
  15. pyweb/compiler/codegen/ir.py +50 -0
  16. pyweb/compiler/errors.py +29 -0
  17. pyweb/compiler/lower.py +733 -0
  18. pyweb/compiler/parser.py +532 -0
  19. pyweb/compiler/pipeline.py +158 -0
  20. pyweb/compiler/pyjs.py +1087 -0
  21. pyweb/compiler/rpc.py +35 -0
  22. pyweb/context.py +193 -0
  23. pyweb/css.py +115 -0
  24. pyweb/db/__init__.py +652 -0
  25. pyweb/db/migrate.py +143 -0
  26. pyweb/decorators.py +49 -0
  27. pyweb/deploy.py +102 -0
  28. pyweb/forms.py +108 -0
  29. pyweb/hosting.py +139 -0
  30. pyweb/jobs.py +248 -0
  31. pyweb/live.py +88 -0
  32. pyweb/lsp.py +214 -0
  33. pyweb/models.py +269 -0
  34. pyweb/npm.py +270 -0
  35. pyweb/observability.py +313 -0
  36. pyweb/platform.py +69 -0
  37. pyweb/plugins.py +112 -0
  38. pyweb/py.typed +0 -0
  39. pyweb/realtime.py +189 -0
  40. pyweb/rpc.py +137 -0
  41. pyweb/runtime/browser/runtime.js +1151 -0
  42. pyweb/runtime/server/__init__.py +445 -0
  43. pyweb/security.py +162 -0
  44. pyweb/serve.py +335 -0
  45. pyweb/ssr.py +260 -0
  46. pyweb/sync.py +118 -0
  47. pyweb/testing.py +154 -0
  48. pyweb/uploads.py +94 -0
  49. pyweb_stack-0.1.0.dist-info/METADATA +198 -0
  50. pyweb_stack-0.1.0.dist-info/RECORD +54 -0
  51. pyweb_stack-0.1.0.dist-info/WHEEL +5 -0
  52. pyweb_stack-0.1.0.dist-info/entry_points.txt +2 -0
  53. pyweb_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  54. pyweb_stack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1151 @@
1
+ // PyWeb browser runtime: fine-grained signals, DOM rendering, Python
2
+ // semantics helpers (`py`), and typed RPC. No virtual DOM: every dynamic
3
+ // expression owns one effect that updates exactly the nodes it renders.
4
+
5
+ // ---------------------------------------------------------------- signals
6
+ //
7
+ // Dependency tracking: reading a signal inside an effect/computed records a
8
+ // dependency; writing it re-runs only the dependents. Effects created while
9
+ // another effect (or root) runs are *owned* by it and disposed when it
10
+ // re-runs or is disposed, so removed list rows and swapped branches never
11
+ // leak subscriptions.
12
+
13
+ let Listener = null;
14
+ let Owner = null;
15
+ let batchDepth = 0;
16
+ let flushing = false;
17
+ const pending = new Set();
18
+
19
+ function cleanNode(node) {
20
+ if (node.sources) {
21
+ for (const s of node.sources) s.observers.delete(node);
22
+ node.sources.clear();
23
+ }
24
+ if (node.owned && node.owned.length) {
25
+ const owned = node.owned;
26
+ node.owned = [];
27
+ for (const o of owned) disposeNode(o);
28
+ }
29
+ if (node.cleanups && node.cleanups.length) {
30
+ const cbs = node.cleanups;
31
+ node.cleanups = [];
32
+ for (const c of cbs) {
33
+ try { c(); } catch (e) { reportError(e); }
34
+ }
35
+ }
36
+ }
37
+
38
+ function disposeNode(node) {
39
+ cleanNode(node);
40
+ node.disposed = true;
41
+ pending.delete(node);
42
+ }
43
+
44
+ function track(src) {
45
+ if (Listener) {
46
+ src.observers.add(Listener);
47
+ Listener.sources.add(src);
48
+ }
49
+ }
50
+
51
+ function notify(src) {
52
+ for (const o of Array.from(src.observers)) {
53
+ if (o.isComputed) {
54
+ if (!o.stale) { o.stale = true; notify(o); }
55
+ } else {
56
+ pending.add(o);
57
+ }
58
+ }
59
+ if (batchDepth === 0) flush();
60
+ }
61
+
62
+ function flush() {
63
+ if (flushing) return;
64
+ flushing = true;
65
+ try {
66
+ let guard = 0;
67
+ while (pending.size) {
68
+ if (++guard > 100000) throw new Error("pyweb: reactive update loop (an effect keeps changing its own inputs)");
69
+ const e = pending.values().next().value;
70
+ pending.delete(e);
71
+ if (!e.disposed) runEffect(e);
72
+ }
73
+ } finally {
74
+ flushing = false;
75
+ }
76
+ }
77
+
78
+ function runEffect(e) {
79
+ cleanNode(e);
80
+ const pl = Listener; const po = Owner;
81
+ Listener = e; Owner = e;
82
+ try { e.fn(); } catch (err) { reportError(err); } finally { Listener = pl; Owner = po; }
83
+ }
84
+
85
+ /** A reactive value. `s()` reads (and tracks), `s(v)` writes. */
86
+ export function signal(value) {
87
+ const node = { value, observers: new Set() };
88
+ function s(next) {
89
+ if (arguments.length === 0) { track(node); return node.value; }
90
+ if (!Object.is(node.value, next)) { node.value = next; notify(node); }
91
+ return next;
92
+ }
93
+ s.peek = () => node.value;
94
+ s.set = (v) => s(v);
95
+ s.update = (fn) => s(fn(node.value));
96
+ s.subscribe = (cb) => effect(() => cb(s()));
97
+ s.$signal = true;
98
+ return s;
99
+ }
100
+ export const sig = signal;
101
+
102
+ /** A lazily cached derivation; recomputes only after an input changed. */
103
+ export function computed(fn) {
104
+ const node = { value: undefined, stale: true, isComputed: true,
105
+ sources: new Set(), observers: new Set() };
106
+ const c = () => {
107
+ if (node.stale) {
108
+ cleanNode(node);
109
+ const pl = Listener;
110
+ Listener = node;
111
+ try { node.value = fn(); } finally { Listener = pl; }
112
+ node.stale = false;
113
+ }
114
+ track(node);
115
+ return node.value;
116
+ };
117
+ c.peek = () => untrack(c);
118
+ c.$signal = true;
119
+ return c;
120
+ }
121
+
122
+ /** Run `fn` now and again whenever a signal it read changes. */
123
+ export function effect(fn) {
124
+ const e = { fn, sources: new Set(), owned: [], cleanups: [], disposed: false };
125
+ if (Owner) Owner.owned.push(e);
126
+ runEffect(e);
127
+ return () => disposeNode(e);
128
+ }
129
+
130
+ export function onCleanup(fn) { if (Owner) Owner.cleanups.push(fn); }
131
+
132
+ /** Run `fn(dispose)` in a fresh ownership scope, detached from the caller. */
133
+ export function root(fn) {
134
+ const r = { sources: new Set(), owned: [], cleanups: [] };
135
+ const pl = Listener; const po = Owner;
136
+ Listener = null; Owner = r;
137
+ try { return fn(() => disposeNode(r)); } finally { Listener = pl; Owner = po; }
138
+ }
139
+
140
+ /** Apply several writes, then re-run each affected effect once. */
141
+ export function batch(fn) {
142
+ batchDepth++;
143
+ try { return fn(); } finally { if (--batchDepth === 0) flush(); }
144
+ }
145
+
146
+ export function untrack(fn) {
147
+ const pl = Listener;
148
+ Listener = null;
149
+ try { return fn(); } finally { Listener = pl; }
150
+ }
151
+
152
+ export function resource(loader) {
153
+ const r = { pending: false, error: null, result: null };
154
+ r.load = async () => {
155
+ r.pending = true;
156
+ try { r.result = await loader(); } catch (e) { r.error = e; }
157
+ r.pending = false;
158
+ return r.result;
159
+ };
160
+ return r;
161
+ }
162
+
163
+ // ------------------------------------------------------------------ errors
164
+
165
+ export function reportError(err) {
166
+ if (typeof console !== "undefined") console.error(err);
167
+ if (typeof window !== "undefined" && typeof CustomEvent !== "undefined") {
168
+ try { window.dispatchEvent(new CustomEvent("pyweb:error", { detail: err })); } catch { /* noop */ }
169
+ }
170
+ }
171
+
172
+ // --------------------------------------------------------------------- DOM
173
+
174
+ const SVG_NS = "http://www.w3.org/2000/svg";
175
+ const SVG_TAGS = new Set(["svg", "path", "circle", "rect", "line", "g", "polyline",
176
+ "polygon", "ellipse", "defs", "use", "linearGradient", "radialGradient", "stop",
177
+ "clipPath", "mask", "pattern", "symbol", "tspan", "foreignObject"]);
178
+ const PROPS = new Set(["value", "checked", "selected", "indeterminate"]);
179
+ const URL_ATTRS = new Set(["href", "src", "action", "formaction", "xlink:href"]);
180
+
181
+ /** Block `javascript:` (and similar) URLs from reaching the DOM. */
182
+ export function safeUrl(v) {
183
+ const s = String(v);
184
+ return /^\s*(javascript|vbscript|data:text\/html)/i.test(s.replace(/[\u0000-\u001f]/g, "")) ? "#" : s;
185
+ }
186
+
187
+ function kebab(k) { return k.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()).replace(/_/g, "-"); }
188
+
189
+ export function clsx(v) {
190
+ if (v == null || v === false) return "";
191
+ if (Array.isArray(v)) return v.map(clsx).filter(Boolean).join(" ");
192
+ if (typeof v === "object") return Object.keys(v).filter((k) => truth(v[k])).join(" ");
193
+ return String(v);
194
+ }
195
+
196
+ function applyAttr(el, k, v) {
197
+ if (k === "class" || k === "class_" || k === "className") {
198
+ const c = clsx(v);
199
+ if (c) el.setAttribute("class", c); else el.removeAttribute("class");
200
+ return;
201
+ }
202
+ if (k === "style" && v && typeof v === "object") {
203
+ el.removeAttribute("style");
204
+ for (const key of Object.keys(v)) {
205
+ if (v[key] != null && v[key] !== false) el.style.setProperty(kebab(key), String(v[key]));
206
+ }
207
+ return;
208
+ }
209
+ if (PROPS.has(k)) {
210
+ el[k] = k === "value" ? (v == null ? "" : String(v)) : truth(v);
211
+ return;
212
+ }
213
+ if (v == null || v === false) { el.removeAttribute(k); return; }
214
+ if (v === true) { el.setAttribute(k, ""); return; }
215
+ el.setAttribute(k, URL_ATTRS.has(k) ? safeUrl(v) : String(v));
216
+ }
217
+
218
+ function listen(el, ev, handler) {
219
+ el.addEventListener(ev, (e) => {
220
+ if (ev === "submit") e.preventDefault();
221
+ let r;
222
+ try { r = batch(() => handler(e)); } catch (err) { reportError(err); return; }
223
+ if (r && typeof r.then === "function") r.then(null, reportError);
224
+ });
225
+ }
226
+
227
+ /** Two-way binding between a form control and a signal. */
228
+ export function bind(el, s) {
229
+ const type = (el.getAttribute("type") || "").toLowerCase();
230
+ if (type === "checkbox") {
231
+ effect(() => { el.checked = truth(s()); });
232
+ el.addEventListener("change", () => s(el.checked));
233
+ return;
234
+ }
235
+ if (type === "radio") {
236
+ effect(() => { el.checked = el.value === String(s()); });
237
+ el.addEventListener("change", () => { if (el.checked) s(el.value); });
238
+ return;
239
+ }
240
+ const numeric = typeof s.peek() === "number";
241
+ effect(() => {
242
+ const v = s();
243
+ const str = v == null ? "" : String(v);
244
+ if (el.value !== str) el.value = str;
245
+ });
246
+ el.addEventListener(el.tagName === "SELECT" ? "change" : "input", () => {
247
+ if (numeric) {
248
+ const t = el.value.trim();
249
+ if (t !== "" && !Number.isNaN(Number(t))) s(Number(t));
250
+ } else {
251
+ s(el.value);
252
+ }
253
+ });
254
+ }
255
+
256
+ function setProp(el, k, v) {
257
+ if (k.length > 2 && k[0] === "o" && k[1] === "n") {
258
+ listen(el, k.slice(2).toLowerCase(), v);
259
+ } else if (typeof v === "function") {
260
+ effect(() => applyAttr(el, k, v()));
261
+ } else {
262
+ applyAttr(el, k, v);
263
+ }
264
+ }
265
+
266
+ function toNodes(v, out) {
267
+ if (v == null || v === false || v === true) return out;
268
+ if (Array.isArray(v)) { for (const c of v) toNodes(c, out); return out; }
269
+ if (typeof Node !== "undefined" && v instanceof Node) {
270
+ if (v.nodeType === 11) out.push(...Array.from(v.childNodes)); else out.push(v);
271
+ return out;
272
+ }
273
+ if (typeof v === "function") { out.push(...toNodes(dyn(v), [])); return out; }
274
+ out.push(document.createTextNode(String(v)));
275
+ return out;
276
+ }
277
+
278
+ function hasNode(v) {
279
+ if (typeof Node !== "undefined" && v instanceof Node) return true;
280
+ return Array.isArray(v) && v.some(hasNode);
281
+ }
282
+
283
+ /** Create an element. Function-valued props and children are reactive. */
284
+ export function h(tag, props, children) {
285
+ const el = SVG_TAGS.has(tag) ? document.createElementNS(SVG_NS, tag) : document.createElement(tag);
286
+ // Bindings register first so state is current when on* handlers run;
287
+ // <select> binds after its <option> children exist.
288
+ const bound = props && props.$bind;
289
+ if (bound && tag !== "select") { if (props.type) el.setAttribute("type", props.type); bind(el, bound); }
290
+ if (props) {
291
+ for (const k of Object.keys(props)) if (k !== "$bind") setProp(el, k, props[k]);
292
+ }
293
+ if (children) for (const n of toNodes(children, [])) el.appendChild(n);
294
+ if (bound && tag === "select") bind(el, bound);
295
+ return el;
296
+ }
297
+
298
+ // Every dynamic region lives between two comment markers, so its content
299
+ // can change shape (text → nodes, branch swaps, nested lists) while the
300
+ // enclosing row or branch can still move/remove it as one unit.
301
+ function region(label) {
302
+ const frag = document.createDocumentFragment();
303
+ const start = document.createComment(label);
304
+ const end = document.createComment("/" + label);
305
+ frag.appendChild(start);
306
+ frag.appendChild(end);
307
+ return [frag, start, end];
308
+ }
309
+
310
+ function rangeNodes(start, end) {
311
+ const out = [];
312
+ for (let n = start; n; n = n.nextSibling) { out.push(n); if (n === end) break; }
313
+ return out;
314
+ }
315
+
316
+ function clearBetween(start, end) {
317
+ let n = start.nextSibling;
318
+ while (n && n !== end) { const next = n.nextSibling; n.parentNode.removeChild(n); n = next; }
319
+ }
320
+
321
+ function insertAll(before, nodes) {
322
+ const parent = before.parentNode;
323
+ for (const n of nodes) parent.insertBefore(n, before);
324
+ }
325
+
326
+ /** A reactive hole: text when `fn()` returns a value, nodes when it returns nodes. */
327
+ export function dyn(fn) {
328
+ const [frag, start, end] = region("pw");
329
+ let textNode = null;
330
+ effect(() => {
331
+ const v = fn();
332
+ if (hasNode(v)) {
333
+ clearBetween(start, end);
334
+ textNode = null;
335
+ insertAll(end, toNodes(v, []));
336
+ return;
337
+ }
338
+ const s = text(v);
339
+ if (textNode) { if (textNode.data !== s) textNode.data = s; return; }
340
+ clearBetween(start, end);
341
+ textNode = document.createTextNode(s);
342
+ insertAll(end, [textNode]);
343
+ });
344
+ return frag;
345
+ }
346
+
347
+ const _ids = new WeakMap();
348
+ let _idSeq = 0;
349
+ function keyOf(item) {
350
+ if (item === null || (typeof item !== "object" && typeof item !== "function")) return item;
351
+ if (Array.isArray(item)) return "t:" + item.map((x) => {
352
+ if (x === null || typeof x !== "object") return typeof x + ":" + String(x);
353
+ if (!_ids.has(x)) _ids.set(x, ++_idSeq);
354
+ return "o:" + _ids.get(x);
355
+ }).join("|");
356
+ return item;
357
+ }
358
+
359
+ /** Keyed list: unchanged items keep their DOM; only added/removed rows render. */
360
+ export function list(getItems, renderRow) {
361
+ const [frag, , end] = region("pw-for");
362
+ let entries = [];
363
+ onCleanup(() => { for (const e of entries) e.dispose(); entries = []; });
364
+ effect(() => {
365
+ const items = iter(getItems());
366
+ untrack(() => {
367
+ const pool = new Map();
368
+ for (const e of entries) {
369
+ const q = pool.get(e.key);
370
+ if (q) q.push(e); else pool.set(e.key, [e]);
371
+ }
372
+ const next = items.map((item, i) => {
373
+ const key = keyOf(item);
374
+ const q = pool.get(key);
375
+ if (q && q.length) return q.shift();
376
+ const [rf, rs, re] = region("pw-row");
377
+ const dispose = root((d) => { const ns = toNodes(renderRow(item, i), []); for (const n of ns) rf.insertBefore(n, re); return d; });
378
+ return { key, start: rs, end: re, dispose, frag: rf };
379
+ });
380
+ for (const q of pool.values()) {
381
+ for (const e of q) { e.dispose(); for (const n of rangeNodes(e.start, e.end)) n.parentNode.removeChild(n); }
382
+ }
383
+ const parent = end.parentNode;
384
+ let ref = end;
385
+ for (let j = next.length - 1; j >= 0; j--) {
386
+ const e = next[j];
387
+ if (e.frag) { parent.insertBefore(e.frag, ref); e.frag = null; }
388
+ else if (e.end.nextSibling !== ref) {
389
+ const ns = rangeNodes(e.start, e.end);
390
+ for (const n of ns) parent.insertBefore(n, ref);
391
+ }
392
+ ref = e.start;
393
+ }
394
+ entries = next;
395
+ });
396
+ });
397
+ return frag;
398
+ }
399
+
400
+ /** Conditional region: renders `yes()` or `no()` and swaps only on change. */
401
+ export function when(test, yes, no) {
402
+ const [frag, start, end] = region("pw-if");
403
+ let cur = -1;
404
+ let disposeBranch = null;
405
+ onCleanup(() => { if (disposeBranch) disposeBranch(); });
406
+ effect(() => {
407
+ const idx = truth(test()) ? 0 : 1;
408
+ if (idx === cur) return;
409
+ cur = idx;
410
+ untrack(() => {
411
+ if (disposeBranch) disposeBranch();
412
+ clearBetween(start, end);
413
+ const branch = idx === 0 ? yes : no;
414
+ let nodes = [];
415
+ disposeBranch = root((d) => { if (branch) nodes = toNodes(branch(), []); return d; });
416
+ insertAll(end, nodes);
417
+ });
418
+ });
419
+ return frag;
420
+ }
421
+
422
+ /** Run `fn` once the current page/component is in the document. */
423
+ export function onMount(fn) {
424
+ const run = () => {
425
+ try {
426
+ const r = batch(fn);
427
+ if (r && typeof r.then === "function") r.then(null, reportError);
428
+ } catch (e) { reportError(e); }
429
+ };
430
+ queueMicrotask(run);
431
+ }
432
+
433
+ /** Render a page into its server-rendered host element. */
434
+ export function mount(name, page) {
435
+ const run = () => {
436
+ const host = document.querySelector(`[data-pw-root="${name}"]`) || document.body;
437
+ const el = document.getElementById("pw-state");
438
+ let state = {};
439
+ if (el) { try { state = JSON.parse(el.textContent || "{}"); } catch (e) { reportError(e); } }
440
+ root(() => {
441
+ const nodes = toNodes(page(state), []);
442
+ host.replaceChildren(...nodes);
443
+ });
444
+ host.setAttribute("data-pw-ready", "");
445
+ };
446
+ if (typeof document === "undefined") return;
447
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", run);
448
+ else run();
449
+ }
450
+
451
+ // ----------------------------------------------------- Python semantics
452
+
453
+ export class PyError extends Error {
454
+ constructor(type, message) {
455
+ super(message);
456
+ this.name = type;
457
+ this.type = type;
458
+ }
459
+ }
460
+ function err(type, msg) { return new PyError(type, msg); }
461
+
462
+ class KW { constructor(o) { Object.assign(this, o); } }
463
+ function kw(o) { return new KW(o); }
464
+ function splitKw(args) {
465
+ if (args.length && args[args.length - 1] instanceof KW) return [args.slice(0, -1), args[args.length - 1]];
466
+ return [args, {}];
467
+ }
468
+
469
+ function isDict(v) {
470
+ return v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Set)
471
+ && !(v instanceof Map) && !(typeof Node !== "undefined" && v instanceof Node);
472
+ }
473
+
474
+ export function truth(v) {
475
+ if (v == null || v === false || v === 0 || v === "" || Number.isNaN(v)) return false;
476
+ if (Array.isArray(v)) return v.length > 0;
477
+ if (v instanceof Set || v instanceof Map) return v.size > 0;
478
+ if (isDict(v) && Object.getPrototypeOf(v) === Object.prototype) return Object.keys(v).length > 0;
479
+ return true;
480
+ }
481
+
482
+ function iter(v) {
483
+ if (Array.isArray(v)) return v;
484
+ if (typeof v === "string") return Array.from(v);
485
+ if (v instanceof Set) return Array.from(v);
486
+ if (v instanceof Map) return Array.from(v.keys());
487
+ if (v && typeof v[Symbol.iterator] === "function") return Array.from(v);
488
+ if (isDict(v)) return Object.keys(v);
489
+ throw err("TypeError", `'${typeName(v)}' object is not iterable`);
490
+ }
491
+
492
+ function typeName(v) {
493
+ if (v == null) return "NoneType";
494
+ if (typeof v === "string") return "str";
495
+ if (typeof v === "boolean") return "bool";
496
+ if (typeof v === "number") return Number.isInteger(v) ? "int" : "float";
497
+ if (Array.isArray(v)) return "list";
498
+ if (v instanceof Set) return "set";
499
+ if (typeof v === "function") return "function";
500
+ return "dict";
501
+ }
502
+
503
+ function repr(v) {
504
+ if (v == null) return "None";
505
+ if (v === true) return "True";
506
+ if (v === false) return "False";
507
+ if (typeof v === "string") {
508
+ const q = v.includes("'") && !v.includes('"') ? '"' : "'";
509
+ return q + v.replace(/\\/g, "\\\\").replace(new RegExp(q, "g"), "\\" + q).replace(/\n/g, "\\n") + q;
510
+ }
511
+ if (typeof v === "number") return numStr(v);
512
+ if (Array.isArray(v)) return "[" + v.map(repr).join(", ") + "]";
513
+ if (v instanceof Set) return v.size ? "{" + Array.from(v).map(repr).join(", ") + "}" : "set()";
514
+ if (isDict(v)) return "{" + Object.keys(v).map((k) => repr(k) + ": " + repr(v[k])).join(", ") + "}";
515
+ return String(v);
516
+ }
517
+
518
+ function numStr(n) {
519
+ if (Number.isNaN(n)) return "nan";
520
+ if (n === Infinity) return "inf";
521
+ if (n === -Infinity) return "-inf";
522
+ return String(n);
523
+ }
524
+
525
+ function str(v) {
526
+ if (typeof v === "string") return v;
527
+ if (v instanceof Error) return v.message;
528
+ if (typeof v === "number") return numStr(v);
529
+ return repr(v);
530
+ }
531
+
532
+ /** Text for a `{expr}` hole: like `str()`, except `None` renders nothing. */
533
+ export function text(v) { return v == null ? "" : str(v); }
534
+
535
+ function eq(a, b) {
536
+ if (a === b) return true;
537
+ if (a == null || b == null) return a == b;
538
+ if (typeof a === "number" && typeof b === "number") return a === b;
539
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((x, i) => eq(x, b[i]));
540
+ if (a instanceof Set && b instanceof Set) return a.size === b.size && Array.from(a).every((x) => b.has(x));
541
+ if (isDict(a) && isDict(b)) {
542
+ const ka = Object.keys(a); const kb = Object.keys(b);
543
+ return ka.length === kb.length && ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && eq(a[k], b[k]));
544
+ }
545
+ return false;
546
+ }
547
+
548
+ function contains(container, x) {
549
+ if (typeof container === "string") {
550
+ if (typeof x !== "string") throw err("TypeError", "'in <string>' requires string as left operand");
551
+ return container.includes(x);
552
+ }
553
+ if (Array.isArray(container)) return container.some((y) => eq(x, y));
554
+ if (container instanceof Set || container instanceof Map) return container.has(x);
555
+ if (isDict(container)) return Object.prototype.hasOwnProperty.call(container, x);
556
+ throw err("TypeError", `argument of type '${typeName(container)}' is not iterable`);
557
+ }
558
+
559
+ function cmp(a, b) {
560
+ if (Array.isArray(a) && Array.isArray(b)) {
561
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
562
+ const c = cmp(a[i], b[i]);
563
+ if (c) return c;
564
+ }
565
+ return a.length - b.length;
566
+ }
567
+ return a < b ? -1 : a > b ? 1 : 0;
568
+ }
569
+
570
+ function len(v) {
571
+ if (typeof v === "string" || Array.isArray(v)) return v.length;
572
+ if (v instanceof Set || v instanceof Map) return v.size;
573
+ if (isDict(v)) return Object.keys(v).length;
574
+ throw err("TypeError", `object of type '${typeName(v)}' has no len()`);
575
+ }
576
+
577
+ function normIndex(seq, i) {
578
+ if (typeof i !== "number" || !Number.isInteger(i)) throw err("TypeError", "indices must be integers");
579
+ const n = i < 0 ? seq.length + i : i;
580
+ if (n < 0 || n >= seq.length) throw err("IndexError", `${typeName(seq)} index out of range`);
581
+ return n;
582
+ }
583
+
584
+ function at(obj, key) {
585
+ if (obj == null) throw err("TypeError", "'NoneType' object is not subscriptable");
586
+ if (Array.isArray(obj) || typeof obj === "string") return obj[normIndex(obj, key)];
587
+ if (obj instanceof Map) {
588
+ if (!obj.has(key)) throw err("KeyError", repr(key));
589
+ return obj.get(key);
590
+ }
591
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) throw err("KeyError", repr(key));
592
+ return obj[key];
593
+ }
594
+
595
+ function slice(seq, lo, hi, step) {
596
+ const n = seq.length;
597
+ step = step == null ? 1 : step;
598
+ if (step === 0) throw err("ValueError", "slice step cannot be zero");
599
+ const clamp = (i, dflt) => {
600
+ if (i == null) return dflt;
601
+ if (i < 0) i += n;
602
+ return step > 0 ? Math.min(Math.max(i, 0), n) : Math.min(Math.max(i, -1), n - 1);
603
+ };
604
+ const start = clamp(lo, step > 0 ? 0 : n - 1);
605
+ const stop = clamp(hi, step > 0 ? n : -1);
606
+ const out = [];
607
+ for (let i = start; step > 0 ? i < stop : i > stop; i += step) out.push(seq[i]);
608
+ return typeof seq === "string" ? out.join("") : out;
609
+ }
610
+
611
+ function add(a, b) {
612
+ if (typeof a === "number" && typeof b === "number") return a + b;
613
+ if (Array.isArray(a) && Array.isArray(b)) return a.concat(b);
614
+ const ta = typeof a; const tb = typeof b;
615
+ if ((ta === "string") !== (tb === "string") || ta === "object" || tb === "object") {
616
+ throw err("TypeError", `unsupported operand type(s) for +: '${typeName(a)}' and '${typeName(b)}'`);
617
+ }
618
+ return a + b;
619
+ }
620
+
621
+ function mul(a, b) {
622
+ if (typeof a === "number" && typeof b === "number") return a * b;
623
+ if (typeof b === "string" || Array.isArray(b)) [a, b] = [b, a];
624
+ if (typeof a === "string") return b > 0 ? a.repeat(b) : "";
625
+ if (Array.isArray(a)) { let out = []; for (let i = 0; i < b; i++) out = out.concat(a); return out; }
626
+ throw err("TypeError", `can't multiply '${typeName(a)}' by '${typeName(b)}'`);
627
+ }
628
+
629
+ function mod(a, b) {
630
+ if (typeof a === "string") {
631
+ const args = Array.isArray(b) ? b.slice() : [b];
632
+ return a.replace(/%([sdrf%]|\.\d+f)/g, (m, t) => {
633
+ if (t === "%") return "%";
634
+ const v = args.shift();
635
+ if (t === "s") return str(v);
636
+ if (t === "r") return repr(v);
637
+ if (t === "d") return String(Math.trunc(v));
638
+ if (t === "f") return Number(v).toFixed(6);
639
+ return Number(v).toFixed(Number(t.slice(1, -1)));
640
+ });
641
+ }
642
+ if (b === 0) throw err("ZeroDivisionError", "integer modulo by zero");
643
+ return ((a % b) + b) % b;
644
+ }
645
+
646
+ function floordiv(a, b) {
647
+ if (b === 0) throw err("ZeroDivisionError", "integer division by zero");
648
+ return Math.floor(a / b);
649
+ }
650
+
651
+ function div(a, b) {
652
+ if (b === 0) throw err("ZeroDivisionError", "division by zero");
653
+ return a / b;
654
+ }
655
+
656
+ function int(v, base) {
657
+ if (typeof v === "number") return Math.trunc(v);
658
+ if (typeof v === "boolean") return v ? 1 : 0;
659
+ if (typeof v === "string") {
660
+ const s = v.trim().replace(/_/g, "");
661
+ const b = base || 10;
662
+ const ok = b === 10 ? /^[+-]?\d+$/ : /^[+-]?[0-9a-z]+$/i;
663
+ const n = parseInt(s, b);
664
+ if (!ok.test(s) || Number.isNaN(n)) throw err("ValueError", `invalid literal for int() with base ${b}: ${repr(v)}`);
665
+ return n;
666
+ }
667
+ throw err("TypeError", `int() argument must be a string or a number, not '${typeName(v)}'`);
668
+ }
669
+
670
+ function float(v) {
671
+ if (typeof v === "number") return v;
672
+ const n = Number(String(v).trim());
673
+ if (String(v).trim() === "" || Number.isNaN(n)) throw err("ValueError", `could not convert string to float: ${repr(v)}`);
674
+ return n;
675
+ }
676
+
677
+ function round(x, nd) {
678
+ if (nd == null) {
679
+ const f = Math.floor(x); const d = x - f;
680
+ if (d === 0.5) return f % 2 === 0 ? f : f + 1;
681
+ return Math.round(x);
682
+ }
683
+ const m = 10 ** nd;
684
+ return Math.round(x * m) / m;
685
+ }
686
+
687
+ function range(a, b, step) {
688
+ if (b == null) { b = a; a = 0; }
689
+ step = step == null ? 1 : step;
690
+ if (step === 0) throw err("ValueError", "range() arg 3 must not be zero");
691
+ const out = [];
692
+ for (let i = a; step > 0 ? i < b : i > b; i += step) out.push(i);
693
+ return out;
694
+ }
695
+
696
+ function keyed(kwargs) {
697
+ const key = kwargs.key || ((x) => x);
698
+ return (x, y) => cmp(key(x), key(y));
699
+ }
700
+
701
+ function sorted(it, ...rest) {
702
+ const [, kws] = splitKw(rest);
703
+ const out = iter(it).slice().sort(keyed(kws));
704
+ if (kws.reverse) out.reverse();
705
+ return out;
706
+ }
707
+
708
+ function extreme(sign, args) {
709
+ const [pos, kws] = splitKw(args);
710
+ const items = pos.length === 1 ? iter(pos[0]) : pos;
711
+ if (!items.length) {
712
+ if ("default" in kws) return kws.default;
713
+ throw err("ValueError", "arg is an empty sequence");
714
+ }
715
+ const c = keyed(kws);
716
+ return items.reduce((best, x) => (sign * c(x, best) > 0 ? x : best));
717
+ }
718
+
719
+ function format(v, spec) {
720
+ if (!spec) return str(v);
721
+ const m = /^(?:(.)?([<>^]))?([+\- ])?(0)?(\d+)?(,)?(?:\.(\d+))?([sdfe%g])?$/.exec(spec);
722
+ if (!m) return str(v);
723
+ const [, fill = " ", align, sign, zero, width, comma, prec, type] = m;
724
+ let s;
725
+ if (type === "%") s = (v * 100).toFixed(prec == null ? 6 : +prec) + "%";
726
+ else if (type === "f") s = Number(v).toFixed(prec == null ? 6 : +prec);
727
+ else if (type === "e") s = Number(v).toExponential(prec == null ? 6 : +prec);
728
+ else if (type === "d") s = String(Math.trunc(v));
729
+ else if (prec != null && typeof v === "number") s = Number(v).toFixed(+prec);
730
+ else if (prec != null) s = str(v).slice(0, +prec);
731
+ else s = str(v);
732
+ if (comma && typeof v === "number") {
733
+ const [ip, fp] = s.split(".");
734
+ s = ip.replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (fp != null ? "." + fp : "");
735
+ }
736
+ if (sign === "+" && typeof v === "number" && v >= 0) s = "+" + s;
737
+ if (width) {
738
+ const w = +width;
739
+ const f = zero ? "0" : fill;
740
+ const al = align || (typeof v === "number" ? ">" : "<");
741
+ if (s.length < w) {
742
+ const pad = w - s.length;
743
+ if (al === "<") s = s + f.repeat(pad);
744
+ else if (al === "^") s = f.repeat(Math.floor(pad / 2)) + s + f.repeat(Math.ceil(pad / 2));
745
+ else s = f.repeat(pad) + s;
746
+ }
747
+ }
748
+ return s;
749
+ }
750
+
751
+ function strip(s, chars, left, right) {
752
+ if (chars == null) {
753
+ if (left && right) return s.trim();
754
+ return left ? s.replace(/^\s+/, "") : s.replace(/\s+$/, "");
755
+ }
756
+ let a = 0; let b = s.length;
757
+ while (left && a < b && chars.includes(s[a])) a++;
758
+ while (right && b > a && chars.includes(s[b - 1])) b--;
759
+ return s.slice(a, b);
760
+ }
761
+
762
+ const STR_METHODS = {
763
+ upper: (s) => s.toUpperCase(),
764
+ lower: (s) => s.toLowerCase(),
765
+ strip: (s, c) => strip(s, c, true, true),
766
+ lstrip: (s, c) => strip(s, c, true, false),
767
+ rstrip: (s, c) => strip(s, c, false, true),
768
+ split: (s, sep, max) => {
769
+ if (sep == null) {
770
+ const parts = s.trim().split(/\s+/).filter(Boolean);
771
+ return max == null || max < 0 ? parts : parts.slice(0, max).concat(parts.length > max ? [parts.slice(max).join(" ")] : []);
772
+ }
773
+ const parts = s.split(sep);
774
+ if (max == null || max < 0 || parts.length <= max + 1) return parts;
775
+ return parts.slice(0, max).concat([parts.slice(max).join(sep)]);
776
+ },
777
+ splitlines: (s) => s.split(/\r?\n/).filter((x, i, a) => i < a.length - 1 || x !== ""),
778
+ join: (s, items) => iter(items).map((x) => {
779
+ if (typeof x !== "string") throw err("TypeError", `sequence item: expected str instance, ${typeName(x)} found`);
780
+ return x;
781
+ }).join(s),
782
+ replace: (s, a, b, count) => {
783
+ if (count == null || count < 0) return s.split(a).join(b);
784
+ let out = s;
785
+ let i = 0; let from = 0;
786
+ while (i < count) {
787
+ const at2 = out.indexOf(a, from);
788
+ if (at2 < 0) break;
789
+ out = out.slice(0, at2) + b + out.slice(at2 + a.length);
790
+ from = at2 + b.length;
791
+ i++;
792
+ }
793
+ return out;
794
+ },
795
+ startswith: (s, p) => (Array.isArray(p) ? p.some((x) => s.startsWith(x)) : s.startsWith(p)),
796
+ endswith: (s, p) => (Array.isArray(p) ? p.some((x) => s.endsWith(x)) : s.endsWith(p)),
797
+ find: (s, x) => s.indexOf(x),
798
+ rfind: (s, x) => s.lastIndexOf(x),
799
+ index: (s, x) => { const i = s.indexOf(x); if (i < 0) throw err("ValueError", "substring not found"); return i; },
800
+ count: (s, x) => (x === "" ? s.length + 1 : s.split(x).length - 1),
801
+ title: (s) => s.toLowerCase().replace(/(^|[^a-z0-9])([a-z])/g, (m, a, b) => a + b.toUpperCase()),
802
+ capitalize: (s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(),
803
+ isdigit: (s) => /^\d+$/.test(s),
804
+ isnumeric: (s) => /^\d+$/.test(s),
805
+ isalpha: (s) => /^[A-Za-z]+$/.test(s),
806
+ isalnum: (s) => /^[A-Za-z0-9]+$/.test(s),
807
+ isspace: (s) => /^\s+$/.test(s),
808
+ isupper: (s) => /[A-Z]/.test(s) && s === s.toUpperCase(),
809
+ islower: (s) => /[a-z]/.test(s) && s === s.toLowerCase(),
810
+ zfill: (s, w) => (s.length >= w ? s : (s[0] === "-" ? "-" + s.slice(1).padStart(w - 1, "0") : s.padStart(w, "0"))),
811
+ ljust: (s, w, f = " ") => s.padEnd(w, f),
812
+ rjust: (s, w, f = " ") => s.padStart(w, f),
813
+ center: (s, w, f = " ") => format(s, `${f}^${w}`),
814
+ format: (s, ...args) => {
815
+ const [pos, kws] = splitKw(args);
816
+ let auto = 0;
817
+ return s.replace(/\{\{|\}\}|\{([^{}:]*)(?::([^{}]*))?\}/g, (m, name, spec) => {
818
+ if (m === "{{") return "{";
819
+ if (m === "}}") return "}";
820
+ const v = name === "" || name == null ? pos[auto++] : /^\d+$/.test(name) ? pos[+name] : kws[name];
821
+ return format(v, spec);
822
+ });
823
+ },
824
+ };
825
+
826
+ const LIST_METHODS = {
827
+ append: (a, x) => { a.push(x); },
828
+ extend: (a, xs) => { a.push(...iter(xs)); },
829
+ insert: (a, i, x) => { a.splice(i < 0 ? Math.max(0, a.length + i) : i, 0, x); },
830
+ pop: (a, i) => {
831
+ if (!a.length) throw err("IndexError", "pop from empty list");
832
+ return a.splice(normIndex(a, i == null ? -1 : i), 1)[0];
833
+ },
834
+ remove: (a, x) => {
835
+ const i = a.findIndex((y) => eq(x, y));
836
+ if (i < 0) throw err("ValueError", "list.remove(x): x not in list");
837
+ a.splice(i, 1);
838
+ },
839
+ index: (a, x) => {
840
+ const i = a.findIndex((y) => eq(x, y));
841
+ if (i < 0) throw err("ValueError", `${repr(x)} is not in list`);
842
+ return i;
843
+ },
844
+ count: (a, x) => a.filter((y) => eq(x, y)).length,
845
+ clear: (a) => { a.length = 0; },
846
+ sort: (a, ...rest) => {
847
+ const [, kws] = splitKw(rest);
848
+ a.sort(keyed(kws));
849
+ if (kws.reverse) a.reverse();
850
+ },
851
+ reverse: (a) => { a.reverse(); },
852
+ copy: (a) => a.slice(),
853
+ };
854
+
855
+ const DICT_METHODS = {
856
+ get: (d, k, dflt = null) => (Object.prototype.hasOwnProperty.call(d, k) ? d[k] : dflt),
857
+ keys: (d) => Object.keys(d),
858
+ values: (d) => Object.values(d),
859
+ items: (d) => Object.entries(d),
860
+ pop: (d, k, ...dflt) => {
861
+ if (Object.prototype.hasOwnProperty.call(d, k)) { const v = d[k]; delete d[k]; return v; }
862
+ if (dflt.length) return dflt[0];
863
+ throw err("KeyError", repr(k));
864
+ },
865
+ update: (d, o) => { Object.assign(d, o instanceof KW ? { ...o } : o); },
866
+ setdefault: (d, k, v = null) => { if (!Object.prototype.hasOwnProperty.call(d, k)) d[k] = v; return d[k]; },
867
+ copy: (d) => ({ ...d }),
868
+ clear: (d) => { for (const k of Object.keys(d)) delete d[k]; },
869
+ };
870
+
871
+ const SET_METHODS = {
872
+ add: (s, x) => { s.add(x); },
873
+ discard: (s, x) => { s.delete(x); },
874
+ remove: (s, x) => { if (!s.delete(x)) throw err("KeyError", repr(x)); },
875
+ clear: (s) => { s.clear(); },
876
+ copy: (s) => new Set(s),
877
+ };
878
+
879
+ /** Call a Python method on a JS value (`obj.method(*args)`). */
880
+ function m(obj, name, ...args) {
881
+ let table = null;
882
+ if (typeof obj === "string") table = STR_METHODS;
883
+ else if (Array.isArray(obj)) table = LIST_METHODS;
884
+ else if (obj instanceof Set) table = SET_METHODS;
885
+ else if (isDict(obj) && typeof obj[name] !== "function") table = DICT_METHODS;
886
+ if (table && table[name]) {
887
+ const r = table[name](obj, ...args);
888
+ return r === undefined ? null : r;
889
+ }
890
+ if (obj != null && typeof obj[name] === "function") {
891
+ const [pos] = splitKw(args);
892
+ return obj[name](...pos);
893
+ }
894
+ throw err("AttributeError", `'${typeName(obj)}' object has no attribute '${name}'`);
895
+ }
896
+
897
+ function clone(v) {
898
+ if (Array.isArray(v)) return v.slice();
899
+ if (v instanceof Set) return new Set(v);
900
+ if (v instanceof Map) return new Map(v);
901
+ if (v && typeof v === "object") return { ...v };
902
+ return v;
903
+ }
904
+
905
+ function walk(rootVal, path) {
906
+ let cur = rootVal;
907
+ for (const k of path) {
908
+ const idx = Array.isArray(cur) ? normIndex(cur, k) : k;
909
+ if (Array.isArray(cur) || isDict(cur)) {
910
+ if (!Array.isArray(cur) && !Object.prototype.hasOwnProperty.call(cur, idx)) throw err("KeyError", repr(k));
911
+ cur[idx] = clone(cur[idx]);
912
+ cur = cur[idx];
913
+ } else {
914
+ throw err("TypeError", `'${typeName(cur)}' object is not subscriptable`);
915
+ }
916
+ }
917
+ return cur;
918
+ }
919
+
920
+ /** Mutate state copy-on-write: `todos.append(x)` → new list, then notify. */
921
+ function mut(s, path, fn) {
922
+ const next = clone(s.peek());
923
+ const r = fn(walk(next, path));
924
+ s(next);
925
+ return r === undefined ? null : r;
926
+ }
927
+
928
+ function setitem(obj, k, v) {
929
+ if (Array.isArray(obj)) obj[normIndex(obj, k)] = v;
930
+ else if (obj instanceof Map) obj.set(k, v);
931
+ else obj[k] = v;
932
+ }
933
+
934
+ function delitem(obj, k) {
935
+ if (Array.isArray(obj)) obj.splice(normIndex(obj, k), 1);
936
+ else if (obj instanceof Map) obj.delete(k);
937
+ else {
938
+ if (!Object.prototype.hasOwnProperty.call(obj, k)) throw err("KeyError", repr(k));
939
+ delete obj[k];
940
+ }
941
+ }
942
+
943
+ /** `state[a][b] = v` with structural sharing so keyed rows see a new item. */
944
+ function setp(s, path, v) {
945
+ if (!path.length) { s(v); return; }
946
+ const next = clone(s.peek());
947
+ setitem(walk(next, path.slice(0, -1)), path[path.length - 1], v);
948
+ s(next);
949
+ }
950
+
951
+ function getp(v, path) {
952
+ for (const k of path) v = at(v, k);
953
+ return v;
954
+ }
955
+
956
+ export const py = {
957
+ kw, truth, iter, str, repr, text, eq, contains, len, at, slice, add, mul, mod, div, floordiv,
958
+ int, float, round, range, sorted, format, m, mut, setp, getp, setitem, delitem, cmp,
959
+ bool: (v) => truth(v),
960
+ abs: (v) => Math.abs(v),
961
+ min: (...a) => extreme(-1, a),
962
+ max: (...a) => extreme(1, a),
963
+ sum: (it, start = 0) => iter(it).reduce((acc, x) => add(acc, x), start),
964
+ any: (it) => iter(it).some(truth),
965
+ all: (it) => iter(it).every(truth),
966
+ list: (it) => (it == null ? [] : iter(it).slice()),
967
+ tuple: (it) => (it == null ? [] : iter(it).slice()),
968
+ set: (it) => new Set(it == null ? [] : iter(it)),
969
+ dict: (it, ...rest) => {
970
+ const [, kws] = splitKw(rest);
971
+ let base = {};
972
+ if (it instanceof KW) base = { ...it };
973
+ else if (isDict(it)) base = { ...it };
974
+ else if (it != null) base = Object.fromEntries(iter(it));
975
+ return Object.assign(base, kws);
976
+ },
977
+ reversed: (it) => iter(it).slice().reverse(),
978
+ enumerate: (it, start = 0) => iter(it).map((x, i) => [i + start, x]),
979
+ zip: (...its) => {
980
+ const arrs = its.map(iter);
981
+ const n = arrs.length ? Math.min(...arrs.map((a) => a.length)) : 0;
982
+ return range(n).map((i) => arrs.map((a) => a[i]));
983
+ },
984
+ chr: (n) => String.fromCodePoint(n),
985
+ ord: (c) => c.codePointAt(0),
986
+ isinstance: (v, t) => (Array.isArray(t) ? t : [t]).some((x) => x === typeName(v)
987
+ || (x === "float" && typeof v === "number" && !Number.isInteger(v)) || (x === "int" && typeof v === "boolean")
988
+ || (x === "tuple" && Array.isArray(v))),
989
+ print: (...a) => console.log(...splitKw(a)[0].map(str)),
990
+ error: (type, msg) => err(type, msg == null ? "" : str(msg)),
991
+ and: (a, f) => (truth(a) ? f() : a),
992
+ or: (a, f) => (truth(a) ? a : f()),
993
+ exc: (e, types) => types.includes("Exception") || types.includes("BaseException")
994
+ || types.includes((e && (e.type || e.name)) || "Error"),
995
+ delp: (s, path) => {
996
+ const next = clone(s.peek());
997
+ delitem(walk(next, path.slice(0, -1)), path[path.length - 1]);
998
+ s(next);
999
+ },
1000
+ };
1001
+
1002
+ // --------------------------------------------------------------------- RPC
1003
+
1004
+ // Typed RPC error: err.code is one of unauthenticated/forbidden/
1005
+ // validation_error/not_found/rate_limited/timeout/conflict/csrf_failed/
1006
+ // internal. err.status is the HTTP status; err.retryAfter on 429.
1007
+ export class RPCError extends Error {
1008
+ constructor(name, code, message, status, details, retryAfter) {
1009
+ super(message || `${name}: ${code}`);
1010
+ this.name = "RPCError";
1011
+ this.type = "RPCError";
1012
+ this.rpc = name;
1013
+ this.code = code;
1014
+ this.status = status;
1015
+ this.details = details || {};
1016
+ if (retryAfter) this.retryAfter = retryAfter;
1017
+ }
1018
+ }
1019
+
1020
+ let _qid = 0;
1021
+ let _csrf = null;
1022
+ export function setCsrfToken(token) { _csrf = token; }
1023
+ export function getCsrfToken() {
1024
+ if (_csrf) return _csrf;
1025
+ if (typeof document === "undefined") return null;
1026
+ const m2 = document.cookie.match(/(?:^|;\s*)pyweb_csrf=([^;]+)/);
1027
+ return m2 ? decodeURIComponent(m2[1]) : null;
1028
+ }
1029
+
1030
+ const RETRYABLE = new Set([502, 503, 504]);
1031
+ function _sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
1032
+
1033
+ export async function rpc(name, args = {}, opts = {}) {
1034
+ const attempts = Math.max(1, opts.retries ?? opts.attempts ?? 1);
1035
+ const timeout = opts.timeout || 0;
1036
+ const external = opts.signal || null;
1037
+ let lastErr = null;
1038
+ for (let attempt = 0; attempt < attempts; attempt++) {
1039
+ const ctrl = new AbortController();
1040
+ const onAbort = () => ctrl.abort();
1041
+ if (external) {
1042
+ if (external.aborted) { const e = new DOMException("aborted", "AbortError"); e.rpc = name; throw e; }
1043
+ external.addEventListener("abort", onAbort, { once: true });
1044
+ }
1045
+ const timer = timeout ? setTimeout(() => ctrl.abort(), timeout) : null;
1046
+ try {
1047
+ const headers = {
1048
+ "Content-Type": "application/json",
1049
+ "X-PyWeb-Trace": String(Date.now()) + "-" + (++_qid),
1050
+ ...(opts.traceparent ? { traceparent: opts.traceparent } : {}),
1051
+ };
1052
+ const csrf = opts.csrf ?? getCsrfToken();
1053
+ if (csrf) headers["X-CSRF-Token"] = csrf;
1054
+ const res = await fetch(`/__pyweb/rpc/${encodeURIComponent(name)}`, {
1055
+ method: "POST",
1056
+ headers,
1057
+ body: JSON.stringify({ args }),
1058
+ signal: ctrl.signal,
1059
+ credentials: "same-origin",
1060
+ });
1061
+ const ctype = res.headers.get("Content-Type") || "";
1062
+ // Streaming RPC: application/x-ndjson, one {"chunk"} per line.
1063
+ if (res.ok && ctype.includes("x-ndjson")) {
1064
+ const body = await res.text();
1065
+ const chunks = [];
1066
+ for (const line of body.split("\n")) {
1067
+ if (!line.trim()) continue;
1068
+ try {
1069
+ const obj = JSON.parse(line);
1070
+ if ("chunk" in obj) { chunks.push(obj.chunk); if (typeof opts.onChunk === "function") opts.onChunk(obj.chunk); }
1071
+ } catch { /* skip malformed line */ }
1072
+ }
1073
+ return chunks;
1074
+ }
1075
+ let data = null;
1076
+ try { data = await res.json(); } catch { data = null; }
1077
+ if (!res.ok || (data && data.error)) {
1078
+ const e = (data && data.error) || {};
1079
+ const code = typeof e === "object" ? (e.code || `http_${res.status}`) : "internal";
1080
+ const msg = typeof e === "object" ? (e.message || `RPC ${name} failed: ${res.status}`) : String(e);
1081
+ const retryAfter = res.headers.get("Retry-After");
1082
+ const rerr = new RPCError(name, code, msg, res.status, (typeof e === "object" && e.details) || {}, retryAfter ? parseInt(retryAfter, 10) : null);
1083
+ const retryable = res.status === 429 || RETRYABLE.has(res.status);
1084
+ if (attempt + 1 < attempts && retryable) {
1085
+ lastErr = rerr;
1086
+ let delay = Math.min(100 * 2 ** attempt, 2000);
1087
+ if (res.status === 429 && retryAfter) delay = parseInt(retryAfter, 10) * 1000 || delay;
1088
+ await _sleep(delay + Math.random() * 100);
1089
+ continue;
1090
+ }
1091
+ throw rerr;
1092
+ }
1093
+ return data ? data.result : null;
1094
+ } catch (e) {
1095
+ if (e && e.name === "AbortError") { e.rpc = e.rpc || name; throw e; }
1096
+ if (e instanceof RPCError) throw e;
1097
+ lastErr = e;
1098
+ if (attempt + 1 < attempts && opts.retryNetwork !== false) {
1099
+ await _sleep(Math.min(100 * 2 ** attempt, 2000) + Math.random() * 100);
1100
+ continue;
1101
+ }
1102
+ throw e;
1103
+ } finally {
1104
+ if (timer) clearTimeout(timer);
1105
+ if (external) external.removeEventListener("abort", onAbort);
1106
+ }
1107
+ }
1108
+ throw lastErr;
1109
+ }
1110
+
1111
+ // Realtime: SSE at /__pyweb/events?channel=NAME with poll fallback.
1112
+ // subscribe(channel, onMsg, {lastId}) returns an unsubscribe function.
1113
+ export function subscribe(channel, onMsg, opts = {}) {
1114
+ let stopped = false;
1115
+ let lastId = opts.lastId || 0;
1116
+ if (typeof EventSource !== "undefined" && !opts.poll) {
1117
+ const src = new EventSource(`/__pyweb/events?channel=${encodeURIComponent(channel)}`);
1118
+ src.addEventListener(channel, (e) => {
1119
+ if (e.lastEventId) lastId = e.lastEventId;
1120
+ try { onMsg(JSON.parse(e.data)); } catch { onMsg(e.data); }
1121
+ });
1122
+ src.onerror = () => { src.close(); if (!stopped) poll(); };
1123
+ return () => { stopped = true; src.close(); };
1124
+ }
1125
+ async function poll() {
1126
+ while (!stopped) {
1127
+ try {
1128
+ const res = await fetch(`/__pyweb/poll?channel=${encodeURIComponent(channel)}&since=${lastId}`);
1129
+ if (!res.ok) throw new Error(`poll ${res.status}`);
1130
+ const data = await res.json();
1131
+ for (const msg of data.messages || []) { lastId = msg.id; onMsg(msg.data); }
1132
+ } catch { /* retry below */ }
1133
+ await _sleep(opts.interval || 2500);
1134
+ }
1135
+ }
1136
+ poll();
1137
+ return () => { stopped = true; };
1138
+ }
1139
+
1140
+ // Optimistic mutation: apply local patch, run server call, reconcile/rollback.
1141
+ export async function mutate({ apply, server, reconcile, rollback }) {
1142
+ const undo = apply ? apply() : null;
1143
+ try {
1144
+ const result = await server();
1145
+ if (reconcile) reconcile(result);
1146
+ return result;
1147
+ } catch (e) {
1148
+ if (rollback) rollback(undo, e); else if (typeof undo === "function") undo();
1149
+ throw e;
1150
+ }
1151
+ }