@askrjs/askr 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/benchmark.js +2 -2
  2. package/dist/bin/askr-ssg.js.map +1 -1
  3. package/dist/boot/index.js.map +1 -1
  4. package/dist/common/env.js.map +1 -1
  5. package/dist/common/route-activity.js.map +1 -1
  6. package/dist/control/case.js.map +1 -1
  7. package/dist/control/show.js.map +1 -1
  8. package/dist/data/index.js.map +1 -1
  9. package/dist/foundations/icon/icon.js.map +1 -1
  10. package/dist/foundations/structures/layer.js.map +1 -1
  11. package/dist/foundations/structures/portal.js.map +1 -1
  12. package/dist/fx/fx.js.map +1 -1
  13. package/dist/fx/timing.js.map +1 -1
  14. package/dist/renderer/children.js.map +1 -1
  15. package/dist/renderer/cleanup.js.map +1 -1
  16. package/dist/renderer/dom.js.map +1 -1
  17. package/dist/renderer/evaluate.js.map +1 -1
  18. package/dist/renderer/fastpath.js.map +1 -1
  19. package/dist/renderer/for-commit.js.map +1 -1
  20. package/dist/renderer/keyed.js.map +1 -1
  21. package/dist/renderer/reconcile.js.map +1 -1
  22. package/dist/renderer/utils.js.map +1 -1
  23. package/dist/router/match.js.map +1 -1
  24. package/dist/router/navigate.js.map +1 -1
  25. package/dist/router/policy.js.map +1 -1
  26. package/dist/router/route.js.map +1 -1
  27. package/dist/runtime/component.d.ts +1 -0
  28. package/dist/runtime/component.d.ts.map +1 -1
  29. package/dist/runtime/component.js +16 -8
  30. package/dist/runtime/component.js.map +1 -1
  31. package/dist/runtime/context.js.map +1 -1
  32. package/dist/runtime/derive.js.map +1 -1
  33. package/dist/runtime/dev-namespace.js.map +1 -1
  34. package/dist/runtime/effect.js.map +1 -1
  35. package/dist/runtime/events.js.map +1 -1
  36. package/dist/runtime/fastlane.js.map +1 -1
  37. package/dist/runtime/for.js.map +1 -1
  38. package/dist/runtime/operations.js.map +1 -1
  39. package/dist/runtime/readable.d.ts +2 -1
  40. package/dist/runtime/readable.d.ts.map +1 -1
  41. package/dist/runtime/readable.js +41 -10
  42. package/dist/runtime/readable.js.map +1 -1
  43. package/dist/runtime/resource-cell.js.map +1 -1
  44. package/dist/runtime/scheduler.js.map +1 -1
  45. package/dist/runtime/selector.js.map +1 -1
  46. package/dist/ssg/batch-render.js.map +1 -1
  47. package/dist/ssg/create-static-gen.js.map +1 -1
  48. package/dist/ssg/generate-metadata.js.map +1 -1
  49. package/dist/ssg/incremental-manifest.js.map +1 -1
  50. package/dist/ssg/resolve-ssg-data.js.map +1 -1
  51. package/dist/ssg/write-static-files.js.map +1 -1
  52. package/dist/ssr/context.js.map +1 -1
  53. package/dist/ssr/escape.js.map +1 -1
  54. package/dist/ssr/index.js.map +1 -1
  55. package/dist/ssr/render-keys.js.map +1 -1
  56. package/dist/testing/index.js.map +1 -1
  57. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"readable.js","names":[],"sources":["../../src/runtime/readable.ts"],"sourcesContent":["import { globalScheduler } from './scheduler';\nimport { getCurrentInstance, type ComponentInstance } from './component';\n\nexport interface DerivedSubscriber {\n _markDirty(): void;\n _pendingDependencySources?: Set<ReadableSource<unknown>>;\n}\n\nexport interface ReadableSource<T = unknown> {\n (): T;\n _hasBeenRead?: boolean;\n _hasEverBeenRead?: boolean;\n _readers?: Map<ComponentInstance, number>;\n _derivedSubscribers?: Set<DerivedSubscriber>;\n}\n\nexport function markReadableUsage(source: unknown): void {\n if (\n source &&\n typeof source === 'function' &&\n ('_hasBeenRead' in source || '_readers' in source)\n ) {\n const readable = source as ReadableSource<unknown>;\n readable._hasBeenRead = true;\n readable._hasEverBeenRead = true;\n }\n}\n\ntype RendererBridge = {\n markReactivePropsDirtySource?: (source: ReadableSource<unknown>) => void;\n};\n\nlet currentDerivedSubscriber: DerivedSubscriber | null = null;\nlet suppressComponentReadTrackingDepth = 0;\nlet currentFineGrainedReadSources: Set<ReadableSource<unknown>> | null = null;\n\nexport function recordReadableRead(source: ReadableSource<unknown>): void {\n source._hasEverBeenRead = true;\n\n if (currentFineGrainedReadSources) {\n currentFineGrainedReadSources.add(source);\n return;\n }\n\n if (currentDerivedSubscriber) {\n if (!currentDerivedSubscriber._pendingDependencySources) {\n currentDerivedSubscriber._pendingDependencySources = new Set();\n }\n currentDerivedSubscriber._pendingDependencySources.add(source);\n }\n\n if (suppressComponentReadTrackingDepth > 0) {\n return;\n }\n\n const inst = getCurrentInstance();\n if (!inst || inst._currentRenderToken === undefined) {\n return;\n }\n\n if (!inst._pendingReadSources) {\n inst._pendingReadSources = new Set();\n }\n inst._pendingReadSources.add(source);\n}\n\nexport function withFineGrainedReadTracking<T>(\n pendingSources: Set<ReadableSource<unknown>>,\n fn: () => T\n): T {\n const previousPendingSources = currentFineGrainedReadSources;\n currentFineGrainedReadSources = pendingSources;\n\n try {\n return fn();\n } finally {\n currentFineGrainedReadSources = previousPendingSources;\n }\n}\n\nexport function finalizeReadableSubscriptions(\n instance: ComponentInstance\n): void {\n const newSet = instance._pendingReadSources;\n const token = instance._currentRenderToken;\n\n finalizeReadableSubscriptionsFromSnapshot(instance, token, newSet);\n instance._pendingReadSources = undefined;\n instance._currentRenderToken = undefined;\n}\n\nexport function finalizeReadableSubscriptionsFromSnapshot(\n instance: ComponentInstance,\n token: number | undefined,\n newSet: Set<ReadableSource<unknown>> | undefined\n): void {\n if (token === undefined) {\n return;\n }\n\n const oldSet = instance._lastReadSources;\n\n if (oldSet) {\n for (const source of oldSet) {\n if (!newSet?.has(source)) {\n source._readers?.delete(instance);\n }\n }\n }\n\n instance.lastRenderToken = token;\n\n if (newSet) {\n for (const source of newSet) {\n let readers = source._readers;\n if (!readers) {\n readers = new Map();\n source._readers = readers;\n }\n readers.set(instance, instance.lastRenderToken ?? 0);\n }\n }\n\n instance._lastReadSources = newSet ?? new Set();\n}\n\nexport function cleanupReadableSubscriptions(\n instance: ComponentInstance\n): void {\n const sources = instance._lastReadSources;\n if (!sources || sources.size === 0) {\n return;\n }\n\n for (const source of sources) {\n source._readers?.delete(instance);\n }\n instance._lastReadSources = new Set();\n}\n\nexport function withDerivedReadTracking<T>(\n subscriber: DerivedSubscriber,\n fn: () => T\n): T {\n const prevDerivedSubscriber = currentDerivedSubscriber;\n currentDerivedSubscriber = subscriber;\n suppressComponentReadTrackingDepth += 1;\n\n try {\n return fn();\n } finally {\n suppressComponentReadTrackingDepth -= 1;\n currentDerivedSubscriber = prevDerivedSubscriber;\n }\n}\n\nexport function syncDerivedDependencySubscriptions(\n subscriber: DerivedSubscriber,\n prevSources: Set<ReadableSource<unknown>>,\n nextSources: Set<ReadableSource<unknown>>\n): void {\n for (const source of prevSources) {\n if (!nextSources.has(source)) {\n source._derivedSubscribers?.delete(subscriber);\n }\n }\n\n for (const source of nextSources) {\n let subscribers = source._derivedSubscribers;\n if (!subscribers) {\n subscribers = new Set();\n source._derivedSubscribers = subscribers;\n }\n subscribers.add(subscriber);\n }\n}\n\nexport function clearDerivedDependencySubscriptions(\n subscriber: DerivedSubscriber,\n sources: Set<ReadableSource<unknown>>\n): void {\n for (const source of sources) {\n source._derivedSubscribers?.delete(subscriber);\n }\n sources.clear();\n}\n\nexport function markReadableDerivedSubscribersDirty(\n source: ReadableSource<unknown>\n): void {\n const subscribers = source._derivedSubscribers;\n if (!subscribers || subscribers.size === 0) {\n return;\n }\n\n for (const subscriber of subscribers) {\n subscriber._markDirty();\n }\n}\n\nexport function markReactivePropsDirtySource(\n source: ReadableSource<unknown>\n): void {\n try {\n (\n globalThis as {\n __ASKR_RENDERER?: RendererBridge;\n }\n ).__ASKR_RENDERER?.markReactivePropsDirtySource?.(source);\n } catch {\n // Keep readable notifications side-effect safe.\n }\n}\n\nexport function notifyReadableReaders(\n source: ReadableSource<unknown>,\n skipInstance?: ComponentInstance | null\n): boolean {\n const readers = source._readers;\n let didScheduleUpdate = false;\n\n if (!readers || readers.size === 0) {\n return false;\n }\n\n for (const [instance, token] of readers) {\n if (skipInstance && instance === skipInstance) {\n continue;\n }\n if (instance.lastRenderToken !== token) {\n continue;\n }\n if (instance.hasPendingUpdate) {\n continue;\n }\n\n instance.hasPendingUpdate = true;\n const task = instance._pendingFlushTask;\n if (task) {\n globalScheduler.enqueue(task);\n } else {\n globalScheduler.enqueue(() => {\n instance.hasPendingUpdate = false;\n instance.notifyUpdate?.();\n });\n }\n didScheduleUpdate = true;\n }\n\n return didScheduleUpdate;\n}\n"],"mappings":";;;AAgBA,SAAgB,kBAAkB,QAAuB;CACvD,IACE,UACA,OAAO,WAAW,eACjB,kBAAkB,UAAU,cAAc,SAC3C;EACA,MAAM,WAAW;EACjB,SAAS,eAAe;EACxB,SAAS,mBAAmB;CAC9B;AACF;AAMA,IAAI,2BAAqD;AACzD,IAAI,qCAAqC;AACzC,IAAI,gCAAqE;AAEzE,SAAgB,mBAAmB,QAAuC;CACxE,OAAO,mBAAmB;CAE1B,IAAI,+BAA+B;EACjC,8BAA8B,IAAI,MAAM;EACxC;CACF;CAEA,IAAI,0BAA0B;EAC5B,IAAI,CAAC,yBAAyB,2BAC5B,yBAAyB,4CAA4B,IAAI,IAAI;EAE/D,yBAAyB,0BAA0B,IAAI,MAAM;CAC/D;CAEA,IAAI,qCAAqC,GACvC;CAGF,MAAM,OAAO,mBAAmB;CAChC,IAAI,CAAC,QAAQ,KAAK,wBAAwB,QACxC;CAGF,IAAI,CAAC,KAAK,qBACR,KAAK,sCAAsB,IAAI,IAAI;CAErC,KAAK,oBAAoB,IAAI,MAAM;AACrC;AAEA,SAAgB,4BACd,gBACA,IACG;CACH,MAAM,yBAAyB;CAC/B,gCAAgC;CAEhC,IAAI;EACF,OAAO,GAAG;CACZ,UAAU;EACR,gCAAgC;CAClC;AACF;AAEA,SAAgB,8BACd,UACM;CACN,MAAM,SAAS,SAAS;CACxB,MAAM,QAAQ,SAAS;CAEvB,0CAA0C,UAAU,OAAO,MAAM;CACjE,SAAS,sBAAsB;CAC/B,SAAS,sBAAsB;AACjC;AAEA,SAAgB,0CACd,UACA,OACA,QACM;CACN,IAAI,UAAU,QACZ;CAGF,MAAM,SAAS,SAAS;CAExB,IAAI,QACF;OAAK,MAAM,UAAU,QACnB,IAAI,CAAC,QAAQ,IAAI,MAAM,GACrB,OAAO,UAAU,OAAO,QAAQ;CAEpC;CAGF,SAAS,kBAAkB;CAE3B,IAAI,QACF,KAAK,MAAM,UAAU,QAAQ;EAC3B,IAAI,UAAU,OAAO;EACrB,IAAI,CAAC,SAAS;GACZ,0BAAU,IAAI,IAAI;GAClB,OAAO,WAAW;EACpB;EACA,QAAQ,IAAI,UAAU,SAAS,mBAAmB,CAAC;CACrD;CAGF,SAAS,mBAAmB,0BAAU,IAAI,IAAI;AAChD;AAEA,SAAgB,6BACd,UACM;CACN,MAAM,UAAU,SAAS;CACzB,IAAI,CAAC,WAAW,QAAQ,SAAS,GAC/B;CAGF,KAAK,MAAM,UAAU,SACnB,OAAO,UAAU,OAAO,QAAQ;CAElC,SAAS,mCAAmB,IAAI,IAAI;AACtC;AAEA,SAAgB,wBACd,YACA,IACG;CACH,MAAM,wBAAwB;CAC9B,2BAA2B;CAC3B,sCAAsC;CAEtC,IAAI;EACF,OAAO,GAAG;CACZ,UAAU;EACR,sCAAsC;EACtC,2BAA2B;CAC7B;AACF;AAEA,SAAgB,mCACd,YACA,aACA,aACM;CACN,KAAK,MAAM,UAAU,aACnB,IAAI,CAAC,YAAY,IAAI,MAAM,GACzB,OAAO,qBAAqB,OAAO,UAAU;CAIjD,KAAK,MAAM,UAAU,aAAa;EAChC,IAAI,cAAc,OAAO;EACzB,IAAI,CAAC,aAAa;GAChB,8BAAc,IAAI,IAAI;GACtB,OAAO,sBAAsB;EAC/B;EACA,YAAY,IAAI,UAAU;CAC5B;AACF;AAEA,SAAgB,oCACd,YACA,SACM;CACN,KAAK,MAAM,UAAU,SACnB,OAAO,qBAAqB,OAAO,UAAU;CAE/C,QAAQ,MAAM;AAChB;AAEA,SAAgB,oCACd,QACM;CACN,MAAM,cAAc,OAAO;CAC3B,IAAI,CAAC,eAAe,YAAY,SAAS,GACvC;CAGF,KAAK,MAAM,cAAc,aACvB,WAAW,WAAW;AAE1B;AAEA,SAAgB,6BACd,QACM;CACN,IAAI;EACF,WAIE,iBAAiB,+BAA+B,MAAM;CAC1D,QAAQ,CAER;AACF;AAEA,SAAgB,sBACd,QACA,cACS;CACT,MAAM,UAAU,OAAO;CACvB,IAAI,oBAAoB;CAExB,IAAI,CAAC,WAAW,QAAQ,SAAS,GAC/B,OAAO;CAGT,KAAK,MAAM,CAAC,UAAU,UAAU,SAAS;EACvC,IAAI,gBAAgB,aAAa,cAC/B;EAEF,IAAI,SAAS,oBAAoB,OAC/B;EAEF,IAAI,SAAS,kBACX;EAGF,SAAS,mBAAmB;EAC5B,MAAM,OAAO,SAAS;EACtB,IAAI,MACF,gBAAgB,QAAQ,IAAI;OAE5B,gBAAgB,cAAc;GAC5B,SAAS,mBAAmB;GAC5B,SAAS,eAAe;EAC1B,CAAC;EAEH,oBAAoB;CACtB;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"readable.js","names":[],"sources":["../../src/runtime/readable.ts"],"sourcesContent":["import { globalScheduler } from './scheduler';\nimport { getCurrentInstance, type ComponentInstance } from './component';\n\nexport interface DerivedSubscriber {\n _markDirty(): void;\n _pendingDependencySources?: Set<ReadableSource<unknown>>;\n}\n\nexport interface ReadableSource<T = unknown> {\n (): T;\n _hasBeenRead?: boolean;\n _hasEverBeenRead?: boolean;\n _readers?: Map<ComponentInstance, number>;\n _derivedSubscribers?: Set<DerivedSubscriber>;\n _version?: number;\n}\n\nexport function markReadableUsage(source: unknown): void {\n if (\n source &&\n typeof source === 'function' &&\n ('_hasBeenRead' in source || '_readers' in source)\n ) {\n const readable = source as ReadableSource<unknown>;\n readable._hasBeenRead = true;\n readable._hasEverBeenRead = true;\n }\n}\n\ntype RendererBridge = {\n markReactivePropsDirtySource?: (source: ReadableSource<unknown>) => void;\n};\n\nlet currentDerivedSubscriber: DerivedSubscriber | null = null;\nlet suppressComponentReadTrackingDepth = 0;\nlet currentFineGrainedReadSources: Set<ReadableSource<unknown>> | null = null;\n\nfunction scheduleReadableInstanceUpdate(instance: ComponentInstance): void {\n if (instance.hasPendingUpdate) {\n return;\n }\n\n instance.hasPendingUpdate = true;\n const task = instance._pendingFlushTask;\n if (task) {\n globalScheduler.enqueue(task);\n return;\n }\n\n globalScheduler.enqueue(() => {\n instance.hasPendingUpdate = false;\n instance.notifyUpdate?.();\n });\n}\n\nexport function recordReadableRead(source: ReadableSource<unknown>): void {\n source._hasEverBeenRead = true;\n\n if (currentFineGrainedReadSources) {\n currentFineGrainedReadSources.add(source);\n return;\n }\n\n if (currentDerivedSubscriber) {\n if (!currentDerivedSubscriber._pendingDependencySources) {\n currentDerivedSubscriber._pendingDependencySources = new Set();\n }\n currentDerivedSubscriber._pendingDependencySources.add(source);\n }\n\n if (suppressComponentReadTrackingDepth > 0) {\n return;\n }\n\n const inst = getCurrentInstance();\n if (!inst || inst._currentRenderToken === undefined) {\n return;\n }\n\n if (!inst._pendingReadSources) {\n inst._pendingReadSources = new Set();\n }\n inst._pendingReadSources.add(source);\n\n const sourceVersion = source._version ?? 0;\n let pendingVersions = inst._pendingReadSourceVersions;\n if (!pendingVersions) {\n pendingVersions = new Map();\n inst._pendingReadSourceVersions = pendingVersions;\n }\n pendingVersions.set(source, sourceVersion);\n}\n\nexport function withFineGrainedReadTracking<T>(\n pendingSources: Set<ReadableSource<unknown>>,\n fn: () => T\n): T {\n const previousPendingSources = currentFineGrainedReadSources;\n currentFineGrainedReadSources = pendingSources;\n\n try {\n return fn();\n } finally {\n currentFineGrainedReadSources = previousPendingSources;\n }\n}\n\nexport function finalizeReadableSubscriptions(\n instance: ComponentInstance\n): void {\n const newSet = instance._pendingReadSources;\n const token = instance._currentRenderToken;\n const pendingVersions = instance._pendingReadSourceVersions;\n\n finalizeReadableSubscriptionsFromSnapshot(\n instance,\n token,\n newSet,\n pendingVersions\n );\n instance._pendingReadSources = undefined;\n instance._currentRenderToken = undefined;\n instance._pendingReadSourceVersions = undefined;\n}\n\nexport function finalizeReadableSubscriptionsFromSnapshot(\n instance: ComponentInstance,\n token: number | undefined,\n newSet: Set<ReadableSource<unknown>> | undefined,\n pendingReadSourceVersions:\n | Map<ReadableSource<unknown>, number>\n | undefined = instance._pendingReadSourceVersions\n): void {\n if (token === undefined) {\n return;\n }\n\n const oldSet = instance._lastReadSources;\n const pendingVersions = pendingReadSourceVersions;\n let needsFollowUpUpdate = false;\n\n if (oldSet) {\n for (const source of oldSet) {\n if (!newSet?.has(source)) {\n source._readers?.delete(instance);\n }\n }\n }\n\n instance.lastRenderToken = token;\n\n if (newSet) {\n for (const source of newSet) {\n // Replay only for sources already owned by this instance, or for its\n // first committed read set. Newly entered branches in mounted components\n // keep their existing scheduler-visible intermediate states.\n if (pendingVersions && (!oldSet || oldSet.has(source))) {\n const sourceVersion = source._version ?? 0;\n const readVersion = pendingVersions.get(source);\n if (readVersion !== undefined && sourceVersion !== readVersion) {\n needsFollowUpUpdate = true;\n }\n }\n\n let readers = source._readers;\n if (!readers) {\n readers = new Map();\n source._readers = readers;\n }\n readers.set(instance, instance.lastRenderToken ?? 0);\n }\n }\n\n instance._lastReadSources = newSet ?? new Set();\n\n if (needsFollowUpUpdate) {\n scheduleReadableInstanceUpdate(instance);\n }\n}\n\nexport function cleanupReadableSubscriptions(\n instance: ComponentInstance\n): void {\n const sources = instance._lastReadSources;\n if (!sources || sources.size === 0) {\n instance._pendingReadSources = undefined;\n instance._pendingReadSourceVersions = undefined;\n return;\n }\n\n for (const source of sources) {\n source._readers?.delete(instance);\n }\n instance._lastReadSources = new Set();\n instance._pendingReadSources = undefined;\n instance._pendingReadSourceVersions = undefined;\n}\n\nexport function withDerivedReadTracking<T>(\n subscriber: DerivedSubscriber,\n fn: () => T\n): T {\n const prevDerivedSubscriber = currentDerivedSubscriber;\n currentDerivedSubscriber = subscriber;\n suppressComponentReadTrackingDepth += 1;\n\n try {\n return fn();\n } finally {\n suppressComponentReadTrackingDepth -= 1;\n currentDerivedSubscriber = prevDerivedSubscriber;\n }\n}\n\nexport function syncDerivedDependencySubscriptions(\n subscriber: DerivedSubscriber,\n prevSources: Set<ReadableSource<unknown>>,\n nextSources: Set<ReadableSource<unknown>>\n): void {\n for (const source of prevSources) {\n if (!nextSources.has(source)) {\n source._derivedSubscribers?.delete(subscriber);\n }\n }\n\n for (const source of nextSources) {\n let subscribers = source._derivedSubscribers;\n if (!subscribers) {\n subscribers = new Set();\n source._derivedSubscribers = subscribers;\n }\n subscribers.add(subscriber);\n }\n}\n\nexport function clearDerivedDependencySubscriptions(\n subscriber: DerivedSubscriber,\n sources: Set<ReadableSource<unknown>>\n): void {\n for (const source of sources) {\n source._derivedSubscribers?.delete(subscriber);\n }\n sources.clear();\n}\n\nexport function markReadableDerivedSubscribersDirty(\n source: ReadableSource<unknown>\n): void {\n const subscribers = source._derivedSubscribers;\n if (!subscribers || subscribers.size === 0) {\n return;\n }\n\n for (const subscriber of subscribers) {\n subscriber._markDirty();\n }\n}\n\nexport function markReactivePropsDirtySource(\n source: ReadableSource<unknown>\n): void {\n try {\n (\n globalThis as {\n __ASKR_RENDERER?: RendererBridge;\n }\n ).__ASKR_RENDERER?.markReactivePropsDirtySource?.(source);\n } catch {\n // Keep readable notifications side-effect safe.\n }\n}\n\nexport function notifyReadableReaders(\n source: ReadableSource<unknown>,\n skipInstance?: ComponentInstance | null\n): boolean {\n source._version = (source._version ?? 0) + 1;\n const readers = source._readers;\n let didScheduleUpdate = false;\n\n if (!readers || readers.size === 0) {\n return false;\n }\n\n for (const [instance, token] of readers) {\n if (skipInstance && instance === skipInstance) {\n continue;\n }\n if (instance.lastRenderToken !== token) {\n continue;\n }\n if (instance.hasPendingUpdate) {\n continue;\n }\n\n scheduleReadableInstanceUpdate(instance);\n didScheduleUpdate = true;\n }\n\n return didScheduleUpdate;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"resource-cell.js","names":[],"sources":["../../src/runtime/resource-cell.ts"],"sourcesContent":["import { withAsyncResourceContext, type ContextFrame } from './context';\nimport { logger } from '../dev/logger';\nimport { getSSRBridge } from './ssr-bridge';\nimport {\n brandSnapshotSource,\n type SnapshotSourceBrand,\n} from './snapshot-source';\nimport { isPromiseLike } from '../common/promise';\n\n/**\n * Pure, component-agnostic ResourceCell state machine.\n * - Holds value/pending/error/generation/controller\n * - Exposes a stable `snapshot` object: { value, pending, error, refresh }\n * - Uses `withAsyncResourceContext` to bind the synchronous execution step\n * to a captured frame. Continuations after await do not see the frame.\n */\nexport class ResourceCell<U> {\n value: U | null = null;\n pending = true;\n error: Error | null = null;\n generation = 0;\n controller: AbortController | null = null;\n deps: readonly unknown[] | null = null;\n resourceFrame: ContextFrame | null = null;\n\n // Optional debug label set by caller (component name) to improve logs\n ownerName?: string;\n\n private subscribers = new Set<() => void>();\n private disposed = false;\n\n readonly snapshot: {\n value: U | null;\n pending: boolean;\n error: Error | null;\n refresh: () => void;\n } & SnapshotSourceBrand;\n\n private fn: (opts: { signal: AbortSignal }) => PromiseLike<U> | U;\n\n constructor(\n fn: (opts: { signal: AbortSignal }) => PromiseLike<U> | U,\n deps: readonly unknown[] | null,\n resourceFrame: ContextFrame | null\n ) {\n this.fn = fn;\n this.deps = deps ? deps.slice() : null;\n this.resourceFrame = resourceFrame;\n this.snapshot = brandSnapshotSource({\n value: null,\n pending: true,\n error: null,\n refresh: () => this.refresh(),\n });\n }\n\n setLoader(fn: (opts: { signal: AbortSignal }) => PromiseLike<U> | U): void {\n this.fn = fn;\n }\n\n subscribe(cb: () => void): () => void {\n this.subscribers.add(cb);\n return () => this.subscribers.delete(cb);\n }\n\n private notifySubscribers() {\n this.snapshot.value = this.value;\n this.snapshot.pending = this.pending;\n this.snapshot.error = this.error;\n for (const cb of this.subscribers) cb();\n }\n\n start(ssr = false, notify = true) {\n if (this.disposed) {\n return;\n }\n\n const generation = this.generation;\n\n this.controller?.abort();\n const controller = new AbortController();\n this.controller = controller;\n this.pending = true;\n this.error = null;\n if (notify) this.notifySubscribers();\n\n let result: PromiseLike<U> | U;\n try {\n // Execute only the synchronous step inside the frozen resource frame.\n result = withAsyncResourceContext(this.resourceFrame, () =>\n this.fn({ signal: controller.signal })\n );\n } catch (err) {\n this.pending = false;\n this.error = err as Error;\n if (notify) this.notifySubscribers();\n return;\n }\n\n if (!isPromiseLike<U>(result)) {\n this.value = result as U;\n this.pending = false;\n this.error = null;\n if (notify) this.notifySubscribers();\n return;\n }\n\n if (ssr) {\n // During SSR async results are disallowed\n getSSRBridge().throwSSRDataMissing();\n }\n\n Promise.resolve(result)\n .then((val) => {\n if (this.generation !== generation) return;\n if (this.controller !== controller) return;\n // A loader that does not reject on abort (e.g. a plain Promise) would\n // otherwise commit its value after the fetch was cancelled by a deps\n // change, refresh(), or unmount. Mirror the AbortError guard used in\n // the rejection path so an aborted fetch's resolution stays inert.\n if (controller.signal.aborted) return;\n this.value = val;\n this.pending = false;\n this.error = null;\n this.notifySubscribers();\n })\n .catch((err) => {\n if (this.generation !== generation) return;\n if (this.controller !== controller) return;\n\n const isAbortError =\n controller.signal.aborted ||\n (err instanceof Error && err.name === 'AbortError') ||\n (typeof DOMException !== 'undefined' &&\n err instanceof DOMException &&\n err.name === 'AbortError');\n\n if (isAbortError) {\n return;\n }\n\n this.pending = false;\n this.error = err as Error;\n try {\n if (this.ownerName) {\n logger.error(\n `[Askr] Async resource error in ${this.ownerName}:`,\n err\n );\n } else {\n logger.error('[Askr] Async resource error:', err);\n }\n } catch {\n /* ignore logging errors */\n }\n this.notifySubscribers();\n });\n }\n\n refresh() {\n if (this.disposed) {\n return;\n }\n\n this.generation++;\n this.controller?.abort();\n this.start();\n }\n\n abort() {\n this.controller?.abort();\n }\n\n dispose() {\n if (this.disposed) {\n return;\n }\n\n this.disposed = true;\n this.controller?.abort();\n this.subscribers.clear();\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAa,eAAb,MAA6B;CAwB3B,YACE,IACA,MACA,eACA;eA3BgB;iBACR;eACY;oBACT;oBACwB;cACH;uBACG;qCAKf,IAAI,IAAgB;kBACvB;EAgBjB,KAAK,KAAK;EACV,KAAK,OAAO,OAAO,KAAK,MAAM,IAAI;EAClC,KAAK,gBAAgB;EACrB,KAAK,WAAW,oBAAoB;GAClC,OAAO;GACP,SAAS;GACT,OAAO;GACP,eAAe,KAAK,QAAQ;EAC9B,CAAC;CACH;CAEA,UAAU,IAAiE;EACzE,KAAK,KAAK;CACZ;CAEA,UAAU,IAA4B;EACpC,KAAK,YAAY,IAAI,EAAE;EACvB,aAAa,KAAK,YAAY,OAAO,EAAE;CACzC;CAEA,oBAA4B;EAC1B,KAAK,SAAS,QAAQ,KAAK;EAC3B,KAAK,SAAS,UAAU,KAAK;EAC7B,KAAK,SAAS,QAAQ,KAAK;EAC3B,KAAK,MAAM,MAAM,KAAK,aAAa,GAAG;CACxC;CAEA,MAAM,MAAM,OAAO,SAAS,MAAM;EAChC,IAAI,KAAK,UACP;EAGF,MAAM,aAAa,KAAK;EAExB,KAAK,YAAY,MAAM;EACvB,MAAM,aAAa,IAAI,gBAAgB;EACvC,KAAK,aAAa;EAClB,KAAK,UAAU;EACf,KAAK,QAAQ;EACb,IAAI,QAAQ,KAAK,kBAAkB;EAEnC,IAAI;EACJ,IAAI;GAEF,SAAS,yBAAyB,KAAK,qBACrC,KAAK,GAAG,EAAE,QAAQ,WAAW,OAAO,CAAC,CACvC;EACF,SAAS,KAAK;GACZ,KAAK,UAAU;GACf,KAAK,QAAQ;GACb,IAAI,QAAQ,KAAK,kBAAkB;GACnC;EACF;EAEA,IAAI,CAAC,cAAiB,MAAM,GAAG;GAC7B,KAAK,QAAQ;GACb,KAAK,UAAU;GACf,KAAK,QAAQ;GACb,IAAI,QAAQ,KAAK,kBAAkB;GACnC;EACF;EAEA,IAAI,KAEF,aAAa,EAAE,oBAAoB;EAGrC,QAAQ,QAAQ,MAAM,EACnB,MAAM,QAAQ;GACb,IAAI,KAAK,eAAe,YAAY;GACpC,IAAI,KAAK,eAAe,YAAY;GAKpC,IAAI,WAAW,OAAO,SAAS;GAC/B,KAAK,QAAQ;GACb,KAAK,UAAU;GACf,KAAK,QAAQ;GACb,KAAK,kBAAkB;EACzB,CAAC,EACA,OAAO,QAAQ;GACd,IAAI,KAAK,eAAe,YAAY;GACpC,IAAI,KAAK,eAAe,YAAY;GASpC,IANE,WAAW,OAAO,WACjB,eAAe,SAAS,IAAI,SAAS,gBACrC,OAAO,iBAAiB,eACvB,eAAe,gBACf,IAAI,SAAS,cAGf;GAGF,KAAK,UAAU;GACf,KAAK,QAAQ;GACb,IAAI;IACF,IAAI,KAAK,WACP,OAAO,MACL,kCAAkC,KAAK,UAAU,IACjD,GACF;SAEA,OAAO,MAAM,gCAAgC,GAAG;GAEpD,QAAQ,CAER;GACA,KAAK,kBAAkB;EACzB,CAAC;CACL;CAEA,UAAU;EACR,IAAI,KAAK,UACP;EAGF,KAAK;EACL,KAAK,YAAY,MAAM;EACvB,KAAK,MAAM;CACb;CAEA,QAAQ;EACN,KAAK,YAAY,MAAM;CACzB;CAEA,UAAU;EACR,IAAI,KAAK,UACP;EAGF,KAAK,WAAW;EAChB,KAAK,YAAY,MAAM;EACvB,KAAK,YAAY,MAAM;CACzB;AACF"}
1
+ {"version":3,"file":"resource-cell.js","names":[],"sources":["../../src/runtime/resource-cell.ts"],"sourcesContent":["import { withAsyncResourceContext, type ContextFrame } from './context';\nimport { logger } from '../dev/logger';\nimport { getSSRBridge } from './ssr-bridge';\nimport {\n brandSnapshotSource,\n type SnapshotSourceBrand,\n} from './snapshot-source';\nimport { isPromiseLike } from '../common/promise';\n\n/**\n * Pure, component-agnostic ResourceCell state machine.\n * - Holds value/pending/error/generation/controller\n * - Exposes a stable `snapshot` object: { value, pending, error, refresh }\n * - Uses `withAsyncResourceContext` to bind the synchronous execution step\n * to a captured frame. Continuations after await do not see the frame.\n */\nexport class ResourceCell<U> {\n value: U | null = null;\n pending = true;\n error: Error | null = null;\n generation = 0;\n controller: AbortController | null = null;\n deps: readonly unknown[] | null = null;\n resourceFrame: ContextFrame | null = null;\n\n // Optional debug label set by caller (component name) to improve logs\n ownerName?: string;\n\n private subscribers = new Set<() => void>();\n private disposed = false;\n\n readonly snapshot: {\n value: U | null;\n pending: boolean;\n error: Error | null;\n refresh: () => void;\n } & SnapshotSourceBrand;\n\n private fn: (opts: { signal: AbortSignal }) => PromiseLike<U> | U;\n\n constructor(\n fn: (opts: { signal: AbortSignal }) => PromiseLike<U> | U,\n deps: readonly unknown[] | null,\n resourceFrame: ContextFrame | null\n ) {\n this.fn = fn;\n this.deps = deps ? deps.slice() : null;\n this.resourceFrame = resourceFrame;\n this.snapshot = brandSnapshotSource({\n value: null,\n pending: true,\n error: null,\n refresh: () => this.refresh(),\n });\n }\n\n setLoader(fn: (opts: { signal: AbortSignal }) => PromiseLike<U> | U): void {\n this.fn = fn;\n }\n\n subscribe(cb: () => void): () => void {\n this.subscribers.add(cb);\n return () => this.subscribers.delete(cb);\n }\n\n private notifySubscribers() {\n this.snapshot.value = this.value;\n this.snapshot.pending = this.pending;\n this.snapshot.error = this.error;\n for (const cb of this.subscribers) cb();\n }\n\n start(ssr = false, notify = true) {\n if (this.disposed) {\n return;\n }\n\n const generation = this.generation;\n\n this.controller?.abort();\n const controller = new AbortController();\n this.controller = controller;\n this.pending = true;\n this.error = null;\n if (notify) this.notifySubscribers();\n\n let result: PromiseLike<U> | U;\n try {\n // Execute only the synchronous step inside the frozen resource frame.\n result = withAsyncResourceContext(this.resourceFrame, () =>\n this.fn({ signal: controller.signal })\n );\n } catch (err) {\n this.pending = false;\n this.error = err as Error;\n if (notify) this.notifySubscribers();\n return;\n }\n\n if (!isPromiseLike<U>(result)) {\n this.value = result as U;\n this.pending = false;\n this.error = null;\n if (notify) this.notifySubscribers();\n return;\n }\n\n if (ssr) {\n // During SSR async results are disallowed\n getSSRBridge().throwSSRDataMissing();\n }\n\n Promise.resolve(result)\n .then((val) => {\n if (this.generation !== generation) return;\n if (this.controller !== controller) return;\n // A loader that does not reject on abort (e.g. a plain Promise) would\n // otherwise commit its value after the fetch was cancelled by a deps\n // change, refresh(), or unmount. Mirror the AbortError guard used in\n // the rejection path so an aborted fetch's resolution stays inert.\n if (controller.signal.aborted) return;\n this.value = val;\n this.pending = false;\n this.error = null;\n this.notifySubscribers();\n })\n .catch((err) => {\n if (this.generation !== generation) return;\n if (this.controller !== controller) return;\n\n const isAbortError =\n controller.signal.aborted ||\n (err instanceof Error && err.name === 'AbortError') ||\n (typeof DOMException !== 'undefined' &&\n err instanceof DOMException &&\n err.name === 'AbortError');\n\n if (isAbortError) {\n return;\n }\n\n this.pending = false;\n this.error = err as Error;\n try {\n if (this.ownerName) {\n logger.error(\n `[Askr] Async resource error in ${this.ownerName}:`,\n err\n );\n } else {\n logger.error('[Askr] Async resource error:', err);\n }\n } catch {\n /* ignore logging errors */\n }\n this.notifySubscribers();\n });\n }\n\n refresh() {\n if (this.disposed) {\n return;\n }\n\n this.generation++;\n this.controller?.abort();\n this.start();\n }\n\n abort() {\n this.controller?.abort();\n }\n\n dispose() {\n if (this.disposed) {\n return;\n }\n\n this.disposed = true;\n this.controller?.abort();\n this.subscribers.clear();\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAa,eAAb,MAA6B;CAwB3B,YACE,IACA,MACA,eACA;eA3BgB;iBACR;eACY;oBACT;oBACwB;cACH;uBACG;qCAKf,IAAI,IAAgB;kBACvB;EAgBjB,KAAK,KAAK;EACV,KAAK,OAAO,OAAO,KAAK,MAAM,IAAI;EAClC,KAAK,gBAAgB;EACrB,KAAK,WAAW,oBAAoB;GAClC,OAAO;GACP,SAAS;GACT,OAAO;GACP,eAAe,KAAK,QAAQ;EAC9B,CAAC;CACH;CAEA,UAAU,IAAiE;EACzE,KAAK,KAAK;CACZ;CAEA,UAAU,IAA4B;EACpC,KAAK,YAAY,IAAI,EAAE;EACvB,aAAa,KAAK,YAAY,OAAO,EAAE;CACzC;CAEA,oBAA4B;EAC1B,KAAK,SAAS,QAAQ,KAAK;EAC3B,KAAK,SAAS,UAAU,KAAK;EAC7B,KAAK,SAAS,QAAQ,KAAK;EAC3B,KAAK,MAAM,MAAM,KAAK,aAAa,GAAG;CACxC;CAEA,MAAM,MAAM,OAAO,SAAS,MAAM;EAChC,IAAI,KAAK,UACP;EAGF,MAAM,aAAa,KAAK;EAExB,KAAK,YAAY,MAAM;EACvB,MAAM,aAAa,IAAI,gBAAgB;EACvC,KAAK,aAAa;EAClB,KAAK,UAAU;EACf,KAAK,QAAQ;EACb,IAAI,QAAQ,KAAK,kBAAkB;EAEnC,IAAI;EACJ,IAAI;GAEF,SAAS,yBAAyB,KAAK,qBACrC,KAAK,GAAG,EAAE,QAAQ,WAAW,OAAO,CAAC,CACvC;EACF,SAAS,KAAK;GACZ,KAAK,UAAU;GACf,KAAK,QAAQ;GACb,IAAI,QAAQ,KAAK,kBAAkB;GACnC;EACF;EAEA,IAAI,CAAC,cAAiB,MAAM,GAAG;GAC7B,KAAK,QAAQ;GACb,KAAK,UAAU;GACf,KAAK,QAAQ;GACb,IAAI,QAAQ,KAAK,kBAAkB;GACnC;EACF;EAEA,IAAI,KAEF,aAAa,CAAC,CAAC,oBAAoB;EAGrC,QAAQ,QAAQ,MAAM,CAAC,CACpB,MAAM,QAAQ;GACb,IAAI,KAAK,eAAe,YAAY;GACpC,IAAI,KAAK,eAAe,YAAY;GAKpC,IAAI,WAAW,OAAO,SAAS;GAC/B,KAAK,QAAQ;GACb,KAAK,UAAU;GACf,KAAK,QAAQ;GACb,KAAK,kBAAkB;EACzB,CAAC,CAAC,CACD,OAAO,QAAQ;GACd,IAAI,KAAK,eAAe,YAAY;GACpC,IAAI,KAAK,eAAe,YAAY;GASpC,IANE,WAAW,OAAO,WACjB,eAAe,SAAS,IAAI,SAAS,gBACrC,OAAO,iBAAiB,eACvB,eAAe,gBACf,IAAI,SAAS,cAGf;GAGF,KAAK,UAAU;GACf,KAAK,QAAQ;GACb,IAAI;IACF,IAAI,KAAK,WACP,OAAO,MACL,kCAAkC,KAAK,UAAU,IACjD,GACF;SAEA,OAAO,MAAM,gCAAgC,GAAG;GAEpD,QAAQ,CAER;GACA,KAAK,kBAAkB;EACzB,CAAC;CACL;CAEA,UAAU;EACR,IAAI,KAAK,UACP;EAGF,KAAK;EACL,KAAK,YAAY,MAAM;EACvB,KAAK,MAAM;CACb;CAEA,QAAQ;EACN,KAAK,YAAY,MAAM;CACzB;CAEA,UAAU;EACR,IAAI,KAAK,UACP;EAGF,KAAK,WAAW;EAChB,KAAK,YAAY,MAAM;EACvB,KAAK,YAAY,MAAM;CACzB;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.js","names":[],"sources":["../../src/runtime/scheduler.ts"],"sourcesContent":["/**\n * Serialized update scheduler — safer design (no inline execution, explicit flush)\n *\n * Key ideas:\n * - Never execute a task inline from `enqueue`.\n * - `flush()` is explicit and non-reentrant.\n * - `runWithSyncProgress()` allows enqueues temporarily but does not run tasks\n * inline; it runs `fn` and then does an explicit `flush()`.\n * - `waitForFlush()` is race-free with a monotonic `flushVersion`.\n */\n\nimport { isDevelopmentEnvironment } from '../common/env';\nimport { assertSchedulingPrecondition, invariant } from '../dev/invariant';\nimport { logger } from '../dev/logger';\nimport { recordSchedulerFlushTaskCount } from './perf-metrics';\n\nconst MAX_FLUSH_DEPTH = 50;\n\ntype Task = () => void;\nexport type SchedulerLane = 'derived' | 'component' | 'reactive' | 'post';\n\nconst SCHEDULER_LANES: SchedulerLane[] = [\n 'derived',\n 'component',\n 'reactive',\n 'post',\n];\n\ninterface LaneQueue {\n tasks: Task[];\n head: number;\n}\n\nfunction isBulkCommitActive(): boolean {\n try {\n const fb = (\n globalThis as {\n __ASKR_FASTLANE?: { isBulkCommitActive?: () => boolean };\n }\n ).__ASKR_FASTLANE;\n return typeof fb?.isBulkCommitActive === 'function'\n ? !!fb.isBulkCommitActive()\n : false;\n } catch (e) {\n void e;\n return false;\n }\n}\n\nexport class Scheduler {\n private lanes: Record<SchedulerLane, LaneQueue> = {\n derived: { tasks: [], head: 0 },\n component: { tasks: [], head: 0 },\n reactive: { tasks: [], head: 0 },\n post: { tasks: [], head: 0 },\n };\n\n private running = false;\n private inHandler = false;\n private depth = 0;\n private executionDepth = 0; // for compat with existing diagnostics\n\n // Monotonic flush version increments at end of each flush\n private flushVersion = 0;\n\n // Best-effort microtask kick scheduling\n private kickScheduled = false;\n\n // Escape hatch flag for runWithSyncProgress\n private allowSyncProgress = false;\n\n // Waiters waiting for flushVersion >= target\n private waiters: Array<{\n target: number;\n resolve: () => void;\n reject: (err: unknown) => void;\n timer?: ReturnType<typeof setTimeout>;\n }> = [];\n\n // Keep a lightweight taskCount for compatibility/diagnostics\n private taskCount = 0;\n\n private hasPendingTasks(): boolean {\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n if (queue.head < queue.tasks.length) {\n return true;\n }\n }\n return false;\n }\n\n private getPendingTaskCount(): number {\n let total = 0;\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n total += queue.tasks.length - queue.head;\n }\n return total;\n }\n\n private compactLane(queue: LaneQueue): void {\n if (queue.head >= queue.tasks.length) {\n queue.tasks.length = 0;\n queue.head = 0;\n return;\n }\n\n if (queue.head <= 0) {\n return;\n }\n\n const remaining = queue.tasks.length - queue.head;\n for (let i = 0; i < remaining; i++) {\n queue.tasks[i] = queue.tasks[queue.head + i];\n }\n queue.tasks.length = remaining;\n queue.head = 0;\n }\n\n private scheduleFlushKick(): void {\n if (\n this.running ||\n this.kickScheduled ||\n this.inHandler ||\n this.allowSyncProgress ||\n isBulkCommitActive() ||\n !this.hasPendingTasks()\n ) {\n return;\n }\n\n this.kickScheduled = true;\n queueMicrotask(() => {\n this.kickScheduled = false;\n if (this.running || isBulkCommitActive() || !this.hasPendingTasks()) {\n return;\n }\n try {\n this.flush();\n } catch (err) {\n setTimeout(() => {\n throw err;\n });\n }\n });\n }\n\n enqueue(task: Task): void {\n this.enqueueInLane('component', task);\n }\n\n enqueueInLane(lane: SchedulerLane, task: Task): void {\n assertSchedulingPrecondition(\n typeof task === 'function',\n 'enqueue() requires a function'\n );\n\n // Strict rule: during bulk commit, only allow enqueues if runWithSyncProgress enabled\n if (isBulkCommitActive() && !this.allowSyncProgress) {\n if (isDevelopmentEnvironment()) {\n throw new Error(\n '[Scheduler] enqueue() during bulk commit (not allowed)'\n );\n }\n return;\n }\n\n // Enqueue task and account counts\n this.lanes[lane].tasks.push(task);\n this.taskCount++;\n\n this.scheduleFlushKick();\n }\n\n flush(): void {\n invariant(\n !this.running,\n '[Scheduler] flush() called while already running'\n );\n\n // Dev-only guard: disallow flush during bulk commit unless allowed\n if (isDevelopmentEnvironment()) {\n if (isBulkCommitActive() && !this.allowSyncProgress) {\n throw new Error(\n '[Scheduler] flush() started during bulk commit (not allowed)'\n );\n }\n }\n\n this.running = true;\n this.depth = 0;\n let fatal: unknown = null;\n let executedTaskCount = 0;\n const checkFlushDepth = isDevelopmentEnvironment();\n const executionsByTask = checkFlushDepth ? new Map<Task, number>() : null;\n\n try {\n while (fatal === null) {\n let didRunTask = false;\n\n for (const lane of SCHEDULER_LANES) {\n const laneQueue = this.lanes[lane];\n let executedInLane = 0;\n\n while (laneQueue.head < laneQueue.tasks.length) {\n const task = laneQueue.tasks[laneQueue.head++];\n if (executionsByTask) {\n const taskExecutions = (executionsByTask.get(task) ?? 0) + 1;\n executionsByTask.set(task, taskExecutions);\n this.depth = Math.max(this.depth, taskExecutions);\n if (taskExecutions > MAX_FLUSH_DEPTH) {\n throw new Error(\n `[Scheduler] exceeded MAX_FLUSH_DEPTH (${MAX_FLUSH_DEPTH}). Likely infinite update loop.`\n );\n }\n }\n\n try {\n this.executionDepth++;\n task();\n this.executionDepth--;\n executedTaskCount++;\n executedInLane++;\n didRunTask = true;\n } catch (err) {\n // ensure executionDepth stays balanced for the task that threw\n if (this.executionDepth > 0) this.executionDepth--;\n fatal = err;\n break;\n }\n }\n\n if (executedInLane > 0) {\n this.taskCount = Math.max(0, this.taskCount - executedInLane);\n }\n\n if (fatal !== null) {\n break;\n }\n }\n\n if (!didRunTask) {\n break;\n }\n }\n } finally {\n this.running = false;\n this.depth = 0;\n this.executionDepth = 0;\n\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n if (queue.head > 0) {\n this.compactLane(queue);\n }\n }\n\n // Advance flush epoch and resolve waiters\n this.flushVersion++;\n this.resolveWaiters();\n recordSchedulerFlushTaskCount(executedTaskCount);\n }\n\n if (fatal) throw fatal;\n }\n\n runWithSyncProgress<T>(fn: () => T): T {\n const prev = this.allowSyncProgress;\n this.allowSyncProgress = true;\n\n // Snapshot flushVersion so we can ensure we always complete an epoch\n const startVersion = this.flushVersion;\n\n try {\n const res = fn();\n\n // Flush deterministically if tasks were enqueued (and we're not already running)\n if (!this.running && this.hasPendingTasks()) {\n this.flush();\n }\n\n if (isDevelopmentEnvironment()) {\n if (!this.running && this.hasPendingTasks()) {\n throw new Error(\n '[Scheduler] tasks remain after runWithSyncProgress flush'\n );\n }\n }\n\n return res;\n } finally {\n // If no flush happened during the protected window, complete an epoch so\n // observers (tests) see progress even when fast-lane did synchronous work\n // without enqueuing tasks.\n try {\n if (this.flushVersion === startVersion) {\n this.flushVersion++;\n this.resolveWaiters();\n }\n } catch (e) {\n void e;\n }\n\n this.allowSyncProgress = prev;\n }\n }\n\n waitForFlush(targetVersion?: number, timeoutMs = 2000): Promise<void> {\n const target =\n typeof targetVersion === 'number' ? targetVersion : this.flushVersion + 1;\n if (this.flushVersion >= target) return Promise.resolve();\n\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n const ns =\n (\n globalThis as unknown as Record<string, unknown> & {\n __ASKR__?: Record<string, unknown>;\n }\n ).__ASKR__ || {};\n const diag = {\n flushVersion: this.flushVersion,\n queueLen: this.getPendingTaskCount(),\n running: this.running,\n inHandler: this.inHandler,\n bulk: isBulkCommitActive(),\n namespace: ns,\n };\n reject(\n new Error(\n `waitForFlush timeout ${timeoutMs}ms: ${JSON.stringify(diag)}`\n )\n );\n }, timeoutMs);\n\n this.waiters.push({ target, resolve, reject, timer });\n });\n }\n\n getState() {\n // Provide the compatibility shape expected by diagnostics/tests\n return {\n queueLength: this.getPendingTaskCount(),\n running: this.running,\n depth: this.depth,\n executionDepth: this.executionDepth,\n taskCount: this.taskCount,\n flushVersion: this.flushVersion,\n laneQueues: {\n derived: this.lanes.derived.tasks.length - this.lanes.derived.head,\n component:\n this.lanes.component.tasks.length - this.lanes.component.head,\n reactive: this.lanes.reactive.tasks.length - this.lanes.reactive.head,\n post: this.lanes.post.tasks.length - this.lanes.post.head,\n },\n // New fields for optional inspection\n inHandler: this.inHandler,\n allowSyncProgress: this.allowSyncProgress,\n };\n }\n\n getFlushVersion(): number {\n return this.flushVersion;\n }\n\n flushIfQueued(): void {\n if (!this.running && this.hasPendingTasks()) {\n this.flush();\n }\n }\n\n runInHandlerScope<T>(fn: () => T, flushMode: 'defer' | 'sync' = 'defer'): T {\n const prevInHandler = this.inHandler;\n this.inHandler = true;\n\n try {\n return fn();\n } finally {\n this.inHandler = prevInHandler;\n\n if (!this.inHandler) {\n if (flushMode === 'sync') {\n this.flushIfQueued();\n } else {\n this.scheduleFlushKick();\n }\n }\n }\n }\n\n setInHandler(v: boolean) {\n this.inHandler = v;\n if (!v) {\n this.scheduleFlushKick();\n }\n }\n\n isInHandler(): boolean {\n return this.inHandler;\n }\n\n isExecuting(): boolean {\n return this.running || this.executionDepth > 0;\n }\n\n // Clear pending synchronous tasks (used by fastlane enter/exit)\n clearPendingSyncTasks(): number {\n const remaining = this.getPendingTaskCount();\n if (remaining <= 0) return 0;\n\n if (this.running) {\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n queue.tasks.length = queue.head;\n }\n this.taskCount = Math.max(0, this.taskCount - remaining);\n queueMicrotask(() => {\n try {\n this.flushVersion++;\n this.resolveWaiters();\n } catch (e) {\n void e;\n }\n });\n return remaining;\n }\n\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n queue.tasks.length = 0;\n queue.head = 0;\n }\n this.taskCount = Math.max(0, this.taskCount - remaining);\n this.flushVersion++;\n this.resolveWaiters();\n return remaining;\n }\n\n private resolveWaiters() {\n if (this.waiters.length === 0) return;\n const ready: Array<() => void> = [];\n const remaining: typeof this.waiters = [];\n\n for (const w of this.waiters) {\n if (this.flushVersion >= w.target) {\n if (w.timer) clearTimeout(w.timer);\n ready.push(w.resolve);\n } else {\n remaining.push(w);\n }\n }\n\n this.waiters = remaining;\n for (const r of ready) r();\n }\n}\n\nexport const globalScheduler = new Scheduler();\n\nexport function isSchedulerExecuting(): boolean {\n return globalScheduler.isExecuting();\n}\n\nexport function scheduleEventHandler(handler: EventListener): EventListener {\n return (event: Event) => {\n try {\n globalScheduler.runInHandlerScope(() => {\n handler.call(null, event);\n });\n } catch (error) {\n logger.error('[Askr] Event handler error:', error);\n }\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scheduler.js","names":[],"sources":["../../src/runtime/scheduler.ts"],"sourcesContent":["/**\n * Serialized update scheduler — safer design (no inline execution, explicit flush)\n *\n * Key ideas:\n * - Never execute a task inline from `enqueue`.\n * - `flush()` is explicit and non-reentrant.\n * - `runWithSyncProgress()` allows enqueues temporarily but does not run tasks\n * inline; it runs `fn` and then does an explicit `flush()`.\n * - `waitForFlush()` is race-free with a monotonic `flushVersion`.\n */\n\nimport { isDevelopmentEnvironment } from '../common/env';\nimport { assertSchedulingPrecondition, invariant } from '../dev/invariant';\nimport { logger } from '../dev/logger';\nimport { recordSchedulerFlushTaskCount } from './perf-metrics';\n\nconst MAX_FLUSH_DEPTH = 50;\n\ntype Task = () => void;\nexport type SchedulerLane = 'derived' | 'component' | 'reactive' | 'post';\n\nconst SCHEDULER_LANES: SchedulerLane[] = [\n 'derived',\n 'component',\n 'reactive',\n 'post',\n];\n\ninterface LaneQueue {\n tasks: Task[];\n head: number;\n}\n\nfunction isBulkCommitActive(): boolean {\n try {\n const fb = (\n globalThis as {\n __ASKR_FASTLANE?: { isBulkCommitActive?: () => boolean };\n }\n ).__ASKR_FASTLANE;\n return typeof fb?.isBulkCommitActive === 'function'\n ? !!fb.isBulkCommitActive()\n : false;\n } catch (e) {\n void e;\n return false;\n }\n}\n\nexport class Scheduler {\n private lanes: Record<SchedulerLane, LaneQueue> = {\n derived: { tasks: [], head: 0 },\n component: { tasks: [], head: 0 },\n reactive: { tasks: [], head: 0 },\n post: { tasks: [], head: 0 },\n };\n\n private running = false;\n private inHandler = false;\n private depth = 0;\n private executionDepth = 0; // for compat with existing diagnostics\n\n // Monotonic flush version increments at end of each flush\n private flushVersion = 0;\n\n // Best-effort microtask kick scheduling\n private kickScheduled = false;\n\n // Escape hatch flag for runWithSyncProgress\n private allowSyncProgress = false;\n\n // Waiters waiting for flushVersion >= target\n private waiters: Array<{\n target: number;\n resolve: () => void;\n reject: (err: unknown) => void;\n timer?: ReturnType<typeof setTimeout>;\n }> = [];\n\n // Keep a lightweight taskCount for compatibility/diagnostics\n private taskCount = 0;\n\n private hasPendingTasks(): boolean {\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n if (queue.head < queue.tasks.length) {\n return true;\n }\n }\n return false;\n }\n\n private getPendingTaskCount(): number {\n let total = 0;\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n total += queue.tasks.length - queue.head;\n }\n return total;\n }\n\n private compactLane(queue: LaneQueue): void {\n if (queue.head >= queue.tasks.length) {\n queue.tasks.length = 0;\n queue.head = 0;\n return;\n }\n\n if (queue.head <= 0) {\n return;\n }\n\n const remaining = queue.tasks.length - queue.head;\n for (let i = 0; i < remaining; i++) {\n queue.tasks[i] = queue.tasks[queue.head + i];\n }\n queue.tasks.length = remaining;\n queue.head = 0;\n }\n\n private scheduleFlushKick(): void {\n if (\n this.running ||\n this.kickScheduled ||\n this.inHandler ||\n this.allowSyncProgress ||\n isBulkCommitActive() ||\n !this.hasPendingTasks()\n ) {\n return;\n }\n\n this.kickScheduled = true;\n queueMicrotask(() => {\n this.kickScheduled = false;\n if (this.running || isBulkCommitActive() || !this.hasPendingTasks()) {\n return;\n }\n try {\n this.flush();\n } catch (err) {\n setTimeout(() => {\n throw err;\n });\n }\n });\n }\n\n enqueue(task: Task): void {\n this.enqueueInLane('component', task);\n }\n\n enqueueInLane(lane: SchedulerLane, task: Task): void {\n assertSchedulingPrecondition(\n typeof task === 'function',\n 'enqueue() requires a function'\n );\n\n // Strict rule: during bulk commit, only allow enqueues if runWithSyncProgress enabled\n if (isBulkCommitActive() && !this.allowSyncProgress) {\n if (isDevelopmentEnvironment()) {\n throw new Error(\n '[Scheduler] enqueue() during bulk commit (not allowed)'\n );\n }\n return;\n }\n\n // Enqueue task and account counts\n this.lanes[lane].tasks.push(task);\n this.taskCount++;\n\n this.scheduleFlushKick();\n }\n\n flush(): void {\n invariant(\n !this.running,\n '[Scheduler] flush() called while already running'\n );\n\n // Dev-only guard: disallow flush during bulk commit unless allowed\n if (isDevelopmentEnvironment()) {\n if (isBulkCommitActive() && !this.allowSyncProgress) {\n throw new Error(\n '[Scheduler] flush() started during bulk commit (not allowed)'\n );\n }\n }\n\n this.running = true;\n this.depth = 0;\n let fatal: unknown = null;\n let executedTaskCount = 0;\n const checkFlushDepth = isDevelopmentEnvironment();\n const executionsByTask = checkFlushDepth ? new Map<Task, number>() : null;\n\n try {\n while (fatal === null) {\n let didRunTask = false;\n\n for (const lane of SCHEDULER_LANES) {\n const laneQueue = this.lanes[lane];\n let executedInLane = 0;\n\n while (laneQueue.head < laneQueue.tasks.length) {\n const task = laneQueue.tasks[laneQueue.head++];\n if (executionsByTask) {\n const taskExecutions = (executionsByTask.get(task) ?? 0) + 1;\n executionsByTask.set(task, taskExecutions);\n this.depth = Math.max(this.depth, taskExecutions);\n if (taskExecutions > MAX_FLUSH_DEPTH) {\n throw new Error(\n `[Scheduler] exceeded MAX_FLUSH_DEPTH (${MAX_FLUSH_DEPTH}). Likely infinite update loop.`\n );\n }\n }\n\n try {\n this.executionDepth++;\n task();\n this.executionDepth--;\n executedTaskCount++;\n executedInLane++;\n didRunTask = true;\n } catch (err) {\n // ensure executionDepth stays balanced for the task that threw\n if (this.executionDepth > 0) this.executionDepth--;\n fatal = err;\n break;\n }\n }\n\n if (executedInLane > 0) {\n this.taskCount = Math.max(0, this.taskCount - executedInLane);\n }\n\n if (fatal !== null) {\n break;\n }\n }\n\n if (!didRunTask) {\n break;\n }\n }\n } finally {\n this.running = false;\n this.depth = 0;\n this.executionDepth = 0;\n\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n if (queue.head > 0) {\n this.compactLane(queue);\n }\n }\n\n // Advance flush epoch and resolve waiters\n this.flushVersion++;\n this.resolveWaiters();\n recordSchedulerFlushTaskCount(executedTaskCount);\n }\n\n if (fatal) throw fatal;\n }\n\n runWithSyncProgress<T>(fn: () => T): T {\n const prev = this.allowSyncProgress;\n this.allowSyncProgress = true;\n\n // Snapshot flushVersion so we can ensure we always complete an epoch\n const startVersion = this.flushVersion;\n\n try {\n const res = fn();\n\n // Flush deterministically if tasks were enqueued (and we're not already running)\n if (!this.running && this.hasPendingTasks()) {\n this.flush();\n }\n\n if (isDevelopmentEnvironment()) {\n if (!this.running && this.hasPendingTasks()) {\n throw new Error(\n '[Scheduler] tasks remain after runWithSyncProgress flush'\n );\n }\n }\n\n return res;\n } finally {\n // If no flush happened during the protected window, complete an epoch so\n // observers (tests) see progress even when fast-lane did synchronous work\n // without enqueuing tasks.\n try {\n if (this.flushVersion === startVersion) {\n this.flushVersion++;\n this.resolveWaiters();\n }\n } catch (e) {\n void e;\n }\n\n this.allowSyncProgress = prev;\n }\n }\n\n waitForFlush(targetVersion?: number, timeoutMs = 2000): Promise<void> {\n const target =\n typeof targetVersion === 'number' ? targetVersion : this.flushVersion + 1;\n if (this.flushVersion >= target) return Promise.resolve();\n\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n const ns =\n (\n globalThis as unknown as Record<string, unknown> & {\n __ASKR__?: Record<string, unknown>;\n }\n ).__ASKR__ || {};\n const diag = {\n flushVersion: this.flushVersion,\n queueLen: this.getPendingTaskCount(),\n running: this.running,\n inHandler: this.inHandler,\n bulk: isBulkCommitActive(),\n namespace: ns,\n };\n reject(\n new Error(\n `waitForFlush timeout ${timeoutMs}ms: ${JSON.stringify(diag)}`\n )\n );\n }, timeoutMs);\n\n this.waiters.push({ target, resolve, reject, timer });\n });\n }\n\n getState() {\n // Provide the compatibility shape expected by diagnostics/tests\n return {\n queueLength: this.getPendingTaskCount(),\n running: this.running,\n depth: this.depth,\n executionDepth: this.executionDepth,\n taskCount: this.taskCount,\n flushVersion: this.flushVersion,\n laneQueues: {\n derived: this.lanes.derived.tasks.length - this.lanes.derived.head,\n component:\n this.lanes.component.tasks.length - this.lanes.component.head,\n reactive: this.lanes.reactive.tasks.length - this.lanes.reactive.head,\n post: this.lanes.post.tasks.length - this.lanes.post.head,\n },\n // New fields for optional inspection\n inHandler: this.inHandler,\n allowSyncProgress: this.allowSyncProgress,\n };\n }\n\n getFlushVersion(): number {\n return this.flushVersion;\n }\n\n flushIfQueued(): void {\n if (!this.running && this.hasPendingTasks()) {\n this.flush();\n }\n }\n\n runInHandlerScope<T>(fn: () => T, flushMode: 'defer' | 'sync' = 'defer'): T {\n const prevInHandler = this.inHandler;\n this.inHandler = true;\n\n try {\n return fn();\n } finally {\n this.inHandler = prevInHandler;\n\n if (!this.inHandler) {\n if (flushMode === 'sync') {\n this.flushIfQueued();\n } else {\n this.scheduleFlushKick();\n }\n }\n }\n }\n\n setInHandler(v: boolean) {\n this.inHandler = v;\n if (!v) {\n this.scheduleFlushKick();\n }\n }\n\n isInHandler(): boolean {\n return this.inHandler;\n }\n\n isExecuting(): boolean {\n return this.running || this.executionDepth > 0;\n }\n\n // Clear pending synchronous tasks (used by fastlane enter/exit)\n clearPendingSyncTasks(): number {\n const remaining = this.getPendingTaskCount();\n if (remaining <= 0) return 0;\n\n if (this.running) {\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n queue.tasks.length = queue.head;\n }\n this.taskCount = Math.max(0, this.taskCount - remaining);\n queueMicrotask(() => {\n try {\n this.flushVersion++;\n this.resolveWaiters();\n } catch (e) {\n void e;\n }\n });\n return remaining;\n }\n\n for (const lane of SCHEDULER_LANES) {\n const queue = this.lanes[lane];\n queue.tasks.length = 0;\n queue.head = 0;\n }\n this.taskCount = Math.max(0, this.taskCount - remaining);\n this.flushVersion++;\n this.resolveWaiters();\n return remaining;\n }\n\n private resolveWaiters() {\n if (this.waiters.length === 0) return;\n const ready: Array<() => void> = [];\n const remaining: typeof this.waiters = [];\n\n for (const w of this.waiters) {\n if (this.flushVersion >= w.target) {\n if (w.timer) clearTimeout(w.timer);\n ready.push(w.resolve);\n } else {\n remaining.push(w);\n }\n }\n\n this.waiters = remaining;\n for (const r of ready) r();\n }\n}\n\nexport const globalScheduler = new Scheduler();\n\nexport function isSchedulerExecuting(): boolean {\n return globalScheduler.isExecuting();\n}\n\nexport function scheduleEventHandler(handler: EventListener): EventListener {\n return (event: Event) => {\n try {\n globalScheduler.runInHandlerScope(() => {\n handler.call(null, event);\n });\n } catch (error) {\n logger.error('[Askr] Event handler error:', error);\n }\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"selector.js","names":[],"sources":["../../src/runtime/selector.ts"],"sourcesContent":["import {\n claimHookIndex,\n getCurrentInstance,\n type ComponentInstance,\n} from './component';\nimport { globalScheduler } from './scheduler';\nimport {\n clearDerivedDependencySubscriptions,\n markReadableDerivedSubscribersDirty,\n markReadableUsage,\n markReactivePropsDirtySource,\n notifyReadableReaders,\n recordReadableRead,\n syncDerivedDependencySubscriptions,\n withDerivedReadTracking,\n type DerivedSubscriber,\n type ReadableSource,\n} from './readable';\nimport { getPerfMetricsStore, incrementPerfMetric } from './perf-metrics';\n\ntype PrimitiveKey =\n | string\n | number\n | boolean\n | symbol\n | bigint\n | null\n | undefined;\n\nexport interface Selector<T> {\n (candidate: T): boolean;\n}\n\ninterface SelectorCandidateSource<T> extends ReadableSource<boolean> {\n _candidate: T;\n}\n\ntype SelectorEquals<T> = {\n bivarianceHack(a: T, b: T): boolean;\n}['bivarianceHack'];\n\ninterface SelectorLane<T> {\n _record: SelectorSourceRecord<T>;\n _equals: SelectorEquals<T>;\n _bindingCount: number;\n _primitiveCandidates: Map<PrimitiveKey, SelectorCandidateSource<T>>;\n _objectCandidates: WeakMap<object, SelectorCandidateSource<T>>;\n _objectCandidateSources: Set<SelectorCandidateSource<T>>;\n _cleanup(): void;\n}\n\ninterface SelectorSourceRecord<T> extends DerivedSubscriber {\n _source: () => T;\n _value: T;\n _hasValue: boolean;\n _dirty: boolean;\n _scheduled: boolean;\n _evaluating: boolean;\n _sources: Set<ReadableSource<unknown>>;\n _pendingDependencySources?: Set<ReadableSource<unknown>>;\n _lanes: Map<SelectorEquals<T>, SelectorLane<T>>;\n _cleanup(): void;\n}\n\ninterface SelectorHook<T> extends Selector<T> {\n _owner: ComponentInstance;\n _hookIndex: number;\n _source: () => T;\n _equals: SelectorEquals<T>;\n _record: SelectorSourceRecord<T> | null;\n _lane: SelectorLane<T> | null;\n _cleanup(): void;\n}\n\nconst selectorCells = new WeakMap<\n ComponentInstance,\n Map<number, SelectorHook<unknown>>\n>();\nconst selectorRecords = new WeakMap<\n ReadableSource<unknown>,\n SelectorSourceRecord<unknown>\n>();\nconst dirtySelectorRecords = new Set<SelectorSourceRecord<unknown>>();\nlet hasPendingSelectorFlush = false;\n\nfunction getSelectorStore(\n instance: ComponentInstance\n): Map<number, SelectorHook<unknown>> {\n let store = selectorCells.get(instance);\n if (!store) {\n store = new Map();\n selectorCells.set(instance, store);\n }\n return store;\n}\n\nfunction scheduleSelectorFlush(): void {\n if (hasPendingSelectorFlush) {\n return;\n }\n\n hasPendingSelectorFlush = true;\n globalScheduler.enqueueInLane('derived', flushDirtySelectorRecords);\n}\n\nfunction markSelectorRecordDirty(record: SelectorSourceRecord<unknown>): void {\n record._dirty = true;\n if (record._scheduled) {\n return;\n }\n\n record._scheduled = true;\n dirtySelectorRecords.add(record);\n scheduleSelectorFlush();\n}\n\nfunction flushDirtySelectorRecords(): void {\n hasPendingSelectorFlush = false;\n\n if (dirtySelectorRecords.size === 0) {\n return;\n }\n\n const pending = Array.from(dirtySelectorRecords);\n dirtySelectorRecords.clear();\n\n for (const record of pending) {\n record._scheduled = false;\n if (!record._dirty) {\n continue;\n }\n recomputeSelectorSourceRecord(record, true);\n }\n}\n\nfunction isObjectCandidate(value: unknown): value is object {\n return (\n (typeof value === 'object' && value !== null) || typeof value === 'function'\n );\n}\n\nfunction isDefaultSelectorEquals<T>(equals: SelectorEquals<T>): boolean {\n return equals === Object.is;\n}\n\nfunction createCandidateSource<T>(candidate: T): SelectorCandidateSource<T> {\n const source = (() => false) as SelectorCandidateSource<T>;\n source._candidate = candidate;\n return source;\n}\n\nfunction getCandidateSource<T>(\n lane: SelectorLane<T>,\n candidate: T\n): SelectorCandidateSource<T> {\n if (isObjectCandidate(candidate)) {\n const cached = lane._objectCandidates.get(candidate);\n if (cached) {\n return cached;\n }\n\n const created = createCandidateSource(candidate);\n lane._objectCandidates.set(candidate, created);\n lane._objectCandidateSources.add(created);\n return created;\n }\n\n const key = candidate as PrimitiveKey;\n const cached = lane._primitiveCandidates.get(key);\n if (cached) {\n return cached;\n }\n\n const created = createCandidateSource(candidate);\n lane._primitiveCandidates.set(key, created);\n return created;\n}\n\nfunction peekCandidateSource<T>(\n lane: SelectorLane<T>,\n candidate: T\n): SelectorCandidateSource<T> | undefined {\n if (isObjectCandidate(candidate)) {\n return lane._objectCandidates.get(candidate);\n }\n return lane._primitiveCandidates.get(candidate as PrimitiveKey);\n}\n\nfunction getSelectorSourceRecord<T>(source: () => T): SelectorSourceRecord<T> {\n const cached = selectorRecords.get(source);\n if (cached) {\n return cached as SelectorSourceRecord<T>;\n }\n\n const record = createSelectorSourceRecord(source);\n selectorRecords.set(source, record as SelectorSourceRecord<unknown>);\n return record;\n}\n\nfunction createSelectorSourceRecord<T>(\n source: () => T\n): SelectorSourceRecord<T> {\n let record!: SelectorSourceRecord<T>;\n\n record = {\n _source: source,\n _value: undefined as T,\n _hasValue: false,\n _dirty: true,\n _scheduled: false,\n _evaluating: false,\n _sources: new Set(),\n _lanes: new Map(),\n _markDirty: () => {\n markSelectorRecordDirty(record as SelectorSourceRecord<unknown>);\n },\n _cleanup: () => {\n record._scheduled = false;\n record._dirty = false;\n record._hasValue = false;\n record._evaluating = false;\n record._pendingDependencySources = undefined;\n clearDerivedDependencySubscriptions(record, record._sources);\n record._lanes.clear();\n selectorRecords.delete(source as ReadableSource<unknown>);\n },\n };\n\n return record;\n}\n\nfunction getSelectorLane<T>(\n record: SelectorSourceRecord<T>,\n equals: SelectorEquals<T>\n): SelectorLane<T> {\n const cached = record._lanes.get(equals);\n if (cached) {\n return cached;\n }\n\n const lane = createSelectorLane(record, equals);\n record._lanes.set(equals, lane);\n return lane;\n}\n\nfunction createSelectorLane<T>(\n record: SelectorSourceRecord<T>,\n equals: SelectorEquals<T>\n): SelectorLane<T> {\n const lane: SelectorLane<T> = {\n _record: record,\n _equals: equals,\n _bindingCount: 0,\n _primitiveCandidates: new Map(),\n _objectCandidates: new WeakMap(),\n _objectCandidateSources: new Set(),\n _cleanup: () => {\n for (const sourceRef of lane._primitiveCandidates.values()) {\n sourceRef._readers?.clear();\n sourceRef._derivedSubscribers?.clear();\n }\n for (const sourceRef of lane._objectCandidateSources) {\n sourceRef._readers?.clear();\n sourceRef._derivedSubscribers?.clear();\n }\n lane._primitiveCandidates.clear();\n lane._objectCandidates = new WeakMap();\n lane._objectCandidateSources.clear();\n },\n };\n\n return lane;\n}\n\nfunction notifySelectorSource(source: SelectorCandidateSource<unknown>): void {\n incrementPerfMetric('selectorInvalidations');\n markReadableDerivedSubscribersDirty(source);\n markReactivePropsDirtySource(source);\n notifyReadableReaders(source);\n}\n\nfunction notifyAllSelectorSources<T>(lane: SelectorLane<T>): void {\n for (const source of lane._primitiveCandidates.values()) {\n notifySelectorSource(source);\n }\n for (const source of lane._objectCandidateSources) {\n notifySelectorSource(source);\n }\n}\n\nfunction notifySelectorLaneValueChange<T>(\n lane: SelectorLane<T>,\n prevValue: T,\n nextValue: T\n): void {\n if (!lane._bindingCount) {\n return;\n }\n\n if (!isDefaultSelectorEquals(lane._equals)) {\n if (lane._equals(prevValue, nextValue)) {\n return;\n }\n notifyAllSelectorSources(lane);\n return;\n }\n\n if (!Object.is(prevValue, nextValue)) {\n // Only notify candidate sources that were actually materialized by a read.\n // Using getCandidateSource here would create-on-miss and leak a candidate\n // source for every distinct value the source ever passed through, even\n // those no component reads. Peek instead so the candidate cache stays\n // bounded to values that components actually compare against.\n const prevSource = peekCandidateSource(lane, prevValue);\n if (prevSource) {\n notifySelectorSource(prevSource);\n }\n const nextSource = peekCandidateSource(lane, nextValue);\n if (nextSource) {\n notifySelectorSource(nextSource);\n }\n }\n}\n\nfunction recomputeSelectorSourceRecord<T>(\n record: SelectorSourceRecord<T>,\n notifyDownstream: boolean\n): T {\n if (!record._dirty && record._hasValue) {\n return record._value;\n }\n\n if (record._evaluating) {\n throw new Error('selector() cannot read itself recursively');\n }\n\n record._evaluating = true;\n record._dirty = false;\n record._pendingDependencySources = new Set();\n\n const prevSources = record._sources;\n const hadValue = record._hasValue;\n const prevValue = record._value;\n let nextValue: T;\n\n try {\n nextValue = withDerivedReadTracking(record, record._source);\n } catch (error) {\n record._dirty = true;\n record._pendingDependencySources = undefined;\n throw error;\n } finally {\n record._evaluating = false;\n }\n\n const nextSources = record._pendingDependencySources ?? new Set();\n record._pendingDependencySources = undefined;\n syncDerivedDependencySubscriptions(record, prevSources, nextSources);\n record._sources = nextSources;\n\n const valueChanged = !record._hasValue || !Object.is(prevValue, nextValue);\n record._hasValue = true;\n record._value = nextValue;\n\n if (valueChanged && notifyDownstream && hadValue) {\n for (const lane of Array.from(record._lanes.values())) {\n notifySelectorLaneValueChange(lane, prevValue, nextValue);\n }\n }\n\n return record._value;\n}\n\nfunction attachSelectorHookBinding<T>(\n hook: SelectorHook<T>,\n source: () => T,\n equals: SelectorEquals<T>\n): void {\n const record = getSelectorSourceRecord(source);\n const lane = getSelectorLane(record, equals);\n\n hook._source = source;\n hook._equals = equals;\n hook._record = record;\n hook._lane = lane;\n lane._bindingCount += 1;\n}\n\nfunction detachSelectorHookBinding<T>(hook: SelectorHook<T>): void {\n const record = hook._record;\n const lane = hook._lane;\n\n hook._record = null;\n hook._lane = null;\n\n if (!record || !lane) {\n return;\n }\n\n if (lane._bindingCount > 0) {\n lane._bindingCount -= 1;\n }\n\n if (lane._bindingCount > 0) {\n return;\n }\n\n lane._cleanup();\n record._lanes.delete(lane._equals);\n\n if (record._lanes.size === 0) {\n record._cleanup();\n }\n}\n\nfunction ensureSelectorHookBinding<T>(\n hook: SelectorHook<T>\n): SelectorSourceRecord<T> {\n const record = hook._record;\n const lane = hook._lane;\n\n if (\n record &&\n lane &&\n lane._bindingCount > 0 &&\n record._lanes.get(hook._equals) === lane &&\n record._source === hook._source\n ) {\n return record;\n }\n\n if (hook._record || hook._lane) {\n detachSelectorHookBinding(hook);\n }\n\n attachSelectorHookBinding(hook, hook._source, hook._equals);\n return hook._record!;\n}\n\nfunction createSelectorHook<T>(\n instance: ComponentInstance,\n hookIndex: number,\n source: () => T,\n equals: SelectorEquals<T>\n): SelectorHook<T> {\n const hook = function selectorPredicate(candidate: T): boolean {\n const selectorHook = hook as SelectorHook<T>;\n const record = ensureSelectorHookBinding(selectorHook);\n const lane = selectorHook._lane;\n if (!lane) {\n throw new Error('selector() binding could not be established.');\n }\n\n const sourceRef = getCandidateSource(lane, candidate);\n recordReadableRead(sourceRef);\n\n const perfMetricsStore = getPerfMetricsStore();\n if (perfMetricsStore) {\n perfMetricsStore.selectorCandidateReads += 1;\n }\n\n const current =\n record._dirty || !record._hasValue\n ? recomputeSelectorSourceRecord(record, record._scheduled)\n : record._value;\n\n return lane._equals(current, candidate);\n } as SelectorHook<T>;\n\n hook._owner = instance;\n hook._hookIndex = hookIndex;\n hook._source = source;\n hook._equals = equals;\n hook._record = null;\n hook._lane = null;\n hook._cleanup = () => {\n detachSelectorHookBinding(hook);\n };\n\n attachSelectorHookBinding(hook, source, equals);\n\n (instance.cleanupFns ??= []).push(() => {\n hook._cleanup();\n selectorCells.get(instance)?.delete(hookIndex);\n });\n\n return hook;\n}\n\nfunction getOrCreateSelectorHook<T>(\n instance: ComponentInstance,\n hookIndex: number,\n source: () => T,\n equals: SelectorEquals<T>\n): SelectorHook<T> {\n const store = getSelectorStore(instance);\n const existing = store.get(hookIndex) as SelectorHook<T> | undefined;\n if (existing) {\n if (existing._source !== source || existing._equals !== equals) {\n detachSelectorHookBinding(existing);\n attachSelectorHookBinding(existing, source, equals);\n } else {\n ensureSelectorHookBinding(existing);\n }\n return existing;\n }\n\n const created = createSelectorHook(instance, hookIndex, source, equals);\n store.set(hookIndex, created as unknown as SelectorHook<unknown>);\n return created;\n}\n\nexport function selector<T>(\n source: () => T,\n equals: SelectorEquals<T> = Object.is\n): Selector<T> {\n markReadableUsage(source);\n\n const instance = getCurrentInstance();\n if (!instance) {\n throw new Error(\n 'selector() can only be called during component render execution. ' +\n 'Move selector() calls to the top level of your component function.'\n );\n }\n\n const hookIndex = claimHookIndex(instance, 'selector');\n const hook = getOrCreateSelectorHook(instance, hookIndex, source, equals);\n if (!hook._record) {\n throw new Error('selector() record binding was not established.');\n }\n\n if (!hook._record._hasValue) {\n // Initialize the shared source once so downstream selectors can subscribe,\n // but do not consume a pending dirty state on later renders.\n recomputeSelectorSourceRecord(hook._record, false);\n }\n return hook;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"selector.js","names":[],"sources":["../../src/runtime/selector.ts"],"sourcesContent":["import {\n claimHookIndex,\n getCurrentInstance,\n type ComponentInstance,\n} from './component';\nimport { globalScheduler } from './scheduler';\nimport {\n clearDerivedDependencySubscriptions,\n markReadableDerivedSubscribersDirty,\n markReadableUsage,\n markReactivePropsDirtySource,\n notifyReadableReaders,\n recordReadableRead,\n syncDerivedDependencySubscriptions,\n withDerivedReadTracking,\n type DerivedSubscriber,\n type ReadableSource,\n} from './readable';\nimport { getPerfMetricsStore, incrementPerfMetric } from './perf-metrics';\n\ntype PrimitiveKey =\n | string\n | number\n | boolean\n | symbol\n | bigint\n | null\n | undefined;\n\nexport interface Selector<T> {\n (candidate: T): boolean;\n}\n\ninterface SelectorCandidateSource<T> extends ReadableSource<boolean> {\n _candidate: T;\n}\n\ntype SelectorEquals<T> = {\n bivarianceHack(a: T, b: T): boolean;\n}['bivarianceHack'];\n\ninterface SelectorLane<T> {\n _record: SelectorSourceRecord<T>;\n _equals: SelectorEquals<T>;\n _bindingCount: number;\n _primitiveCandidates: Map<PrimitiveKey, SelectorCandidateSource<T>>;\n _objectCandidates: WeakMap<object, SelectorCandidateSource<T>>;\n _objectCandidateSources: Set<SelectorCandidateSource<T>>;\n _cleanup(): void;\n}\n\ninterface SelectorSourceRecord<T> extends DerivedSubscriber {\n _source: () => T;\n _value: T;\n _hasValue: boolean;\n _dirty: boolean;\n _scheduled: boolean;\n _evaluating: boolean;\n _sources: Set<ReadableSource<unknown>>;\n _pendingDependencySources?: Set<ReadableSource<unknown>>;\n _lanes: Map<SelectorEquals<T>, SelectorLane<T>>;\n _cleanup(): void;\n}\n\ninterface SelectorHook<T> extends Selector<T> {\n _owner: ComponentInstance;\n _hookIndex: number;\n _source: () => T;\n _equals: SelectorEquals<T>;\n _record: SelectorSourceRecord<T> | null;\n _lane: SelectorLane<T> | null;\n _cleanup(): void;\n}\n\nconst selectorCells = new WeakMap<\n ComponentInstance,\n Map<number, SelectorHook<unknown>>\n>();\nconst selectorRecords = new WeakMap<\n ReadableSource<unknown>,\n SelectorSourceRecord<unknown>\n>();\nconst dirtySelectorRecords = new Set<SelectorSourceRecord<unknown>>();\nlet hasPendingSelectorFlush = false;\n\nfunction getSelectorStore(\n instance: ComponentInstance\n): Map<number, SelectorHook<unknown>> {\n let store = selectorCells.get(instance);\n if (!store) {\n store = new Map();\n selectorCells.set(instance, store);\n }\n return store;\n}\n\nfunction scheduleSelectorFlush(): void {\n if (hasPendingSelectorFlush) {\n return;\n }\n\n hasPendingSelectorFlush = true;\n globalScheduler.enqueueInLane('derived', flushDirtySelectorRecords);\n}\n\nfunction markSelectorRecordDirty(record: SelectorSourceRecord<unknown>): void {\n record._dirty = true;\n if (record._scheduled) {\n return;\n }\n\n record._scheduled = true;\n dirtySelectorRecords.add(record);\n scheduleSelectorFlush();\n}\n\nfunction flushDirtySelectorRecords(): void {\n hasPendingSelectorFlush = false;\n\n if (dirtySelectorRecords.size === 0) {\n return;\n }\n\n const pending = Array.from(dirtySelectorRecords);\n dirtySelectorRecords.clear();\n\n for (const record of pending) {\n record._scheduled = false;\n if (!record._dirty) {\n continue;\n }\n recomputeSelectorSourceRecord(record, true);\n }\n}\n\nfunction isObjectCandidate(value: unknown): value is object {\n return (\n (typeof value === 'object' && value !== null) || typeof value === 'function'\n );\n}\n\nfunction isDefaultSelectorEquals<T>(equals: SelectorEquals<T>): boolean {\n return equals === Object.is;\n}\n\nfunction createCandidateSource<T>(candidate: T): SelectorCandidateSource<T> {\n const source = (() => false) as SelectorCandidateSource<T>;\n source._candidate = candidate;\n return source;\n}\n\nfunction getCandidateSource<T>(\n lane: SelectorLane<T>,\n candidate: T\n): SelectorCandidateSource<T> {\n if (isObjectCandidate(candidate)) {\n const cached = lane._objectCandidates.get(candidate);\n if (cached) {\n return cached;\n }\n\n const created = createCandidateSource(candidate);\n lane._objectCandidates.set(candidate, created);\n lane._objectCandidateSources.add(created);\n return created;\n }\n\n const key = candidate as PrimitiveKey;\n const cached = lane._primitiveCandidates.get(key);\n if (cached) {\n return cached;\n }\n\n const created = createCandidateSource(candidate);\n lane._primitiveCandidates.set(key, created);\n return created;\n}\n\nfunction peekCandidateSource<T>(\n lane: SelectorLane<T>,\n candidate: T\n): SelectorCandidateSource<T> | undefined {\n if (isObjectCandidate(candidate)) {\n return lane._objectCandidates.get(candidate);\n }\n return lane._primitiveCandidates.get(candidate as PrimitiveKey);\n}\n\nfunction getSelectorSourceRecord<T>(source: () => T): SelectorSourceRecord<T> {\n const cached = selectorRecords.get(source);\n if (cached) {\n return cached as SelectorSourceRecord<T>;\n }\n\n const record = createSelectorSourceRecord(source);\n selectorRecords.set(source, record as SelectorSourceRecord<unknown>);\n return record;\n}\n\nfunction createSelectorSourceRecord<T>(\n source: () => T\n): SelectorSourceRecord<T> {\n let record!: SelectorSourceRecord<T>;\n\n record = {\n _source: source,\n _value: undefined as T,\n _hasValue: false,\n _dirty: true,\n _scheduled: false,\n _evaluating: false,\n _sources: new Set(),\n _lanes: new Map(),\n _markDirty: () => {\n markSelectorRecordDirty(record as SelectorSourceRecord<unknown>);\n },\n _cleanup: () => {\n record._scheduled = false;\n record._dirty = false;\n record._hasValue = false;\n record._evaluating = false;\n record._pendingDependencySources = undefined;\n clearDerivedDependencySubscriptions(record, record._sources);\n record._lanes.clear();\n selectorRecords.delete(source as ReadableSource<unknown>);\n },\n };\n\n return record;\n}\n\nfunction getSelectorLane<T>(\n record: SelectorSourceRecord<T>,\n equals: SelectorEquals<T>\n): SelectorLane<T> {\n const cached = record._lanes.get(equals);\n if (cached) {\n return cached;\n }\n\n const lane = createSelectorLane(record, equals);\n record._lanes.set(equals, lane);\n return lane;\n}\n\nfunction createSelectorLane<T>(\n record: SelectorSourceRecord<T>,\n equals: SelectorEquals<T>\n): SelectorLane<T> {\n const lane: SelectorLane<T> = {\n _record: record,\n _equals: equals,\n _bindingCount: 0,\n _primitiveCandidates: new Map(),\n _objectCandidates: new WeakMap(),\n _objectCandidateSources: new Set(),\n _cleanup: () => {\n for (const sourceRef of lane._primitiveCandidates.values()) {\n sourceRef._readers?.clear();\n sourceRef._derivedSubscribers?.clear();\n }\n for (const sourceRef of lane._objectCandidateSources) {\n sourceRef._readers?.clear();\n sourceRef._derivedSubscribers?.clear();\n }\n lane._primitiveCandidates.clear();\n lane._objectCandidates = new WeakMap();\n lane._objectCandidateSources.clear();\n },\n };\n\n return lane;\n}\n\nfunction notifySelectorSource(source: SelectorCandidateSource<unknown>): void {\n incrementPerfMetric('selectorInvalidations');\n markReadableDerivedSubscribersDirty(source);\n markReactivePropsDirtySource(source);\n notifyReadableReaders(source);\n}\n\nfunction notifyAllSelectorSources<T>(lane: SelectorLane<T>): void {\n for (const source of lane._primitiveCandidates.values()) {\n notifySelectorSource(source);\n }\n for (const source of lane._objectCandidateSources) {\n notifySelectorSource(source);\n }\n}\n\nfunction notifySelectorLaneValueChange<T>(\n lane: SelectorLane<T>,\n prevValue: T,\n nextValue: T\n): void {\n if (!lane._bindingCount) {\n return;\n }\n\n if (!isDefaultSelectorEquals(lane._equals)) {\n if (lane._equals(prevValue, nextValue)) {\n return;\n }\n notifyAllSelectorSources(lane);\n return;\n }\n\n if (!Object.is(prevValue, nextValue)) {\n // Only notify candidate sources that were actually materialized by a read.\n // Using getCandidateSource here would create-on-miss and leak a candidate\n // source for every distinct value the source ever passed through, even\n // those no component reads. Peek instead so the candidate cache stays\n // bounded to values that components actually compare against.\n const prevSource = peekCandidateSource(lane, prevValue);\n if (prevSource) {\n notifySelectorSource(prevSource);\n }\n const nextSource = peekCandidateSource(lane, nextValue);\n if (nextSource) {\n notifySelectorSource(nextSource);\n }\n }\n}\n\nfunction recomputeSelectorSourceRecord<T>(\n record: SelectorSourceRecord<T>,\n notifyDownstream: boolean\n): T {\n if (!record._dirty && record._hasValue) {\n return record._value;\n }\n\n if (record._evaluating) {\n throw new Error('selector() cannot read itself recursively');\n }\n\n record._evaluating = true;\n record._dirty = false;\n record._pendingDependencySources = new Set();\n\n const prevSources = record._sources;\n const hadValue = record._hasValue;\n const prevValue = record._value;\n let nextValue: T;\n\n try {\n nextValue = withDerivedReadTracking(record, record._source);\n } catch (error) {\n record._dirty = true;\n record._pendingDependencySources = undefined;\n throw error;\n } finally {\n record._evaluating = false;\n }\n\n const nextSources = record._pendingDependencySources ?? new Set();\n record._pendingDependencySources = undefined;\n syncDerivedDependencySubscriptions(record, prevSources, nextSources);\n record._sources = nextSources;\n\n const valueChanged = !record._hasValue || !Object.is(prevValue, nextValue);\n record._hasValue = true;\n record._value = nextValue;\n\n if (valueChanged && notifyDownstream && hadValue) {\n for (const lane of Array.from(record._lanes.values())) {\n notifySelectorLaneValueChange(lane, prevValue, nextValue);\n }\n }\n\n return record._value;\n}\n\nfunction attachSelectorHookBinding<T>(\n hook: SelectorHook<T>,\n source: () => T,\n equals: SelectorEquals<T>\n): void {\n const record = getSelectorSourceRecord(source);\n const lane = getSelectorLane(record, equals);\n\n hook._source = source;\n hook._equals = equals;\n hook._record = record;\n hook._lane = lane;\n lane._bindingCount += 1;\n}\n\nfunction detachSelectorHookBinding<T>(hook: SelectorHook<T>): void {\n const record = hook._record;\n const lane = hook._lane;\n\n hook._record = null;\n hook._lane = null;\n\n if (!record || !lane) {\n return;\n }\n\n if (lane._bindingCount > 0) {\n lane._bindingCount -= 1;\n }\n\n if (lane._bindingCount > 0) {\n return;\n }\n\n lane._cleanup();\n record._lanes.delete(lane._equals);\n\n if (record._lanes.size === 0) {\n record._cleanup();\n }\n}\n\nfunction ensureSelectorHookBinding<T>(\n hook: SelectorHook<T>\n): SelectorSourceRecord<T> {\n const record = hook._record;\n const lane = hook._lane;\n\n if (\n record &&\n lane &&\n lane._bindingCount > 0 &&\n record._lanes.get(hook._equals) === lane &&\n record._source === hook._source\n ) {\n return record;\n }\n\n if (hook._record || hook._lane) {\n detachSelectorHookBinding(hook);\n }\n\n attachSelectorHookBinding(hook, hook._source, hook._equals);\n return hook._record!;\n}\n\nfunction createSelectorHook<T>(\n instance: ComponentInstance,\n hookIndex: number,\n source: () => T,\n equals: SelectorEquals<T>\n): SelectorHook<T> {\n const hook = function selectorPredicate(candidate: T): boolean {\n const selectorHook = hook as SelectorHook<T>;\n const record = ensureSelectorHookBinding(selectorHook);\n const lane = selectorHook._lane;\n if (!lane) {\n throw new Error('selector() binding could not be established.');\n }\n\n const sourceRef = getCandidateSource(lane, candidate);\n recordReadableRead(sourceRef);\n\n const perfMetricsStore = getPerfMetricsStore();\n if (perfMetricsStore) {\n perfMetricsStore.selectorCandidateReads += 1;\n }\n\n const current =\n record._dirty || !record._hasValue\n ? recomputeSelectorSourceRecord(record, record._scheduled)\n : record._value;\n\n return lane._equals(current, candidate);\n } as SelectorHook<T>;\n\n hook._owner = instance;\n hook._hookIndex = hookIndex;\n hook._source = source;\n hook._equals = equals;\n hook._record = null;\n hook._lane = null;\n hook._cleanup = () => {\n detachSelectorHookBinding(hook);\n };\n\n attachSelectorHookBinding(hook, source, equals);\n\n (instance.cleanupFns ??= []).push(() => {\n hook._cleanup();\n selectorCells.get(instance)?.delete(hookIndex);\n });\n\n return hook;\n}\n\nfunction getOrCreateSelectorHook<T>(\n instance: ComponentInstance,\n hookIndex: number,\n source: () => T,\n equals: SelectorEquals<T>\n): SelectorHook<T> {\n const store = getSelectorStore(instance);\n const existing = store.get(hookIndex) as SelectorHook<T> | undefined;\n if (existing) {\n if (existing._source !== source || existing._equals !== equals) {\n detachSelectorHookBinding(existing);\n attachSelectorHookBinding(existing, source, equals);\n } else {\n ensureSelectorHookBinding(existing);\n }\n return existing;\n }\n\n const created = createSelectorHook(instance, hookIndex, source, equals);\n store.set(hookIndex, created as unknown as SelectorHook<unknown>);\n return created;\n}\n\nexport function selector<T>(\n source: () => T,\n equals: SelectorEquals<T> = Object.is\n): Selector<T> {\n markReadableUsage(source);\n\n const instance = getCurrentInstance();\n if (!instance) {\n throw new Error(\n 'selector() can only be called during component render execution. ' +\n 'Move selector() calls to the top level of your component function.'\n );\n }\n\n const hookIndex = claimHookIndex(instance, 'selector');\n const hook = getOrCreateSelectorHook(instance, hookIndex, source, equals);\n if (!hook._record) {\n throw new Error('selector() record binding was not established.');\n }\n\n if (!hook._record._hasValue) {\n // Initialize the shared source once so downstream selectors can subscribe,\n // but do not consume a pending dirty state on later renders.\n recomputeSelectorSourceRecord(hook._record, false);\n }\n return hook;\n}\n"],"mappings":";;;;;AA0EA,MAAM,gCAAgB,IAAI,QAGxB;AACF,MAAM,kCAAkB,IAAI,QAG1B;AACF,MAAM,uCAAuB,IAAI,IAAmC;AACpE,IAAI,0BAA0B;AAE9B,SAAS,iBACP,UACoC;CACpC,IAAI,QAAQ,cAAc,IAAI,QAAQ;CACtC,IAAI,CAAC,OAAO;EACV,wBAAQ,IAAI,IAAI;EAChB,cAAc,IAAI,UAAU,KAAK;CACnC;CACA,OAAO;AACT;AAEA,SAAS,wBAA8B;CACrC,IAAI,yBACF;CAGF,0BAA0B;CAC1B,gBAAgB,cAAc,WAAW,yBAAyB;AACpE;AAEA,SAAS,wBAAwB,QAA6C;CAC5E,OAAO,SAAS;CAChB,IAAI,OAAO,YACT;CAGF,OAAO,aAAa;CACpB,qBAAqB,IAAI,MAAM;CAC/B,sBAAsB;AACxB;AAEA,SAAS,4BAAkC;CACzC,0BAA0B;CAE1B,IAAI,qBAAqB,SAAS,GAChC;CAGF,MAAM,UAAU,MAAM,KAAK,oBAAoB;CAC/C,qBAAqB,MAAM;CAE3B,KAAK,MAAM,UAAU,SAAS;EAC5B,OAAO,aAAa;EACpB,IAAI,CAAC,OAAO,QACV;EAEF,8BAA8B,QAAQ,IAAI;CAC5C;AACF;AAEA,SAAS,kBAAkB,OAAiC;CAC1D,OACG,OAAO,UAAU,YAAY,UAAU,QAAS,OAAO,UAAU;AAEtE;AAEA,SAAS,wBAA2B,QAAoC;CACtE,OAAO,WAAW,OAAO;AAC3B;AAEA,SAAS,sBAAyB,WAA0C;CAC1E,MAAM,gBAAgB;CACtB,OAAO,aAAa;CACpB,OAAO;AACT;AAEA,SAAS,mBACP,MACA,WAC4B;CAC5B,IAAI,kBAAkB,SAAS,GAAG;EAChC,MAAM,SAAS,KAAK,kBAAkB,IAAI,SAAS;EACnD,IAAI,QACF,OAAO;EAGT,MAAM,UAAU,sBAAsB,SAAS;EAC/C,KAAK,kBAAkB,IAAI,WAAW,OAAO;EAC7C,KAAK,wBAAwB,IAAI,OAAO;EACxC,OAAO;CACT;CAEA,MAAM,MAAM;CACZ,MAAM,SAAS,KAAK,qBAAqB,IAAI,GAAG;CAChD,IAAI,QACF,OAAO;CAGT,MAAM,UAAU,sBAAsB,SAAS;CAC/C,KAAK,qBAAqB,IAAI,KAAK,OAAO;CAC1C,OAAO;AACT;AAEA,SAAS,oBACP,MACA,WACwC;CACxC,IAAI,kBAAkB,SAAS,GAC7B,OAAO,KAAK,kBAAkB,IAAI,SAAS;CAE7C,OAAO,KAAK,qBAAqB,IAAI,SAAyB;AAChE;AAEA,SAAS,wBAA2B,QAA0C;CAC5E,MAAM,SAAS,gBAAgB,IAAI,MAAM;CACzC,IAAI,QACF,OAAO;CAGT,MAAM,SAAS,2BAA2B,MAAM;CAChD,gBAAgB,IAAI,QAAQ,MAAuC;CACnE,OAAO;AACT;AAEA,SAAS,2BACP,QACyB;CACzB,IAAI;CAEJ,SAAS;EACP,SAAS;EACT,QAAQ;EACR,WAAW;EACX,QAAQ;EACR,YAAY;EACZ,aAAa;EACb,0BAAU,IAAI,IAAI;EAClB,wBAAQ,IAAI,IAAI;EAChB,kBAAkB;GAChB,wBAAwB,MAAuC;EACjE;EACA,gBAAgB;GACd,OAAO,aAAa;GACpB,OAAO,SAAS;GAChB,OAAO,YAAY;GACnB,OAAO,cAAc;GACrB,OAAO,4BAA4B;GACnC,oCAAoC,QAAQ,OAAO,QAAQ;GAC3D,OAAO,OAAO,MAAM;GACpB,gBAAgB,OAAO,MAAiC;EAC1D;CACF;CAEA,OAAO;AACT;AAEA,SAAS,gBACP,QACA,QACiB;CACjB,MAAM,SAAS,OAAO,OAAO,IAAI,MAAM;CACvC,IAAI,QACF,OAAO;CAGT,MAAM,OAAO,mBAAmB,QAAQ,MAAM;CAC9C,OAAO,OAAO,IAAI,QAAQ,IAAI;CAC9B,OAAO;AACT;AAEA,SAAS,mBACP,QACA,QACiB;CACjB,MAAM,OAAwB;EAC5B,SAAS;EACT,SAAS;EACT,eAAe;EACf,sCAAsB,IAAI,IAAI;EAC9B,mCAAmB,IAAI,QAAQ;EAC/B,yCAAyB,IAAI,IAAI;EACjC,gBAAgB;GACd,KAAK,MAAM,aAAa,KAAK,qBAAqB,OAAO,GAAG;IAC1D,UAAU,UAAU,MAAM;IAC1B,UAAU,qBAAqB,MAAM;GACvC;GACA,KAAK,MAAM,aAAa,KAAK,yBAAyB;IACpD,UAAU,UAAU,MAAM;IAC1B,UAAU,qBAAqB,MAAM;GACvC;GACA,KAAK,qBAAqB,MAAM;GAChC,KAAK,oCAAoB,IAAI,QAAQ;GACrC,KAAK,wBAAwB,MAAM;EACrC;CACF;CAEA,OAAO;AACT;AAEA,SAAS,qBAAqB,QAAgD;CAC5E,oBAAoB,uBAAuB;CAC3C,oCAAoC,MAAM;CAC1C,6BAA6B,MAAM;CACnC,sBAAsB,MAAM;AAC9B;AAEA,SAAS,yBAA4B,MAA6B;CAChE,KAAK,MAAM,UAAU,KAAK,qBAAqB,OAAO,GACpD,qBAAqB,MAAM;CAE7B,KAAK,MAAM,UAAU,KAAK,yBACxB,qBAAqB,MAAM;AAE/B;AAEA,SAAS,8BACP,MACA,WACA,WACM;CACN,IAAI,CAAC,KAAK,eACR;CAGF,IAAI,CAAC,wBAAwB,KAAK,OAAO,GAAG;EAC1C,IAAI,KAAK,QAAQ,WAAW,SAAS,GACnC;EAEF,yBAAyB,IAAI;EAC7B;CACF;CAEA,IAAI,CAAC,OAAO,GAAG,WAAW,SAAS,GAAG;EAMpC,MAAM,aAAa,oBAAoB,MAAM,SAAS;EACtD,IAAI,YACF,qBAAqB,UAAU;EAEjC,MAAM,aAAa,oBAAoB,MAAM,SAAS;EACtD,IAAI,YACF,qBAAqB,UAAU;CAEnC;AACF;AAEA,SAAS,8BACP,QACA,kBACG;CACH,IAAI,CAAC,OAAO,UAAU,OAAO,WAC3B,OAAO,OAAO;CAGhB,IAAI,OAAO,aACT,MAAM,IAAI,MAAM,2CAA2C;CAG7D,OAAO,cAAc;CACrB,OAAO,SAAS;CAChB,OAAO,4CAA4B,IAAI,IAAI;CAE3C,MAAM,cAAc,OAAO;CAC3B,MAAM,WAAW,OAAO;CACxB,MAAM,YAAY,OAAO;CACzB,IAAI;CAEJ,IAAI;EACF,YAAY,wBAAwB,QAAQ,OAAO,OAAO;CAC5D,SAAS,OAAO;EACd,OAAO,SAAS;EAChB,OAAO,4BAA4B;EACnC,MAAM;CACR,UAAU;EACR,OAAO,cAAc;CACvB;CAEA,MAAM,cAAc,OAAO,6CAA6B,IAAI,IAAI;CAChE,OAAO,4BAA4B;CACnC,mCAAmC,QAAQ,aAAa,WAAW;CACnE,OAAO,WAAW;CAElB,MAAM,eAAe,CAAC,OAAO,aAAa,CAAC,OAAO,GAAG,WAAW,SAAS;CACzE,OAAO,YAAY;CACnB,OAAO,SAAS;CAEhB,IAAI,gBAAgB,oBAAoB,UACtC,KAAK,MAAM,QAAQ,MAAM,KAAK,OAAO,OAAO,OAAO,CAAC,GAClD,8BAA8B,MAAM,WAAW,SAAS;CAI5D,OAAO,OAAO;AAChB;AAEA,SAAS,0BACP,MACA,QACA,QACM;CACN,MAAM,SAAS,wBAAwB,MAAM;CAC7C,MAAM,OAAO,gBAAgB,QAAQ,MAAM;CAE3C,KAAK,UAAU;CACf,KAAK,UAAU;CACf,KAAK,UAAU;CACf,KAAK,QAAQ;CACb,KAAK,iBAAiB;AACxB;AAEA,SAAS,0BAA6B,MAA6B;CACjE,MAAM,SAAS,KAAK;CACpB,MAAM,OAAO,KAAK;CAElB,KAAK,UAAU;CACf,KAAK,QAAQ;CAEb,IAAI,CAAC,UAAU,CAAC,MACd;CAGF,IAAI,KAAK,gBAAgB,GACvB,KAAK,iBAAiB;CAGxB,IAAI,KAAK,gBAAgB,GACvB;CAGF,KAAK,SAAS;CACd,OAAO,OAAO,OAAO,KAAK,OAAO;CAEjC,IAAI,OAAO,OAAO,SAAS,GACzB,OAAO,SAAS;AAEpB;AAEA,SAAS,0BACP,MACyB;CACzB,MAAM,SAAS,KAAK;CACpB,MAAM,OAAO,KAAK;CAElB,IACE,UACA,QACA,KAAK,gBAAgB,KACrB,OAAO,OAAO,IAAI,KAAK,OAAO,MAAM,QACpC,OAAO,YAAY,KAAK,SAExB,OAAO;CAGT,IAAI,KAAK,WAAW,KAAK,OACvB,0BAA0B,IAAI;CAGhC,0BAA0B,MAAM,KAAK,SAAS,KAAK,OAAO;CAC1D,OAAO,KAAK;AACd;AAEA,SAAS,mBACP,UACA,WACA,QACA,QACiB;CACjB,MAAM,OAAO,SAAS,kBAAkB,WAAuB;EAC7D,MAAM,eAAe;EACrB,MAAM,SAAS,0BAA0B,YAAY;EACrD,MAAM,OAAO,aAAa;EAC1B,IAAI,CAAC,MACH,MAAM,IAAI,MAAM,8CAA8C;EAIhE,mBADkB,mBAAmB,MAAM,SACxB,CAAS;EAE5B,MAAM,mBAAmB,oBAAoB;EAC7C,IAAI,kBACF,iBAAiB,0BAA0B;EAG7C,MAAM,UACJ,OAAO,UAAU,CAAC,OAAO,YACrB,8BAA8B,QAAQ,OAAO,UAAU,IACvD,OAAO;EAEb,OAAO,KAAK,QAAQ,SAAS,SAAS;CACxC;CAEA,KAAK,SAAS;CACd,KAAK,aAAa;CAClB,KAAK,UAAU;CACf,KAAK,UAAU;CACf,KAAK,UAAU;CACf,KAAK,QAAQ;CACb,KAAK,iBAAiB;EACpB,0BAA0B,IAAI;CAChC;CAEA,0BAA0B,MAAM,QAAQ,MAAM;CAE9C,CAAC,SAAS,eAAe,CAAC,EAAA,CAAG,WAAW;EACtC,KAAK,SAAS;EACd,cAAc,IAAI,QAAQ,CAAC,EAAE,OAAO,SAAS;CAC/C,CAAC;CAED,OAAO;AACT;AAEA,SAAS,wBACP,UACA,WACA,QACA,QACiB;CACjB,MAAM,QAAQ,iBAAiB,QAAQ;CACvC,MAAM,WAAW,MAAM,IAAI,SAAS;CACpC,IAAI,UAAU;EACZ,IAAI,SAAS,YAAY,UAAU,SAAS,YAAY,QAAQ;GAC9D,0BAA0B,QAAQ;GAClC,0BAA0B,UAAU,QAAQ,MAAM;EACpD,OACE,0BAA0B,QAAQ;EAEpC,OAAO;CACT;CAEA,MAAM,UAAU,mBAAmB,UAAU,WAAW,QAAQ,MAAM;CACtE,MAAM,IAAI,WAAW,OAA2C;CAChE,OAAO;AACT;AAEA,SAAgB,SACd,QACA,SAA4B,OAAO,IACtB;CACb,kBAAkB,MAAM;CAExB,MAAM,WAAW,mBAAmB;CACpC,IAAI,CAAC,UACH,MAAM,IAAI,MACR,qIAEF;CAIF,MAAM,OAAO,wBAAwB,UADnB,eAAe,UAAU,UACI,GAAW,QAAQ,MAAM;CACxE,IAAI,CAAC,KAAK,SACR,MAAM,IAAI,MAAM,gDAAgD;CAGlE,IAAI,CAAC,KAAK,QAAQ,WAGhB,8BAA8B,KAAK,SAAS,KAAK;CAEnD,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"batch-render.js","names":[],"sources":["../../src/ssg/batch-render.ts"],"sourcesContent":["/**\n * Batch rendering of multiple routes for SSG\n */\n\nimport { renderToString } from '../ssr';\nimport { renderDocument, type DocumentRenderer } from '../common/ssr';\nimport type { RouteConfig, RouteRenderResult } from './types';\nimport type { RouteHandler } from '../common/router';\nimport type { ComponentFunction } from '../common/component';\nimport type { SSRData } from '../common/ssr';\nimport { resolveSsgRouteData } from './resolve-ssg-data';\nimport { getOutputFilePath, interpolateRoutePath } from './route-utils';\n\ninterface BatchRenderOptions {\n seed?: number;\n dataMap?: Record<string, SSRData>;\n concurrency?: number;\n document?: DocumentRenderer;\n}\n\n/**\n * Render multiple routes in parallel with error handling\n */\nexport async function batchRenderRoutes(\n routes: RouteConfig[],\n options: BatchRenderOptions = {}\n): Promise<RouteRenderResult[]> {\n const { seed = 12345, dataMap = {}, concurrency = 1, document } = options;\n\n const workerCount = Math.max(1, Math.min(concurrency, routes.length || 1));\n const results: RouteRenderResult[] = [];\n results.length = routes.length;\n let nextIndex = 0;\n\n const renderOne = async (route: RouteConfig): Promise<RouteRenderResult> => {\n const startTime = performance.now();\n const url = interpolateRoutePath(route.path, route.params);\n const requestUrl = new URL(url, 'http://localhost');\n const resolvedData = resolveSsgRouteData(dataMap, route.path, url);\n const baseData = resolvedData.hasData ? resolvedData.data : undefined;\n const resourceCount =\n resolvedData.hasData && baseData ? Object.keys(baseData).length : 0;\n\n const mergedHandler: RouteHandler = route.handler\n ? route.handler\n : (params, ctx?: unknown) => {\n const component = route.component as ComponentFunction;\n return component(\n { ...route.props, ...params },\n ctx as { signal: AbortSignal; ssr?: unknown }\n );\n };\n\n try {\n const routeEntry = {\n path: route.path,\n handler: mergedHandler,\n namespace: route.namespace,\n };\n\n const html = renderToString({\n url,\n routes: [routeEntry],\n seed,\n data: baseData,\n document:\n document === undefined\n ? undefined\n : ({ appHtml }) =>\n renderDocument(\n document,\n {\n appHtml,\n context: {\n mode: 'ssg',\n url,\n pathname: requestUrl.pathname,\n search: requestUrl.search,\n hash: requestUrl.hash,\n params: route.params ?? {},\n data: baseData,\n seed,\n route: {\n path: route.path,\n namespace: route.namespace,\n },\n },\n },\n 'createStaticGen()'\n ),\n });\n\n const duration = performance.now() - startTime;\n return {\n path: url,\n filePath: getOutputFilePath(url),\n html,\n fileSize: Buffer.byteLength(html, 'utf8'),\n renderDuration: Math.round(duration),\n resourceCount,\n status: 'success',\n reason: 'full',\n written: false,\n };\n } catch (error) {\n const duration = performance.now() - startTime;\n return {\n path: url,\n filePath: getOutputFilePath(url),\n html: '',\n fileSize: 0,\n renderDuration: Math.round(duration),\n resourceCount,\n status: 'error',\n reason: 'full',\n written: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n };\n\n const worker = async () => {\n while (true) {\n const current = nextIndex;\n nextIndex += 1;\n if (current >= routes.length) return;\n results[current] = await renderOne(routes[current]);\n }\n };\n\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n}\n"],"mappings":";;;;;;;;;;;AAuBA,eAAsB,kBACpB,QACA,UAA8B,CAAC,GACD;CAC9B,MAAM,EAAE,OAAO,OAAO,UAAU,CAAC,GAAG,cAAc,GAAG,aAAa;CAElE,MAAM,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,aAAa,OAAO,UAAU,CAAC,CAAC;CACzE,MAAM,UAA+B,CAAC;CACtC,QAAQ,SAAS,OAAO;CACxB,IAAI,YAAY;CAEhB,MAAM,YAAY,OAAO,UAAmD;EAC1E,MAAM,YAAY,YAAY,IAAI;EAClC,MAAM,MAAM,qBAAqB,MAAM,MAAM,MAAM,MAAM;EACzD,MAAM,aAAa,IAAI,IAAI,KAAK,kBAAkB;EAClD,MAAM,eAAe,oBAAoB,SAAS,MAAM,MAAM,GAAG;EACjE,MAAM,WAAW,aAAa,UAAU,aAAa,OAAO;EAC5D,MAAM,gBACJ,aAAa,WAAW,WAAW,OAAO,KAAK,QAAQ,EAAE,SAAS;EAEpE,MAAM,gBAA8B,MAAM,UACtC,MAAM,WACL,QAAQ,QAAkB;GACzB,MAAM,YAAY,MAAM;GACxB,OAAO,UACL;IAAE,GAAG,MAAM;IAAO,GAAG;GAAO,GAC5B,GACF;EACF;EAEJ,IAAI;GAOF,MAAM,OAAO,eAAe;IAC1B;IACA,QAAQ,CAAC;KAPT,MAAM,MAAM;KACZ,SAAS;KACT,WAAW,MAAM;IAKR,CAAU;IACnB;IACA,MAAM;IACN,UACE,aAAa,SACT,UACC,EAAE,cACD,eACE,UACA;KACE;KACA,SAAS;MACP,MAAM;MACN;MACA,UAAU,WAAW;MACrB,QAAQ,WAAW;MACnB,MAAM,WAAW;MACjB,QAAQ,MAAM,UAAU,CAAC;MACzB,MAAM;MACN;MACA,OAAO;OACL,MAAM,MAAM;OACZ,WAAW,MAAM;MACnB;KACF;IACF,GACA,mBACF;GACV,CAAC;GAED,MAAM,WAAW,YAAY,IAAI,IAAI;GACrC,OAAO;IACL,MAAM;IACN,UAAU,kBAAkB,GAAG;IAC/B;IACA,UAAU,OAAO,WAAW,MAAM,MAAM;IACxC,gBAAgB,KAAK,MAAM,QAAQ;IACnC;IACA,QAAQ;IACR,QAAQ;IACR,SAAS;GACX;EACF,SAAS,OAAO;GACd,MAAM,WAAW,YAAY,IAAI,IAAI;GACrC,OAAO;IACL,MAAM;IACN,UAAU,kBAAkB,GAAG;IAC/B,MAAM;IACN,UAAU;IACV,gBAAgB,KAAK,MAAM,QAAQ;IACnC;IACA,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D;EACF;CACF;CAEA,MAAM,SAAS,YAAY;EACzB,OAAO,MAAM;GACX,MAAM,UAAU;GAChB,aAAa;GACb,IAAI,WAAW,OAAO,QAAQ;GAC9B,QAAQ,WAAW,MAAM,UAAU,OAAO,QAAQ;EACpD;CACF;CAEA,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,YAAY,SAAS,OAAO,CAAC,CAAC;CACrE,OAAO;AACT"}
1
+ {"version":3,"file":"batch-render.js","names":[],"sources":["../../src/ssg/batch-render.ts"],"sourcesContent":["/**\n * Batch rendering of multiple routes for SSG\n */\n\nimport { renderToString } from '../ssr';\nimport { renderDocument, type DocumentRenderer } from '../common/ssr';\nimport type { RouteConfig, RouteRenderResult } from './types';\nimport type { RouteHandler } from '../common/router';\nimport type { ComponentFunction } from '../common/component';\nimport type { SSRData } from '../common/ssr';\nimport { resolveSsgRouteData } from './resolve-ssg-data';\nimport { getOutputFilePath, interpolateRoutePath } from './route-utils';\n\ninterface BatchRenderOptions {\n seed?: number;\n dataMap?: Record<string, SSRData>;\n concurrency?: number;\n document?: DocumentRenderer;\n}\n\n/**\n * Render multiple routes in parallel with error handling\n */\nexport async function batchRenderRoutes(\n routes: RouteConfig[],\n options: BatchRenderOptions = {}\n): Promise<RouteRenderResult[]> {\n const { seed = 12345, dataMap = {}, concurrency = 1, document } = options;\n\n const workerCount = Math.max(1, Math.min(concurrency, routes.length || 1));\n const results: RouteRenderResult[] = [];\n results.length = routes.length;\n let nextIndex = 0;\n\n const renderOne = async (route: RouteConfig): Promise<RouteRenderResult> => {\n const startTime = performance.now();\n const url = interpolateRoutePath(route.path, route.params);\n const requestUrl = new URL(url, 'http://localhost');\n const resolvedData = resolveSsgRouteData(dataMap, route.path, url);\n const baseData = resolvedData.hasData ? resolvedData.data : undefined;\n const resourceCount =\n resolvedData.hasData && baseData ? Object.keys(baseData).length : 0;\n\n const mergedHandler: RouteHandler = route.handler\n ? route.handler\n : (params, ctx?: unknown) => {\n const component = route.component as ComponentFunction;\n return component(\n { ...route.props, ...params },\n ctx as { signal: AbortSignal; ssr?: unknown }\n );\n };\n\n try {\n const routeEntry = {\n path: route.path,\n handler: mergedHandler,\n namespace: route.namespace,\n };\n\n const html = renderToString({\n url,\n routes: [routeEntry],\n seed,\n data: baseData,\n document:\n document === undefined\n ? undefined\n : ({ appHtml }) =>\n renderDocument(\n document,\n {\n appHtml,\n context: {\n mode: 'ssg',\n url,\n pathname: requestUrl.pathname,\n search: requestUrl.search,\n hash: requestUrl.hash,\n params: route.params ?? {},\n data: baseData,\n seed,\n route: {\n path: route.path,\n namespace: route.namespace,\n },\n },\n },\n 'createStaticGen()'\n ),\n });\n\n const duration = performance.now() - startTime;\n return {\n path: url,\n filePath: getOutputFilePath(url),\n html,\n fileSize: Buffer.byteLength(html, 'utf8'),\n renderDuration: Math.round(duration),\n resourceCount,\n status: 'success',\n reason: 'full',\n written: false,\n };\n } catch (error) {\n const duration = performance.now() - startTime;\n return {\n path: url,\n filePath: getOutputFilePath(url),\n html: '',\n fileSize: 0,\n renderDuration: Math.round(duration),\n resourceCount,\n status: 'error',\n reason: 'full',\n written: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n };\n\n const worker = async () => {\n while (true) {\n const current = nextIndex;\n nextIndex += 1;\n if (current >= routes.length) return;\n results[current] = await renderOne(routes[current]);\n }\n };\n\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n}\n"],"mappings":";;;;;;;;;;;AAuBA,eAAsB,kBACpB,QACA,UAA8B,CAAC,GACD;CAC9B,MAAM,EAAE,OAAO,OAAO,UAAU,CAAC,GAAG,cAAc,GAAG,aAAa;CAElE,MAAM,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,aAAa,OAAO,UAAU,CAAC,CAAC;CACzE,MAAM,UAA+B,CAAC;CACtC,QAAQ,SAAS,OAAO;CACxB,IAAI,YAAY;CAEhB,MAAM,YAAY,OAAO,UAAmD;EAC1E,MAAM,YAAY,YAAY,IAAI;EAClC,MAAM,MAAM,qBAAqB,MAAM,MAAM,MAAM,MAAM;EACzD,MAAM,aAAa,IAAI,IAAI,KAAK,kBAAkB;EAClD,MAAM,eAAe,oBAAoB,SAAS,MAAM,MAAM,GAAG;EACjE,MAAM,WAAW,aAAa,UAAU,aAAa,OAAO;EAC5D,MAAM,gBACJ,aAAa,WAAW,WAAW,OAAO,KAAK,QAAQ,CAAC,CAAC,SAAS;EAEpE,MAAM,gBAA8B,MAAM,UACtC,MAAM,WACL,QAAQ,QAAkB;GACzB,MAAM,YAAY,MAAM;GACxB,OAAO,UACL;IAAE,GAAG,MAAM;IAAO,GAAG;GAAO,GAC5B,GACF;EACF;EAEJ,IAAI;GAOF,MAAM,OAAO,eAAe;IAC1B;IACA,QAAQ,CAAC;KAPT,MAAM,MAAM;KACZ,SAAS;KACT,WAAW,MAAM;IAKR,CAAU;IACnB;IACA,MAAM;IACN,UACE,aAAa,SACT,UACC,EAAE,cACD,eACE,UACA;KACE;KACA,SAAS;MACP,MAAM;MACN;MACA,UAAU,WAAW;MACrB,QAAQ,WAAW;MACnB,MAAM,WAAW;MACjB,QAAQ,MAAM,UAAU,CAAC;MACzB,MAAM;MACN;MACA,OAAO;OACL,MAAM,MAAM;OACZ,WAAW,MAAM;MACnB;KACF;IACF,GACA,mBACF;GACV,CAAC;GAED,MAAM,WAAW,YAAY,IAAI,IAAI;GACrC,OAAO;IACL,MAAM;IACN,UAAU,kBAAkB,GAAG;IAC/B;IACA,UAAU,OAAO,WAAW,MAAM,MAAM;IACxC,gBAAgB,KAAK,MAAM,QAAQ;IACnC;IACA,QAAQ;IACR,QAAQ;IACR,SAAS;GACX;EACF,SAAS,OAAO;GACd,MAAM,WAAW,YAAY,IAAI,IAAI;GACrC,OAAO;IACL,MAAM;IACN,UAAU,kBAAkB,GAAG;IAC/B,MAAM;IACN,UAAU;IACV,gBAAgB,KAAK,MAAM,QAAQ;IACnC;IACA,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D;EACF;CACF;CAEA,MAAM,SAAS,YAAY;EACzB,OAAO,MAAM;GACX,MAAM,UAAU;GAChB,aAAa;GACb,IAAI,WAAW,OAAO,QAAQ;GAC9B,QAAQ,WAAW,MAAM,UAAU,OAAO,QAAQ;EACpD;CACF;CAEA,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,YAAY,SAAS,OAAO,CAAC,CAAC;CACrE,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-static-gen.js","names":[],"sources":["../../src/ssg/create-static-gen.ts"],"sourcesContent":["/**\n * Static Site Generation API\n *\n * Main orchestrator for batch rendering, file I/O, and metadata generation\n */\n\nimport type {\n RouteConfig,\n RouteRenderReason,\n RouteRenderResult,\n SSGGenerateOptions,\n SSGMode,\n SSGOptions,\n SSGResult,\n} from './types';\nimport {\n expandRoutes,\n resolveSsgRouteData,\n resolveSsgData,\n validateRoutes,\n} from './resolve-ssg-data';\nimport { batchRenderRoutes } from './batch-render';\nimport { writeStaticFiles } from './write-static-files';\nimport {\n generateSSGResult,\n resultToMetadata,\n writeMetadata,\n} from './generate-metadata';\nimport {\n getExistingOutputFileSize,\n outputFileExists,\n readIncrementalManifest,\n type IncrementalManifest,\n type IncrementalManifestRouteEntry,\n writeIncrementalManifest,\n hashHtml,\n SSG_MANIFEST_SCHEMA_VERSION,\n} from './incremental-manifest';\nimport {\n resolveRouteDescriptor,\n type ResolvedRouteDescriptor,\n} from './route-utils';\nimport { addPerfDuration, incrementPerfMetric } from '../runtime/perf-metrics';\n\ntype AnyRouteConfig = RouteConfig<string>;\n\ntype StrictRouteConfig<TRoute extends AnyRouteConfig> = Omit<\n TRoute,\n 'params' | 'entries'\n> & {\n params?: RouteConfig<TRoute['path']>['params'];\n entries?: RouteConfig<TRoute['path']>['entries'];\n};\n\ntype StrictRouteConfigs<TRoutes extends readonly AnyRouteConfig[]> = {\n [TIndex in keyof TRoutes]: TRoutes[TIndex] extends AnyRouteConfig\n ? StrictRouteConfig<TRoutes[TIndex]>\n : never;\n};\n\ninterface SelectedRoute {\n descriptor: ResolvedRouteDescriptor;\n reason: RouteRenderReason;\n previous: IncrementalManifestRouteEntry | null;\n}\n\nfunction getRuntimeOnlyDiagnostic(\n descriptor: ResolvedRouteDescriptor\n): string | null {\n const { route } = descriptor;\n\n if (route.auth === true) {\n return `Skipped prerender for \"${descriptor.path}\": authenticated routes are runtime-only by default.`;\n }\n\n if (route.role) {\n return `Skipped prerender for \"${descriptor.path}\": role-gated routes are runtime-only by default.`;\n }\n\n if (route.permission) {\n return `Skipped prerender for \"${descriptor.path}\": permission-gated routes are runtime-only by default.`;\n }\n\n if (route.policies && route.policies.length > 0) {\n return `Skipped prerender for \"${descriptor.path}\": routes with custom policies are runtime-only by default.`;\n }\n\n return null;\n}\n\nfunction resolveParallelism(requested: number | 'auto' | undefined): number {\n if (requested !== 'auto') {\n return Math.max(1, requested ?? 1);\n }\n\n const maybeNavigator = globalThis as typeof globalThis & {\n navigator?: { hardwareConcurrency?: number };\n process?: { env?: Record<string, string | undefined> };\n };\n\n const envWorkers = Number(\n maybeNavigator.process?.env?.ASKR_SSG_WORKERS ??\n maybeNavigator.process?.env?.NUMBER_OF_PROCESSORS ??\n maybeNavigator.process?.env?.UV_THREADPOOL_SIZE\n );\n if (Number.isFinite(envWorkers) && envWorkers > 0) {\n return Math.max(1, Math.trunc(envWorkers));\n }\n\n if (typeof maybeNavigator.navigator?.hardwareConcurrency === 'number') {\n return Math.max(1, maybeNavigator.navigator.hardwareConcurrency);\n }\n\n return 1;\n}\n\n/**\n * Create a Static Site Generator\n *\n * Usage:\n * ```ts\n * const ssg = createStaticGen({\n * routes: [\n * { path: '/', component: HomePage },\n * { path: '/about', component: AboutPage },\n * ],\n * outputDir: './dist',\n * dataOverrides: {\n * '/api/posts': { posts: [...] }\n * }\n * });\n *\n * const result = await ssg.generate();\n * console.log(`Generated ${result.successful}/${result.totalRoutes} routes`);\n * ```\n */\nexport function createStaticGen<\n const TRoutes extends readonly AnyRouteConfig[],\n>(\n options: Omit<SSGOptions<TRoutes>, 'routes'> & {\n routes: TRoutes & StrictRouteConfigs<TRoutes>;\n }\n) {\n let result: SSGResult | null = null;\n const seed = options.seed ?? 12345;\n const resolvedParallelism = resolveParallelism(options.parallelism);\n const resolvedConcurrency = Math.max(\n 1,\n options.concurrency ?? resolvedParallelism\n );\n\n if (!Array.isArray(options.routes) || options.routes.length === 0) {\n throw new Error('routes array is required');\n }\n\n if (!options.outputDir || options.outputDir.trim().length === 0) {\n throw new Error('outputDir is required');\n }\n\n return {\n /**\n * Generate static HTML for all routes\n * Writes to outputDir with metadata.json\n * Returns detailed results\n */\n async generate(\n generateOptions: SSGGenerateOptions = {}\n ): Promise<SSGResult> {\n const routes = await expandRoutes(options.routes as RouteConfig[]);\n validateRoutes(routes);\n\n const changedKeys = dedupeStrings(generateOptions.changedKeys);\n const changedRoutes = dedupeStrings(generateOptions.changedRoutes);\n const requestedMode = generateOptions.mode ?? 'full';\n const previousManifest =\n !generateOptions.forceFull && requestedMode === 'incremental'\n ? readIncrementalManifest(options.outputDir, seed)\n : null;\n const effectiveMode: SSGMode =\n generateOptions.forceFull ||\n requestedMode !== 'incremental' ||\n previousManifest === null\n ? 'full'\n : 'incremental';\n\n // Resolve data\n const dataMap = resolveSsgData(routes, {\n dataOverrides: options.dataOverrides,\n });\n\n const descriptors = routes.map(resolveRouteDescriptor);\n const routeResultsById = new Map<string, RouteRenderResult>();\n const eligibleDescriptors: ResolvedRouteDescriptor[] = [];\n\n for (const descriptor of descriptors) {\n const diagnostic = getRuntimeOnlyDiagnostic(descriptor);\n if (diagnostic) {\n routeResultsById.set(descriptor.routeId, {\n path: descriptor.path,\n filePath: descriptor.filePath,\n html: '',\n fileSize: 0,\n renderDuration: 0,\n resourceCount: 0,\n status: 'skipped',\n reason: 'runtime-only',\n written: false,\n error: diagnostic,\n });\n continue;\n }\n\n eligibleDescriptors.push(descriptor);\n }\n\n const previousEntries = new Map(\n (previousManifest?.routes ?? []).map((entry) => [entry.routeId, entry])\n );\n const currentRouteIds = new Set(\n eligibleDescriptors.map((descriptor) => descriptor.routeId)\n );\n\n const selected: SelectedRoute[] = [];\n const routesToRender: SelectedRoute[] = [];\n for (let index = 0; index < eligibleDescriptors.length; index += 1) {\n const descriptor = eligibleDescriptors[index];\n const entry = selectRouteForGeneration(\n descriptor,\n previousEntries.get(descriptor.routeId) ?? null,\n effectiveMode,\n changedKeys,\n changedRoutes\n );\n selected.push(entry);\n if (entry.reason !== 'unchanged') {\n routesToRender.push(entry);\n }\n }\n const renderStartTime = performance.now();\n incrementPerfMetric('ssgWorkerCount', resolvedParallelism);\n const renderedResults =\n routesToRender.length > 0\n ? await batchRenderRoutes(getRoutesToRender(routesToRender), {\n seed,\n dataMap,\n concurrency: resolvedConcurrency,\n document: options.document,\n })\n : [];\n addPerfDuration('ssgRenderTimeMs', performance.now() - renderStartTime);\n const renderedByRouteId = new Map<string, RouteRenderResult>();\n for (let index = 0; index < renderedResults.length; index += 1) {\n renderedByRouteId.set(\n routesToRender[index].descriptor.routeId,\n renderedResults[index]\n );\n }\n\n let cacheHits = 0;\n const nextManifestRoutes: IncrementalManifestRouteEntry[] = [];\n\n for (const entry of selected) {\n const { descriptor, previous, reason } = entry;\n const resolvedData = resolveSsgRouteData(\n dataMap,\n descriptor.route.path,\n descriptor.path\n );\n const baseData = resolvedData.hasData ? resolvedData.data : undefined;\n const resourceCount =\n resolvedData.hasData && baseData ? Object.keys(baseData).length : 0;\n\n if (reason === 'unchanged') {\n routeResultsById.set(descriptor.routeId, {\n path: descriptor.path,\n filePath: descriptor.filePath,\n html: '',\n fileSize:\n previous?.htmlHash !== null && previous !== null\n ? getExistingOutputFileSize(\n options.outputDir,\n descriptor.filePath\n )\n : 0,\n renderDuration: 0,\n resourceCount,\n status: 'skipped',\n reason: 'unchanged',\n written: false,\n });\n\n if (previous) {\n nextManifestRoutes.push({\n ...previous,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n });\n }\n continue;\n }\n\n const rendered = renderedByRouteId.get(descriptor.routeId);\n if (!rendered) {\n throw new Error(\n `Missing rendered result for route \"${descriptor.path}\"`\n );\n }\n\n const nextResult: RouteRenderResult = {\n ...rendered,\n path: descriptor.path,\n filePath: descriptor.filePath,\n resourceCount,\n reason,\n written: false,\n };\n\n if (nextResult.status === 'success') {\n const htmlDigest = hashHtml(nextResult.html);\n const shouldWrite =\n effectiveMode === 'full' ||\n !previous ||\n previous.htmlHash !== htmlDigest ||\n !outputFileExists(options.outputDir, descriptor.filePath);\n\n nextResult.written = shouldWrite;\n if (!shouldWrite) {\n cacheHits += 1;\n }\n\n nextManifestRoutes.push({\n routeId: descriptor.routeId,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n htmlHash: htmlDigest,\n lastStatus: 'success',\n });\n } else {\n nextManifestRoutes.push({\n routeId: descriptor.routeId,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n htmlHash: previous?.htmlHash ?? null,\n lastStatus: 'error',\n });\n }\n\n routeResultsById.set(descriptor.routeId, nextResult);\n }\n\n const removedResults = collectRemovedRouteResults(\n previousManifest,\n currentRouteIds\n );\n const routeResults: RouteRenderResult[] = [];\n for (let index = 0; index < descriptors.length; index += 1) {\n const routeResult = routeResultsById.get(descriptors[index].routeId);\n if (routeResult) {\n routeResults.push(routeResult);\n }\n }\n routeResults.push(...removedResults);\n\n // Write HTML files to disk and remove stale output\n const writeStartTime = performance.now();\n await writeStaticFiles(routeResults, options.outputDir, {\n concurrency: resolvedConcurrency,\n });\n addPerfDuration('ssgWriteTimeMs', performance.now() - writeStartTime);\n\n // Generate result object\n result = generateSSGResult(routeResults, {\n mode: effectiveMode,\n cacheHits,\n invalidatedKeys: changedKeys,\n invalidatedRoutes: changedRoutes,\n });\n\n // Write metadata\n const metadata = resultToMetadata(result);\n await writeMetadata(metadata, options.outputDir);\n\n await writeIncrementalManifest(\n {\n schemaVersion: SSG_MANIFEST_SCHEMA_VERSION,\n seed,\n mode: effectiveMode,\n routes: nextManifestRoutes,\n },\n options.outputDir\n );\n\n return result;\n },\n\n /**\n * Get effective config in a serializable form for diagnostics.\n */\n getConfig() {\n return {\n routeCount: options.routes.length,\n outputDir: options.outputDir,\n seed,\n concurrency: resolvedConcurrency,\n parallelism: resolvedParallelism,\n hasDataOverrides: !!options.dataOverrides,\n };\n },\n\n /**\n * Get the last generation result\n * Returns null if generate() hasn't been called\n */\n getResult(): SSGResult | null {\n return result;\n },\n };\n}\n\nfunction dedupeStrings(values?: string[]): string[] {\n return values ? Array.from(new Set(values)) : [];\n}\n\nfunction getRoutesToRender(selected: SelectedRoute[]): RouteConfig[] {\n const routes: RouteConfig[] = [];\n for (let index = 0; index < selected.length; index += 1) {\n routes.push(selected[index].descriptor.route);\n }\n return routes;\n}\n\nfunction selectRouteForGeneration(\n descriptor: ResolvedRouteDescriptor,\n previous: IncrementalManifestRouteEntry | null,\n mode: SSGMode,\n changedKeys: string[],\n changedRoutes: string[]\n): SelectedRoute {\n if (mode === 'full') {\n return { descriptor, reason: 'full', previous };\n }\n\n if (!previous) {\n return { descriptor, reason: 'new-route', previous };\n }\n\n if (previous.lastStatus === 'error' || previous.htmlHash === null) {\n return { descriptor, reason: 'new-route', previous };\n }\n\n if (descriptor.invalidationKeys.length === 0) {\n return { descriptor, reason: 'no-keys', previous };\n }\n\n if (changedRoutes.includes(descriptor.path)) {\n return { descriptor, reason: 'changed-route', previous };\n }\n\n if (descriptor.invalidationKeys.some((key) => changedKeys.includes(key))) {\n return { descriptor, reason: 'changed-key', previous };\n }\n\n return { descriptor, reason: 'unchanged', previous };\n}\n\nfunction collectRemovedRouteResults(\n manifest: IncrementalManifest | null,\n currentRouteIds: Set<string>\n): RouteRenderResult[] {\n if (!manifest) {\n return [];\n }\n\n const results: RouteRenderResult[] = [];\n for (let index = 0; index < manifest.routes.length; index += 1) {\n const entry = manifest.routes[index];\n if (currentRouteIds.has(entry.routeId)) {\n continue;\n }\n results.push({\n path: entry.path,\n filePath: entry.filePath,\n html: '',\n fileSize: 0,\n renderDuration: 0,\n resourceCount: 0,\n status: 'removed' as const,\n reason: 'deleted' as const,\n written: false,\n });\n }\n\n return results;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-static-gen.js","names":[],"sources":["../../src/ssg/create-static-gen.ts"],"sourcesContent":["/**\n * Static Site Generation API\n *\n * Main orchestrator for batch rendering, file I/O, and metadata generation\n */\n\nimport type {\n RouteConfig,\n RouteRenderReason,\n RouteRenderResult,\n SSGGenerateOptions,\n SSGMode,\n SSGOptions,\n SSGResult,\n} from './types';\nimport {\n expandRoutes,\n resolveSsgRouteData,\n resolveSsgData,\n validateRoutes,\n} from './resolve-ssg-data';\nimport { batchRenderRoutes } from './batch-render';\nimport { writeStaticFiles } from './write-static-files';\nimport {\n generateSSGResult,\n resultToMetadata,\n writeMetadata,\n} from './generate-metadata';\nimport {\n getExistingOutputFileSize,\n outputFileExists,\n readIncrementalManifest,\n type IncrementalManifest,\n type IncrementalManifestRouteEntry,\n writeIncrementalManifest,\n hashHtml,\n SSG_MANIFEST_SCHEMA_VERSION,\n} from './incremental-manifest';\nimport {\n resolveRouteDescriptor,\n type ResolvedRouteDescriptor,\n} from './route-utils';\nimport { addPerfDuration, incrementPerfMetric } from '../runtime/perf-metrics';\n\ntype AnyRouteConfig = RouteConfig<string>;\n\ntype StrictRouteConfig<TRoute extends AnyRouteConfig> = Omit<\n TRoute,\n 'params' | 'entries'\n> & {\n params?: RouteConfig<TRoute['path']>['params'];\n entries?: RouteConfig<TRoute['path']>['entries'];\n};\n\ntype StrictRouteConfigs<TRoutes extends readonly AnyRouteConfig[]> = {\n [TIndex in keyof TRoutes]: TRoutes[TIndex] extends AnyRouteConfig\n ? StrictRouteConfig<TRoutes[TIndex]>\n : never;\n};\n\ninterface SelectedRoute {\n descriptor: ResolvedRouteDescriptor;\n reason: RouteRenderReason;\n previous: IncrementalManifestRouteEntry | null;\n}\n\nfunction getRuntimeOnlyDiagnostic(\n descriptor: ResolvedRouteDescriptor\n): string | null {\n const { route } = descriptor;\n\n if (route.auth === true) {\n return `Skipped prerender for \"${descriptor.path}\": authenticated routes are runtime-only by default.`;\n }\n\n if (route.role) {\n return `Skipped prerender for \"${descriptor.path}\": role-gated routes are runtime-only by default.`;\n }\n\n if (route.permission) {\n return `Skipped prerender for \"${descriptor.path}\": permission-gated routes are runtime-only by default.`;\n }\n\n if (route.policies && route.policies.length > 0) {\n return `Skipped prerender for \"${descriptor.path}\": routes with custom policies are runtime-only by default.`;\n }\n\n return null;\n}\n\nfunction resolveParallelism(requested: number | 'auto' | undefined): number {\n if (requested !== 'auto') {\n return Math.max(1, requested ?? 1);\n }\n\n const maybeNavigator = globalThis as typeof globalThis & {\n navigator?: { hardwareConcurrency?: number };\n process?: { env?: Record<string, string | undefined> };\n };\n\n const envWorkers = Number(\n maybeNavigator.process?.env?.ASKR_SSG_WORKERS ??\n maybeNavigator.process?.env?.NUMBER_OF_PROCESSORS ??\n maybeNavigator.process?.env?.UV_THREADPOOL_SIZE\n );\n if (Number.isFinite(envWorkers) && envWorkers > 0) {\n return Math.max(1, Math.trunc(envWorkers));\n }\n\n if (typeof maybeNavigator.navigator?.hardwareConcurrency === 'number') {\n return Math.max(1, maybeNavigator.navigator.hardwareConcurrency);\n }\n\n return 1;\n}\n\n/**\n * Create a Static Site Generator\n *\n * Usage:\n * ```ts\n * const ssg = createStaticGen({\n * routes: [\n * { path: '/', component: HomePage },\n * { path: '/about', component: AboutPage },\n * ],\n * outputDir: './dist',\n * dataOverrides: {\n * '/api/posts': { posts: [...] }\n * }\n * });\n *\n * const result = await ssg.generate();\n * console.log(`Generated ${result.successful}/${result.totalRoutes} routes`);\n * ```\n */\nexport function createStaticGen<\n const TRoutes extends readonly AnyRouteConfig[],\n>(\n options: Omit<SSGOptions<TRoutes>, 'routes'> & {\n routes: TRoutes & StrictRouteConfigs<TRoutes>;\n }\n) {\n let result: SSGResult | null = null;\n const seed = options.seed ?? 12345;\n const resolvedParallelism = resolveParallelism(options.parallelism);\n const resolvedConcurrency = Math.max(\n 1,\n options.concurrency ?? resolvedParallelism\n );\n\n if (!Array.isArray(options.routes) || options.routes.length === 0) {\n throw new Error('routes array is required');\n }\n\n if (!options.outputDir || options.outputDir.trim().length === 0) {\n throw new Error('outputDir is required');\n }\n\n return {\n /**\n * Generate static HTML for all routes\n * Writes to outputDir with metadata.json\n * Returns detailed results\n */\n async generate(\n generateOptions: SSGGenerateOptions = {}\n ): Promise<SSGResult> {\n const routes = await expandRoutes(options.routes as RouteConfig[]);\n validateRoutes(routes);\n\n const changedKeys = dedupeStrings(generateOptions.changedKeys);\n const changedRoutes = dedupeStrings(generateOptions.changedRoutes);\n const requestedMode = generateOptions.mode ?? 'full';\n const previousManifest =\n !generateOptions.forceFull && requestedMode === 'incremental'\n ? readIncrementalManifest(options.outputDir, seed)\n : null;\n const effectiveMode: SSGMode =\n generateOptions.forceFull ||\n requestedMode !== 'incremental' ||\n previousManifest === null\n ? 'full'\n : 'incremental';\n\n // Resolve data\n const dataMap = resolveSsgData(routes, {\n dataOverrides: options.dataOverrides,\n });\n\n const descriptors = routes.map(resolveRouteDescriptor);\n const routeResultsById = new Map<string, RouteRenderResult>();\n const eligibleDescriptors: ResolvedRouteDescriptor[] = [];\n\n for (const descriptor of descriptors) {\n const diagnostic = getRuntimeOnlyDiagnostic(descriptor);\n if (diagnostic) {\n routeResultsById.set(descriptor.routeId, {\n path: descriptor.path,\n filePath: descriptor.filePath,\n html: '',\n fileSize: 0,\n renderDuration: 0,\n resourceCount: 0,\n status: 'skipped',\n reason: 'runtime-only',\n written: false,\n error: diagnostic,\n });\n continue;\n }\n\n eligibleDescriptors.push(descriptor);\n }\n\n const previousEntries = new Map(\n (previousManifest?.routes ?? []).map((entry) => [entry.routeId, entry])\n );\n const currentRouteIds = new Set(\n eligibleDescriptors.map((descriptor) => descriptor.routeId)\n );\n\n const selected: SelectedRoute[] = [];\n const routesToRender: SelectedRoute[] = [];\n for (let index = 0; index < eligibleDescriptors.length; index += 1) {\n const descriptor = eligibleDescriptors[index];\n const entry = selectRouteForGeneration(\n descriptor,\n previousEntries.get(descriptor.routeId) ?? null,\n effectiveMode,\n changedKeys,\n changedRoutes\n );\n selected.push(entry);\n if (entry.reason !== 'unchanged') {\n routesToRender.push(entry);\n }\n }\n const renderStartTime = performance.now();\n incrementPerfMetric('ssgWorkerCount', resolvedParallelism);\n const renderedResults =\n routesToRender.length > 0\n ? await batchRenderRoutes(getRoutesToRender(routesToRender), {\n seed,\n dataMap,\n concurrency: resolvedConcurrency,\n document: options.document,\n })\n : [];\n addPerfDuration('ssgRenderTimeMs', performance.now() - renderStartTime);\n const renderedByRouteId = new Map<string, RouteRenderResult>();\n for (let index = 0; index < renderedResults.length; index += 1) {\n renderedByRouteId.set(\n routesToRender[index].descriptor.routeId,\n renderedResults[index]\n );\n }\n\n let cacheHits = 0;\n const nextManifestRoutes: IncrementalManifestRouteEntry[] = [];\n\n for (const entry of selected) {\n const { descriptor, previous, reason } = entry;\n const resolvedData = resolveSsgRouteData(\n dataMap,\n descriptor.route.path,\n descriptor.path\n );\n const baseData = resolvedData.hasData ? resolvedData.data : undefined;\n const resourceCount =\n resolvedData.hasData && baseData ? Object.keys(baseData).length : 0;\n\n if (reason === 'unchanged') {\n routeResultsById.set(descriptor.routeId, {\n path: descriptor.path,\n filePath: descriptor.filePath,\n html: '',\n fileSize:\n previous?.htmlHash !== null && previous !== null\n ? getExistingOutputFileSize(\n options.outputDir,\n descriptor.filePath\n )\n : 0,\n renderDuration: 0,\n resourceCount,\n status: 'skipped',\n reason: 'unchanged',\n written: false,\n });\n\n if (previous) {\n nextManifestRoutes.push({\n ...previous,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n });\n }\n continue;\n }\n\n const rendered = renderedByRouteId.get(descriptor.routeId);\n if (!rendered) {\n throw new Error(\n `Missing rendered result for route \"${descriptor.path}\"`\n );\n }\n\n const nextResult: RouteRenderResult = {\n ...rendered,\n path: descriptor.path,\n filePath: descriptor.filePath,\n resourceCount,\n reason,\n written: false,\n };\n\n if (nextResult.status === 'success') {\n const htmlDigest = hashHtml(nextResult.html);\n const shouldWrite =\n effectiveMode === 'full' ||\n !previous ||\n previous.htmlHash !== htmlDigest ||\n !outputFileExists(options.outputDir, descriptor.filePath);\n\n nextResult.written = shouldWrite;\n if (!shouldWrite) {\n cacheHits += 1;\n }\n\n nextManifestRoutes.push({\n routeId: descriptor.routeId,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n htmlHash: htmlDigest,\n lastStatus: 'success',\n });\n } else {\n nextManifestRoutes.push({\n routeId: descriptor.routeId,\n path: descriptor.path,\n filePath: descriptor.filePath,\n invalidationKeys: descriptor.invalidationKeys.slice(),\n htmlHash: previous?.htmlHash ?? null,\n lastStatus: 'error',\n });\n }\n\n routeResultsById.set(descriptor.routeId, nextResult);\n }\n\n const removedResults = collectRemovedRouteResults(\n previousManifest,\n currentRouteIds\n );\n const routeResults: RouteRenderResult[] = [];\n for (let index = 0; index < descriptors.length; index += 1) {\n const routeResult = routeResultsById.get(descriptors[index].routeId);\n if (routeResult) {\n routeResults.push(routeResult);\n }\n }\n routeResults.push(...removedResults);\n\n // Write HTML files to disk and remove stale output\n const writeStartTime = performance.now();\n await writeStaticFiles(routeResults, options.outputDir, {\n concurrency: resolvedConcurrency,\n });\n addPerfDuration('ssgWriteTimeMs', performance.now() - writeStartTime);\n\n // Generate result object\n result = generateSSGResult(routeResults, {\n mode: effectiveMode,\n cacheHits,\n invalidatedKeys: changedKeys,\n invalidatedRoutes: changedRoutes,\n });\n\n // Write metadata\n const metadata = resultToMetadata(result);\n await writeMetadata(metadata, options.outputDir);\n\n await writeIncrementalManifest(\n {\n schemaVersion: SSG_MANIFEST_SCHEMA_VERSION,\n seed,\n mode: effectiveMode,\n routes: nextManifestRoutes,\n },\n options.outputDir\n );\n\n return result;\n },\n\n /**\n * Get effective config in a serializable form for diagnostics.\n */\n getConfig() {\n return {\n routeCount: options.routes.length,\n outputDir: options.outputDir,\n seed,\n concurrency: resolvedConcurrency,\n parallelism: resolvedParallelism,\n hasDataOverrides: !!options.dataOverrides,\n };\n },\n\n /**\n * Get the last generation result\n * Returns null if generate() hasn't been called\n */\n getResult(): SSGResult | null {\n return result;\n },\n };\n}\n\nfunction dedupeStrings(values?: string[]): string[] {\n return values ? Array.from(new Set(values)) : [];\n}\n\nfunction getRoutesToRender(selected: SelectedRoute[]): RouteConfig[] {\n const routes: RouteConfig[] = [];\n for (let index = 0; index < selected.length; index += 1) {\n routes.push(selected[index].descriptor.route);\n }\n return routes;\n}\n\nfunction selectRouteForGeneration(\n descriptor: ResolvedRouteDescriptor,\n previous: IncrementalManifestRouteEntry | null,\n mode: SSGMode,\n changedKeys: string[],\n changedRoutes: string[]\n): SelectedRoute {\n if (mode === 'full') {\n return { descriptor, reason: 'full', previous };\n }\n\n if (!previous) {\n return { descriptor, reason: 'new-route', previous };\n }\n\n if (previous.lastStatus === 'error' || previous.htmlHash === null) {\n return { descriptor, reason: 'new-route', previous };\n }\n\n if (descriptor.invalidationKeys.length === 0) {\n return { descriptor, reason: 'no-keys', previous };\n }\n\n if (changedRoutes.includes(descriptor.path)) {\n return { descriptor, reason: 'changed-route', previous };\n }\n\n if (descriptor.invalidationKeys.some((key) => changedKeys.includes(key))) {\n return { descriptor, reason: 'changed-key', previous };\n }\n\n return { descriptor, reason: 'unchanged', previous };\n}\n\nfunction collectRemovedRouteResults(\n manifest: IncrementalManifest | null,\n currentRouteIds: Set<string>\n): RouteRenderResult[] {\n if (!manifest) {\n return [];\n }\n\n const results: RouteRenderResult[] = [];\n for (let index = 0; index < manifest.routes.length; index += 1) {\n const entry = manifest.routes[index];\n if (currentRouteIds.has(entry.routeId)) {\n continue;\n }\n results.push({\n path: entry.path,\n filePath: entry.filePath,\n html: '',\n fileSize: 0,\n renderDuration: 0,\n resourceCount: 0,\n status: 'removed' as const,\n reason: 'deleted' as const,\n written: false,\n });\n }\n\n return results;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"generate-metadata.js","names":[],"sources":["../../src/ssg/generate-metadata.ts"],"sourcesContent":["/**\n * Metadata generation for SSG\n *\n * Generates and serializes metadata about generated static files.\n * This module is Node-only and not intended for browser builds.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as pathModule from 'node:path';\nimport type {\n RouteRenderResult,\n SSGMetadata,\n SSGMode,\n SSGResult,\n} from './types';\n\ninterface GenerateResultOptions {\n mode?: SSGMode;\n cacheHits?: number;\n invalidatedKeys?: string[];\n invalidatedRoutes?: string[];\n}\n\n/**\n * Generate SSGResult from render results\n */\nexport function generateSSGResult(\n results: RouteRenderResult[],\n options: GenerateResultOptions = {}\n): SSGResult {\n let successful = 0;\n let failed = 0;\n let totalDuration = 0;\n let rebuilt = 0;\n let skipped = 0;\n let removed = 0;\n\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index];\n totalDuration += result.renderDuration;\n\n if (result.status === 'success') {\n successful += 1;\n rebuilt += 1;\n } else if (result.status === 'error') {\n failed += 1;\n rebuilt += 1;\n } else if (result.status === 'skipped') {\n skipped += 1;\n } else if (result.status === 'removed') {\n removed += 1;\n }\n }\n\n return {\n generatedAt: new Date().toISOString(),\n totalRoutes: results.length,\n successful,\n failed,\n totalDuration: Math.round(totalDuration),\n mode: options.mode ?? 'full',\n rebuilt,\n skipped,\n removed,\n cacheHits: options.cacheHits ?? 0,\n invalidatedKeys: options.invalidatedKeys?.slice() ?? [],\n invalidatedRoutes: options.invalidatedRoutes?.slice() ?? [],\n routes: results,\n };\n}\n\n/**\n * Convert SSGResult to metadata for JSON serialization\n */\nexport function resultToMetadata(result: SSGResult): SSGMetadata {\n const routes: SSGMetadata['routes'] = [];\n\n for (let index = 0; index < result.routes.length; index += 1) {\n const route = result.routes[index];\n routes.push({\n path: route.path,\n filePath: route.filePath,\n fileSize: route.fileSize,\n renderDuration: route.renderDuration,\n resourceCount: route.resourceCount,\n status: route.status,\n reason: route.reason,\n written: route.written,\n error: route.error,\n });\n }\n\n return {\n generatedAt: result.generatedAt,\n totalRoutes: result.totalRoutes,\n successful: result.successful,\n failed: result.failed,\n totalDuration: result.totalDuration,\n mode: result.mode,\n rebuilt: result.rebuilt,\n skipped: result.skipped,\n removed: result.removed,\n cacheHits: result.cacheHits,\n invalidatedKeys: result.invalidatedKeys.slice(),\n invalidatedRoutes: result.invalidatedRoutes.slice(),\n routes,\n };\n}\n\n/**\n * Write metadata.json to output directory\n */\nexport async function writeMetadata(\n metadata: Record<string, unknown>,\n outputDir: string\n): Promise<void> {\n const filePath = pathModule.join(outputDir, 'metadata.json');\n\n // Ensure directory exists\n await fs.mkdir(outputDir, { recursive: true });\n\n // Write metadata file with formatting\n await fs.writeFile(filePath, JSON.stringify(metadata, null, 2), 'utf8');\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,kBACd,SACA,UAAiC,CAAC,GACvB;CACX,IAAI,aAAa;CACjB,IAAI,SAAS;CACb,IAAI,gBAAgB;CACpB,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,UAAU;CAEd,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;EACtD,MAAM,SAAS,QAAQ;EACvB,iBAAiB,OAAO;EAExB,IAAI,OAAO,WAAW,WAAW;GAC/B,cAAc;GACd,WAAW;EACb,OAAO,IAAI,OAAO,WAAW,SAAS;GACpC,UAAU;GACV,WAAW;EACb,OAAO,IAAI,OAAO,WAAW,WAC3B,WAAW;OACN,IAAI,OAAO,WAAW,WAC3B,WAAW;CAEf;CAEA,OAAO;EACL,8BAAa,IAAI,KAAK,GAAE,YAAY;EACpC,aAAa,QAAQ;EACrB;EACA;EACA,eAAe,KAAK,MAAM,aAAa;EACvC,MAAM,QAAQ,QAAQ;EACtB;EACA;EACA;EACA,WAAW,QAAQ,aAAa;EAChC,iBAAiB,QAAQ,iBAAiB,MAAM,KAAK,CAAC;EACtD,mBAAmB,QAAQ,mBAAmB,MAAM,KAAK,CAAC;EAC1D,QAAQ;CACV;AACF;;;;AAKA,SAAgB,iBAAiB,QAAgC;CAC/D,MAAM,SAAgC,CAAC;CAEvC,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,OAAO,QAAQ,SAAS,GAAG;EAC5D,MAAM,QAAQ,OAAO,OAAO;EAC5B,OAAO,KAAK;GACV,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,UAAU,MAAM;GAChB,gBAAgB,MAAM;GACtB,eAAe,MAAM;GACrB,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,OAAO,MAAM;EACf,CAAC;CACH;CAEA,OAAO;EACL,aAAa,OAAO;EACpB,aAAa,OAAO;EACpB,YAAY,OAAO;EACnB,QAAQ,OAAO;EACf,eAAe,OAAO;EACtB,MAAM,OAAO;EACb,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB,WAAW,OAAO;EAClB,iBAAiB,OAAO,gBAAgB,MAAM;EAC9C,mBAAmB,OAAO,kBAAkB,MAAM;EAClD;CACF;AACF;;;;AAKA,eAAsB,cACpB,UACA,WACe;CACf,MAAM,WAAW,WAAW,KAAK,WAAW,eAAe;CAG3D,MAAM,GAAG,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAG7C,MAAM,GAAG,UAAU,UAAU,KAAK,UAAU,UAAU,MAAM,CAAC,GAAG,MAAM;AACxE"}
1
+ {"version":3,"file":"generate-metadata.js","names":[],"sources":["../../src/ssg/generate-metadata.ts"],"sourcesContent":["/**\n * Metadata generation for SSG\n *\n * Generates and serializes metadata about generated static files.\n * This module is Node-only and not intended for browser builds.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as pathModule from 'node:path';\nimport type {\n RouteRenderResult,\n SSGMetadata,\n SSGMode,\n SSGResult,\n} from './types';\n\ninterface GenerateResultOptions {\n mode?: SSGMode;\n cacheHits?: number;\n invalidatedKeys?: string[];\n invalidatedRoutes?: string[];\n}\n\n/**\n * Generate SSGResult from render results\n */\nexport function generateSSGResult(\n results: RouteRenderResult[],\n options: GenerateResultOptions = {}\n): SSGResult {\n let successful = 0;\n let failed = 0;\n let totalDuration = 0;\n let rebuilt = 0;\n let skipped = 0;\n let removed = 0;\n\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index];\n totalDuration += result.renderDuration;\n\n if (result.status === 'success') {\n successful += 1;\n rebuilt += 1;\n } else if (result.status === 'error') {\n failed += 1;\n rebuilt += 1;\n } else if (result.status === 'skipped') {\n skipped += 1;\n } else if (result.status === 'removed') {\n removed += 1;\n }\n }\n\n return {\n generatedAt: new Date().toISOString(),\n totalRoutes: results.length,\n successful,\n failed,\n totalDuration: Math.round(totalDuration),\n mode: options.mode ?? 'full',\n rebuilt,\n skipped,\n removed,\n cacheHits: options.cacheHits ?? 0,\n invalidatedKeys: options.invalidatedKeys?.slice() ?? [],\n invalidatedRoutes: options.invalidatedRoutes?.slice() ?? [],\n routes: results,\n };\n}\n\n/**\n * Convert SSGResult to metadata for JSON serialization\n */\nexport function resultToMetadata(result: SSGResult): SSGMetadata {\n const routes: SSGMetadata['routes'] = [];\n\n for (let index = 0; index < result.routes.length; index += 1) {\n const route = result.routes[index];\n routes.push({\n path: route.path,\n filePath: route.filePath,\n fileSize: route.fileSize,\n renderDuration: route.renderDuration,\n resourceCount: route.resourceCount,\n status: route.status,\n reason: route.reason,\n written: route.written,\n error: route.error,\n });\n }\n\n return {\n generatedAt: result.generatedAt,\n totalRoutes: result.totalRoutes,\n successful: result.successful,\n failed: result.failed,\n totalDuration: result.totalDuration,\n mode: result.mode,\n rebuilt: result.rebuilt,\n skipped: result.skipped,\n removed: result.removed,\n cacheHits: result.cacheHits,\n invalidatedKeys: result.invalidatedKeys.slice(),\n invalidatedRoutes: result.invalidatedRoutes.slice(),\n routes,\n };\n}\n\n/**\n * Write metadata.json to output directory\n */\nexport async function writeMetadata(\n metadata: Record<string, unknown>,\n outputDir: string\n): Promise<void> {\n const filePath = pathModule.join(outputDir, 'metadata.json');\n\n // Ensure directory exists\n await fs.mkdir(outputDir, { recursive: true });\n\n // Write metadata file with formatting\n await fs.writeFile(filePath, JSON.stringify(metadata, null, 2), 'utf8');\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,kBACd,SACA,UAAiC,CAAC,GACvB;CACX,IAAI,aAAa;CACjB,IAAI,SAAS;CACb,IAAI,gBAAgB;CACpB,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,UAAU;CAEd,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;EACtD,MAAM,SAAS,QAAQ;EACvB,iBAAiB,OAAO;EAExB,IAAI,OAAO,WAAW,WAAW;GAC/B,cAAc;GACd,WAAW;EACb,OAAO,IAAI,OAAO,WAAW,SAAS;GACpC,UAAU;GACV,WAAW;EACb,OAAO,IAAI,OAAO,WAAW,WAC3B,WAAW;OACN,IAAI,OAAO,WAAW,WAC3B,WAAW;CAEf;CAEA,OAAO;EACL,8BAAa,IAAI,KAAK,EAAC,CAAC,YAAY;EACpC,aAAa,QAAQ;EACrB;EACA;EACA,eAAe,KAAK,MAAM,aAAa;EACvC,MAAM,QAAQ,QAAQ;EACtB;EACA;EACA;EACA,WAAW,QAAQ,aAAa;EAChC,iBAAiB,QAAQ,iBAAiB,MAAM,KAAK,CAAC;EACtD,mBAAmB,QAAQ,mBAAmB,MAAM,KAAK,CAAC;EAC1D,QAAQ;CACV;AACF;;;;AAKA,SAAgB,iBAAiB,QAAgC;CAC/D,MAAM,SAAgC,CAAC;CAEvC,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,OAAO,QAAQ,SAAS,GAAG;EAC5D,MAAM,QAAQ,OAAO,OAAO;EAC5B,OAAO,KAAK;GACV,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,UAAU,MAAM;GAChB,gBAAgB,MAAM;GACtB,eAAe,MAAM;GACrB,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,OAAO,MAAM;EACf,CAAC;CACH;CAEA,OAAO;EACL,aAAa,OAAO;EACpB,aAAa,OAAO;EACpB,YAAY,OAAO;EACnB,QAAQ,OAAO;EACf,eAAe,OAAO;EACtB,MAAM,OAAO;EACb,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB,WAAW,OAAO;EAClB,iBAAiB,OAAO,gBAAgB,MAAM;EAC9C,mBAAmB,OAAO,kBAAkB,MAAM;EAClD;CACF;AACF;;;;AAKA,eAAsB,cACpB,UACA,WACe;CACf,MAAM,WAAW,WAAW,KAAK,WAAW,eAAe;CAG3D,MAAM,GAAG,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAG7C,MAAM,GAAG,UAAU,UAAU,KAAK,UAAU,UAAU,MAAM,CAAC,GAAG,MAAM;AACxE"}
@@ -1 +1 @@
1
- {"version":3,"file":"incremental-manifest.js","names":[],"sources":["../../src/ssg/incremental-manifest.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as fsPromises from 'node:fs/promises';\nimport * as pathModule from 'node:path';\nimport type { RouteRenderStatus, SSGMode } from './types';\n\nexport const SSG_MANIFEST_SCHEMA_VERSION = 1;\nconst MANIFEST_DIR = '.askr';\nconst MANIFEST_FILE = 'ssg-manifest.json';\n\nexport interface IncrementalManifestRouteEntry {\n routeId: string;\n path: string;\n filePath: string;\n invalidationKeys: string[];\n htmlHash: string | null;\n lastStatus: Extract<RouteRenderStatus, 'success' | 'error'>;\n}\n\nexport interface IncrementalManifest {\n schemaVersion: number;\n seed: number;\n mode: SSGMode;\n routes: IncrementalManifestRouteEntry[];\n}\n\nexport function hashHtml(html: string): string {\n // Deterministic 32-bit FNV-1a hash is sufficient for incremental change checks.\n let hash = 0x811c9dc5;\n for (let index = 0; index < html.length; index += 1) {\n hash ^= html.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n\nexport function getIncrementalManifestPath(outputDir: string): string {\n return pathModule.join(outputDir, MANIFEST_DIR, MANIFEST_FILE);\n}\n\nexport function readIncrementalManifest(\n outputDir: string,\n seed: number\n): IncrementalManifest | null {\n const manifestPath = getIncrementalManifestPath(outputDir);\n if (!fs.existsSync(manifestPath)) {\n return null;\n }\n\n try {\n const raw = fs.readFileSync(manifestPath, 'utf8');\n const parsed = JSON.parse(raw) as Partial<IncrementalManifest>;\n\n if (\n parsed.schemaVersion !== SSG_MANIFEST_SCHEMA_VERSION ||\n parsed.seed !== seed ||\n !Array.isArray(parsed.routes)\n ) {\n return null;\n }\n\n const routes = parsed.routes.filter(\n (entry): entry is IncrementalManifestRouteEntry =>\n !!entry &&\n typeof entry.routeId === 'string' &&\n typeof entry.path === 'string' &&\n typeof entry.filePath === 'string' &&\n Array.isArray(entry.invalidationKeys) &&\n (entry.htmlHash === null || typeof entry.htmlHash === 'string') &&\n (entry.lastStatus === 'success' || entry.lastStatus === 'error')\n );\n\n if (routes.length !== parsed.routes.length) {\n return null;\n }\n\n return {\n schemaVersion: SSG_MANIFEST_SCHEMA_VERSION,\n seed,\n mode: parsed.mode === 'incremental' ? 'incremental' : 'full',\n routes,\n };\n } catch {\n return null;\n }\n}\n\nexport async function writeIncrementalManifest(\n manifest: IncrementalManifest,\n outputDir: string\n): Promise<void> {\n const manifestPath = getIncrementalManifestPath(outputDir);\n await fsPromises.mkdir(pathModule.dirname(manifestPath), { recursive: true });\n await fsPromises.writeFile(\n manifestPath,\n JSON.stringify(manifest, null, 2),\n 'utf8'\n );\n}\n\nexport function getExistingOutputFileSize(\n outputDir: string,\n filePath: string\n): number {\n const fullPath = pathModule.join(outputDir, filePath);\n if (!fs.existsSync(fullPath)) {\n return 0;\n }\n\n try {\n return fs.statSync(fullPath).size;\n } catch {\n return 0;\n }\n}\n\nexport function outputFileExists(outputDir: string, filePath: string): boolean {\n return fs.existsSync(pathModule.join(outputDir, filePath));\n}\n"],"mappings":";;;AAMA,MAAM,eAAe;AACrB,MAAM,gBAAgB;AAkBtB,SAAgB,SAAS,MAAsB;CAE7C,IAAI,OAAO;CACX,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;EACnD,QAAQ,KAAK,WAAW,KAAK;EAC7B,OAAO,KAAK,KAAK,MAAM,QAAU;CACnC;CACA,QAAQ,SAAS,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAClD;AAEA,SAAgB,2BAA2B,WAA2B;CACpE,OAAO,WAAW,KAAK,WAAW,cAAc,aAAa;AAC/D;AAEA,SAAgB,wBACd,WACA,MAC4B;CAC5B,MAAM,eAAe,2BAA2B,SAAS;CACzD,IAAI,CAAC,GAAG,WAAW,YAAY,GAC7B,OAAO;CAGT,IAAI;EACF,MAAM,MAAM,GAAG,aAAa,cAAc,MAAM;EAChD,MAAM,SAAS,KAAK,MAAM,GAAG;EAE7B,IACE,OAAO,kBAAA,KACP,OAAO,SAAS,QAChB,CAAC,MAAM,QAAQ,OAAO,MAAM,GAE5B,OAAO;EAGT,MAAM,SAAS,OAAO,OAAO,QAC1B,UACC,CAAC,CAAC,SACF,OAAO,MAAM,YAAY,YACzB,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,aAAa,YAC1B,MAAM,QAAQ,MAAM,gBAAgB,MACnC,MAAM,aAAa,QAAQ,OAAO,MAAM,aAAa,cACrD,MAAM,eAAe,aAAa,MAAM,eAAe,QAC5D;EAEA,IAAI,OAAO,WAAW,OAAO,OAAO,QAClC,OAAO;EAGT,OAAO;GACL,eAAA;GACA;GACA,MAAM,OAAO,SAAS,gBAAgB,gBAAgB;GACtD;EACF;CACF,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAsB,yBACpB,UACA,WACe;CACf,MAAM,eAAe,2BAA2B,SAAS;CACzD,MAAM,WAAW,MAAM,WAAW,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;CAC5E,MAAM,WAAW,UACf,cACA,KAAK,UAAU,UAAU,MAAM,CAAC,GAChC,MACF;AACF;AAEA,SAAgB,0BACd,WACA,UACQ;CACR,MAAM,WAAW,WAAW,KAAK,WAAW,QAAQ;CACpD,IAAI,CAAC,GAAG,WAAW,QAAQ,GACzB,OAAO;CAGT,IAAI;EACF,OAAO,GAAG,SAAS,QAAQ,EAAE;CAC/B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,iBAAiB,WAAmB,UAA2B;CAC7E,OAAO,GAAG,WAAW,WAAW,KAAK,WAAW,QAAQ,CAAC;AAC3D"}
1
+ {"version":3,"file":"incremental-manifest.js","names":[],"sources":["../../src/ssg/incremental-manifest.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as fsPromises from 'node:fs/promises';\nimport * as pathModule from 'node:path';\nimport type { RouteRenderStatus, SSGMode } from './types';\n\nexport const SSG_MANIFEST_SCHEMA_VERSION = 1;\nconst MANIFEST_DIR = '.askr';\nconst MANIFEST_FILE = 'ssg-manifest.json';\n\nexport interface IncrementalManifestRouteEntry {\n routeId: string;\n path: string;\n filePath: string;\n invalidationKeys: string[];\n htmlHash: string | null;\n lastStatus: Extract<RouteRenderStatus, 'success' | 'error'>;\n}\n\nexport interface IncrementalManifest {\n schemaVersion: number;\n seed: number;\n mode: SSGMode;\n routes: IncrementalManifestRouteEntry[];\n}\n\nexport function hashHtml(html: string): string {\n // Deterministic 32-bit FNV-1a hash is sufficient for incremental change checks.\n let hash = 0x811c9dc5;\n for (let index = 0; index < html.length; index += 1) {\n hash ^= html.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n\nexport function getIncrementalManifestPath(outputDir: string): string {\n return pathModule.join(outputDir, MANIFEST_DIR, MANIFEST_FILE);\n}\n\nexport function readIncrementalManifest(\n outputDir: string,\n seed: number\n): IncrementalManifest | null {\n const manifestPath = getIncrementalManifestPath(outputDir);\n if (!fs.existsSync(manifestPath)) {\n return null;\n }\n\n try {\n const raw = fs.readFileSync(manifestPath, 'utf8');\n const parsed = JSON.parse(raw) as Partial<IncrementalManifest>;\n\n if (\n parsed.schemaVersion !== SSG_MANIFEST_SCHEMA_VERSION ||\n parsed.seed !== seed ||\n !Array.isArray(parsed.routes)\n ) {\n return null;\n }\n\n const routes = parsed.routes.filter(\n (entry): entry is IncrementalManifestRouteEntry =>\n !!entry &&\n typeof entry.routeId === 'string' &&\n typeof entry.path === 'string' &&\n typeof entry.filePath === 'string' &&\n Array.isArray(entry.invalidationKeys) &&\n (entry.htmlHash === null || typeof entry.htmlHash === 'string') &&\n (entry.lastStatus === 'success' || entry.lastStatus === 'error')\n );\n\n if (routes.length !== parsed.routes.length) {\n return null;\n }\n\n return {\n schemaVersion: SSG_MANIFEST_SCHEMA_VERSION,\n seed,\n mode: parsed.mode === 'incremental' ? 'incremental' : 'full',\n routes,\n };\n } catch {\n return null;\n }\n}\n\nexport async function writeIncrementalManifest(\n manifest: IncrementalManifest,\n outputDir: string\n): Promise<void> {\n const manifestPath = getIncrementalManifestPath(outputDir);\n await fsPromises.mkdir(pathModule.dirname(manifestPath), { recursive: true });\n await fsPromises.writeFile(\n manifestPath,\n JSON.stringify(manifest, null, 2),\n 'utf8'\n );\n}\n\nexport function getExistingOutputFileSize(\n outputDir: string,\n filePath: string\n): number {\n const fullPath = pathModule.join(outputDir, filePath);\n if (!fs.existsSync(fullPath)) {\n return 0;\n }\n\n try {\n return fs.statSync(fullPath).size;\n } catch {\n return 0;\n }\n}\n\nexport function outputFileExists(outputDir: string, filePath: string): boolean {\n return fs.existsSync(pathModule.join(outputDir, filePath));\n}\n"],"mappings":";;;AAMA,MAAM,eAAe;AACrB,MAAM,gBAAgB;AAkBtB,SAAgB,SAAS,MAAsB;CAE7C,IAAI,OAAO;CACX,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;EACnD,QAAQ,KAAK,WAAW,KAAK;EAC7B,OAAO,KAAK,KAAK,MAAM,QAAU;CACnC;CACA,QAAQ,SAAS,EAAA,CAAG,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;AAClD;AAEA,SAAgB,2BAA2B,WAA2B;CACpE,OAAO,WAAW,KAAK,WAAW,cAAc,aAAa;AAC/D;AAEA,SAAgB,wBACd,WACA,MAC4B;CAC5B,MAAM,eAAe,2BAA2B,SAAS;CACzD,IAAI,CAAC,GAAG,WAAW,YAAY,GAC7B,OAAO;CAGT,IAAI;EACF,MAAM,MAAM,GAAG,aAAa,cAAc,MAAM;EAChD,MAAM,SAAS,KAAK,MAAM,GAAG;EAE7B,IACE,OAAO,kBAAA,KACP,OAAO,SAAS,QAChB,CAAC,MAAM,QAAQ,OAAO,MAAM,GAE5B,OAAO;EAGT,MAAM,SAAS,OAAO,OAAO,QAC1B,UACC,CAAC,CAAC,SACF,OAAO,MAAM,YAAY,YACzB,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,aAAa,YAC1B,MAAM,QAAQ,MAAM,gBAAgB,MACnC,MAAM,aAAa,QAAQ,OAAO,MAAM,aAAa,cACrD,MAAM,eAAe,aAAa,MAAM,eAAe,QAC5D;EAEA,IAAI,OAAO,WAAW,OAAO,OAAO,QAClC,OAAO;EAGT,OAAO;GACL,eAAA;GACA;GACA,MAAM,OAAO,SAAS,gBAAgB,gBAAgB;GACtD;EACF;CACF,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAsB,yBACpB,UACA,WACe;CACf,MAAM,eAAe,2BAA2B,SAAS;CACzD,MAAM,WAAW,MAAM,WAAW,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;CAC5E,MAAM,WAAW,UACf,cACA,KAAK,UAAU,UAAU,MAAM,CAAC,GAChC,MACF;AACF;AAEA,SAAgB,0BACd,WACA,UACQ;CACR,MAAM,WAAW,WAAW,KAAK,WAAW,QAAQ;CACpD,IAAI,CAAC,GAAG,WAAW,QAAQ,GACzB,OAAO;CAGT,IAAI;EACF,OAAO,GAAG,SAAS,QAAQ,CAAC,CAAC;CAC/B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,iBAAiB,WAAmB,UAA2B;CAC7E,OAAO,GAAG,WAAW,WAAW,KAAK,WAAW,QAAQ,CAAC;AAC3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-ssg-data.js","names":[],"sources":["../../src/ssg/resolve-ssg-data.ts"],"sourcesContent":["/**\n * Data resolution for SSG\n *\n * Merges user-supplied data overrides with any auto-discovered resources.\n * In phase 1, primarily handles user-supplied data.\n */\n\nimport type { SSRData } from '../common/ssr';\nimport type { RouteConfig } from './types';\nimport { interpolateRoutePath } from './route-utils';\n\ninterface DataResolutionOptions {\n /** User-supplied data overrides per route path */\n dataOverrides?: Record<string, unknown>;\n}\n\nexport interface ResolvedSsgRouteData {\n hasData: boolean;\n data?: SSRData;\n}\n\nfunction hasOwnRouteData(\n dataMap: Record<string, SSRData>,\n path: string\n): boolean {\n return Object.prototype.hasOwnProperty.call(dataMap, path);\n}\n\n/**\n * Resolve and validate data for SSG routes\n * Returns a map of route path -> SSRData\n *\n * In phase 1: accepts user-supplied dataOverrides\n * In phase 2: can be extended to auto-discover resources\n */\nexport function resolveSsgData(\n routes: RouteConfig[],\n options: DataResolutionOptions = {}\n): Record<string, SSRData> {\n const { dataOverrides = {} } = options;\n const dataMap: Record<string, SSRData> = {};\n\n for (const route of routes) {\n const concretePath = interpolateRoutePath(route.path, route.params);\n const overridePath =\n concretePath in dataOverrides\n ? concretePath\n : route.path in dataOverrides\n ? route.path\n : null;\n\n // Check if user provided data for this route\n if (overridePath) {\n const data = dataOverrides[overridePath];\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n throw new Error(\n `data for route \"${overridePath}\" must be an object, got ${typeof data}`\n );\n }\n dataMap[overridePath] = data as SSRData;\n }\n // In phase 1, routes without data are rendered with no SSR data\n // Phase 2 can add auto-discovery here\n }\n\n return dataMap;\n}\n\nexport function resolveSsgRouteData(\n dataMap: Record<string, SSRData>,\n routePath: string,\n concretePath: string\n): ResolvedSsgRouteData {\n if (hasOwnRouteData(dataMap, concretePath)) {\n return {\n hasData: true,\n data: dataMap[concretePath],\n };\n }\n\n if (hasOwnRouteData(dataMap, routePath)) {\n return {\n hasData: true,\n data: dataMap[routePath],\n };\n }\n\n return {\n hasData: false,\n data: undefined,\n };\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n\n for (const recordValue of Object.values(value)) {\n if (typeof recordValue !== 'string') {\n return false;\n }\n }\n\n return true;\n}\n\nfunction validateRouteShape(route: RouteConfig): void {\n if (typeof route.path !== 'string' || !route.path.startsWith('/')) {\n throw new Error(\n `route path must be a string starting with \"/\", got \"${route.path}\"`\n );\n }\n\n const handler = route.handler ?? route.component;\n if (typeof handler !== 'function') {\n throw new Error(\n `route handler must be a function for path \"${route.path}\"`\n );\n }\n\n if (route.params !== undefined && !isStringRecord(route.params)) {\n throw new Error(\n `route \"${route.path}\" params must be an object containing only string values`\n );\n }\n\n if (route.invalidationKeys !== undefined) {\n if (!Array.isArray(route.invalidationKeys)) {\n throw new Error(\n `route \"${route.path}\" invalidationKeys must be an array of strings`\n );\n }\n for (const key of route.invalidationKeys) {\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `route \"${route.path}\" invalidationKeys must contain only non-empty strings`\n );\n }\n }\n }\n}\n\nfunction validateEntriesResult(\n route: RouteConfig,\n entries: unknown\n): Array<Record<string, string>> {\n if (!Array.isArray(entries)) {\n throw new Error(\n `route \"${route.path}\" entries must return an array of param maps`\n );\n }\n\n return entries.map((entry, index) => {\n if (!isStringRecord(entry)) {\n throw new Error(\n `route \"${route.path}\" entries[${index}] must be an object containing only string values`\n );\n }\n\n return entry;\n });\n}\n\nexport async function expandRoutes(\n routes: RouteConfig[]\n): Promise<RouteConfig[]> {\n if (!Array.isArray(routes)) {\n throw new Error('routes must be an array');\n }\n\n const expanded: RouteConfig[] = [];\n\n for (const route of routes) {\n validateRouteShape(route);\n\n if (route.params) {\n expanded.push({ ...route, entries: undefined });\n }\n\n if (!route.entries) {\n if (!route.params) {\n expanded.push(route);\n }\n continue;\n }\n\n const entries = validateEntriesResult(route, await route.entries());\n for (const params of entries) {\n expanded.push({ ...route, params, entries: undefined });\n }\n }\n\n return expanded;\n}\n\n/**\n * Validate routes are properly configured\n */\nexport function validateRoutes(routes: RouteConfig[]): void {\n if (!Array.isArray(routes)) {\n throw new Error('routes must be an array');\n }\n\n const seen = new Set<string>();\n\n for (const route of routes) {\n validateRouteShape(route);\n\n const key = `${route.path}::${JSON.stringify(route.params || {})}`;\n if (seen.has(key)) {\n throw new Error(\n `duplicate route entry detected for path \"${route.path}\"`\n );\n }\n seen.add(key);\n\n if (route.path.includes('{')) {\n const paramNames = Array.from(route.path.matchAll(/\\{([^}]+)\\}/g)).map(\n (m) => m[1]\n );\n if (!route.params) {\n throw new Error(\n `route \"${route.path}\" uses path parameters and requires params`\n );\n }\n for (const name of paramNames) {\n if (!(name in route.params)) {\n throw new Error(\n `route \"${route.path}\" missing required param \"${name}\"`\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resolve-ssg-data.js","names":[],"sources":["../../src/ssg/resolve-ssg-data.ts"],"sourcesContent":["/**\n * Data resolution for SSG\n *\n * Merges user-supplied data overrides with any auto-discovered resources.\n * In phase 1, primarily handles user-supplied data.\n */\n\nimport type { SSRData } from '../common/ssr';\nimport type { RouteConfig } from './types';\nimport { interpolateRoutePath } from './route-utils';\n\ninterface DataResolutionOptions {\n /** User-supplied data overrides per route path */\n dataOverrides?: Record<string, unknown>;\n}\n\nexport interface ResolvedSsgRouteData {\n hasData: boolean;\n data?: SSRData;\n}\n\nfunction hasOwnRouteData(\n dataMap: Record<string, SSRData>,\n path: string\n): boolean {\n return Object.prototype.hasOwnProperty.call(dataMap, path);\n}\n\n/**\n * Resolve and validate data for SSG routes\n * Returns a map of route path -> SSRData\n *\n * In phase 1: accepts user-supplied dataOverrides\n * In phase 2: can be extended to auto-discover resources\n */\nexport function resolveSsgData(\n routes: RouteConfig[],\n options: DataResolutionOptions = {}\n): Record<string, SSRData> {\n const { dataOverrides = {} } = options;\n const dataMap: Record<string, SSRData> = {};\n\n for (const route of routes) {\n const concretePath = interpolateRoutePath(route.path, route.params);\n const overridePath =\n concretePath in dataOverrides\n ? concretePath\n : route.path in dataOverrides\n ? route.path\n : null;\n\n // Check if user provided data for this route\n if (overridePath) {\n const data = dataOverrides[overridePath];\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\n throw new Error(\n `data for route \"${overridePath}\" must be an object, got ${typeof data}`\n );\n }\n dataMap[overridePath] = data as SSRData;\n }\n // In phase 1, routes without data are rendered with no SSR data\n // Phase 2 can add auto-discovery here\n }\n\n return dataMap;\n}\n\nexport function resolveSsgRouteData(\n dataMap: Record<string, SSRData>,\n routePath: string,\n concretePath: string\n): ResolvedSsgRouteData {\n if (hasOwnRouteData(dataMap, concretePath)) {\n return {\n hasData: true,\n data: dataMap[concretePath],\n };\n }\n\n if (hasOwnRouteData(dataMap, routePath)) {\n return {\n hasData: true,\n data: dataMap[routePath],\n };\n }\n\n return {\n hasData: false,\n data: undefined,\n };\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n\n for (const recordValue of Object.values(value)) {\n if (typeof recordValue !== 'string') {\n return false;\n }\n }\n\n return true;\n}\n\nfunction validateRouteShape(route: RouteConfig): void {\n if (typeof route.path !== 'string' || !route.path.startsWith('/')) {\n throw new Error(\n `route path must be a string starting with \"/\", got \"${route.path}\"`\n );\n }\n\n const handler = route.handler ?? route.component;\n if (typeof handler !== 'function') {\n throw new Error(\n `route handler must be a function for path \"${route.path}\"`\n );\n }\n\n if (route.params !== undefined && !isStringRecord(route.params)) {\n throw new Error(\n `route \"${route.path}\" params must be an object containing only string values`\n );\n }\n\n if (route.invalidationKeys !== undefined) {\n if (!Array.isArray(route.invalidationKeys)) {\n throw new Error(\n `route \"${route.path}\" invalidationKeys must be an array of strings`\n );\n }\n for (const key of route.invalidationKeys) {\n if (typeof key !== 'string' || key.length === 0) {\n throw new Error(\n `route \"${route.path}\" invalidationKeys must contain only non-empty strings`\n );\n }\n }\n }\n}\n\nfunction validateEntriesResult(\n route: RouteConfig,\n entries: unknown\n): Array<Record<string, string>> {\n if (!Array.isArray(entries)) {\n throw new Error(\n `route \"${route.path}\" entries must return an array of param maps`\n );\n }\n\n return entries.map((entry, index) => {\n if (!isStringRecord(entry)) {\n throw new Error(\n `route \"${route.path}\" entries[${index}] must be an object containing only string values`\n );\n }\n\n return entry;\n });\n}\n\nexport async function expandRoutes(\n routes: RouteConfig[]\n): Promise<RouteConfig[]> {\n if (!Array.isArray(routes)) {\n throw new Error('routes must be an array');\n }\n\n const expanded: RouteConfig[] = [];\n\n for (const route of routes) {\n validateRouteShape(route);\n\n if (route.params) {\n expanded.push({ ...route, entries: undefined });\n }\n\n if (!route.entries) {\n if (!route.params) {\n expanded.push(route);\n }\n continue;\n }\n\n const entries = validateEntriesResult(route, await route.entries());\n for (const params of entries) {\n expanded.push({ ...route, params, entries: undefined });\n }\n }\n\n return expanded;\n}\n\n/**\n * Validate routes are properly configured\n */\nexport function validateRoutes(routes: RouteConfig[]): void {\n if (!Array.isArray(routes)) {\n throw new Error('routes must be an array');\n }\n\n const seen = new Set<string>();\n\n for (const route of routes) {\n validateRouteShape(route);\n\n const key = `${route.path}::${JSON.stringify(route.params || {})}`;\n if (seen.has(key)) {\n throw new Error(\n `duplicate route entry detected for path \"${route.path}\"`\n );\n }\n seen.add(key);\n\n if (route.path.includes('{')) {\n const paramNames = Array.from(route.path.matchAll(/\\{([^}]+)\\}/g)).map(\n (m) => m[1]\n );\n if (!route.params) {\n throw new Error(\n `route \"${route.path}\" uses path parameters and requires params`\n );\n }\n for (const name of paramNames) {\n if (!(name in route.params)) {\n throw new Error(\n `route \"${route.path}\" missing required param \"${name}\"`\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"write-static-files.js","names":[],"sources":["../../src/ssg/write-static-files.ts"],"sourcesContent":["/**\n * File I/O for Static Site Generation\n *\n * Uses Node.js fs/path modules to write rendered HTML files to disk.\n * This module is Node-only and not intended for browser builds.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as fsSync from 'node:fs';\nimport * as pathModule from 'node:path';\nimport type { RouteRenderResult } from './types';\n\ninterface WriteStaticFilesOptions {\n concurrency?: number;\n}\n\n/**\n * Write rendered routes to disk\n * Creates outputDir/{route-path}/index.html structure\n */\nexport async function writeStaticFiles(\n results: RouteRenderResult[],\n outputDir: string,\n options: WriteStaticFilesOptions = {}\n): Promise<void> {\n await fs.mkdir(outputDir, { recursive: true });\n\n for (const result of results) {\n if (result.status !== 'removed') {\n continue;\n }\n\n const fullPath = pathModule.join(outputDir, result.filePath);\n if (fsSync.existsSync(fullPath)) {\n await fs.rm(fullPath, { force: true });\n await pruneEmptyDirs(pathModule.dirname(fullPath), outputDir);\n }\n }\n\n const pendingWrites: RouteRenderResult[] = [];\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index];\n if (result.status === 'error') {\n console.warn(`Skipping failed route: ${result.path} - ${result.error}`);\n continue;\n }\n if (result.status === 'success' && result.written) {\n pendingWrites.push(result);\n }\n }\n\n const directories: string[] = [];\n const seenDirectories = new Set<string>();\n for (const result of pendingWrites) {\n const dir = pathModule.dirname(pathModule.join(outputDir, result.filePath));\n if (!seenDirectories.has(dir)) {\n seenDirectories.add(dir);\n directories.push(dir);\n }\n }\n\n for (const dir of directories) {\n await fs.mkdir(dir, { recursive: true });\n }\n\n const concurrency = Math.max(\n 1,\n Math.min(options.concurrency ?? 8, pendingWrites.length || 1)\n );\n let nextIndex = 0;\n\n const worker = async () => {\n while (true) {\n const current = nextIndex;\n nextIndex += 1;\n if (current >= pendingWrites.length) {\n return;\n }\n\n const result = pendingWrites[current];\n const fullPath = pathModule.join(outputDir, result.filePath);\n await fs.writeFile(fullPath, result.html, 'utf8');\n }\n };\n\n await Promise.all(Array.from({ length: concurrency }, () => worker()));\n}\n\n/**\n * Get the output file path for a route\n * E.g., \"/blog/post\" -> \"blog/post\" or \"/\" -> \"\"\n */\nexport { getOutputFilePath } from './route-utils';\n\nasync function pruneEmptyDirs(\n startDir: string,\n rootDir: string\n): Promise<void> {\n let current = startDir;\n const normalizedRoot = pathModule.resolve(rootDir);\n\n while (current.startsWith(normalizedRoot)) {\n if (!fsSync.existsSync(current)) {\n break;\n }\n\n if ((await fs.readdir(current)).length > 0) {\n break;\n }\n\n await fs.rmdir(current);\n if (pathModule.resolve(current) === normalizedRoot) {\n break;\n }\n current = pathModule.dirname(current);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoBA,eAAsB,iBACpB,SACA,WACA,UAAmC,CAAC,GACrB;CACf,MAAM,GAAG,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAE7C,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,OAAO,WAAW,WACpB;EAGF,MAAM,WAAW,WAAW,KAAK,WAAW,OAAO,QAAQ;EAC3D,IAAI,OAAO,WAAW,QAAQ,GAAG;GAC/B,MAAM,GAAG,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC;GACrC,MAAM,eAAe,WAAW,QAAQ,QAAQ,GAAG,SAAS;EAC9D;CACF;CAEA,MAAM,gBAAqC,CAAC;CAC5C,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;EACtD,MAAM,SAAS,QAAQ;EACvB,IAAI,OAAO,WAAW,SAAS;GAC7B,QAAQ,KAAK,0BAA0B,OAAO,KAAK,KAAK,OAAO,OAAO;GACtE;EACF;EACA,IAAI,OAAO,WAAW,aAAa,OAAO,SACxC,cAAc,KAAK,MAAM;CAE7B;CAEA,MAAM,cAAwB,CAAC;CAC/B,MAAM,kCAAkB,IAAI,IAAY;CACxC,KAAK,MAAM,UAAU,eAAe;EAClC,MAAM,MAAM,WAAW,QAAQ,WAAW,KAAK,WAAW,OAAO,QAAQ,CAAC;EAC1E,IAAI,CAAC,gBAAgB,IAAI,GAAG,GAAG;GAC7B,gBAAgB,IAAI,GAAG;GACvB,YAAY,KAAK,GAAG;EACtB;CACF;CAEA,KAAK,MAAM,OAAO,aAChB,MAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;CAGzC,MAAM,cAAc,KAAK,IACvB,GACA,KAAK,IAAI,QAAQ,eAAe,GAAG,cAAc,UAAU,CAAC,CAC9D;CACA,IAAI,YAAY;CAEhB,MAAM,SAAS,YAAY;EACzB,OAAO,MAAM;GACX,MAAM,UAAU;GAChB,aAAa;GACb,IAAI,WAAW,cAAc,QAC3B;GAGF,MAAM,SAAS,cAAc;GAC7B,MAAM,WAAW,WAAW,KAAK,WAAW,OAAO,QAAQ;GAC3D,MAAM,GAAG,UAAU,UAAU,OAAO,MAAM,MAAM;EAClD;CACF;CAEA,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,YAAY,SAAS,OAAO,CAAC,CAAC;AACvE;AAQA,eAAe,eACb,UACA,SACe;CACf,IAAI,UAAU;CACd,MAAM,iBAAiB,WAAW,QAAQ,OAAO;CAEjD,OAAO,QAAQ,WAAW,cAAc,GAAG;EACzC,IAAI,CAAC,OAAO,WAAW,OAAO,GAC5B;EAGF,KAAK,MAAM,GAAG,QAAQ,OAAO,GAAG,SAAS,GACvC;EAGF,MAAM,GAAG,MAAM,OAAO;EACtB,IAAI,WAAW,QAAQ,OAAO,MAAM,gBAClC;EAEF,UAAU,WAAW,QAAQ,OAAO;CACtC;AACF"}
1
+ {"version":3,"file":"write-static-files.js","names":[],"sources":["../../src/ssg/write-static-files.ts"],"sourcesContent":["/**\n * File I/O for Static Site Generation\n *\n * Uses Node.js fs/path modules to write rendered HTML files to disk.\n * This module is Node-only and not intended for browser builds.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as fsSync from 'node:fs';\nimport * as pathModule from 'node:path';\nimport type { RouteRenderResult } from './types';\n\ninterface WriteStaticFilesOptions {\n concurrency?: number;\n}\n\n/**\n * Write rendered routes to disk\n * Creates outputDir/{route-path}/index.html structure\n */\nexport async function writeStaticFiles(\n results: RouteRenderResult[],\n outputDir: string,\n options: WriteStaticFilesOptions = {}\n): Promise<void> {\n await fs.mkdir(outputDir, { recursive: true });\n\n for (const result of results) {\n if (result.status !== 'removed') {\n continue;\n }\n\n const fullPath = pathModule.join(outputDir, result.filePath);\n if (fsSync.existsSync(fullPath)) {\n await fs.rm(fullPath, { force: true });\n await pruneEmptyDirs(pathModule.dirname(fullPath), outputDir);\n }\n }\n\n const pendingWrites: RouteRenderResult[] = [];\n for (let index = 0; index < results.length; index += 1) {\n const result = results[index];\n if (result.status === 'error') {\n console.warn(`Skipping failed route: ${result.path} - ${result.error}`);\n continue;\n }\n if (result.status === 'success' && result.written) {\n pendingWrites.push(result);\n }\n }\n\n const directories: string[] = [];\n const seenDirectories = new Set<string>();\n for (const result of pendingWrites) {\n const dir = pathModule.dirname(pathModule.join(outputDir, result.filePath));\n if (!seenDirectories.has(dir)) {\n seenDirectories.add(dir);\n directories.push(dir);\n }\n }\n\n for (const dir of directories) {\n await fs.mkdir(dir, { recursive: true });\n }\n\n const concurrency = Math.max(\n 1,\n Math.min(options.concurrency ?? 8, pendingWrites.length || 1)\n );\n let nextIndex = 0;\n\n const worker = async () => {\n while (true) {\n const current = nextIndex;\n nextIndex += 1;\n if (current >= pendingWrites.length) {\n return;\n }\n\n const result = pendingWrites[current];\n const fullPath = pathModule.join(outputDir, result.filePath);\n await fs.writeFile(fullPath, result.html, 'utf8');\n }\n };\n\n await Promise.all(Array.from({ length: concurrency }, () => worker()));\n}\n\n/**\n * Get the output file path for a route\n * E.g., \"/blog/post\" -> \"blog/post\" or \"/\" -> \"\"\n */\nexport { getOutputFilePath } from './route-utils';\n\nasync function pruneEmptyDirs(\n startDir: string,\n rootDir: string\n): Promise<void> {\n let current = startDir;\n const normalizedRoot = pathModule.resolve(rootDir);\n\n while (current.startsWith(normalizedRoot)) {\n if (!fsSync.existsSync(current)) {\n break;\n }\n\n if ((await fs.readdir(current)).length > 0) {\n break;\n }\n\n await fs.rmdir(current);\n if (pathModule.resolve(current) === normalizedRoot) {\n break;\n }\n current = pathModule.dirname(current);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoBA,eAAsB,iBACpB,SACA,WACA,UAAmC,CAAC,GACrB;CACf,MAAM,GAAG,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAE7C,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,OAAO,WAAW,WACpB;EAGF,MAAM,WAAW,WAAW,KAAK,WAAW,OAAO,QAAQ;EAC3D,IAAI,OAAO,WAAW,QAAQ,GAAG;GAC/B,MAAM,GAAG,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC;GACrC,MAAM,eAAe,WAAW,QAAQ,QAAQ,GAAG,SAAS;EAC9D;CACF;CAEA,MAAM,gBAAqC,CAAC;CAC5C,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;EACtD,MAAM,SAAS,QAAQ;EACvB,IAAI,OAAO,WAAW,SAAS;GAC7B,QAAQ,KAAK,0BAA0B,OAAO,KAAK,KAAK,OAAO,OAAO;GACtE;EACF;EACA,IAAI,OAAO,WAAW,aAAa,OAAO,SACxC,cAAc,KAAK,MAAM;CAE7B;CAEA,MAAM,cAAwB,CAAC;CAC/B,MAAM,kCAAkB,IAAI,IAAY;CACxC,KAAK,MAAM,UAAU,eAAe;EAClC,MAAM,MAAM,WAAW,QAAQ,WAAW,KAAK,WAAW,OAAO,QAAQ,CAAC;EAC1E,IAAI,CAAC,gBAAgB,IAAI,GAAG,GAAG;GAC7B,gBAAgB,IAAI,GAAG;GACvB,YAAY,KAAK,GAAG;EACtB;CACF;CAEA,KAAK,MAAM,OAAO,aAChB,MAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;CAGzC,MAAM,cAAc,KAAK,IACvB,GACA,KAAK,IAAI,QAAQ,eAAe,GAAG,cAAc,UAAU,CAAC,CAC9D;CACA,IAAI,YAAY;CAEhB,MAAM,SAAS,YAAY;EACzB,OAAO,MAAM;GACX,MAAM,UAAU;GAChB,aAAa;GACb,IAAI,WAAW,cAAc,QAC3B;GAGF,MAAM,SAAS,cAAc;GAC7B,MAAM,WAAW,WAAW,KAAK,WAAW,OAAO,QAAQ;GAC3D,MAAM,GAAG,UAAU,UAAU,OAAO,MAAM,MAAM;EAClD;CACF;CAEA,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,YAAY,SAAS,OAAO,CAAC,CAAC;AACvE;AAQA,eAAe,eACb,UACA,SACe;CACf,IAAI,UAAU;CACd,MAAM,iBAAiB,WAAW,QAAQ,OAAO;CAEjD,OAAO,QAAQ,WAAW,cAAc,GAAG;EACzC,IAAI,CAAC,OAAO,WAAW,OAAO,GAC5B;EAGF,KAAK,MAAM,GAAG,QAAQ,OAAO,EAAA,CAAG,SAAS,GACvC;EAGF,MAAM,GAAG,MAAM,OAAO;EACtB,IAAI,WAAW,QAAQ,OAAO,MAAM,gBAClC;EAEF,UAAU,WAAW,QAAQ,OAAO;CACtC;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":[],"sources":["../../src/ssr/context.ts"],"sourcesContent":["/**\n * SSR Context Management\n *\n * Provides render-context storage for server-side rendering.\n * Node SSR lazily installs AsyncLocalStorage on first use; browser builds use\n * the fallback stack.\n */\n\nimport { SSRDataMissingError } from './errors';\nimport { clearEscapeCache } from './escape';\n\nexport type { SSRData } from '../common/ssr';\nimport type { SSRData } from '../common/ssr';\nimport type { Route, RouteAuthOptions } from '../common/router';\n\n// Unified per-render context combining SSRContext and RenderContext\nexport interface RenderContext {\n url: string;\n seed: number;\n data?: SSRData;\n params?: Record<string, string>;\n routes?: readonly Route[];\n routeAuth?: RouteAuthOptions;\n signal?: AbortSignal;\n queryCache?: Map<string, unknown>;\n ssrCleanupFns: Array<() => void>;\n // Per-render key state (moved from render-keys.ts globals)\n keyCounter: number;\n renderData: Record<string, unknown> | null;\n}\n\n// Legacy alias for compatibility\nexport type SSRContext = RenderContext;\n\ntype RenderContextAccessor = {\n getStore(): RenderContext | undefined;\n run<R>(store: RenderContext, fn: () => R): R;\n};\n\ntype AsyncHooksModule = {\n AsyncLocalStorage?: new () => RenderContextAccessor;\n};\n\nlet renderContextAccessor: RenderContextAccessor | null = null;\nlet renderContextAccessorInitialized = false;\n\n// Fallback stack for non-Node environments\nlet fallbackStack: RenderContext | null = null;\n\nfunction ensureRenderContextAccessor(): void {\n if (renderContextAccessorInitialized) {\n return;\n }\n\n renderContextAccessorInitialized = true;\n\n if (typeof process === 'undefined' || !process.versions?.node) {\n return;\n }\n\n try {\n // Hide the Node builtin from browser dependency scanners.\n // eslint-disable-next-line @typescript-eslint/no-implied-eval\n const loadAsyncHooks = new Function(\n 'return require(' + JSON.stringify('async_hooks') + ');'\n ) as () => AsyncHooksModule;\n const asyncHooks = loadAsyncHooks();\n\n if (asyncHooks.AsyncLocalStorage) {\n const asyncLocalStorage = new asyncHooks.AsyncLocalStorage();\n renderContextAccessor = {\n getStore() {\n return asyncLocalStorage.getStore();\n },\n run<R>(store: RenderContext, fn: () => R): R {\n return asyncLocalStorage.run(store, fn);\n },\n };\n }\n } catch {\n // Keep the fallback stack when async_hooks is unavailable.\n }\n}\n\nexport function createRenderContext(\n seed = 12345,\n opts: {\n url?: string;\n data?: SSRData;\n params?: Record<string, string>;\n routes?: readonly Route[];\n routeAuth?: RouteAuthOptions;\n signal?: AbortSignal;\n } = {}\n): RenderContext {\n clearEscapeCache();\n\n return {\n url: opts.url ?? '',\n seed,\n data: opts.data,\n params: opts.params,\n routes: opts.routes,\n routeAuth: opts.routeAuth,\n signal: opts.signal,\n queryCache: new Map<string, unknown>(),\n ssrCleanupFns: [],\n keyCounter: 0,\n renderData: null,\n };\n}\n\n/**\n * Run a function with the given render context.\n * Concurrency-safe in Node.js via AsyncLocalStorage.\n */\nexport function withRenderContext<T>(ctx: RenderContext, fn: () => T): T {\n ensureRenderContextAccessor();\n if (renderContextAccessor) {\n return renderContextAccessor.run(ctx, fn);\n }\n // Fallback: stack-based (not concurrency-safe)\n const prev = fallbackStack;\n fallbackStack = ctx;\n try {\n return fn();\n } finally {\n fallbackStack = prev;\n }\n}\n\n/**\n * Get the current render context.\n * Returns null if not inside a render.\n */\nexport function getRenderContext(): RenderContext | null {\n ensureRenderContextAccessor();\n if (renderContextAccessor) {\n return renderContextAccessor.getStore() ?? null;\n }\n return fallbackStack;\n}\n\n// Legacy API aliases (deprecated, for backwards compatibility)\nexport const getSSRContext = getRenderContext;\nexport const withSSRContext = withRenderContext;\nexport const getCurrentSSRContext = getRenderContext;\n\nexport function runWithSSRContext<T>(ctx: RenderContext, fn: () => T): T {\n // This was a separate path for sync detection; now unified\n return withRenderContext(ctx, fn);\n}\n\n/**\n * Centralized SSR enforcement helper — throws a consistent error when async\n * data is encountered during synchronous SSR.\n */\nexport function throwSSRDataMissing(): never {\n throw new SSRDataMissingError();\n}\n\nexport { SSRDataMissingError };\n"],"mappings":";;;;;;;;;;;AA2CA,IAAI,wBAAsD;AAC1D,IAAI,mCAAmC;AAGvC,IAAI,gBAAsC;AAE1C,SAAS,8BAAoC;CAC3C,IAAI,kCACF;CAGF,mCAAmC;CAEnC,IAAI,OAAO,YAAY,eAAe,CAAC,QAAQ,UAAU,MACvD;CAGF,IAAI;EAMF,MAAM,aAAa,IAHQ,SACzB,oBAAoB,KAAK,UAAU,aAAa,IAAI,IAEnC,EAAe;EAElC,IAAI,WAAW,mBAAmB;GAChC,MAAM,oBAAoB,IAAI,WAAW,kBAAkB;GAC3D,wBAAwB;IACtB,WAAW;KACT,OAAO,kBAAkB,SAAS;IACpC;IACA,IAAO,OAAsB,IAAgB;KAC3C,OAAO,kBAAkB,IAAI,OAAO,EAAE;IACxC;GACF;EACF;CACF,QAAQ,CAER;AACF;AAEA,SAAgB,oBACd,OAAO,OACP,OAOI,CAAC,GACU;CACf,iBAAiB;CAEjB,OAAO;EACL,KAAK,KAAK,OAAO;EACjB;EACA,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,QAAQ,KAAK;EACb,4BAAY,IAAI,IAAqB;EACrC,eAAe,CAAC;EAChB,YAAY;EACZ,YAAY;CACd;AACF;;;;;AAMA,SAAgB,kBAAqB,KAAoB,IAAgB;CACvE,4BAA4B;CAC5B,IAAI,uBACF,OAAO,sBAAsB,IAAI,KAAK,EAAE;CAG1C,MAAM,OAAO;CACb,gBAAgB;CAChB,IAAI;EACF,OAAO,GAAG;CACZ,UAAU;EACR,gBAAgB;CAClB;AACF;;;;;AAMA,SAAgB,mBAAyC;CACvD,4BAA4B;CAC5B,IAAI,uBACF,OAAO,sBAAsB,SAAS,KAAK;CAE7C,OAAO;AACT;;;;;AAgBA,SAAgB,sBAA6B;CAC3C,MAAM,IAAI,oBAAoB;AAChC"}
1
+ {"version":3,"file":"context.js","names":[],"sources":["../../src/ssr/context.ts"],"sourcesContent":["/**\n * SSR Context Management\n *\n * Provides render-context storage for server-side rendering.\n * Node SSR lazily installs AsyncLocalStorage on first use; browser builds use\n * the fallback stack.\n */\n\nimport { SSRDataMissingError } from './errors';\nimport { clearEscapeCache } from './escape';\n\nexport type { SSRData } from '../common/ssr';\nimport type { SSRData } from '../common/ssr';\nimport type { Route, RouteAuthOptions } from '../common/router';\n\n// Unified per-render context combining SSRContext and RenderContext\nexport interface RenderContext {\n url: string;\n seed: number;\n data?: SSRData;\n params?: Record<string, string>;\n routes?: readonly Route[];\n routeAuth?: RouteAuthOptions;\n signal?: AbortSignal;\n queryCache?: Map<string, unknown>;\n ssrCleanupFns: Array<() => void>;\n // Per-render key state (moved from render-keys.ts globals)\n keyCounter: number;\n renderData: Record<string, unknown> | null;\n}\n\n// Legacy alias for compatibility\nexport type SSRContext = RenderContext;\n\ntype RenderContextAccessor = {\n getStore(): RenderContext | undefined;\n run<R>(store: RenderContext, fn: () => R): R;\n};\n\ntype AsyncHooksModule = {\n AsyncLocalStorage?: new () => RenderContextAccessor;\n};\n\nlet renderContextAccessor: RenderContextAccessor | null = null;\nlet renderContextAccessorInitialized = false;\n\n// Fallback stack for non-Node environments\nlet fallbackStack: RenderContext | null = null;\n\nfunction ensureRenderContextAccessor(): void {\n if (renderContextAccessorInitialized) {\n return;\n }\n\n renderContextAccessorInitialized = true;\n\n if (typeof process === 'undefined' || !process.versions?.node) {\n return;\n }\n\n try {\n // Hide the Node builtin from browser dependency scanners.\n // eslint-disable-next-line @typescript-eslint/no-implied-eval\n const loadAsyncHooks = new Function(\n 'return require(' + JSON.stringify('async_hooks') + ');'\n ) as () => AsyncHooksModule;\n const asyncHooks = loadAsyncHooks();\n\n if (asyncHooks.AsyncLocalStorage) {\n const asyncLocalStorage = new asyncHooks.AsyncLocalStorage();\n renderContextAccessor = {\n getStore() {\n return asyncLocalStorage.getStore();\n },\n run<R>(store: RenderContext, fn: () => R): R {\n return asyncLocalStorage.run(store, fn);\n },\n };\n }\n } catch {\n // Keep the fallback stack when async_hooks is unavailable.\n }\n}\n\nexport function createRenderContext(\n seed = 12345,\n opts: {\n url?: string;\n data?: SSRData;\n params?: Record<string, string>;\n routes?: readonly Route[];\n routeAuth?: RouteAuthOptions;\n signal?: AbortSignal;\n } = {}\n): RenderContext {\n clearEscapeCache();\n\n return {\n url: opts.url ?? '',\n seed,\n data: opts.data,\n params: opts.params,\n routes: opts.routes,\n routeAuth: opts.routeAuth,\n signal: opts.signal,\n queryCache: new Map<string, unknown>(),\n ssrCleanupFns: [],\n keyCounter: 0,\n renderData: null,\n };\n}\n\n/**\n * Run a function with the given render context.\n * Concurrency-safe in Node.js via AsyncLocalStorage.\n */\nexport function withRenderContext<T>(ctx: RenderContext, fn: () => T): T {\n ensureRenderContextAccessor();\n if (renderContextAccessor) {\n return renderContextAccessor.run(ctx, fn);\n }\n // Fallback: stack-based (not concurrency-safe)\n const prev = fallbackStack;\n fallbackStack = ctx;\n try {\n return fn();\n } finally {\n fallbackStack = prev;\n }\n}\n\n/**\n * Get the current render context.\n * Returns null if not inside a render.\n */\nexport function getRenderContext(): RenderContext | null {\n ensureRenderContextAccessor();\n if (renderContextAccessor) {\n return renderContextAccessor.getStore() ?? null;\n }\n return fallbackStack;\n}\n\n// Legacy API aliases (deprecated, for backwards compatibility)\nexport const getSSRContext = getRenderContext;\nexport const withSSRContext = withRenderContext;\nexport const getCurrentSSRContext = getRenderContext;\n\nexport function runWithSSRContext<T>(ctx: RenderContext, fn: () => T): T {\n // This was a separate path for sync detection; now unified\n return withRenderContext(ctx, fn);\n}\n\n/**\n * Centralized SSR enforcement helper — throws a consistent error when async\n * data is encountered during synchronous SSR.\n */\nexport function throwSSRDataMissing(): never {\n throw new SSRDataMissingError();\n}\n\nexport { SSRDataMissingError };\n"],"mappings":";;;;;;;;;;;AA2CA,IAAI,wBAAsD;AAC1D,IAAI,mCAAmC;AAGvC,IAAI,gBAAsC;AAE1C,SAAS,8BAAoC;CAC3C,IAAI,kCACF;CAGF,mCAAmC;CAEnC,IAAI,OAAO,YAAY,eAAe,CAAC,QAAQ,UAAU,MACvD;CAGF,IAAI;EAMF,MAAM,aAAa,IAHQ,SACzB,oBAAoB,KAAK,UAAU,aAAa,IAAI,IAEnC,CAAA,CAAe;EAElC,IAAI,WAAW,mBAAmB;GAChC,MAAM,oBAAoB,IAAI,WAAW,kBAAkB;GAC3D,wBAAwB;IACtB,WAAW;KACT,OAAO,kBAAkB,SAAS;IACpC;IACA,IAAO,OAAsB,IAAgB;KAC3C,OAAO,kBAAkB,IAAI,OAAO,EAAE;IACxC;GACF;EACF;CACF,QAAQ,CAER;AACF;AAEA,SAAgB,oBACd,OAAO,OACP,OAOI,CAAC,GACU;CACf,iBAAiB;CAEjB,OAAO;EACL,KAAK,KAAK,OAAO;EACjB;EACA,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,QAAQ,KAAK;EACb,4BAAY,IAAI,IAAqB;EACrC,eAAe,CAAC;EAChB,YAAY;EACZ,YAAY;CACd;AACF;;;;;AAMA,SAAgB,kBAAqB,KAAoB,IAAgB;CACvE,4BAA4B;CAC5B,IAAI,uBACF,OAAO,sBAAsB,IAAI,KAAK,EAAE;CAG1C,MAAM,OAAO;CACb,gBAAgB;CAChB,IAAI;EACF,OAAO,GAAG;CACZ,UAAU;EACR,gBAAgB;CAClB;AACF;;;;;AAMA,SAAgB,mBAAyC;CACvD,4BAA4B;CAC5B,IAAI,uBACF,OAAO,sBAAsB,SAAS,KAAK;CAE7C,OAAO;AACT;;;;;AAgBA,SAAgB,sBAA6B;CAC3C,MAAM,IAAI,oBAAoB;AAChC"}