rosefc 0.2.0 → 0.2.1
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/LICENSE +1 -1
- package/deploy/Dockerfile.node +2 -2
- package/deploy/Dockerfile.static +5 -5
- package/deploy/cloudflare/functions/[[path]].ts +2 -2
- package/deploy/cloudflare/wrangler.toml +2 -2
- package/deploy/fly.toml +4 -4
- package/deploy/k8s/{rosefd.yaml → rosefc.yaml} +22 -22
- package/deploy/native/{RosefdApp.xcscheme → RosefcApp.xcscheme} +16 -16
- package/deploy/native/android.yml +3 -3
- package/deploy/native/flutter.yml +8 -8
- package/deploy/native/ios.yml +8 -8
- package/deploy/netlify/functions/{rosefd.ts → rosefc.ts} +5 -5
- package/deploy/netlify.toml +1 -1
- package/deploy/vercel.json +2 -2
- package/dist-cli/compiler.mjs +149 -149
- package/dist-cli/edge.mjs +14 -14
- package/dist-cli/export.mjs +546 -546
- package/dist-cli/{rosefd.mjs → rosefc.mjs} +398 -398
- package/dist-cli/runtime.mjs +2 -2
- package/dist-cli/test.mjs +159 -159
- package/package.json +7 -7
- package/src/FRAMEWORK_VERSION +1 -1
- package/src/cli/content.ts +1 -1
- package/src/cli/edge.ts +3 -3
- package/src/cli/prod-server.ts +12 -12
- package/src/cli/shell.ts +17 -17
- package/src/cli/shutdown.ts +3 -3
- package/src/runtime/db.ts +3 -3
- package/src/runtime/index.ts +60 -60
- package/src/types/{rosefd.d.ts → rosefc.d.ts} +12 -12
- /package/deploy/api/{rosefd.ts → rosefc.ts} +0 -0
package/src/runtime/index.ts
CHANGED
|
@@ -64,7 +64,7 @@ function untrack<T>(fn: () => T): T {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Trade-off: global signal map, shared across all inlined modules
|
|
67
|
-
const signalMap: Map<string, SignalEntry> = shared('
|
|
67
|
+
const signalMap: Map<string, SignalEntry> = shared('__rosefc_signalMap', () => new Map());
|
|
68
68
|
|
|
69
69
|
// One get-or-create helper shared by state / setState / resumeState.
|
|
70
70
|
function entry(key: string, initial: unknown): { value: unknown; subs: Set<Subscriber> } {
|
|
@@ -125,7 +125,7 @@ export function hasState(key: string): boolean {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// === Per-RENDER $data seeding === The compiled `$data(() => loader())` prologue is const __seeded = isSeeded("k"); const [get.
|
|
128
|
-
const seededKeys: Set<string> = shared('
|
|
128
|
+
const seededKeys: Set<string> = shared('__rosefc_seededKeys', () => new Set());
|
|
129
129
|
|
|
130
130
|
export function isSeeded(key: string): boolean {
|
|
131
131
|
return renderMaps().seeded.has(key);
|
|
@@ -155,12 +155,12 @@ export function setState<T>(key: string, value: T): void {
|
|
|
155
155
|
if (persistedKeys.has(key)) writeStorage(key, value);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
// F9 ($persist): keys declared with $persist survive reloads — every write lands in localStorage (JSON-encoded under a
|
|
159
|
-
const persistedKeys: Set<string> = shared('
|
|
158
|
+
// F9 ($persist): keys declared with $persist survive reloads — every write lands in localStorage (JSON-encoded under a rosefc: prefix) and every client.
|
|
159
|
+
const persistedKeys: Set<string> = shared('__rosefc_persistedKeys', () => new Set());
|
|
160
160
|
|
|
161
161
|
function writeStorage(key: string, v: unknown): void {
|
|
162
162
|
try {
|
|
163
|
-
(globalThis as { localStorage?: Storage }).localStorage?.setItem('
|
|
163
|
+
(globalThis as { localStorage?: Storage }).localStorage?.setItem('rosefc:' + key, JSON.stringify(v));
|
|
164
164
|
} catch { }
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -170,7 +170,7 @@ export function persistState<T>(key: string, initial: T): [() => T, (v: T) => vo
|
|
|
170
170
|
let seeded = initial;
|
|
171
171
|
let hasStored = false;
|
|
172
172
|
try {
|
|
173
|
-
const raw = (globalThis as { localStorage?: Storage }).localStorage?.getItem('
|
|
173
|
+
const raw = (globalThis as { localStorage?: Storage }).localStorage?.getItem('rosefc:' + key);
|
|
174
174
|
if (raw != null) {
|
|
175
175
|
seeded = JSON.parse(raw) as T;
|
|
176
176
|
hasStored = true;
|
|
@@ -184,7 +184,7 @@ export function persistState<T>(key: string, initial: T): [() => T, (v: T) => vo
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
// F12: the error a {#boundary} just caught — a custom {:fallback} reads it ({$boundaryError()?.message}) to render its own failure UX.
|
|
187
|
-
const lastBoundaryError = sharedCell<Error | null>('
|
|
187
|
+
const lastBoundaryError = sharedCell<Error | null>('__rosefc_lastBoundaryError', null);
|
|
188
188
|
|
|
189
189
|
export function $setBoundaryError(e: unknown): void {
|
|
190
190
|
lastBoundaryError.value = e instanceof Error ? e : new Error(String(e));
|
|
@@ -195,12 +195,12 @@ export function $boundaryError(): Error | null {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
|
|
198
|
-
const cleanups: Set<Cleanup> = shared('
|
|
198
|
+
const cleanups: Set<Cleanup> = shared('__rosefc_cleanups', () => new Set());
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
let fxOwner: object | null = null;
|
|
202
|
-
const fxByOwner: WeakMap<object, Set<Cleanup>> = shared('
|
|
203
|
-
const keysByOwner: WeakMap<object, Set<string>> = shared('
|
|
202
|
+
const fxByOwner: WeakMap<object, Set<Cleanup>> = shared('__rosefc_fxByOwner', () => new WeakMap());
|
|
203
|
+
const keysByOwner: WeakMap<object, Set<string>> = shared('__rosefc_keysByOwner', () => new WeakMap());
|
|
204
204
|
const fxSet = (): Set<Cleanup> => {
|
|
205
205
|
if (!fxOwner) return cleanups;
|
|
206
206
|
let s = fxByOwner.get(fxOwner);
|
|
@@ -412,7 +412,7 @@ interface EachRow {
|
|
|
412
412
|
// attribute (apps use it for active-row styling), its value is part of the html
|
|
413
413
|
// the reuse check compares, and it carries no closure - forbidding it forced a
|
|
414
414
|
// full rebuild of every such list on any selection change.
|
|
415
|
-
const EACH_REUSE_FORBID = /data-on-|data-use|data-bind-this|data-transition|data-animate|data-
|
|
415
|
+
const EACH_REUSE_FORBID = /data-on-|data-use|data-bind-this|data-transition|data-animate|data-rosefc-c|data-rv-island/;
|
|
416
416
|
const EACH_HTML_NORM = (h: string): string => h.replace(/\u27e6\/?[a-z]+:\d+\u27e7/g, '').replace(/data-b\d+/g, 'data-b');
|
|
417
417
|
const normHtml = (row: EachRow): string => (row.norm ??= EACH_HTML_NORM(row.html));
|
|
418
418
|
|
|
@@ -481,12 +481,12 @@ export function slotBlockMark(
|
|
|
481
481
|
const MARK_RE = /^\u27e6([milh]):(\d+)\u27e7$/;
|
|
482
482
|
const END_RE = /^\u27e6\/([milh]):(\d+)\u27e7$/;
|
|
483
483
|
|
|
484
|
-
const wired: WeakSet<Node> = shared('
|
|
484
|
+
const wired: WeakSet<Node> = shared('__rosefc_wired', () => new WeakSet());
|
|
485
485
|
|
|
486
|
-
const pendingIslandObservers: Set<IntersectionObserver> = shared('
|
|
486
|
+
const pendingIslandObservers: Set<IntersectionObserver> = shared('__rosefc_pendingIslandObservers', () => new Set());
|
|
487
487
|
|
|
488
488
|
const TPL_CACHE_MAX = 512;
|
|
489
|
-
const tplCache: Map<string, HTMLTemplateElement> = shared('
|
|
489
|
+
const tplCache: Map<string, HTMLTemplateElement> = shared('__rosefc_tplCache', () => new Map());
|
|
490
490
|
|
|
491
491
|
/** Parse an html string into a DocumentFragment. Parsed templates are cached per string and cloned per call, so repeated renders (keyed each rows, if-branch re-entry) skip the innerHTML parse. */
|
|
492
492
|
export function tpl(html: string): DocumentFragment {
|
|
@@ -503,21 +503,21 @@ export function tpl(html: string): DocumentFragment {
|
|
|
503
503
|
return t.content.cloneNode(true) as DocumentFragment;
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
const HEAD_ATTR = 'data-
|
|
506
|
+
const HEAD_ATTR = 'data-rosefc-head';
|
|
507
507
|
|
|
508
508
|
export const handlers: Record<string, (e: Event) => void> =
|
|
509
|
-
(globalThis as {
|
|
509
|
+
(globalThis as { __rosefc_handlers?: Record<string, (e: Event) => void> }).__rosefc_handlers ??= {};
|
|
510
510
|
|
|
511
511
|
// The action dispatcher the client entry installs. Shared, not per-copy: the
|
|
512
512
|
// elements it dispatches for are in the one document every copy wires, so the
|
|
513
513
|
// last entry to install wins for all of them - the same rule `handlers` and
|
|
514
514
|
// `directives` already follow.
|
|
515
|
-
const actionDispatchCell = sharedCell<((name: string, e: Event) => void) | null>('
|
|
515
|
+
const actionDispatchCell = sharedCell<((name: string, e: Event) => void) | null>('__rosefc_actionDispatch', null);
|
|
516
516
|
export function setActionDispatcher(fn: (name: string, e: Event) => void): void {
|
|
517
517
|
actionDispatchCell.value = fn;
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
const directBound: WeakMap<Element, Set<string>> = shared('
|
|
520
|
+
const directBound: WeakMap<Element, Set<string>> = shared('__rosefc_directBound', () => new WeakMap());
|
|
521
521
|
|
|
522
522
|
/**
|
|
523
523
|
* F5: `use:NAME` element actions - the plugin point for DOM behaviors
|
|
@@ -529,10 +529,10 @@ const directBound: WeakMap<Element, Set<string>> = shared('__rosefd_directBound'
|
|
|
529
529
|
* of the reactive graph keeps the action contract one argument.
|
|
530
530
|
*/
|
|
531
531
|
export const directives: Record<string, (el: Element, param?: unknown) => void | (() => void) | { update?: (p: unknown) => void; destroy?: () => void }> =
|
|
532
|
-
(globalThis as {
|
|
533
|
-
.
|
|
532
|
+
(globalThis as { __rosefc_directives?: Record<string, (el: Element, param?: unknown) => void | (() => void) | { update?: (p: unknown) => void; destroy?: () => void }> })
|
|
533
|
+
.__rosefc_directives ??= {};
|
|
534
534
|
|
|
535
|
-
const actionRecords: WeakMap<Element, { update?: (p: unknown) => void; destroy?: () => void }> = shared('
|
|
535
|
+
const actionRecords: WeakMap<Element, { update?: (p: unknown) => void; destroy?: () => void }> = shared('__rosefc_actionRecords', () => new WeakMap());
|
|
536
536
|
|
|
537
537
|
/**
|
|
538
538
|
* Apply one route's whole <head> (every block of its chain, page first) to the
|
|
@@ -603,7 +603,7 @@ function linkIdentitySuffix(rel: string, el: Element): string {
|
|
|
603
603
|
return `@${el.getAttribute('href') ?? el.getAttribute('type') ?? el.getAttribute('as') ?? ''}`;
|
|
604
604
|
}
|
|
605
605
|
|
|
606
|
-
/** Drop every
|
|
606
|
+
/** Drop every Rosefc-managed head element (routes without <head> blocks). */
|
|
607
607
|
export function clearHead(): void {
|
|
608
608
|
document.head.querySelectorAll(`[${HEAD_ATTR}]`).forEach((el) => el.remove());
|
|
609
609
|
}
|
|
@@ -1031,7 +1031,7 @@ export function wire(root: Node, closes: Array<unknown>, scope?: unknown): Clean
|
|
|
1031
1031
|
}, duration + delay);
|
|
1032
1032
|
});
|
|
1033
1033
|
} else {
|
|
1034
|
-
const custom = (globalThis as any).
|
|
1034
|
+
const custom = (globalThis as any).__rosefcTransitions?.[name];
|
|
1035
1035
|
if (custom) {
|
|
1036
1036
|
const ret = custom(el, duration, 'out');
|
|
1037
1037
|
if (typeof ret === 'function') created.push(ret);
|
|
@@ -1077,7 +1077,7 @@ export function wire(root: Node, closes: Array<unknown>, scope?: unknown): Clean
|
|
|
1077
1077
|
}, duration + delay);
|
|
1078
1078
|
});
|
|
1079
1079
|
} else {
|
|
1080
|
-
const custom = (globalThis as any).
|
|
1080
|
+
const custom = (globalThis as any).__rosefcTransitions?.[name];
|
|
1081
1081
|
if (custom) {
|
|
1082
1082
|
const ret = custom(el, duration, dir || 'both');
|
|
1083
1083
|
if (typeof ret === 'function') created.push(ret);
|
|
@@ -1491,7 +1491,7 @@ export function $data<T>(fn: () => T | Promise<T>): Promise<T> {
|
|
|
1491
1491
|
return p as Promise<T>;
|
|
1492
1492
|
}
|
|
1493
1493
|
|
|
1494
|
-
const bffCache: Map<string, Promise<unknown>> = shared('
|
|
1494
|
+
const bffCache: Map<string, Promise<unknown>> = shared('__rosefc_bffCache', () => new Map());
|
|
1495
1495
|
|
|
1496
1496
|
export function loader<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
1497
1497
|
let p = bffCache.get(key) as Promise<T> | undefined;
|
|
@@ -1597,7 +1597,7 @@ export async function isolateRender<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
1597
1597
|
// The tail of the fallback render queue; see isolateRender. Shared across
|
|
1598
1598
|
// bundle copies because the tables the renders swap are shared; a module-level
|
|
1599
1599
|
// mutex would order one copy's renders and let another copy's interleave.
|
|
1600
|
-
const renderMutexCell = sharedCell<Promise<void>>('
|
|
1600
|
+
const renderMutexCell = sharedCell<Promise<void>>('__rosefc_renderMutex', Promise.resolve());
|
|
1601
1601
|
|
|
1602
1602
|
let requestQuery: Record<string, string> = {};
|
|
1603
1603
|
/** R248: the request-scoped query slot in the context-store bag. */
|
|
@@ -1712,7 +1712,7 @@ type RenderMaps = { data: Map<string, Promise<unknown>>; signals: Map<string, Si
|
|
|
1712
1712
|
// same single-render maps, so a $data promise or a seeded key written by one
|
|
1713
1713
|
// copy's render is visible to the next one's - which is exactly what a
|
|
1714
1714
|
// client-side navigation between two separately-bundled routes needs.
|
|
1715
|
-
const fallbackRenderMaps: RenderMaps = shared('
|
|
1715
|
+
const fallbackRenderMaps: RenderMaps = shared('__rosefc_fallbackRenderMaps', () => ({ data: new Map(), signals: signalMap, seeded: seededKeys }));
|
|
1716
1716
|
|
|
1717
1717
|
function renderMaps(): RenderMaps {
|
|
1718
1718
|
// The store's map-of-maps is typed Record<string, unknown> but carries symbol
|
|
@@ -1859,8 +1859,8 @@ export function resumeState(json: string): void {
|
|
|
1859
1859
|
}
|
|
1860
1860
|
|
|
1861
1861
|
const SDUI_TAGS = ['div', 'span', 'a', 'p', 'img', 'ul', 'li', 'button', 'input', 'section', 'h1', 'h2', 'h3', 'nav', 'header', 'footer', 'form', 'label', 'strong', 'em', 'table', 'tr', 'td', 'code', 'pre', 'article', 'main', 'aside'];
|
|
1862
|
-
const sduiTagToId: Map<string, number> = shared('
|
|
1863
|
-
const sduiIdToTag: Map<number, string> = shared('
|
|
1862
|
+
const sduiTagToId: Map<string, number> = shared('__rosefc_sduiTagToId', () => new Map(SDUI_TAGS.map((t, i) => [t, i])));
|
|
1863
|
+
const sduiIdToTag: Map<number, string> = shared('__rosefc_sduiIdToTag', () => new Map(SDUI_TAGS.map((t, i) => [i, t])));
|
|
1864
1864
|
|
|
1865
1865
|
export interface SduiNode {
|
|
1866
1866
|
t: string;
|
|
@@ -1889,7 +1889,7 @@ export function decodeSdui(json: string): SduiNode {
|
|
|
1889
1889
|
|
|
1890
1890
|
// The SWR refresh hook the client entry installs - shared like the action
|
|
1891
1891
|
// dispatcher: it drives the one document every copy wired, last install wins.
|
|
1892
|
-
const refreshHookCell = sharedCell<(() => Promise<void>) | null>('
|
|
1892
|
+
const refreshHookCell = sharedCell<(() => Promise<void>) | null>('__rosefc_refreshHook', null);
|
|
1893
1893
|
|
|
1894
1894
|
export function setRefreshHook(fn: () => Promise<void>): void {
|
|
1895
1895
|
refreshHookCell.value = fn;
|
|
@@ -1900,7 +1900,7 @@ export function refresh(): Promise<void> {
|
|
|
1900
1900
|
return hook ? hook() : Promise.resolve();
|
|
1901
1901
|
}
|
|
1902
1902
|
|
|
1903
|
-
const mountQueue: Array<() => void> = shared('
|
|
1903
|
+
const mountQueue: Array<() => void> = shared('__rosefc_mountQueue', () => []);
|
|
1904
1904
|
|
|
1905
1905
|
export function onMount(fn: () => void): void {
|
|
1906
1906
|
if (typeof document === 'undefined') return;
|
|
@@ -1942,7 +1942,7 @@ const localeCell = sharedCell<{
|
|
|
1942
1942
|
dicts: Record<string, Record<string, string>>;
|
|
1943
1943
|
def: string;
|
|
1944
1944
|
packs: string[];
|
|
1945
|
-
}>('
|
|
1945
|
+
}>('__rosefc_localeCell', { dicts: {}, def: 'en', packs: [] });
|
|
1946
1946
|
|
|
1947
1947
|
export function setLocales(dicts: Record<string, Record<string, string>>, def: string): void {
|
|
1948
1948
|
const c = localeCell.value;
|
|
@@ -2015,7 +2015,7 @@ export function bestLocale(header: string | null | undefined): string | null {
|
|
|
2015
2015
|
|
|
2016
2016
|
/** The signal `redirect()` throws. */
|
|
2017
2017
|
export class RoseRedirect extends Error {
|
|
2018
|
-
readonly
|
|
2018
|
+
readonly __rosefc = 'redirect';
|
|
2019
2019
|
constructor(readonly path: string, readonly status = 307) {
|
|
2020
2020
|
super(`redirect to ${path}`);
|
|
2021
2021
|
}
|
|
@@ -2023,7 +2023,7 @@ export class RoseRedirect extends Error {
|
|
|
2023
2023
|
|
|
2024
2024
|
/** The signal `notFound()` throws. */
|
|
2025
2025
|
export class RoseNotFound extends Error {
|
|
2026
|
-
readonly
|
|
2026
|
+
readonly __rosefc = 'notFound';
|
|
2027
2027
|
constructor() {
|
|
2028
2028
|
super('notFound()');
|
|
2029
2029
|
}
|
|
@@ -2066,7 +2066,7 @@ export function localeDir(lang: string): string {
|
|
|
2066
2066
|
const ICU_RE = /\{([\w$]+),\s*(plural|selectordinal|select),\s*((?:\s*[=\w]+\s*\{(?:[^{}]|\{[^{}]*\})*\})*)\}/g;
|
|
2067
2067
|
const BRANCH_RE = /([=\w]+)\s*\{((?:[^{}]|\{[^{}]*\})*)\}/g;
|
|
2068
2068
|
|
|
2069
|
-
const PLURAL_RULES: Map<string, Intl.PluralRules> = shared('
|
|
2069
|
+
const PLURAL_RULES: Map<string, Intl.PluralRules> = shared('__rosefc_pluralRules', () => new Map());
|
|
2070
2070
|
|
|
2071
2071
|
/** The CLDR category of `n` in `locale`, or null where Intl has no answer. */
|
|
2072
2072
|
function pluralCategory(locale: string, ordinal: boolean, n: number): string | null {
|
|
@@ -2115,11 +2115,11 @@ export function $t(key: string, vars?: Record<string, string | number>): string
|
|
|
2115
2115
|
return out;
|
|
2116
2116
|
}
|
|
2117
2117
|
|
|
2118
|
-
const STORES: Map<string, { value: unknown; subs: Set<Subscriber> }> = shared('
|
|
2118
|
+
const STORES: Map<string, { value: unknown; subs: Set<Subscriber> }> = shared('__rosefc_stores', () => new Map());
|
|
2119
2119
|
// The SSE transport for $store - shared like the dispatcher and the refresh
|
|
2120
2120
|
// hook: it is installed once per page by the client entry, and every copy's
|
|
2121
2121
|
// store writes must travel over the one channel that entry opened.
|
|
2122
|
-
const storeTransportCell = sharedCell<((name: string, value: unknown) => void) | null>('
|
|
2122
|
+
const storeTransportCell = sharedCell<((name: string, value: unknown) => void) | null>('__rosefc_storeTransport', null);
|
|
2123
2123
|
|
|
2124
2124
|
// A store write (or an applied patch) fans out to whoever read the store inside a
|
|
2125
2125
|
// tracked scope - the wire() effects that render {store.get()} / {#each store.get()}.
|
|
@@ -2198,7 +2198,7 @@ export function $store<T>(name: string, initial: T): { get(): T; set(v: T): void
|
|
|
2198
2198
|
// === Network listeners (isomorphic) ===
|
|
2199
2199
|
// $udp / $tcp / $listen / $websocket let an app open a socket and react to the
|
|
2200
2200
|
// outside world (LoRa gateways, MQTT-ish feeds, raw TCP devices, live dashboards)
|
|
2201
|
-
// from inside a
|
|
2201
|
+
// from inside a Rosefc app. They only REGISTER a spec here; the host binds them
|
|
2202
2202
|
// via __startNetwork() at serve-worker boot - so a build, a prerender, or a dev
|
|
2203
2203
|
// hot-re-import never opens a socket. The registries live on globalThis: a dev
|
|
2204
2204
|
// re-import re-registers specs against the same live handles.
|
|
@@ -2212,10 +2212,10 @@ export function $store<T>(name: string, initial: T): { get(): T; set(v: T): void
|
|
|
2212
2212
|
// ReferenceError.
|
|
2213
2213
|
type NetOpts = Record<string, unknown>;
|
|
2214
2214
|
const __netSpecs: { kind: string; port: number; host?: string; opts: NetOpts }[] =
|
|
2215
|
-
(globalThis as any).
|
|
2216
|
-
const __netHandles: any[] = (globalThis as any).
|
|
2215
|
+
(globalThis as any).__rosefc_netSpecs || ((globalThis as any).__rosefc_netSpecs = []);
|
|
2216
|
+
const __netHandles: any[] = (globalThis as any).__rosefc_netHandles || ((globalThis as any).__rosefc_netHandles = []);
|
|
2217
2217
|
/** key ('kind:port:host') -> live handle. A Map, not a Set: re-registering a key (per-request render or dev hot re-import) swaps the callbacks in place. */
|
|
2218
|
-
const __netBound: Map<string, any> = (globalThis as any).
|
|
2218
|
+
const __netBound: Map<string, any> = (globalThis as any).__rosefc_netBound || ((globalThis as any).__rosefc_netBound = new Map());
|
|
2219
2219
|
|
|
2220
2220
|
function __netBind(spec: { kind: string; port: number; host?: string; opts: NetOpts }): void {
|
|
2221
2221
|
const key = spec.kind + ':' + spec.port + ':' + (spec.host || '');
|
|
@@ -2227,18 +2227,18 @@ function __netBind(spec: { kind: string; port: number; host?: string; opts: NetO
|
|
|
2227
2227
|
}
|
|
2228
2228
|
const o = spec.opts || {};
|
|
2229
2229
|
if (spec.kind === 'udp') {
|
|
2230
|
-
const dg = (globalThis as any).
|
|
2230
|
+
const dg = (globalThis as any).__rosefc_dgram;
|
|
2231
2231
|
if (!dg) return;
|
|
2232
2232
|
const handle = { opts: o, sock: null as any };
|
|
2233
2233
|
const sock = dg.createSocket('udp4');
|
|
2234
2234
|
handle.sock = sock;
|
|
2235
2235
|
sock.on('message', (msg: Buffer, rinfo: unknown) => (handle.opts.onMessage as (m: Buffer, r: unknown) => void)?.(msg, rinfo));
|
|
2236
|
-
sock.on('error', (err: Error) => { if (handle.opts.onError) (handle.opts.onError as (e: Error) => void)(err); else console.error('[
|
|
2236
|
+
sock.on('error', (err: Error) => { if (handle.opts.onError) (handle.opts.onError as (e: Error) => void)(err); else console.error('[rosefc] UDP error:', err); });
|
|
2237
2237
|
sock.bind(spec.port, spec.host || '0.0.0.0');
|
|
2238
2238
|
__netHandles.push(sock);
|
|
2239
2239
|
__netBound.set(key, handle);
|
|
2240
2240
|
} else if (spec.kind === 'tcp') {
|
|
2241
|
-
const nt = (globalThis as any).
|
|
2241
|
+
const nt = (globalThis as any).__rosefc_net;
|
|
2242
2242
|
if (!nt) return;
|
|
2243
2243
|
const handle = { opts: o, srv: null as any };
|
|
2244
2244
|
const idleMs = ((o.idleTimeout as number) ?? 0) * 1000;
|
|
@@ -2260,13 +2260,13 @@ function __netBind(spec: { kind: string; port: number; host?: string; opts: NetO
|
|
|
2260
2260
|
socket.on('error', () => {});
|
|
2261
2261
|
});
|
|
2262
2262
|
handle.srv = srv;
|
|
2263
|
-
srv.on('error', (err: Error) => console.error('[
|
|
2263
|
+
srv.on('error', (err: Error) => console.error('[rosefc] TCP server error:', err));
|
|
2264
2264
|
srv.listen(spec.port, spec.host || '0.0.0.0');
|
|
2265
2265
|
__netHandles.push(srv);
|
|
2266
2266
|
__netBound.set(key, handle);
|
|
2267
2267
|
} else if (spec.kind === 'ws') {
|
|
2268
|
-
const http = (globalThis as any).
|
|
2269
|
-
const cryptoMod = (globalThis as any).
|
|
2268
|
+
const http = (globalThis as any).__rosefc_http;
|
|
2269
|
+
const cryptoMod = (globalThis as any).__rosefc_crypto;
|
|
2270
2270
|
if (!http || !cryptoMod) return;
|
|
2271
2271
|
const clients = new Set<any>();
|
|
2272
2272
|
const handle = {
|
|
@@ -2296,7 +2296,7 @@ function __netBind(spec: { kind: string; port: number; host?: string; opts: NetO
|
|
|
2296
2296
|
clients.add(wrapped);
|
|
2297
2297
|
(opts.onConnection as (ws: unknown) => void)?.(wrapped);
|
|
2298
2298
|
});
|
|
2299
|
-
srv.on('error', (err: Error) => { if (handle.opts.onError) (handle.opts.onError as (e: Error) => void)(err); else console.error('[
|
|
2299
|
+
srv.on('error', (err: Error) => { if (handle.opts.onError) (handle.opts.onError as (e: Error) => void)(err); else console.error('[rosefc] WebSocket server error:', err); });
|
|
2300
2300
|
srv.listen(spec.port, spec.host || '0.0.0.0');
|
|
2301
2301
|
// Keepalive: a half-open peer (network partition, crashed device) never
|
|
2302
2302
|
// sends FIN, so its socket would sit in `clients` forever and every
|
|
@@ -2317,7 +2317,7 @@ function __netBind(spec: { kind: string; port: number; host?: string; opts: NetO
|
|
|
2317
2317
|
__netHandles.push(handle);
|
|
2318
2318
|
__netBound.set(key, handle);
|
|
2319
2319
|
} else if (spec.kind === 'mqtt') {
|
|
2320
|
-
const nt = (globalThis as any).
|
|
2320
|
+
const nt = (globalThis as any).__rosefc_net;
|
|
2321
2321
|
if (!nt) return;
|
|
2322
2322
|
const client = __mqttConnect(nt, spec.port, spec.host || 'localhost', o);
|
|
2323
2323
|
__netHandles.push(client);
|
|
@@ -2327,7 +2327,7 @@ function __netBind(spec: { kind: string; port: number; host?: string; opts: NetO
|
|
|
2327
2327
|
function __netReg(kind: string, port: number, host: string | undefined, opts: NetOpts): void {
|
|
2328
2328
|
const spec = { kind, port, host, opts };
|
|
2329
2329
|
__netSpecs.push(spec);
|
|
2330
|
-
if ((globalThis as any).
|
|
2330
|
+
if ((globalThis as any).__rosefc_netStarted === true) __netBind(spec);
|
|
2331
2331
|
}
|
|
2332
2332
|
/** Drop every pending spec for a key and close its live handle (app-level close()). */
|
|
2333
2333
|
function __netUnreg(kind: string, port: number, host: string | undefined): void {
|
|
@@ -2345,7 +2345,7 @@ function __netUnreg(kind: string, port: number, host: string | undefined): void
|
|
|
2345
2345
|
}
|
|
2346
2346
|
/** Bind every registered listener. Called once at serve-worker boot by the host. */
|
|
2347
2347
|
export function __startNetwork(): void {
|
|
2348
|
-
(globalThis as any).
|
|
2348
|
+
(globalThis as any).__rosefc_netStarted = true;
|
|
2349
2349
|
for (const spec of __netSpecs) __netBind(spec);
|
|
2350
2350
|
}
|
|
2351
2351
|
/** Close every bound listener. Called on worker shutdown. */
|
|
@@ -2505,7 +2505,7 @@ export function $tcp(portOrHost: number | string, opts: NetOpts = {}): any {
|
|
|
2505
2505
|
__netReg('tcp', port, opts.host as string | undefined, opts);
|
|
2506
2506
|
return { ...__NOOP_NET, address: () => ({ port, host: (opts.host as string) || '0.0.0.0' }) };
|
|
2507
2507
|
}
|
|
2508
|
-
const nt = (globalThis as any).
|
|
2508
|
+
const nt = (globalThis as any).__rosefc_net;
|
|
2509
2509
|
if (!nt) return { ...__NOOP_NET };
|
|
2510
2510
|
const host = typeof portOrHost === 'string' ? portOrHost : (opts.host as string);
|
|
2511
2511
|
const sock = nt.createConnection({ host, port: opts.port as number });
|
|
@@ -2569,7 +2569,7 @@ export function $mqtt(broker: string, opts: NetOpts = {}): any {
|
|
|
2569
2569
|
if (h) return (h.connect as () => Promise<void>)();
|
|
2570
2570
|
__netBind({ kind: 'mqtt', port, host, opts });
|
|
2571
2571
|
const h2 = bound();
|
|
2572
|
-
return h2 ? (h2.connect as () => Promise<void>)() : Promise.reject(new Error('[
|
|
2572
|
+
return h2 ? (h2.connect as () => Promise<void>)() : Promise.reject(new Error('[rosefc] MQTT client unavailable (node:net not injected)'));
|
|
2573
2573
|
},
|
|
2574
2574
|
disconnect: () => { (bound() as { disconnect?: () => void } | undefined)?.disconnect?.(); },
|
|
2575
2575
|
subscribe: (topic: string, sopts: NetOpts = {}) => { (bound() as { subscribe?: (t: string, s: NetOpts) => void } | undefined)?.subscribe?.(topic, sopts); },
|
|
@@ -2749,7 +2749,7 @@ function __mqttConnect(nt: any, port: number, host: string, o: NetOpts): any {
|
|
|
2749
2749
|
const password = opts.password as string | undefined;
|
|
2750
2750
|
if (username) flags |= 0x80;
|
|
2751
2751
|
if (password) flags |= 0x40;
|
|
2752
|
-
const parts: Buffer[] = [__mqttStr('MQTT'), Buffer.from([4]), Buffer.from([flags]), __mqttU16((opts.keepalive as number) ?? 60), __mqttStr((opts.clientId as string) || ('
|
|
2752
|
+
const parts: Buffer[] = [__mqttStr('MQTT'), Buffer.from([4]), Buffer.from([flags]), __mqttU16((opts.keepalive as number) ?? 60), __mqttStr((opts.clientId as string) || ('rosefc_' + Math.random().toString(16).slice(2, 10)))];
|
|
2753
2753
|
if (username) parts.push(__mqttStr(username));
|
|
2754
2754
|
if (password) parts.push(__mqttStr(password));
|
|
2755
2755
|
s.write(__mqttPacket(1, 0, Buffer.concat(parts)));
|
|
@@ -2777,7 +2777,7 @@ function __mqttConnect(nt: any, port: number, host: string, o: NetOpts): any {
|
|
|
2777
2777
|
resolveAck();
|
|
2778
2778
|
(client.opts.onConnect as (() => void) | undefined)?.();
|
|
2779
2779
|
} else {
|
|
2780
|
-
const err = new Error('[
|
|
2780
|
+
const err = new Error('[rosefc] MQTT broker refused the connection (code ' + (pkt.body[1] ?? '?') + ')');
|
|
2781
2781
|
rejectAck(err);
|
|
2782
2782
|
(client.opts.onError as ((e: Error) => void) | undefined)?.(err);
|
|
2783
2783
|
}
|
|
@@ -2803,7 +2803,7 @@ function __mqttConnect(nt: any, port: number, host: string, o: NetOpts): any {
|
|
|
2803
2803
|
client.connected = false;
|
|
2804
2804
|
if (client.__keepalive) { clearInterval(client.__keepalive); client.__keepalive = null; }
|
|
2805
2805
|
if (client.__closed) return; // an app-level disconnect() is final
|
|
2806
|
-
rejectAck(new Error('[
|
|
2806
|
+
rejectAck(new Error('[rosefc] MQTT connection closed before CONNACK'));
|
|
2807
2807
|
scheduleReconnect();
|
|
2808
2808
|
});
|
|
2809
2809
|
s.on('error', (err: Error) => {
|
|
@@ -2870,7 +2870,7 @@ export function $actionErrorOrThrow(): void {
|
|
|
2870
2870
|
|
|
2871
2871
|
/** The {#boundary} fallback: the action's own message when the failure is a business one. */
|
|
2872
2872
|
export function $boundaryFallback(err: unknown): string {
|
|
2873
|
-
if (err instanceof ActionError) return `<p class="
|
|
2873
|
+
if (err instanceof ActionError) return `<p class="rosefc-action-error">${esc(err.message)}</p>`;
|
|
2874
2874
|
return '<p>This section failed to render.</p>';
|
|
2875
2875
|
}
|
|
2876
2876
|
|
|
@@ -2889,7 +2889,7 @@ export const $prepend = (value: unknown): StateDelta => new StateDelta('p', valu
|
|
|
2889
2889
|
/** Shallow-merge an object into an object-valued state key. */
|
|
2890
2890
|
export const $merge = (value: Record<string, unknown>): StateDelta => new StateDelta('m', value);
|
|
2891
2891
|
|
|
2892
|
-
const pendingDeltas: Map<string, StateDelta> = shared('
|
|
2892
|
+
const pendingDeltas: Map<string, StateDelta> = shared('__rosefc_pendingDeltas', () => new Map());
|
|
2893
2893
|
|
|
2894
2894
|
/** Record the deltas of the action that just ran (the dispatch calls this). */
|
|
2895
2895
|
export function setStateDelta(key: string, delta: StateDelta): void {
|
|
@@ -2961,10 +2961,10 @@ export function $sessionCookie(
|
|
|
2961
2961
|
export function reportVitals(): void {
|
|
2962
2962
|
if (typeof window === 'undefined' || typeof performance === 'undefined') return;
|
|
2963
2963
|
const live: Record<string, number> = {};
|
|
2964
|
-
(window as unknown as {
|
|
2964
|
+
(window as unknown as { __rosefc_vitals?: Record<string, number> }).__rosefc_vitals = live;
|
|
2965
2965
|
const send = (name: string, value: number) => {
|
|
2966
2966
|
live[name] = value;
|
|
2967
|
-
window.dispatchEvent(new CustomEvent('
|
|
2967
|
+
window.dispatchEvent(new CustomEvent('rosefc:vitals', { detail: { name, value, route: location.pathname } }));
|
|
2968
2968
|
};
|
|
2969
2969
|
const nav = performance.getEntriesByType?.('navigation')?.[0] as PerformanceNavigationTiming | undefined;
|
|
2970
2970
|
if (nav) send('ttfb', Math.round(nav.responseStart));
|
|
@@ -19,12 +19,12 @@ declare function $setState<T>(key: string, value: T): void;
|
|
|
19
19
|
/**
|
|
20
20
|
* Computed state: compiles to a plain getter over the signals the expression
|
|
21
21
|
* reads, so every read recomputes - no subscription machinery, never
|
|
22
|
-
* serialized. Top-level declarations only. The expression is
|
|
22
|
+
* serialized. Top-level declarations only. The expression is Rosefc syntax
|
|
23
23
|
* (bare state reads are callified at compile time), so it checks as `any`.
|
|
24
24
|
*/
|
|
25
25
|
declare function $derived(expr: any): Getter<any>;
|
|
26
26
|
|
|
27
|
-
/** Like $state, but every write mirrors to localStorage (JSON, `
|
|
27
|
+
/** Like $state, but every write mirrors to localStorage (JSON, `rosefc:` prefix) and boot re-seeds from it. */
|
|
28
28
|
declare function $persist<T>(initial: T): Getter<T>;
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -116,9 +116,9 @@ declare function bestLocale(header: string | null | undefined): string | null;
|
|
|
116
116
|
|
|
117
117
|
// === Globals the compiler injects when a route uses them ===
|
|
118
118
|
// These live on globalThis in the built bundle; declaring them here keeps
|
|
119
|
-
// `
|
|
119
|
+
// `rosefc check` honest about code that reaches for them directly.
|
|
120
120
|
|
|
121
|
-
declare var
|
|
121
|
+
declare var __rosefcAuth: {
|
|
122
122
|
hashPassword(pw: string): string;
|
|
123
123
|
verifyPassword(pw: string, hash: string): boolean;
|
|
124
124
|
signSession(data: Record<string, unknown>, secret: string, ttlSeconds?: number): string;
|
|
@@ -127,7 +127,7 @@ declare var __rosefdAuth: {
|
|
|
127
127
|
verifyCsrf(token: string, secret: string, sessionId: string): boolean;
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
-
declare var
|
|
130
|
+
declare var __rosefcKvApi: {
|
|
131
131
|
kv: {
|
|
132
132
|
get(key: string): unknown;
|
|
133
133
|
set(key: string, value: unknown, ttlSeconds?: number): void;
|
|
@@ -137,7 +137,7 @@ declare var __rosefdKvApi: {
|
|
|
137
137
|
kvMemo: typeof $kv;
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
interface
|
|
140
|
+
interface RosefcContentEntry {
|
|
141
141
|
slug: string;
|
|
142
142
|
title?: string;
|
|
143
143
|
date?: string;
|
|
@@ -151,13 +151,13 @@ interface RosefdContentEntry {
|
|
|
151
151
|
[key: string]: unknown;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
declare var
|
|
154
|
+
declare var __rosefcContent: {
|
|
155
155
|
list(coll: string, filter?: { tag: string }): Array<Record<string, unknown>>;
|
|
156
156
|
rss(coll: string, opts: { title: string; origin: string; description: string }): string;
|
|
157
|
-
get(coll: string, slug: string, vars?: Record<string, string>):
|
|
157
|
+
get(coll: string, slug: string, vars?: Record<string, string>): RosefcContentEntry | null;
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
-
declare var
|
|
160
|
+
declare var __rosefcExtras: {
|
|
161
161
|
v: {
|
|
162
162
|
validate(body: Record<string, unknown>, rules: Record<string, string[]>): {
|
|
163
163
|
ok: boolean;
|
|
@@ -167,8 +167,8 @@ declare var __rosefdExtras: {
|
|
|
167
167
|
};
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
-
/** Named view transitions: `globalThis.
|
|
171
|
-
declare var
|
|
170
|
+
/** Named view transitions: `globalThis.__rosefcTransitions.pulse = (el, ms) => () => {}` - used by `transition:pulse` in templates. */
|
|
171
|
+
declare var __rosefcTransitions: Record<string, (el: HTMLElement, duration: number) => () => void>;
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
declare function redirect(path: string, status?: 301 | 302 | 303 | 307 | 308): never;
|
|
@@ -198,7 +198,7 @@ interface UDPSocket {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
/**
|
|
201
|
-
* Listen for UDP datagrams (LoRa gateways, sensor feeds) from inside a
|
|
201
|
+
* Listen for UDP datagrams (LoRa gateways, sensor feeds) from inside a Rosefc
|
|
202
202
|
* app. Only registers the spec; the framework binds it at serve-worker boot, so
|
|
203
203
|
* a build/prerender never opens a socket.
|
|
204
204
|
*/
|
|
File without changes
|