@getforma/core 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-AJP4OVCN.js → chunk-3U2IQIKB.js} +4 -4
- package/dist/{chunk-AJP4OVCN.js.map → chunk-3U2IQIKB.js.map} +1 -1
- package/dist/{chunk-FBM7V4NE.cjs → chunk-D46EJ3VS.cjs} +43 -43
- package/dist/{chunk-FBM7V4NE.cjs.map → chunk-D46EJ3VS.cjs.map} +1 -1
- package/dist/{chunk-Y3A2EVVS.js → chunk-F5QUU76U.js} +4 -4
- package/dist/{chunk-Y3A2EVVS.js.map → chunk-F5QUU76U.js.map} +1 -1
- package/dist/{chunk-ETPJTPYD.js → chunk-FAIWRZKJ.js} +6 -3
- package/dist/chunk-FAIWRZKJ.js.map +1 -0
- package/dist/{chunk-H7MDUHS5.cjs → chunk-HAULTMNX.cjs} +6 -2
- package/dist/chunk-HAULTMNX.cjs.map +1 -0
- package/dist/{chunk-KUXNZ5MG.cjs → chunk-PAKPQRVE.cjs} +14 -12
- package/dist/{chunk-KUXNZ5MG.cjs.map → chunk-PAKPQRVE.cjs.map} +1 -1
- package/dist/formajs.global.js +69 -11
- package/dist/formajs.global.js.map +1 -1
- package/dist/http.cjs +11 -11
- package/dist/http.js +2 -2
- package/dist/index.cjs +143 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -5
- package/dist/index.d.ts +39 -5
- package/dist/index.js +67 -16
- package/dist/index.js.map +1 -1
- package/dist/{resource-D6HfVgiA.d.cts → resource-CBTcorBJ.d.ts} +2 -2
- package/dist/{resource-ljs2X5NM.d.ts → resource-CaFfIB5i.d.cts} +2 -2
- package/dist/runtime.cjs +28 -28
- package/dist/runtime.js +3 -3
- package/dist/server.cjs +69 -16
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +31 -5
- package/dist/server.d.ts +31 -5
- package/dist/server.js +64 -12
- package/dist/server.js.map +1 -1
- package/dist/{signal-CRBJYQ6B.d.cts → signal-CIXTR4Qz.d.cts} +29 -1
- package/dist/{signal-CRBJYQ6B.d.ts → signal-CIXTR4Qz.d.ts} +29 -1
- package/dist/ssr/index.cjs +22 -4
- package/dist/ssr/index.cjs.map +1 -1
- package/dist/ssr/index.d.cts +7 -0
- package/dist/ssr/index.d.ts +7 -0
- package/dist/ssr/index.js +22 -4
- package/dist/ssr/index.js.map +1 -1
- package/dist/storage.cjs +66 -16
- package/dist/storage.cjs.map +1 -1
- package/dist/storage.js +66 -16
- package/dist/storage.js.map +1 -1
- package/dist/tc39-compat.cjs +3 -3
- package/dist/tc39-compat.d.cts +1 -1
- package/dist/tc39-compat.d.ts +1 -1
- package/dist/tc39-compat.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ETPJTPYD.js.map +0 -1
- package/dist/chunk-H7MDUHS5.cjs.map +0 -1
package/dist/ssr/index.js
CHANGED
|
@@ -264,6 +264,23 @@ function shSuspense(fallback, asyncChildren) {
|
|
|
264
264
|
children: [asyncChildren]
|
|
265
265
|
};
|
|
266
266
|
}
|
|
267
|
+
var TIMED_OUT = /* @__PURE__ */ Symbol("forma-suspense-timeout");
|
|
268
|
+
function raceTimeout(p, ms) {
|
|
269
|
+
if (ms == null || ms <= 0) return p;
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
const t = setTimeout(() => resolve(TIMED_OUT), ms);
|
|
272
|
+
p.then(
|
|
273
|
+
(v) => {
|
|
274
|
+
clearTimeout(t);
|
|
275
|
+
resolve(v);
|
|
276
|
+
},
|
|
277
|
+
(e) => {
|
|
278
|
+
clearTimeout(t);
|
|
279
|
+
reject(e);
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
267
284
|
async function* renderToStreamNew(node, options) {
|
|
268
285
|
const state = {
|
|
269
286
|
suspenseCounter: 0,
|
|
@@ -279,17 +296,18 @@ async function* renderToStreamNew(node, options) {
|
|
|
279
296
|
if (options?.bootstrapScript) {
|
|
280
297
|
yield options.bootstrapScript;
|
|
281
298
|
}
|
|
299
|
+
const suspenseTimeout = options?.suspenseTimeout;
|
|
282
300
|
while (state.pendingBoundaries.length > 0) {
|
|
283
301
|
const pending = [...state.pendingBoundaries];
|
|
284
302
|
state.pendingBoundaries = [];
|
|
285
303
|
const results = await Promise.allSettled(
|
|
286
304
|
pending.map(async (boundary) => {
|
|
287
|
-
const html = await boundary.promise;
|
|
305
|
+
const html = await raceTimeout(boundary.promise, suspenseTimeout);
|
|
288
306
|
return { id: boundary.id, html };
|
|
289
307
|
})
|
|
290
308
|
);
|
|
291
309
|
for (const result of results) {
|
|
292
|
-
if (result.status === "fulfilled") {
|
|
310
|
+
if (result.status === "fulfilled" && result.value.html !== TIMED_OUT) {
|
|
293
311
|
yield getSwapTag(result.value.id, result.value.html);
|
|
294
312
|
}
|
|
295
313
|
}
|
|
@@ -320,9 +338,9 @@ function renderStreamNode(node, parts, state) {
|
|
|
320
338
|
parts.push(`<div id="${id}" style="display:contents">`);
|
|
321
339
|
renderSync(fallback, parts);
|
|
322
340
|
parts.push("</div>");
|
|
323
|
-
const promise =
|
|
341
|
+
const promise = Promise.resolve().then(asyncFn).then((resolved) => {
|
|
324
342
|
const resolvedParts = [];
|
|
325
|
-
|
|
343
|
+
renderStreamNode(resolved, resolvedParts, state);
|
|
326
344
|
return resolvedParts.join("");
|
|
327
345
|
});
|
|
328
346
|
state.pendingBoundaries.push({ id, promise });
|
package/dist/ssr/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ssr/render.ts","../../src/ssr/ssr-reactive.ts","../../src/ssr/client-script.ts","../../src/ssr/stream.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// Void elements that don't have closing tags\nexport const VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr',\n]);\n\n// Map of prop names to HTML attribute names\nexport const PROP_TO_ATTR: Record<string, string> = {\n className: 'class',\n htmlFor: 'for',\n tabIndex: 'tabindex',\n};\n\n// Escape HTML entities\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// Escape attribute values\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/'/g, ''');\n}\n\nimport { isDangerousUrl, isUrlAttr, isEventHandlerAttr, isSafeAttrName } from '../security/url-safety.js';\n\n// Dangerous URI detection for href, src, action, formaction attributes.\n// Retained for backwards compatibility with external importers; the actual\n// scheme check now goes through `isDangerousUrl`, which additionally strips the\n// control characters browsers ignore in a URL scheme (see security/url-safety).\nexport const DANGEROUS_URI_ATTRS = new Set(['href', 'src', 'action', 'formaction']);\nexport const DANGEROUS_URI_RE = /^\\s*(javascript|vbscript|data\\s*:\\s*text\\/html)/i;\n\n/**\n * Decide whether a resolved prop should be emitted as an attribute, and return\n * the escaped `name=\"value\"` fragment (or `name` for boolean `true`). Returns\n * null when the attribute must be dropped for safety. Shared by the plain and\n * hydration-aware renderers so the security rules cannot drift apart.\n */\nexport function renderAttr(key: string, resolved: unknown): string | null {\n const attrName = PROP_TO_ATTR[key] ?? key;\n // Never emit event-handler or malformed attribute names — an attacker who\n // controls a prop key could otherwise inject `onload=…` or break out of the\n // attribute entirely with whitespace/quotes.\n if (isEventHandlerAttr(attrName) || !isSafeAttrName(attrName)) return null;\n if (resolved === true) return ' ' + attrName;\n if (resolved === false || resolved == null) return null;\n const str = String(resolved);\n if (isUrlAttr(attrName) && isDangerousUrl(str)) return null;\n return ' ' + attrName + '=\"' + escapeAttr(str) + '\"';\n}\n\nexport interface VNode {\n tag: string;\n props: Record<string, unknown> | null;\n children: unknown[];\n}\n\n/**\n * Server-side hyperscript — creates a virtual node instead of a DOM element.\n */\nexport function sh(tag: string, props?: Record<string, unknown> | null, ...children: unknown[]): VNode {\n return { tag, props: props ?? null, children };\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string.\n *\n * Uses an array buffer internally to avoid O(n^2) string concatenation\n * for large trees, then joins once at the end.\n *\n * Usage:\n * ```ts\n * import { renderToString, sh } from '@getforma/core/ssr';\n *\n * const html = renderToString(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()), // signal getters resolved synchronously\n * )\n * );\n * ```\n */\nexport function renderToString(node: unknown): string {\n const parts: string[] = [];\n renderToBuffer(node, parts);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer.\n * Avoids O(n^2) string concatenation for large trees.\n */\nfunction renderToBuffer(node: unknown, parts: string[]): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter) → resolve and render\n if (typeof node === 'function') { renderToBuffer(node(), parts); return; }\n\n // Array → render each\n if (Array.isArray(node)) {\n for (const child of node) renderToBuffer(child, parts);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip refs and internal props; event handlers and unsafe attribute\n // names are dropped inside renderAttr.\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBuffer(child, parts);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\nexport function isVNode(v: unknown): v is VNode {\n return v != null && typeof v === 'object' && 'tag' in v && 'children' in v;\n}\n\n// ---------------------------------------------------------------------------\n// Hydration-aware rendering\n// ---------------------------------------------------------------------------\n\n/** Per-render hydration context — avoids shared module-level counter. */\ninterface HydrationContext {\n id: number;\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string with hydration markers.\n *\n * Like `renderToString`, but injects comment markers and `data-forma-h`\n * attributes so the client-side `hydrate()` function can adopt existing\n * DOM nodes without re-creating them.\n *\n * Each call creates its own hydration counter, so concurrent calls\n * (e.g. multiple SSR requests in the same process) produce independent,\n * non-overlapping hydration IDs.\n *\n * Marker types:\n * - `data-forma-h=\"N\"` — element boundary (attribute on the element)\n * - `<!--forma-t:N-->` / `<!--/forma-t:N-->` — reactive text boundary\n * - `<!--forma-l:N-->` / `<!--/forma-l:N-->` — list boundary\n *\n * Usage:\n * ```ts\n * import { renderToStringWithHydration, sh } from '@getforma/core/ssr';\n *\n * const html = renderToStringWithHydration(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()),\n * )\n * );\n * ```\n */\nexport function renderToStringWithHydration(node: unknown): string {\n const ctx: HydrationContext = { id: 0 };\n const parts: string[] = [];\n renderToBufferHydrated(node, parts, ctx);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer with hydration markers.\n * The `ctx` object carries the hydration counter so concurrent renders are isolated.\n */\nfunction renderToBufferHydrated(node: unknown, parts: string[], ctx: HydrationContext): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter / reactive text) → wrap with text markers\n if (typeof node === 'function') {\n const id = ctx.id++;\n parts.push(`<!--forma-t:${id}-->`);\n renderToBufferHydrated(node(), parts, ctx);\n parts.push(`<!--/forma-t:${id}-->`);\n return;\n }\n\n // Array → wrap with list markers\n if (Array.isArray(node)) {\n const id = ctx.id++;\n parts.push(`<!--forma-l:${id}-->`);\n for (const child of node) renderToBufferHydrated(child, parts, ctx);\n parts.push(`<!--/forma-l:${id}-->`);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const id = ctx.id++;\n const { tag, props, children } = node;\n\n // Add hydration data-attribute to element\n parts.push('<', tag, ` data-forma-h=\"${id}\"`);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip refs and internal props; event handlers and unsafe attribute\n // names are dropped inside renderAttr.\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBufferHydrated(child, parts, ctx);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\n","/**\n * SSR-only signal: just a value holder, no reactivity needed on server.\n */\nexport function ssrSignal<T>(initial: T): [get: () => T, set: (v: T) => void] {\n let value = initial;\n return [() => value, (v) => { value = v; }];\n}\n\nexport function ssrComputed<T>(fn: () => T): () => T {\n return fn; // On server, just call the function directly\n}\n","/**\n * FormaJS SSR - Client Swap Script\n *\n * Tiny inline script injected into the HTML stream that handles\n * out-of-order Suspense boundary resolution.\n *\n * When the server resolves a Suspense boundary, it sends:\n * <script>$FORMA_SWAP(\"forma-s:0\",\"<resolved html>\")</script>\n *\n * This script replaces the fallback placeholder with the resolved content.\n */\n\n/**\n * Returns the client-side swap script as a string, ready to be injected\n * into the HTML stream (typically right after <body> or before first Suspense).\n */\nexport function getSwapScript(): string {\n return `<script>\nfunction $FORMA_SWAP(id,html){\n var el=document.getElementById(id);\n if(el){\n var tpl=document.createElement('template');\n tpl.innerHTML=html;\n el.replaceWith(tpl.content);\n }\n}\n</script>`;\n}\n\n/**\n * Serializes a value to a JSON string that is safe to embed inside a\n * `<script>` block. Standard `JSON.stringify` does not escape `<` or `>`,\n * so a payload containing `</script>` would prematurely close the script\n * tag, enabling an injection attack. We also escape U+2028 and U+2029\n * which are valid JSON but treated as line terminators in JavaScript source.\n */\nfunction safeJsonStringify(val: unknown): string {\n return JSON.stringify(val)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n\n/**\n * Returns a script tag that swaps a specific Suspense boundary's content.\n * Called when an async resource resolves during streaming.\n */\nexport function getSwapTag(id: string, html: string): string {\n // safeJsonStringify escapes <, >, U+2028, and U+2029 so the serialized\n // content cannot break out of the surrounding <script> block.\n return `<script>$FORMA_SWAP(${safeJsonStringify(id)},${safeJsonStringify(html)})</script>`;\n}\n","/**\n * FormaJS SSR - Streaming Renderer\n *\n * Implements true chunked HTML streaming with out-of-order Suspense resolution.\n *\n * How it works:\n * 1. Yields shell HTML immediately (opening tags, head, static content)\n * 2. When a Suspense boundary is encountered:\n * - Yields the fallback HTML with a placeholder: <div id=\"forma-s:N\" style=\"display:contents\">\n * - Continues rendering siblings without blocking\n * - When the async resource resolves, yields a swap script\n * 3. Suspense boundaries resolve independently in any order (out-of-order streaming)\n */\n\nimport { getSwapScript, getSwapTag } from './client-script.js';\nimport { escapeHtml, isVNode, VOID_ELEMENTS, renderAttr, type VNode } from './render.js';\n\n// ---------------------------------------------------------------------------\n// Suspense boundary tracking\n// ---------------------------------------------------------------------------\n\ninterface PendingBoundary {\n id: string;\n promise: Promise<string>;\n}\n\n/** Per-render mutable state, scoped to a single renderToStreamNew call. */\ninterface StreamState {\n suspenseCounter: number;\n pendingBoundaries: PendingBoundary[];\n}\n\n// ---------------------------------------------------------------------------\n// Synchronous render to buffer (for non-async content)\n// ---------------------------------------------------------------------------\n\nfunction renderSync(node: unknown, parts: string[]): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderSync(node(), parts); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderSync(child, parts);\n return;\n }\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const resolved = typeof value === 'function' ? value() : value;\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderSync(child, parts);\n }\n parts.push('</', tag, '>');\n return;\n }\n parts.push(escapeHtml(String(node)));\n}\n\n// ---------------------------------------------------------------------------\n// Suspense VNode detection\n// ---------------------------------------------------------------------------\n\n/**\n * A Suspense boundary VNode has a special shape:\n * { tag: 'forma-suspense', props: { fallback: VNode }, children: [asyncChildren] }\n *\n * Or it can be created via:\n * shSuspense(fallbackVNode, () => asyncContent)\n */\nexport interface SuspenseVNode {\n tag: 'forma-suspense';\n props: { fallback: unknown };\n children: [() => Promise<unknown>];\n}\n\nfunction isSuspenseVNode(v: unknown): v is SuspenseVNode {\n return isVNode(v) && v.tag === 'forma-suspense';\n}\n\n/**\n * Create a server-side Suspense VNode for streaming SSR.\n */\nexport function shSuspense(fallback: unknown, asyncChildren: () => Promise<unknown>): SuspenseVNode {\n return {\n tag: 'forma-suspense',\n props: { fallback },\n children: [asyncChildren],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Streaming renderer\n// ---------------------------------------------------------------------------\n\nexport interface StreamOptions {\n /** Script to bootstrap the client-side app (e.g. <script src=\"/app.js\"></script>) */\n bootstrapScript?: string;\n /** Whether to inject the $FORMA_SWAP client script (default: true) */\n injectSwapScript?: boolean;\n}\n\n/**\n * Render a FormaJS virtual tree to a stream of HTML chunks.\n *\n * Yields HTML progressively:\n * - Static content is yielded immediately\n * - Suspense boundaries yield their fallback, then resolve later\n * - Resolved Suspense boundaries yield swap scripts\n *\n * Usage:\n * ```ts\n * import { renderToStream, shSuspense, sh } from '@getforma/core/ssr';\n *\n * const stream = renderToStream(\n * sh('html', null,\n * sh('body', null,\n * sh('h1', null, 'Hello'),\n * shSuspense(\n * sh('div', null, 'Loading...'),\n * async () => {\n * const data = await fetchData();\n * return sh('div', null, data.name);\n * },\n * ),\n * ),\n * ),\n * );\n *\n * for await (const chunk of stream) {\n * response.write(chunk);\n * }\n * ```\n */\nexport async function* renderToStreamNew(\n node: unknown,\n options?: StreamOptions,\n): AsyncGenerator<string> {\n // Per-render state — scoped to this generator invocation\n const state: StreamState = {\n suspenseCounter: 0,\n pendingBoundaries: [],\n };\n\n const injectSwap = options?.injectSwapScript !== false;\n\n // Inject the swap script first (if there could be Suspense boundaries)\n if (injectSwap) {\n yield getSwapScript();\n }\n\n // Render the main tree synchronously, collecting Suspense boundaries\n const mainParts: string[] = [];\n renderStreamNode(node, mainParts, state);\n yield mainParts.join('');\n\n // Yield bootstrap script if provided\n if (options?.bootstrapScript) {\n yield options.bootstrapScript;\n }\n\n // Wait for and yield all pending Suspense boundaries\n // Each resolves independently (out-of-order)\n while (state.pendingBoundaries.length > 0) {\n const pending = [...state.pendingBoundaries];\n state.pendingBoundaries = [];\n\n // Race all pending boundaries — yield each as it resolves\n const results = await Promise.allSettled(\n pending.map(async (boundary) => {\n const html = await boundary.promise;\n return { id: boundary.id, html };\n }),\n );\n\n for (const result of results) {\n if (result.status === 'fulfilled') {\n yield getSwapTag(result.value.id, result.value.html);\n }\n // On rejection, the fallback stays visible (no swap)\n }\n }\n}\n\n/**\n * Render a node synchronously. If a Suspense boundary is encountered,\n * render the fallback inline and queue the async resolution.\n */\nfunction renderStreamNode(node: unknown, parts: string[], state: StreamState): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderStreamNode(node(), parts, state); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderStreamNode(child, parts, state);\n return;\n }\n\n // Suspense boundary: render fallback, queue async resolution\n if (isSuspenseVNode(node)) {\n const id = `forma-s:${state.suspenseCounter++}`;\n const fallback = node.props.fallback;\n const asyncFn = node.children[0]!;\n\n // Render fallback inside a replaceable container\n parts.push(`<div id=\"${id}\" style=\"display:contents\">`);\n renderSync(fallback, parts);\n parts.push('</div>');\n\n // Queue the async resolution\n const promise = asyncFn().then((resolved: unknown) => {\n const resolvedParts: string[] = [];\n renderSync(resolved, resolvedParts);\n return resolvedParts.join('');\n });\n state.pendingBoundaries.push({ id, promise });\n return;\n }\n\n // Regular VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'fallback') continue; // skip internal suspense props\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const resolved = typeof value === 'function' ? value() : value;\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderStreamNode(child, parts, state);\n }\n parts.push('</', tag, '>');\n return;\n }\n\n parts.push(escapeHtml(String(node)));\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/ssr/render.ts","../../src/ssr/ssr-reactive.ts","../../src/ssr/client-script.ts","../../src/ssr/stream.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// Void elements that don't have closing tags\nexport const VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr',\n]);\n\n// Map of prop names to HTML attribute names\nexport const PROP_TO_ATTR: Record<string, string> = {\n className: 'class',\n htmlFor: 'for',\n tabIndex: 'tabindex',\n};\n\n// Escape HTML entities\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// Escape attribute values\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/'/g, ''');\n}\n\nimport { isDangerousUrl, isUrlAttr, isEventHandlerAttr, isSafeAttrName } from '../security/url-safety.js';\n\n// Dangerous URI detection for href, src, action, formaction attributes.\n// Retained for backwards compatibility with external importers; the actual\n// scheme check now goes through `isDangerousUrl`, which additionally strips the\n// control characters browsers ignore in a URL scheme (see security/url-safety).\nexport const DANGEROUS_URI_ATTRS = new Set(['href', 'src', 'action', 'formaction']);\nexport const DANGEROUS_URI_RE = /^\\s*(javascript|vbscript|data\\s*:\\s*text\\/html)/i;\n\n/**\n * Decide whether a resolved prop should be emitted as an attribute, and return\n * the escaped `name=\"value\"` fragment (or `name` for boolean `true`). Returns\n * null when the attribute must be dropped for safety. Shared by the plain and\n * hydration-aware renderers so the security rules cannot drift apart.\n */\nexport function renderAttr(key: string, resolved: unknown): string | null {\n const attrName = PROP_TO_ATTR[key] ?? key;\n // Never emit event-handler or malformed attribute names — an attacker who\n // controls a prop key could otherwise inject `onload=…` or break out of the\n // attribute entirely with whitespace/quotes.\n if (isEventHandlerAttr(attrName) || !isSafeAttrName(attrName)) return null;\n if (resolved === true) return ' ' + attrName;\n if (resolved === false || resolved == null) return null;\n const str = String(resolved);\n if (isUrlAttr(attrName) && isDangerousUrl(str)) return null;\n return ' ' + attrName + '=\"' + escapeAttr(str) + '\"';\n}\n\nexport interface VNode {\n tag: string;\n props: Record<string, unknown> | null;\n children: unknown[];\n}\n\n/**\n * Server-side hyperscript — creates a virtual node instead of a DOM element.\n */\nexport function sh(tag: string, props?: Record<string, unknown> | null, ...children: unknown[]): VNode {\n return { tag, props: props ?? null, children };\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string.\n *\n * Uses an array buffer internally to avoid O(n^2) string concatenation\n * for large trees, then joins once at the end.\n *\n * Usage:\n * ```ts\n * import { renderToString, sh } from '@getforma/core/ssr';\n *\n * const html = renderToString(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()), // signal getters resolved synchronously\n * )\n * );\n * ```\n */\nexport function renderToString(node: unknown): string {\n const parts: string[] = [];\n renderToBuffer(node, parts);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer.\n * Avoids O(n^2) string concatenation for large trees.\n */\nfunction renderToBuffer(node: unknown, parts: string[]): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter) → resolve and render\n if (typeof node === 'function') { renderToBuffer(node(), parts); return; }\n\n // Array → render each\n if (Array.isArray(node)) {\n for (const child of node) renderToBuffer(child, parts);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip refs and internal props; event handlers and unsafe attribute\n // names are dropped inside renderAttr.\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBuffer(child, parts);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\nexport function isVNode(v: unknown): v is VNode {\n return v != null && typeof v === 'object' && 'tag' in v && 'children' in v;\n}\n\n// ---------------------------------------------------------------------------\n// Hydration-aware rendering\n// ---------------------------------------------------------------------------\n\n/** Per-render hydration context — avoids shared module-level counter. */\ninterface HydrationContext {\n id: number;\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string with hydration markers.\n *\n * Like `renderToString`, but injects comment markers and `data-forma-h`\n * attributes so the client-side `hydrate()` function can adopt existing\n * DOM nodes without re-creating them.\n *\n * Each call creates its own hydration counter, so concurrent calls\n * (e.g. multiple SSR requests in the same process) produce independent,\n * non-overlapping hydration IDs.\n *\n * Marker types:\n * - `data-forma-h=\"N\"` — element boundary (attribute on the element)\n * - `<!--forma-t:N-->` / `<!--/forma-t:N-->` — reactive text boundary\n * - `<!--forma-l:N-->` / `<!--/forma-l:N-->` — list boundary\n *\n * Usage:\n * ```ts\n * import { renderToStringWithHydration, sh } from '@getforma/core/ssr';\n *\n * const html = renderToStringWithHydration(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()),\n * )\n * );\n * ```\n */\nexport function renderToStringWithHydration(node: unknown): string {\n const ctx: HydrationContext = { id: 0 };\n const parts: string[] = [];\n renderToBufferHydrated(node, parts, ctx);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer with hydration markers.\n * The `ctx` object carries the hydration counter so concurrent renders are isolated.\n */\nfunction renderToBufferHydrated(node: unknown, parts: string[], ctx: HydrationContext): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter / reactive text) → wrap with text markers\n if (typeof node === 'function') {\n const id = ctx.id++;\n parts.push(`<!--forma-t:${id}-->`);\n renderToBufferHydrated(node(), parts, ctx);\n parts.push(`<!--/forma-t:${id}-->`);\n return;\n }\n\n // Array → wrap with list markers\n if (Array.isArray(node)) {\n const id = ctx.id++;\n parts.push(`<!--forma-l:${id}-->`);\n for (const child of node) renderToBufferHydrated(child, parts, ctx);\n parts.push(`<!--/forma-l:${id}-->`);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const id = ctx.id++;\n const { tag, props, children } = node;\n\n // Add hydration data-attribute to element\n parts.push('<', tag, ` data-forma-h=\"${id}\"`);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip refs and internal props; event handlers and unsafe attribute\n // names are dropped inside renderAttr.\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBufferHydrated(child, parts, ctx);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\n","/**\n * SSR-only signal: just a value holder, no reactivity needed on server.\n */\nexport function ssrSignal<T>(initial: T): [get: () => T, set: (v: T) => void] {\n let value = initial;\n return [() => value, (v) => { value = v; }];\n}\n\nexport function ssrComputed<T>(fn: () => T): () => T {\n return fn; // On server, just call the function directly\n}\n","/**\n * FormaJS SSR - Client Swap Script\n *\n * Tiny inline script injected into the HTML stream that handles\n * out-of-order Suspense boundary resolution.\n *\n * When the server resolves a Suspense boundary, it sends:\n * <script>$FORMA_SWAP(\"forma-s:0\",\"<resolved html>\")</script>\n *\n * This script replaces the fallback placeholder with the resolved content.\n */\n\n/**\n * Returns the client-side swap script as a string, ready to be injected\n * into the HTML stream (typically right after <body> or before first Suspense).\n */\nexport function getSwapScript(): string {\n return `<script>\nfunction $FORMA_SWAP(id,html){\n var el=document.getElementById(id);\n if(el){\n var tpl=document.createElement('template');\n tpl.innerHTML=html;\n el.replaceWith(tpl.content);\n }\n}\n</script>`;\n}\n\n/**\n * Serializes a value to a JSON string that is safe to embed inside a\n * `<script>` block. Standard `JSON.stringify` does not escape `<` or `>`,\n * so a payload containing `</script>` would prematurely close the script\n * tag, enabling an injection attack. We also escape U+2028 and U+2029\n * which are valid JSON but treated as line terminators in JavaScript source.\n */\nfunction safeJsonStringify(val: unknown): string {\n return JSON.stringify(val)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n\n/**\n * Returns a script tag that swaps a specific Suspense boundary's content.\n * Called when an async resource resolves during streaming.\n */\nexport function getSwapTag(id: string, html: string): string {\n // safeJsonStringify escapes <, >, U+2028, and U+2029 so the serialized\n // content cannot break out of the surrounding <script> block.\n return `<script>$FORMA_SWAP(${safeJsonStringify(id)},${safeJsonStringify(html)})</script>`;\n}\n","/**\n * FormaJS SSR - Streaming Renderer\n *\n * Implements true chunked HTML streaming with out-of-order Suspense resolution.\n *\n * How it works:\n * 1. Yields shell HTML immediately (opening tags, head, static content)\n * 2. When a Suspense boundary is encountered:\n * - Yields the fallback HTML with a placeholder: <div id=\"forma-s:N\" style=\"display:contents\">\n * - Continues rendering siblings without blocking\n * - When the async resource resolves, yields a swap script\n * 3. Suspense boundaries resolve independently in any order (out-of-order streaming)\n */\n\nimport { getSwapScript, getSwapTag } from './client-script.js';\nimport { escapeHtml, isVNode, VOID_ELEMENTS, renderAttr, type VNode } from './render.js';\n\n// ---------------------------------------------------------------------------\n// Suspense boundary tracking\n// ---------------------------------------------------------------------------\n\ninterface PendingBoundary {\n id: string;\n promise: Promise<string>;\n}\n\n/** Per-render mutable state, scoped to a single renderToStreamNew call. */\ninterface StreamState {\n suspenseCounter: number;\n pendingBoundaries: PendingBoundary[];\n}\n\n// ---------------------------------------------------------------------------\n// Synchronous render to buffer (for non-async content)\n// ---------------------------------------------------------------------------\n\nfunction renderSync(node: unknown, parts: string[]): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderSync(node(), parts); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderSync(child, parts);\n return;\n }\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const resolved = typeof value === 'function' ? value() : value;\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderSync(child, parts);\n }\n parts.push('</', tag, '>');\n return;\n }\n parts.push(escapeHtml(String(node)));\n}\n\n// ---------------------------------------------------------------------------\n// Suspense VNode detection\n// ---------------------------------------------------------------------------\n\n/**\n * A Suspense boundary VNode has a special shape:\n * { tag: 'forma-suspense', props: { fallback: VNode }, children: [asyncChildren] }\n *\n * Or it can be created via:\n * shSuspense(fallbackVNode, () => asyncContent)\n */\nexport interface SuspenseVNode {\n tag: 'forma-suspense';\n props: { fallback: unknown };\n children: [() => Promise<unknown>];\n}\n\nfunction isSuspenseVNode(v: unknown): v is SuspenseVNode {\n return isVNode(v) && v.tag === 'forma-suspense';\n}\n\n/**\n * Create a server-side Suspense VNode for streaming SSR.\n */\nexport function shSuspense(fallback: unknown, asyncChildren: () => Promise<unknown>): SuspenseVNode {\n return {\n tag: 'forma-suspense',\n props: { fallback },\n children: [asyncChildren],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Streaming renderer\n// ---------------------------------------------------------------------------\n\nexport interface StreamOptions {\n /** Script to bootstrap the client-side app (e.g. <script src=\"/app.js\"></script>) */\n bootstrapScript?: string;\n /** Whether to inject the $FORMA_SWAP client script (default: true) */\n injectSwapScript?: boolean;\n /**\n * Max ms to wait for each Suspense boundary before giving up (its fallback\n * stays visible and no swap is emitted). Omit or `<= 0` to wait forever — a\n * never-settling resource would otherwise hang the stream and the HTTP\n * connection indefinitely.\n */\n suspenseTimeout?: number;\n}\n\n/** A Suspense boundary that did not settle within suspenseTimeout. */\nconst TIMED_OUT = Symbol('forma-suspense-timeout');\n\n/** Resolve to TIMED_OUT if `p` does not settle within `ms` (no timeout if ms is unset/<=0). */\nfunction raceTimeout<T>(p: Promise<T>, ms: number | undefined): Promise<T | typeof TIMED_OUT> {\n if (ms == null || ms <= 0) return p;\n return new Promise((resolve, reject) => {\n const t = setTimeout(() => resolve(TIMED_OUT), ms);\n p.then(\n (v) => { clearTimeout(t); resolve(v); },\n (e) => { clearTimeout(t); reject(e); },\n );\n });\n}\n\n/**\n * Render a FormaJS virtual tree to a stream of HTML chunks.\n *\n * Yields HTML progressively:\n * - Static content is yielded immediately\n * - Suspense boundaries yield their fallback, then resolve later\n * - Resolved Suspense boundaries yield swap scripts\n *\n * Usage:\n * ```ts\n * import { renderToStream, shSuspense, sh } from '@getforma/core/ssr';\n *\n * const stream = renderToStream(\n * sh('html', null,\n * sh('body', null,\n * sh('h1', null, 'Hello'),\n * shSuspense(\n * sh('div', null, 'Loading...'),\n * async () => {\n * const data = await fetchData();\n * return sh('div', null, data.name);\n * },\n * ),\n * ),\n * ),\n * );\n *\n * for await (const chunk of stream) {\n * response.write(chunk);\n * }\n * ```\n */\nexport async function* renderToStreamNew(\n node: unknown,\n options?: StreamOptions,\n): AsyncGenerator<string> {\n // Per-render state — scoped to this generator invocation\n const state: StreamState = {\n suspenseCounter: 0,\n pendingBoundaries: [],\n };\n\n const injectSwap = options?.injectSwapScript !== false;\n\n // Inject the swap script first (if there could be Suspense boundaries)\n if (injectSwap) {\n yield getSwapScript();\n }\n\n // Render the main tree synchronously, collecting Suspense boundaries\n const mainParts: string[] = [];\n renderStreamNode(node, mainParts, state);\n yield mainParts.join('');\n\n // Yield bootstrap script if provided\n if (options?.bootstrapScript) {\n yield options.bootstrapScript;\n }\n\n // Wait for and yield all pending Suspense boundaries\n // Each resolves independently (out-of-order)\n const suspenseTimeout = options?.suspenseTimeout;\n while (state.pendingBoundaries.length > 0) {\n const pending = [...state.pendingBoundaries];\n state.pendingBoundaries = [];\n\n // Race all pending boundaries against the optional timeout — yield each as\n // it resolves. A timed-out boundary resolves to TIMED_OUT (no swap), so the\n // loop drains and the generator returns instead of hanging.\n const results = await Promise.allSettled(\n pending.map(async (boundary) => {\n const html = await raceTimeout(boundary.promise, suspenseTimeout);\n return { id: boundary.id, html };\n }),\n );\n\n for (const result of results) {\n if (result.status === 'fulfilled' && result.value.html !== TIMED_OUT) {\n yield getSwapTag(result.value.id, result.value.html as string);\n }\n // On timeout or rejection, the fallback stays visible (no swap).\n }\n }\n}\n\n/**\n * Render a node synchronously. If a Suspense boundary is encountered,\n * render the fallback inline and queue the async resolution.\n */\nfunction renderStreamNode(node: unknown, parts: string[], state: StreamState): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderStreamNode(node(), parts, state); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderStreamNode(child, parts, state);\n return;\n }\n\n // Suspense boundary: render fallback, queue async resolution\n if (isSuspenseVNode(node)) {\n const id = `forma-s:${state.suspenseCounter++}`;\n const fallback = node.props.fallback;\n const asyncFn = node.children[0]!;\n\n // Render fallback inside a replaceable container\n parts.push(`<div id=\"${id}\" style=\"display:contents\">`);\n renderSync(fallback, parts);\n parts.push('</div>');\n\n // Queue the async resolution. Invoke asyncFn via Promise.resolve().then so a\n // SYNCHRONOUS throw becomes a rejection routed through allSettled (fallback\n // stays) instead of aborting the whole generator before the shell is flushed.\n // Render resolved content through the Suspense-aware path so a NESTED\n // forma-suspense inside it is detected and queued onto the shared state\n // (drained out-of-order), rather than emitted as a literal element.\n const promise = Promise.resolve().then(asyncFn).then((resolved: unknown) => {\n const resolvedParts: string[] = [];\n renderStreamNode(resolved, resolvedParts, state);\n return resolvedParts.join('');\n });\n state.pendingBoundaries.push({ id, promise });\n return;\n }\n\n // Regular VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'fallback') continue; // skip internal suspense props\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const resolved = typeof value === 'function' ? value() : value;\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderStreamNode(child, parts, state);\n }\n parts.push('</', tag, '>');\n return;\n }\n\n parts.push(escapeHtml(String(node)));\n}\n"]}
|
package/dist/storage.cjs
CHANGED
|
@@ -26,7 +26,10 @@ function createLocalStorage(key, options) {
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
remove() {
|
|
29
|
-
|
|
29
|
+
try {
|
|
30
|
+
localStorage.removeItem(key);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
30
33
|
}
|
|
31
34
|
};
|
|
32
35
|
}
|
|
@@ -57,37 +60,70 @@ function createSessionStorage(key, options) {
|
|
|
57
60
|
}
|
|
58
61
|
},
|
|
59
62
|
remove() {
|
|
60
|
-
|
|
63
|
+
try {
|
|
64
|
+
sessionStorage.removeItem(key);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
61
67
|
}
|
|
62
68
|
};
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
// src/storage/indexed.ts
|
|
66
72
|
var dbCache = /* @__PURE__ */ new Map();
|
|
73
|
+
var openLocks = /* @__PURE__ */ new Map();
|
|
67
74
|
function openDB(dbName, storeName) {
|
|
68
75
|
const cacheKey = `${dbName}::${storeName}`;
|
|
69
76
|
const cached = dbCache.get(cacheKey);
|
|
70
77
|
if (cached) return cached;
|
|
71
|
-
const
|
|
78
|
+
const prevLock = openLocks.get(dbName) ?? Promise.resolve();
|
|
79
|
+
const promise = prevLock.then(() => doOpen(dbName, storeName, cacheKey), () => doOpen(dbName, storeName, cacheKey));
|
|
80
|
+
dbCache.set(cacheKey, promise);
|
|
81
|
+
openLocks.set(dbName, promise.then(() => void 0, () => void 0));
|
|
82
|
+
promise.catch(() => {
|
|
83
|
+
dbCache.delete(cacheKey);
|
|
84
|
+
});
|
|
85
|
+
return promise;
|
|
86
|
+
}
|
|
87
|
+
function doOpen(dbName, storeName, cacheKey) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const wire = (db) => {
|
|
90
|
+
db.onversionchange = () => {
|
|
91
|
+
db.close();
|
|
92
|
+
dbCache.delete(cacheKey);
|
|
93
|
+
};
|
|
94
|
+
db.onclose = () => {
|
|
95
|
+
dbCache.delete(cacheKey);
|
|
96
|
+
};
|
|
97
|
+
resolve(db);
|
|
98
|
+
};
|
|
72
99
|
const probe = indexedDB.open(dbName);
|
|
73
100
|
probe.onerror = () => reject(probe.error);
|
|
74
101
|
probe.onsuccess = () => {
|
|
75
102
|
const db = probe.result;
|
|
76
103
|
if (db.objectStoreNames.contains(storeName)) {
|
|
77
|
-
|
|
104
|
+
wire(db);
|
|
78
105
|
return;
|
|
79
106
|
}
|
|
80
107
|
const nextVersion = db.version + 1;
|
|
81
108
|
db.close();
|
|
82
109
|
const upgrade = indexedDB.open(dbName, nextVersion);
|
|
83
110
|
upgrade.onerror = () => reject(upgrade.error);
|
|
111
|
+
upgrade.onblocked = () => reject(upgrade.error ?? new DOMException("Upgrade blocked: another connection is open", "AbortError"));
|
|
84
112
|
upgrade.onupgradeneeded = () => {
|
|
85
113
|
const upgradedDB = upgrade.result;
|
|
86
114
|
if (!upgradedDB.objectStoreNames.contains(storeName)) {
|
|
87
115
|
upgradedDB.createObjectStore(storeName);
|
|
88
116
|
}
|
|
89
117
|
};
|
|
90
|
-
upgrade.onsuccess = () =>
|
|
118
|
+
upgrade.onsuccess = () => {
|
|
119
|
+
const upgradedDB = upgrade.result;
|
|
120
|
+
if (!upgradedDB.objectStoreNames.contains(storeName)) {
|
|
121
|
+
upgradedDB.close();
|
|
122
|
+
reject(new DOMException(`Object store "${storeName}" was not created`, "NotFoundError"));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
wire(upgradedDB);
|
|
126
|
+
};
|
|
91
127
|
};
|
|
92
128
|
probe.onupgradeneeded = () => {
|
|
93
129
|
const db = probe.result;
|
|
@@ -96,23 +132,37 @@ function openDB(dbName, storeName) {
|
|
|
96
132
|
}
|
|
97
133
|
};
|
|
98
134
|
});
|
|
99
|
-
dbCache.set(cacheKey, promise);
|
|
100
|
-
promise.catch(() => {
|
|
101
|
-
dbCache.delete(cacheKey);
|
|
102
|
-
});
|
|
103
|
-
return promise;
|
|
104
135
|
}
|
|
105
136
|
function createIndexedDB(dbName, storeName = "default") {
|
|
137
|
+
const cacheKey = `${dbName}::${storeName}`;
|
|
106
138
|
function withStore(mode, fn) {
|
|
107
139
|
return openDB(dbName, storeName).then(
|
|
108
140
|
(db) => new Promise((resolve, reject) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
141
|
+
let request;
|
|
142
|
+
let tx;
|
|
143
|
+
try {
|
|
144
|
+
tx = db.transaction(storeName, mode);
|
|
145
|
+
request = fn(tx.objectStore(storeName));
|
|
146
|
+
} catch (err) {
|
|
147
|
+
reject(err);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let result;
|
|
151
|
+
request.onsuccess = () => {
|
|
152
|
+
result = request.result;
|
|
153
|
+
if (mode === "readonly") resolve(result);
|
|
154
|
+
};
|
|
155
|
+
tx.oncomplete = () => resolve(result);
|
|
156
|
+
tx.onerror = () => reject(tx.error ?? request.error);
|
|
157
|
+
tx.onabort = () => reject(tx.error ?? request.error ?? new DOMException("Transaction aborted", "AbortError"));
|
|
114
158
|
})
|
|
115
|
-
)
|
|
159
|
+
).catch((err) => {
|
|
160
|
+
const name = err?.name;
|
|
161
|
+
if (name === "InvalidStateError" || name === "AbortError" || name === "NotFoundError") {
|
|
162
|
+
dbCache.delete(cacheKey);
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
});
|
|
116
166
|
}
|
|
117
167
|
return {
|
|
118
168
|
get(key) {
|
package/dist/storage.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/storage/local.ts","../src/storage/session.ts","../src/storage/indexed.ts"],"names":[],"mappings":"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","file":"storage.cjs","sourcesContent":["/**\n * Forma Storage - Local\n *\n * Typed localStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed localStorage wrapper for the given key.\n *\n * ```ts\n * const store = createLocalStorage<{ name: string }>('user');\n * store.set({ name: 'Alice' });\n * store.get(); // { name: 'Alice' }\n * store.remove();\n * ```\n */\nexport function createLocalStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = localStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n localStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n localStorage.removeItem(key);\n },\n };\n}\n","/**\n * Forma Storage - Session\n *\n * Typed sessionStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed sessionStorage wrapper for the given key.\n *\n * ```ts\n * const store = createSessionStorage<{ token: string }>('auth');\n * store.set({ token: 'abc123' });\n * store.get(); // { token: 'abc123' }\n * store.remove();\n * ```\n */\nexport function createSessionStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = sessionStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n sessionStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n sessionStorage.removeItem(key);\n },\n };\n}\n","/**\n * Forma Storage - IndexedDB\n *\n * Simplified IndexedDB wrapper with lazy connection and promise-based API.\n * Zero dependencies — native browser APIs only.\n */\n\nexport interface IDBStore<T> {\n get(key: string): Promise<T | undefined>;\n set(key: string, value: T): Promise<void>;\n delete(key: string): Promise<void>;\n getAll(): Promise<T[]>;\n keys(): Promise<string[]>;\n clear(): Promise<void>;\n}\n\n/** Cached database connections keyed by \"dbName::storeName\". */\nconst dbCache = new Map<string, Promise<IDBDatabase>>();\n\n/**\n * Open (or retrieve a cached) IndexedDB database, ensuring the\n * requested object store exists.\n */\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n const cacheKey = `${dbName}::${storeName}`;\n const cached = dbCache.get(cacheKey);\n if (cached) return cached;\n\n const promise = new Promise<IDBDatabase>((resolve, reject) => {\n // First, try opening at the current (or default) version.\n const probe = indexedDB.open(dbName);\n\n probe.onerror = () => reject(probe.error);\n\n probe.onsuccess = () => {\n const db = probe.result;\n if (db.objectStoreNames.contains(storeName)) {\n resolve(db);\n return;\n }\n // Store doesn't exist yet — close and reopen with a version bump.\n const nextVersion = db.version + 1;\n db.close();\n\n const upgrade = indexedDB.open(dbName, nextVersion);\n upgrade.onerror = () => reject(upgrade.error);\n upgrade.onupgradeneeded = () => {\n const upgradedDB = upgrade.result;\n if (!upgradedDB.objectStoreNames.contains(storeName)) {\n upgradedDB.createObjectStore(storeName);\n }\n };\n upgrade.onsuccess = () => resolve(upgrade.result);\n };\n\n probe.onupgradeneeded = () => {\n const db = probe.result;\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName);\n }\n };\n });\n\n dbCache.set(cacheKey, promise);\n\n // If the open fails, remove from cache so the next call retries.\n promise.catch(() => {\n dbCache.delete(cacheKey);\n });\n\n return promise;\n}\n\n/**\n * Create a simplified IndexedDB store.\n *\n * ```ts\n * const store = createIndexedDB<User>('myApp', 'users');\n * await store.set('u1', { name: 'Alice' });\n * const user = await store.get('u1'); // { name: 'Alice' }\n * ```\n */\nexport function createIndexedDB<T>(\n dbName: string,\n storeName: string = 'default',\n): IDBStore<T> {\n /** Helper: run a single read/write transaction and return the request result. */\n function withStore<R>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<R>,\n ): Promise<R> {\n return openDB(dbName, storeName).then(\n (db) =>\n new Promise<R>((resolve, reject) => {\n const tx = db.transaction(storeName, mode);\n const store = tx.objectStore(storeName);\n const request = fn(store);\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n }),\n );\n }\n\n return {\n get(key: string): Promise<T | undefined> {\n return withStore('readonly', (store) => store.get(key)) as Promise<T | undefined>;\n },\n\n set(key: string, value: T): Promise<void> {\n return withStore('readwrite', (store) => store.put(value, key)).then(\n () => undefined,\n );\n },\n\n delete(key: string): Promise<void> {\n return withStore('readwrite', (store) => store.delete(key)).then(\n () => undefined,\n );\n },\n\n getAll(): Promise<T[]> {\n return withStore('readonly', (store) => store.getAll()) as Promise<T[]>;\n },\n\n keys(): Promise<string[]> {\n return withStore('readonly', (store) => store.getAllKeys()).then(\n (keys) => keys.map(String),\n );\n },\n\n clear(): Promise<void> {\n return withStore('readwrite', (store) => store.clear()).then(\n () => undefined,\n );\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/storage/local.ts","../src/storage/session.ts","../src/storage/indexed.ts"],"names":[],"mappings":"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","file":"storage.cjs","sourcesContent":["/**\n * Forma Storage - Local\n *\n * Typed localStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed localStorage wrapper for the given key.\n *\n * ```ts\n * const store = createLocalStorage<{ name: string }>('user');\n * store.set({ name: 'Alice' });\n * store.get(); // { name: 'Alice' }\n * store.remove();\n * ```\n */\nexport function createLocalStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = localStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n localStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n try {\n localStorage.removeItem(key);\n } catch { /* storage disabled — silently ignore, matching get/set */ }\n },\n };\n}\n","/**\n * Forma Storage - Session\n *\n * Typed sessionStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed sessionStorage wrapper for the given key.\n *\n * ```ts\n * const store = createSessionStorage<{ token: string }>('auth');\n * store.set({ token: 'abc123' });\n * store.get(); // { token: 'abc123' }\n * store.remove();\n * ```\n */\nexport function createSessionStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = sessionStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n sessionStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n try {\n sessionStorage.removeItem(key);\n } catch { /* storage disabled — silently ignore, matching get/set */ }\n },\n };\n}\n","/**\n * Forma Storage - IndexedDB\n *\n * Simplified IndexedDB wrapper with lazy connection and promise-based API.\n * Zero dependencies — native browser APIs only.\n */\n\nexport interface IDBStore<T> {\n get(key: string): Promise<T | undefined>;\n set(key: string, value: T): Promise<void>;\n delete(key: string): Promise<void>;\n getAll(): Promise<T[]>;\n keys(): Promise<string[]>;\n clear(): Promise<void>;\n}\n\n/** Cached database connections keyed by \"dbName::storeName\". */\nconst dbCache = new Map<string, Promise<IDBDatabase>>();\n\n/**\n * Per-database open serialization. Concurrent createIndexedDB(db, storeA/B/...)\n * calls each bump db.version+1; without serialization they compute the version\n * from a stale probe and race, so some stores are never created and their\n * connections are permanently wedged. Chaining opens for the same db name makes\n * each see the previous open's committed version.\n */\nconst openLocks = new Map<string, Promise<unknown>>();\n\n/**\n * Open (or retrieve a cached) IndexedDB database, ensuring the\n * requested object store exists.\n */\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n const cacheKey = `${dbName}::${storeName}`;\n const cached = dbCache.get(cacheKey);\n if (cached) return cached;\n\n // Serialize this open behind any in-flight open for the same db name.\n const prevLock = openLocks.get(dbName) ?? Promise.resolve();\n const promise = prevLock.then(() => doOpen(dbName, storeName, cacheKey), () => doOpen(dbName, storeName, cacheKey));\n\n dbCache.set(cacheKey, promise);\n // Advance the lock (never rejects, so the chain keeps flowing).\n openLocks.set(dbName, promise.then(() => undefined, () => undefined));\n\n // If the open fails, remove from cache so the next call retries.\n promise.catch(() => {\n dbCache.delete(cacheKey);\n });\n\n return promise;\n}\n\nfunction doOpen(dbName: string, storeName: string, cacheKey: string): Promise<IDBDatabase> {\n return new Promise<IDBDatabase>((resolve, reject) => {\n // Wire a resolved connection so it closes + evicts on a version change\n // (another tab/store bumping the version) or a forced close. Without this a\n // later bump blocks forever and a stale connection is never dropped.\n const wire = (db: IDBDatabase) => {\n db.onversionchange = () => { db.close(); dbCache.delete(cacheKey); };\n db.onclose = () => { dbCache.delete(cacheKey); };\n resolve(db);\n };\n\n // First, try opening at the current (or default) version.\n const probe = indexedDB.open(dbName);\n\n probe.onerror = () => reject(probe.error);\n\n probe.onsuccess = () => {\n const db = probe.result;\n if (db.objectStoreNames.contains(storeName)) {\n wire(db);\n return;\n }\n // Store doesn't exist yet — close and reopen with a version bump. Opens\n // are serialized per db, so this version is current, not stale.\n const nextVersion = db.version + 1;\n db.close();\n\n const upgrade = indexedDB.open(dbName, nextVersion);\n upgrade.onerror = () => reject(upgrade.error);\n // Backstop: if another connection blocks the bump, settle (reject) so the\n // promise does not leak; the next call retries.\n upgrade.onblocked = () =>\n reject(upgrade.error ?? new DOMException('Upgrade blocked: another connection is open', 'AbortError'));\n upgrade.onupgradeneeded = () => {\n const upgradedDB = upgrade.result;\n if (!upgradedDB.objectStoreNames.contains(storeName)) {\n upgradedDB.createObjectStore(storeName);\n }\n };\n upgrade.onsuccess = () => {\n const upgradedDB = upgrade.result;\n if (!upgradedDB.objectStoreNames.contains(storeName)) {\n // Should not happen with serialized opens; reject (do not cache a\n // store-less connection) so the next call retries cleanly.\n upgradedDB.close();\n reject(new DOMException(`Object store \"${storeName}\" was not created`, 'NotFoundError'));\n return;\n }\n wire(upgradedDB);\n };\n };\n\n probe.onupgradeneeded = () => {\n const db = probe.result;\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName);\n }\n };\n });\n}\n\n/**\n * Create a simplified IndexedDB store.\n *\n * ```ts\n * const store = createIndexedDB<User>('myApp', 'users');\n * await store.set('u1', { name: 'Alice' });\n * const user = await store.get('u1'); // { name: 'Alice' }\n * ```\n */\nexport function createIndexedDB<T>(\n dbName: string,\n storeName: string = 'default',\n): IDBStore<T> {\n const cacheKey = `${dbName}::${storeName}`;\n\n /** Helper: run a single read/write transaction and return the request result. */\n function withStore<R>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<R>,\n ): Promise<R> {\n return openDB(dbName, storeName).then(\n (db) =>\n new Promise<R>((resolve, reject) => {\n let request: IDBRequest<R>;\n let tx: IDBTransaction;\n try {\n tx = db.transaction(storeName, mode);\n request = fn(tx.objectStore(storeName));\n } catch (err) {\n // A transaction on a closing connection throws synchronously.\n reject(err);\n return;\n }\n let result: R;\n request.onsuccess = () => {\n result = request.result;\n // Reads are durable at request success; writes only at commit.\n if (mode === 'readonly') resolve(result);\n };\n // A readwrite value is only durable once the transaction COMMITS —\n // resolving on request.onsuccess would falsely report success for a\n // write that later aborts (e.g. QuotaExceededError at commit).\n tx.oncomplete = () => resolve(result);\n tx.onerror = () => reject(tx.error ?? request.error);\n tx.onabort = () => reject(tx.error ?? request.error ?? new DOMException('Transaction aborted', 'AbortError'));\n }),\n ).catch((err: unknown) => {\n // Evict a broken/closing/store-less connection so the next call reopens\n // cleanly (NotFoundError = the cached connection lacks the store).\n const name = (err as { name?: string } | null)?.name;\n if (name === 'InvalidStateError' || name === 'AbortError' || name === 'NotFoundError') {\n dbCache.delete(cacheKey);\n }\n throw err;\n });\n }\n\n return {\n get(key: string): Promise<T | undefined> {\n return withStore('readonly', (store) => store.get(key)) as Promise<T | undefined>;\n },\n\n set(key: string, value: T): Promise<void> {\n return withStore('readwrite', (store) => store.put(value, key)).then(\n () => undefined,\n );\n },\n\n delete(key: string): Promise<void> {\n return withStore('readwrite', (store) => store.delete(key)).then(\n () => undefined,\n );\n },\n\n getAll(): Promise<T[]> {\n return withStore('readonly', (store) => store.getAll()) as Promise<T[]>;\n },\n\n keys(): Promise<string[]> {\n return withStore('readonly', (store) => store.getAllKeys()).then(\n (keys) => keys.map(String),\n );\n },\n\n clear(): Promise<void> {\n return withStore('readwrite', (store) => store.clear()).then(\n () => undefined,\n );\n },\n };\n}\n"]}
|
package/dist/storage.js
CHANGED
|
@@ -24,7 +24,10 @@ function createLocalStorage(key, options) {
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
remove() {
|
|
27
|
-
|
|
27
|
+
try {
|
|
28
|
+
localStorage.removeItem(key);
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
28
31
|
}
|
|
29
32
|
};
|
|
30
33
|
}
|
|
@@ -55,37 +58,70 @@ function createSessionStorage(key, options) {
|
|
|
55
58
|
}
|
|
56
59
|
},
|
|
57
60
|
remove() {
|
|
58
|
-
|
|
61
|
+
try {
|
|
62
|
+
sessionStorage.removeItem(key);
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
};
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
// src/storage/indexed.ts
|
|
64
70
|
var dbCache = /* @__PURE__ */ new Map();
|
|
71
|
+
var openLocks = /* @__PURE__ */ new Map();
|
|
65
72
|
function openDB(dbName, storeName) {
|
|
66
73
|
const cacheKey = `${dbName}::${storeName}`;
|
|
67
74
|
const cached = dbCache.get(cacheKey);
|
|
68
75
|
if (cached) return cached;
|
|
69
|
-
const
|
|
76
|
+
const prevLock = openLocks.get(dbName) ?? Promise.resolve();
|
|
77
|
+
const promise = prevLock.then(() => doOpen(dbName, storeName, cacheKey), () => doOpen(dbName, storeName, cacheKey));
|
|
78
|
+
dbCache.set(cacheKey, promise);
|
|
79
|
+
openLocks.set(dbName, promise.then(() => void 0, () => void 0));
|
|
80
|
+
promise.catch(() => {
|
|
81
|
+
dbCache.delete(cacheKey);
|
|
82
|
+
});
|
|
83
|
+
return promise;
|
|
84
|
+
}
|
|
85
|
+
function doOpen(dbName, storeName, cacheKey) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const wire = (db) => {
|
|
88
|
+
db.onversionchange = () => {
|
|
89
|
+
db.close();
|
|
90
|
+
dbCache.delete(cacheKey);
|
|
91
|
+
};
|
|
92
|
+
db.onclose = () => {
|
|
93
|
+
dbCache.delete(cacheKey);
|
|
94
|
+
};
|
|
95
|
+
resolve(db);
|
|
96
|
+
};
|
|
70
97
|
const probe = indexedDB.open(dbName);
|
|
71
98
|
probe.onerror = () => reject(probe.error);
|
|
72
99
|
probe.onsuccess = () => {
|
|
73
100
|
const db = probe.result;
|
|
74
101
|
if (db.objectStoreNames.contains(storeName)) {
|
|
75
|
-
|
|
102
|
+
wire(db);
|
|
76
103
|
return;
|
|
77
104
|
}
|
|
78
105
|
const nextVersion = db.version + 1;
|
|
79
106
|
db.close();
|
|
80
107
|
const upgrade = indexedDB.open(dbName, nextVersion);
|
|
81
108
|
upgrade.onerror = () => reject(upgrade.error);
|
|
109
|
+
upgrade.onblocked = () => reject(upgrade.error ?? new DOMException("Upgrade blocked: another connection is open", "AbortError"));
|
|
82
110
|
upgrade.onupgradeneeded = () => {
|
|
83
111
|
const upgradedDB = upgrade.result;
|
|
84
112
|
if (!upgradedDB.objectStoreNames.contains(storeName)) {
|
|
85
113
|
upgradedDB.createObjectStore(storeName);
|
|
86
114
|
}
|
|
87
115
|
};
|
|
88
|
-
upgrade.onsuccess = () =>
|
|
116
|
+
upgrade.onsuccess = () => {
|
|
117
|
+
const upgradedDB = upgrade.result;
|
|
118
|
+
if (!upgradedDB.objectStoreNames.contains(storeName)) {
|
|
119
|
+
upgradedDB.close();
|
|
120
|
+
reject(new DOMException(`Object store "${storeName}" was not created`, "NotFoundError"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
wire(upgradedDB);
|
|
124
|
+
};
|
|
89
125
|
};
|
|
90
126
|
probe.onupgradeneeded = () => {
|
|
91
127
|
const db = probe.result;
|
|
@@ -94,23 +130,37 @@ function openDB(dbName, storeName) {
|
|
|
94
130
|
}
|
|
95
131
|
};
|
|
96
132
|
});
|
|
97
|
-
dbCache.set(cacheKey, promise);
|
|
98
|
-
promise.catch(() => {
|
|
99
|
-
dbCache.delete(cacheKey);
|
|
100
|
-
});
|
|
101
|
-
return promise;
|
|
102
133
|
}
|
|
103
134
|
function createIndexedDB(dbName, storeName = "default") {
|
|
135
|
+
const cacheKey = `${dbName}::${storeName}`;
|
|
104
136
|
function withStore(mode, fn) {
|
|
105
137
|
return openDB(dbName, storeName).then(
|
|
106
138
|
(db) => new Promise((resolve, reject) => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
139
|
+
let request;
|
|
140
|
+
let tx;
|
|
141
|
+
try {
|
|
142
|
+
tx = db.transaction(storeName, mode);
|
|
143
|
+
request = fn(tx.objectStore(storeName));
|
|
144
|
+
} catch (err) {
|
|
145
|
+
reject(err);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
let result;
|
|
149
|
+
request.onsuccess = () => {
|
|
150
|
+
result = request.result;
|
|
151
|
+
if (mode === "readonly") resolve(result);
|
|
152
|
+
};
|
|
153
|
+
tx.oncomplete = () => resolve(result);
|
|
154
|
+
tx.onerror = () => reject(tx.error ?? request.error);
|
|
155
|
+
tx.onabort = () => reject(tx.error ?? request.error ?? new DOMException("Transaction aborted", "AbortError"));
|
|
112
156
|
})
|
|
113
|
-
)
|
|
157
|
+
).catch((err) => {
|
|
158
|
+
const name = err?.name;
|
|
159
|
+
if (name === "InvalidStateError" || name === "AbortError" || name === "NotFoundError") {
|
|
160
|
+
dbCache.delete(cacheKey);
|
|
161
|
+
}
|
|
162
|
+
throw err;
|
|
163
|
+
});
|
|
114
164
|
}
|
|
115
165
|
return {
|
|
116
166
|
get(key) {
|