@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,69 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/storage/boot.js — THE browser boot of the storage seam: install a LocalStorageProvider
5
+ * as the active provider, once, unless a host has already set one.
6
+ *
7
+ * Every browser surface that persists through lib/shim/storage calls this at its boot — FraqtlIde
8
+ * when it mounts, a framework-free page before it mounts a run-once card — and nothing else
9
+ * constructs a LocalStorageProvider (gated by test/boot.test.mjs). Plain ESM, no DOM beyond
10
+ * `window.localStorage`, shim-internal imports only.
11
+ *
12
+ * bootLocalStorage() → { row, detail? } (a row of STORAGE_BOOT_OUTCOMES)
13
+ * recordStorageBoot(el, outcome) (writes STORAGE_BOOT_ATTRS on the host's element)
14
+ *
15
+ * A browser without usable localStorage is a declared outcome, never a throw: persistence is a
16
+ * convenience for its consumers, each of which records its own unavailable state (the run-once
17
+ * card's `storage-unavailable-shown`). The reason the provider could not be built is the detail.
18
+ * A host never discards the outcome: it writes it with recordStorageBoot, the ONE writer of the
19
+ * boot record's attributes.
20
+ */
21
+
22
+ import { peekProvider, setActiveProvider } from './index.js';
23
+ import { LocalStorageProvider } from './local-storage-provider.js';
24
+
25
+ /** The closed table of boot outcomes. */
26
+ export const STORAGE_BOOT_OUTCOMES = Object.freeze([
27
+ { id: 'installed', means: 'a LocalStorageProvider was constructed and is now the active provider' },
28
+ { id: 'already-active', means: 'a provider was already active (a host set its own, or boot ran before); left as is' },
29
+ { id: 'unavailable', means: 'localStorage is absent or write-blocked: no provider is set; detail is the reason' },
30
+ ].map(Object.freeze));
31
+
32
+ /**
33
+ * Install localStorage as the active storage provider unless one is already active.
34
+ * @returns {{row: 'installed'|'already-active'|'unavailable', detail?: string}}
35
+ */
36
+ export function bootLocalStorage() {
37
+ if (peekProvider()) { return { row: 'already-active' }; }
38
+ let provider;
39
+ try {
40
+ provider = new LocalStorageProvider();
41
+ } catch (e) {
42
+ return { row: 'unavailable', detail: (e && e.message) ? String(e.message) : String(e) };
43
+ }
44
+ setActiveProvider(provider);
45
+ return { row: 'installed' };
46
+ }
47
+
48
+ /** The boot record's attribute names on a host element — the ONE vocabulary readers and the writer share. */
49
+ export const STORAGE_BOOT_ATTRS = Object.freeze({ row: 'data-storage-boot', detail: 'data-storage-boot-detail' });
50
+
51
+ const BOOT_ROWS = new Set(STORAGE_BOOT_OUTCOMES.map((r) => r.id));
52
+
53
+ /**
54
+ * Write a bootLocalStorage() outcome onto `el` (STORAGE_BOOT_ATTRS). A row without a detail
55
+ * clears any previous detail. Refuses a row STORAGE_BOOT_OUTCOMES does not list.
56
+ * @param {{setAttribute: function, removeAttribute: function}} el
57
+ * @param {{row: string, detail?: string}} outcome
58
+ */
59
+ export function recordStorageBoot(el, outcome) {
60
+ if (!el || typeof el.setAttribute !== 'function' || typeof el.removeAttribute !== 'function') {
61
+ throw new TypeError('storage boot: recordStorageBoot needs a DOM element');
62
+ }
63
+ if (!outcome || !BOOT_ROWS.has(outcome.row)) {
64
+ throw new Error('storage boot: ' + JSON.stringify(outcome && outcome.row) + ' is not a STORAGE_BOOT_OUTCOMES row');
65
+ }
66
+ el.setAttribute(STORAGE_BOOT_ATTRS.row, outcome.row);
67
+ if (outcome.detail === undefined) { el.removeAttribute(STORAGE_BOOT_ATTRS.detail); }
68
+ else { el.setAttribute(STORAGE_BOOT_ATTRS.detail, outcome.detail); }
69
+ }
@@ -0,0 +1,169 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/storage — THE StorageProvider seam.
5
+ *
6
+ * This module is the CONTRACT for all persistent storage used by the FraqtlIde
7
+ * author surface. It defines the interface every provider must implement and
8
+ * exposes the active provider instance chosen at boot time.
9
+ *
10
+ * ── Why this exists ────────────────────────────────────────────────────────
11
+ *
12
+ * The IDE needs project persistence. Today the active driver is browser
13
+ * localStorage (see local-storage-provider.js). Tomorrow it may be a
14
+ * server-side REST backend (see lib/server/mounts.js §/api/storage).
15
+ *
16
+ * The seam lives here so the IDE and all tooling import only this module and
17
+ * never depend on localStorage or any backend directly. Swapping the backend
18
+ * requires changing only which provider is passed to setActiveProvider() —
19
+ * ZERO changes to IDE or AuthorView code.
20
+ *
21
+ * ── The contract (every provider MUST implement) ────────────────────────────
22
+ *
23
+ * get(namespace, key) → value (any JSON-serialisable) | null
24
+ * put(namespace, key, value) → void
25
+ * delete(namespace, key) → void
26
+ * list(namespace) → Array<{ key: string, ...metadata }>
27
+ *
28
+ * Rules:
29
+ * • namespace and key are non-empty strings. "/" is reserved.
30
+ * • value must be JSON-serialisable. Providers are free to stringify it.
31
+ * • list returns one entry per key in the namespace (order unspecified).
32
+ * Each entry MUST contain at least { key: string }. Providers may add
33
+ * metadata (e.g. updatedAt).
34
+ * • get on a missing key returns null — never throws.
35
+ * • delete on a missing key is a no-op — never throws.
36
+ * • All other contract violations (bad types, absent localStorage, etc.)
37
+ * MUST throw a descriptive Error — FAIL-LOUD, never silent.
38
+ *
39
+ * ── Usage ──────────────────────────────────────────────────────────────────
40
+ *
41
+ * // During browser app boot, wire the active driver once (the ONE installer):
42
+ * import { bootLocalStorage } from 'lib/shim/storage/boot.js';
43
+ * bootLocalStorage(); // → { row: 'installed' | 'already-active' | 'unavailable', detail? }
44
+ *
45
+ * // Then anywhere in the IDE:
46
+ * import { getProvider } from 'lib/shim/storage';
47
+ * const storage = getProvider();
48
+ * storage.put('projects', 'abc123', { name: 'My project' });
49
+ * const saved = storage.get('projects', 'abc123');
50
+ * const all = storage.list('projects');
51
+ * storage.delete('projects', 'abc123');
52
+ *
53
+ * ── Contract validation ─────────────────────────────────────────────────────
54
+ *
55
+ * validateProvider(provider) — throws if provider does not implement the
56
+ * contract. Called by setActiveProvider; consumers may also call it.
57
+ *
58
+ * ── Server-side seam ───────────────────────────────────────────────────────
59
+ *
60
+ * The matching server routes are defined in lib/server/mounts.js under
61
+ * /api/storage/:namespace (LIST)
62
+ * /api/storage/:namespace/:key (GET / PUT / DELETE)
63
+ * They are documented as the future-backend seam: when a server-side driver
64
+ * is ready, a RemoteStorageProvider that calls those routes will slot in here
65
+ * with no IDE changes.
66
+ */
67
+
68
+ 'use strict';
69
+
70
+ /** @type {StorageProvider|null} */
71
+ var _activeProvider = null;
72
+
73
+ // ── Contract typedefs ──────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * @typedef {object} StorageProvider
77
+ * Every storage driver must implement this interface exactly.
78
+ *
79
+ * @property {function(string, string): any|null} get
80
+ * Retrieve a value. Returns null if not found; never throws on missing key.
81
+ *
82
+ * @property {function(string, string, any): void} put
83
+ * Write a JSON-serialisable value. Overwrites any existing value.
84
+ *
85
+ * @property {function(string, string): void} delete
86
+ * Remove a key. No-op if key does not exist; never throws.
87
+ *
88
+ * @property {function(string): Array<{key: string}>} list
89
+ * Enumerate all keys in a namespace. Each entry contains at least
90
+ * { key: string } and may contain additional metadata fields.
91
+ */
92
+
93
+ // ── Contract validation ────────────────────────────────────────────────────
94
+
95
+ var _REQUIRED_METHODS = ['get', 'put', 'delete', 'list'];
96
+
97
+ /**
98
+ * Assert that provider implements the StorageProvider contract.
99
+ * Throws a descriptive Error on the first violation found — FAIL-LOUD.
100
+ *
101
+ * @param {any} provider
102
+ * @throws {Error}
103
+ */
104
+ export function validateProvider(provider) {
105
+ if (!provider || typeof provider !== 'object') {
106
+ throw new Error(
107
+ '[StorageProvider] provider must be a non-null object; got ' + typeof provider
108
+ );
109
+ }
110
+ for (var i = 0; i < _REQUIRED_METHODS.length; i++) {
111
+ var method = _REQUIRED_METHODS[i];
112
+ if (typeof provider[method] !== 'function') {
113
+ throw new Error(
114
+ '[StorageProvider] provider is missing required method "' + method + '"'
115
+ );
116
+ }
117
+ }
118
+ }
119
+
120
+ // ── Active provider management ─────────────────────────────────────────────
121
+
122
+ /**
123
+ * Register the active storage driver. Must be called once at boot before
124
+ * any call to getProvider().
125
+ *
126
+ * @param {StorageProvider} provider
127
+ * @throws {Error} if provider does not implement the contract.
128
+ */
129
+ export function setActiveProvider(provider) {
130
+ validateProvider(provider);
131
+ _activeProvider = provider;
132
+ }
133
+
134
+ /**
135
+ * Retrieve the active storage driver.
136
+ * FAIL-LOUD: throws if no provider has been registered.
137
+ *
138
+ * @returns {StorageProvider}
139
+ * @throws {Error}
140
+ */
141
+ export function getProvider() {
142
+ if (!_activeProvider) {
143
+ throw new Error(
144
+ '[StorageProvider] no active provider has been set. ' +
145
+ 'Call bootLocalStorage() (lib/shim/storage/boot.js) during app boot, or setActiveProvider(<provider>) ' +
146
+ 'for a non-browser driver.'
147
+ );
148
+ }
149
+ return _activeProvider;
150
+ }
151
+
152
+ /**
153
+ * Return the active provider, or null if none has been registered.
154
+ * Use this instead of getProvider() when a nil-safe check is appropriate
155
+ * (e.g. during test setup before a provider is wired).
156
+ *
157
+ * @returns {StorageProvider|null}
158
+ */
159
+ export function peekProvider() {
160
+ return _activeProvider;
161
+ }
162
+
163
+ /**
164
+ * Reset the active provider to null. For tests only — do not call in
165
+ * production code.
166
+ */
167
+ export function resetProvider() {
168
+ _activeProvider = null;
169
+ }
@@ -0,0 +1,230 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * LocalStorageProvider — THE active storage driver (browser localStorage).
5
+ *
6
+ * Implements the StorageProvider contract defined in lib/shim/storage/index.js.
7
+ *
8
+ * ── Key format ─────────────────────────────────────────────────────────────
9
+ *
10
+ * frqtl:<namespace>:<key>
11
+ *
12
+ * e.g. "frqtl:projects:abc123"
13
+ *
14
+ * ── Serialisation ──────────────────────────────────────────────────────────
15
+ *
16
+ * Values are stored as JSON strings. Non-serialisable values (functions,
17
+ * circular references, etc.) FAIL-LOUD — the error surfaces immediately so
18
+ * the caller knows the write did not persist, rather than silently losing data.
19
+ *
20
+ * ── Namespace enumeration ──────────────────────────────────────────────────
21
+ *
22
+ * list() iterates all localStorage keys, collects those with the prefix
23
+ * "frqtl:<namespace>:", and parses the stored JSON to extract any metadata
24
+ * fields that were stored alongside the value.
25
+ *
26
+ * list() always returns plain-object entries of the form
27
+ * { key: string, ...any metadata from the stored envelope }.
28
+ * It never throws on individual malformed entries — those are skipped and
29
+ * a console.warn is emitted so the problem is visible without crashing the
30
+ * list view.
31
+ *
32
+ * ── Absent localStorage ─────────────────────────────────────────────────────
33
+ *
34
+ * The constructor FAIL-LOUDs with a clear message if window.localStorage is
35
+ * not available (e.g. in a sandboxed iframe with storage blocked, or in a
36
+ * non-browser environment). Use a different provider in those contexts.
37
+ *
38
+ * ── Usage ──────────────────────────────────────────────────────────────────
39
+ *
40
+ * Constructed in ONE place: lib/shim/storage/boot.js bootLocalStorage(), which a browser
41
+ * surface calls at boot (gated by test/boot.test.mjs). Never construct it elsewhere.
42
+ */
43
+
44
+ 'use strict';
45
+
46
+ var _PREFIX = 'frqtl';
47
+
48
+ /**
49
+ * Build the localStorage key for a given namespace + key pair — the ONE owner of the
50
+ * key format (exported so a harness that seeds storage never re-types it).
51
+ * @param {string} namespace
52
+ * @param {string} key
53
+ * @returns {string}
54
+ */
55
+ export function storageKeyOf(namespace, key) {
56
+ return _PREFIX + ':' + namespace + ':' + key;
57
+ }
58
+
59
+ /**
60
+ * Build the namespace prefix used to enumerate all keys in a namespace.
61
+ * @param {string} namespace
62
+ * @returns {string}
63
+ */
64
+ function _namespacePrefix(namespace) {
65
+ return _PREFIX + ':' + namespace + ':';
66
+ }
67
+
68
+ /**
69
+ * Assert that a string argument is a valid namespace or key.
70
+ * FAIL-LOUD.
71
+ *
72
+ * @param {string} value
73
+ * @param {string} argName — used in the error message
74
+ */
75
+ function _assertNonEmptyString(value, argName) {
76
+ if (typeof value !== 'string' || value.length === 0) {
77
+ throw new Error(
78
+ '[LocalStorageProvider] ' + argName + ' must be a non-empty string; got: ' + JSON.stringify(value)
79
+ );
80
+ }
81
+ if (value.indexOf('/') !== -1) {
82
+ throw new Error(
83
+ '[LocalStorageProvider] ' + argName + ' must not contain "/"; got: ' + JSON.stringify(value)
84
+ );
85
+ }
86
+ }
87
+
88
+ export class LocalStorageProvider {
89
+ /**
90
+ * @throws {Error} if window.localStorage is not available.
91
+ */
92
+ constructor() {
93
+ // FAIL-LOUD if localStorage is not available.
94
+ if (typeof window === 'undefined' || typeof window.localStorage === 'undefined' || window.localStorage === null) {
95
+ throw new Error(
96
+ '[LocalStorageProvider] window.localStorage is not available in this environment. ' +
97
+ 'Use a different StorageProvider (e.g. a server-backed RemoteStorageProvider) ' +
98
+ 'or ensure the page is served in a context where localStorage is permitted.'
99
+ );
100
+ }
101
+ // Probe with a write+delete to detect private/blocked storage.
102
+ try {
103
+ var _probe = '__frqtl_storage_probe__';
104
+ window.localStorage.setItem(_probe, '1');
105
+ window.localStorage.removeItem(_probe);
106
+ } catch (e) {
107
+ throw new Error(
108
+ '[LocalStorageProvider] window.localStorage is present but write-blocked (private ' +
109
+ 'browsing with strict storage restrictions?). Error: ' + (e && e.message ? e.message : String(e))
110
+ );
111
+ }
112
+ /** @type {Storage} */
113
+ this._ls = window.localStorage;
114
+ }
115
+
116
+ /**
117
+ * Retrieve a stored value.
118
+ *
119
+ * @param {string} namespace
120
+ * @param {string} key
121
+ * @returns {any|null} Parsed JSON value, or null if not found.
122
+ */
123
+ get(namespace, key) {
124
+ _assertNonEmptyString(namespace, 'namespace');
125
+ _assertNonEmptyString(key, 'key');
126
+ var raw = this._ls.getItem(storageKeyOf(namespace, key));
127
+ if (raw === null) { return null; }
128
+ try {
129
+ return JSON.parse(raw);
130
+ } catch (e) {
131
+ throw new Error(
132
+ '[LocalStorageProvider] stored value for "' + namespace + '/' + key +
133
+ '" is not valid JSON. Storage may be corrupted. Error: ' +
134
+ (e && e.message ? e.message : String(e))
135
+ );
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Write a value. Overwrites any existing value.
141
+ *
142
+ * @param {string} namespace
143
+ * @param {string} key
144
+ * @param {any} value Must be JSON-serialisable — FAIL-LOUD if not.
145
+ */
146
+ put(namespace, key, value) {
147
+ _assertNonEmptyString(namespace, 'namespace');
148
+ _assertNonEmptyString(key, 'key');
149
+ var serialised;
150
+ try {
151
+ serialised = JSON.stringify(value);
152
+ } catch (e) {
153
+ throw new Error(
154
+ '[LocalStorageProvider] value for "' + namespace + '/' + key +
155
+ '" is not JSON-serialisable: ' + (e && e.message ? e.message : String(e))
156
+ );
157
+ }
158
+ try {
159
+ this._ls.setItem(storageKeyOf(namespace, key), serialised);
160
+ } catch (e) {
161
+ throw new Error(
162
+ '[LocalStorageProvider] localStorage.setItem failed for "' + namespace + '/' + key +
163
+ '" — storage may be full (QuotaExceededError) or blocked. Error: ' +
164
+ (e && e.message ? e.message : String(e))
165
+ );
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Remove a key. No-op if key does not exist.
171
+ *
172
+ * @param {string} namespace
173
+ * @param {string} key
174
+ */
175
+ delete(namespace, key) {
176
+ _assertNonEmptyString(namespace, 'namespace');
177
+ _assertNonEmptyString(key, 'key');
178
+ this._ls.removeItem(storageKeyOf(namespace, key));
179
+ }
180
+
181
+ /**
182
+ * List all entries in a namespace.
183
+ *
184
+ * Each entry contains at minimum { key: string }. If the stored value is
185
+ * an object with additional metadata fields (e.g. id, name, updatedAt),
186
+ * those are spread into the entry.
187
+ *
188
+ * Malformed entries (unparseable JSON, unexpected shapes) are skipped with a
189
+ * console.warn — the list call itself never throws.
190
+ *
191
+ * @param {string} namespace
192
+ * @returns {Array<{key: string}>}
193
+ */
194
+ list(namespace) {
195
+ _assertNonEmptyString(namespace, 'namespace');
196
+ var prefix = _namespacePrefix(namespace);
197
+ var results = [];
198
+ var ls = this._ls;
199
+ var len = ls.length;
200
+ for (var i = 0; i < len; i++) {
201
+ var storageKey = ls.key(i);
202
+ if (storageKey === null) { continue; }
203
+ if (storageKey.indexOf(prefix) !== 0) { continue; }
204
+ // Extract the user-facing key from the storage key.
205
+ var userKey = storageKey.slice(prefix.length);
206
+ var raw = ls.getItem(storageKey);
207
+ var parsed = null;
208
+ try {
209
+ parsed = JSON.parse(raw);
210
+ } catch (e) {
211
+ console.warn(
212
+ '[LocalStorageProvider] skipping malformed entry "' + storageKey + '": ' +
213
+ (e && e.message ? e.message : String(e))
214
+ );
215
+ continue;
216
+ }
217
+ // If the stored value is an object, spread its fields as metadata.
218
+ var entry = { key: userKey };
219
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
220
+ for (var field in parsed) {
221
+ if (Object.prototype.hasOwnProperty.call(parsed, field) && field !== 'key') {
222
+ entry[field] = parsed[field];
223
+ }
224
+ }
225
+ }
226
+ results.push(entry);
227
+ }
228
+ return results;
229
+ }
230
+ }
@@ -0,0 +1,132 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/storage/make-namespace-store.js — Generic namespace-CRUD factory.
5
+ *
6
+ * Factors out the byte-identical pattern shared by modules.js, projects.js,
7
+ * and lib/shim/notebook/store.js. Each of those files is a thin config call
8
+ * over this factory; the public-API exports they expose are unchanged.
9
+ *
10
+ * ── Signature ──────────────────────────────────────────────────────────────
11
+ *
12
+ * makeNamespaceStore(config) → { newId, save, load, list, delete: del }
13
+ *
14
+ * ── Config ─────────────────────────────────────────────────────────────────
15
+ *
16
+ * namespace {string} — StorageProvider namespace key (e.g. 'files').
17
+ * schemaVersion {number} — Current schema version integer (stamped on save
18
+ * by callers; used here only in the returned
19
+ * constant — actual stamping is the caller's job,
20
+ * as in the originals).
21
+ * idPrefix {string} — Prefix for generated ids (e.g. 'mod_').
22
+ * validate {function} — validate(record): throws on invalid envelope.
23
+ * summaryFields {function} — summaryFields(record) → summary object, or
24
+ * returns null to silently skip the record.
25
+ * migrate {function?} — Optional. migrate(raw) → migrated record.
26
+ * Applied in load() and list() after provider.get().
27
+ * listFilter {function?} — Optional. listFilter(record, opts) → boolean.
28
+ * Applied after migration. When absent, all
29
+ * non-null summaryFields results are included.
30
+ *
31
+ * ── Returned surface ───────────────────────────────────────────────────────
32
+ *
33
+ * newId() → string
34
+ * save(provider, record) → void (FAIL-LOUD; validate then put)
35
+ * load(provider, id) → object|null (migrate applied; FAIL-LOUD on bad id)
36
+ * list(provider, opts?) → Array (skip-malformed; sort updatedAt desc)
37
+ * del(provider, id) → void (no-op if absent; FAIL-LOUD on bad id)
38
+ *
39
+ * ── Arrows ─────────────────────────────────────────────────────────────────
40
+ *
41
+ * lib/shim → lib/shim (legal).
42
+ * lib/shim/notebook/store.js imports via ../storage/make-namespace-store.js.
43
+ */
44
+
45
+ 'use strict';
46
+
47
+ /**
48
+ * @param {{
49
+ * namespace: string,
50
+ * schemaVersion: number,
51
+ * idPrefix: string,
52
+ * validate: function,
53
+ * summaryFields: function,
54
+ * migrate?: function,
55
+ * listFilter?: function,
56
+ * }} config
57
+ * @returns {{ newId: function, save: function, load: function, list: function, delete: function }}
58
+ */
59
+ export function makeNamespaceStore(config) {
60
+ var namespace = config.namespace;
61
+ var idPrefix = config.idPrefix;
62
+ var validate = config.validate;
63
+ var summaryFields = config.summaryFields;
64
+ var migrate = config.migrate || null;
65
+ var listFilter = config.listFilter || null;
66
+
67
+ // ── newId ──────────────────────────────────────────────────────────────────
68
+
69
+ function newId() {
70
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
71
+ return idPrefix + crypto.randomUUID().replace(/-/g, '');
72
+ }
73
+ // Fallback: timestamp (base-36) + random bits (base-36)
74
+ var ts = Date.now().toString(36);
75
+ var rnd = Math.random().toString(36).slice(2, 10);
76
+ return idPrefix + ts + rnd;
77
+ }
78
+
79
+ // ── save ───────────────────────────────────────────────────────────────────
80
+
81
+ function save(provider, record) {
82
+ validate(record);
83
+ provider.put(namespace, record.id, record);
84
+ }
85
+
86
+ // ── load ───────────────────────────────────────────────────────────────────
87
+
88
+ function load(provider, id) {
89
+ if (typeof id !== 'string' || id.length === 0) {
90
+ throw new Error('[' + namespace + '] load: id must be a non-empty string');
91
+ }
92
+ var raw = provider.get(namespace, id);
93
+ if (raw === null || raw === undefined) { return null; }
94
+ return migrate ? migrate(raw) : raw;
95
+ }
96
+
97
+ // ── list ───────────────────────────────────────────────────────────────────
98
+
99
+ function list(provider, opts) {
100
+ var entries = provider.list(namespace);
101
+ var summaries = [];
102
+ for (var i = 0; i < entries.length; i++) {
103
+ var raw = provider.get(namespace, entries[i].key);
104
+ if (!raw || typeof raw.id !== 'string') {
105
+ continue; // silently skip malformed entries
106
+ }
107
+ var record = migrate ? migrate(raw) : raw;
108
+ if (listFilter && !listFilter(record, opts)) { continue; }
109
+ var summary = summaryFields(record);
110
+ if (summary === null || summary === undefined) { continue; }
111
+ summaries.push(summary);
112
+ }
113
+ // Sort most-recently-modified first.
114
+ summaries.sort(function (a, b) {
115
+ if (a.updatedAt < b.updatedAt) { return 1; }
116
+ if (a.updatedAt > b.updatedAt) { return -1; }
117
+ return 0;
118
+ });
119
+ return summaries;
120
+ }
121
+
122
+ // ── delete ─────────────────────────────────────────────────────────────────
123
+
124
+ function del(provider, id) {
125
+ if (typeof id !== 'string' || id.length === 0) {
126
+ throw new Error('[' + namespace + '] delete: id must be a non-empty string');
127
+ }
128
+ provider.delete(namespace, id);
129
+ }
130
+
131
+ return { newId: newId, save: save, load: load, list: list, 'delete': del };
132
+ }