@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.
- package/CLAUDE.md +87 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2922 -0
- package/dist/frqtl/objects/ObjectFactory.js +297 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +711 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +354 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
- package/dist/frqtl/objects/v1-replacements.js +30 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +35 -0
- package/dist/frqtl/parts/CollisionTypes.js +31 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
- package/dist/frqtl/parts/LargeObject.js +406 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +720 -0
- package/dist/frqtl/parts/OvenBox.js +612 -0
- package/dist/frqtl/parts/PointSource.js +763 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +212 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +692 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +326 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +672 -0
- package/dist/shim/notebook/knob.js +151 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +510 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +214 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +807 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- 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
|
+
}
|