@getforma/core 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-AJP4OVCN.js → chunk-3U2IQIKB.js} +4 -4
- package/dist/{chunk-AJP4OVCN.js.map → chunk-3U2IQIKB.js.map} +1 -1
- package/dist/{chunk-FBM7V4NE.cjs → chunk-D46EJ3VS.cjs} +43 -43
- package/dist/{chunk-FBM7V4NE.cjs.map → chunk-D46EJ3VS.cjs.map} +1 -1
- package/dist/{chunk-Y3A2EVVS.js → chunk-F5QUU76U.js} +4 -4
- package/dist/{chunk-Y3A2EVVS.js.map → chunk-F5QUU76U.js.map} +1 -1
- package/dist/{chunk-ETPJTPYD.js → chunk-FAIWRZKJ.js} +6 -3
- package/dist/chunk-FAIWRZKJ.js.map +1 -0
- package/dist/{chunk-H7MDUHS5.cjs → chunk-HAULTMNX.cjs} +6 -2
- package/dist/chunk-HAULTMNX.cjs.map +1 -0
- package/dist/{chunk-KUXNZ5MG.cjs → chunk-PAKPQRVE.cjs} +14 -12
- package/dist/{chunk-KUXNZ5MG.cjs.map → chunk-PAKPQRVE.cjs.map} +1 -1
- package/dist/formajs.global.js +69 -11
- package/dist/formajs.global.js.map +1 -1
- package/dist/http.cjs +11 -11
- package/dist/http.js +2 -2
- package/dist/index.cjs +143 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -5
- package/dist/index.d.ts +39 -5
- package/dist/index.js +67 -16
- package/dist/index.js.map +1 -1
- package/dist/{resource-D6HfVgiA.d.cts → resource-CBTcorBJ.d.ts} +2 -2
- package/dist/{resource-ljs2X5NM.d.ts → resource-CaFfIB5i.d.cts} +2 -2
- package/dist/runtime.cjs +28 -28
- package/dist/runtime.js +3 -3
- package/dist/server.cjs +7 -7
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/dist/{signal-CRBJYQ6B.d.cts → signal-CIXTR4Qz.d.cts} +29 -1
- package/dist/{signal-CRBJYQ6B.d.ts → signal-CIXTR4Qz.d.ts} +29 -1
- package/dist/storage.cjs +66 -16
- package/dist/storage.cjs.map +1 -1
- package/dist/storage.js +66 -16
- package/dist/storage.js.map +1 -1
- package/dist/tc39-compat.cjs +3 -3
- package/dist/tc39-compat.d.cts +1 -1
- package/dist/tc39-compat.d.ts +1 -1
- package/dist/tc39-compat.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ETPJTPYD.js.map +0 -1
- package/dist/chunk-H7MDUHS5.cjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SignalGetter } from './signal-
|
|
1
|
+
import { S as SignalGetter } from './signal-CIXTR4Qz.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Forma Reactive - Resource
|
|
@@ -66,4 +66,4 @@ interface ResourceOptions<T> {
|
|
|
66
66
|
*/
|
|
67
67
|
declare function createResource<T, S = true>(source: SignalGetter<S>, fetcher: (source: S, info: ResourceFetcherInfo) => Promise<T>, options?: ResourceOptions<T>): Resource<T>;
|
|
68
68
|
|
|
69
|
-
export { type Resource as R, type
|
|
69
|
+
export { type Resource as R, type ResourceFetcherInfo as a, type ResourceOptions as b, createResource as c };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as SignalGetter } from './signal-
|
|
1
|
+
import { S as SignalGetter } from './signal-CIXTR4Qz.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Forma Reactive - Resource
|
|
@@ -66,4 +66,4 @@ interface ResourceOptions<T> {
|
|
|
66
66
|
*/
|
|
67
67
|
declare function createResource<T, S = true>(source: SignalGetter<S>, fetcher: (source: S, info: ResourceFetcherInfo) => Promise<T>, options?: ResourceOptions<T>): Resource<T>;
|
|
68
68
|
|
|
69
|
-
export { type Resource as R, type
|
|
69
|
+
export { type Resource as R, type ResourceFetcherInfo as a, type ResourceOptions as b, createResource as c };
|
package/dist/runtime.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkD46EJ3VS_cjs = require('./chunk-D46EJ3VS.cjs');
|
|
4
4
|
var chunk263HW3KN_cjs = require('./chunk-263HW3KN.cjs');
|
|
5
|
-
var
|
|
6
|
-
var
|
|
5
|
+
var chunkPAKPQRVE_cjs = require('./chunk-PAKPQRVE.cjs');
|
|
6
|
+
var chunkHAULTMNX_cjs = require('./chunk-HAULTMNX.cjs');
|
|
7
7
|
|
|
8
8
|
// src/dom/reconcile.ts
|
|
9
9
|
function getBindTargets(el) {
|
|
@@ -1062,7 +1062,7 @@ function parseIfHandler(expr, scope) {
|
|
|
1062
1062
|
}
|
|
1063
1063
|
if (rest.length > 0) return null;
|
|
1064
1064
|
return (e) => {
|
|
1065
|
-
|
|
1065
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1066
1066
|
if (condExpr()) thenHandler(e);
|
|
1067
1067
|
else elseHandler?.(e);
|
|
1068
1068
|
});
|
|
@@ -1816,7 +1816,7 @@ function parseHandler(expr, scope) {
|
|
|
1816
1816
|
const handlers = stmts.map((s) => parseHandler(s, scope));
|
|
1817
1817
|
if (handlers.every((h) => h !== null)) {
|
|
1818
1818
|
return (e) => {
|
|
1819
|
-
|
|
1819
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1820
1820
|
for (const h of handlers) h(e);
|
|
1821
1821
|
});
|
|
1822
1822
|
};
|
|
@@ -1829,7 +1829,7 @@ function parseHandler(expr, scope) {
|
|
|
1829
1829
|
const name = incrMatch[1];
|
|
1830
1830
|
const op = incrMatch[2];
|
|
1831
1831
|
return () => {
|
|
1832
|
-
|
|
1832
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1833
1833
|
const val = scope.getters[name]?.() ?? 0;
|
|
1834
1834
|
scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
|
|
1835
1835
|
});
|
|
@@ -1840,7 +1840,7 @@ function parseHandler(expr, scope) {
|
|
|
1840
1840
|
const op = preIncrMatch[1];
|
|
1841
1841
|
const name = preIncrMatch[2];
|
|
1842
1842
|
return () => {
|
|
1843
|
-
|
|
1843
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1844
1844
|
const val = scope.getters[name]?.() ?? 0;
|
|
1845
1845
|
scope.setters[name]?.(op === "++" ? val + 1 : val - 1);
|
|
1846
1846
|
});
|
|
@@ -1850,7 +1850,7 @@ function parseHandler(expr, scope) {
|
|
|
1850
1850
|
if (toggleMatch && toggleMatch[1] === toggleMatch[2]) {
|
|
1851
1851
|
const name = toggleMatch[1];
|
|
1852
1852
|
return () => {
|
|
1853
|
-
|
|
1853
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1854
1854
|
scope.setters[name]?.(!scope.getters[name]?.());
|
|
1855
1855
|
});
|
|
1856
1856
|
};
|
|
@@ -1862,7 +1862,7 @@ function parseHandler(expr, scope) {
|
|
|
1862
1862
|
if (valExpr) {
|
|
1863
1863
|
if (_debug) dbg(`parseHandler: assignment "${name} = ..." \u2014 setter exists:`, !!scope.setters[name], ", getter exists:", !!scope.getters[name]);
|
|
1864
1864
|
return () => {
|
|
1865
|
-
|
|
1865
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1866
1866
|
const val = valExpr();
|
|
1867
1867
|
if (_debug) dbg(`SETTER: ${name} = ${val} (was: ${scope.getters[name]?.()})`);
|
|
1868
1868
|
scope.setters[name]?.(val);
|
|
@@ -1877,7 +1877,7 @@ function parseHandler(expr, scope) {
|
|
|
1877
1877
|
const valExpr = parseExpression(compoundMatch[3].trim(), scope);
|
|
1878
1878
|
if (valExpr) {
|
|
1879
1879
|
return () => {
|
|
1880
|
-
|
|
1880
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
1881
1881
|
const current = scope.getters[name]?.() ?? 0;
|
|
1882
1882
|
const val = valExpr();
|
|
1883
1883
|
switch (op) {
|
|
@@ -1958,7 +1958,7 @@ function buildHandler(expr, scope) {
|
|
|
1958
1958
|
}
|
|
1959
1959
|
});
|
|
1960
1960
|
const unsafeHandler = (e) => {
|
|
1961
|
-
|
|
1961
|
+
chunkPAKPQRVE_cjs.batch(() => fn(proxy, e, e));
|
|
1962
1962
|
};
|
|
1963
1963
|
const result = {
|
|
1964
1964
|
handler: unsafeHandler,
|
|
@@ -2006,7 +2006,7 @@ function initScope(stateEl) {
|
|
|
2006
2006
|
const getters = {};
|
|
2007
2007
|
const setters = {};
|
|
2008
2008
|
for (const [key, initial] of Object.entries(state)) {
|
|
2009
|
-
const [get, set] =
|
|
2009
|
+
const [get, set] = chunkHAULTMNX_cjs.createSignal(initial);
|
|
2010
2010
|
getters[key] = get;
|
|
2011
2011
|
setters[key] = set;
|
|
2012
2012
|
}
|
|
@@ -2118,7 +2118,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2118
2118
|
const prevGetter = scope.getters[name];
|
|
2119
2119
|
delete scope.getters[name];
|
|
2120
2120
|
const evaluate = buildEvaluator(`{${expr}}`, scope);
|
|
2121
|
-
const getter =
|
|
2121
|
+
const getter = chunkHAULTMNX_cjs.createComputed(evaluate);
|
|
2122
2122
|
scope.getters[name] = getter;
|
|
2123
2123
|
if (!prevGetter) {
|
|
2124
2124
|
delete scope.setters[name];
|
|
@@ -2129,7 +2129,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2129
2129
|
const textExpr = !known || known.has("data-text") ? el.getAttribute("data-text") : null;
|
|
2130
2130
|
if (textExpr) {
|
|
2131
2131
|
const evaluate = buildEvaluator(textExpr, scope);
|
|
2132
|
-
const dispose =
|
|
2132
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2133
2133
|
setElementTextFast(el, toTextValue(evaluate()));
|
|
2134
2134
|
});
|
|
2135
2135
|
disposers.push(dispose);
|
|
@@ -2144,7 +2144,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2144
2144
|
dbg(`bindElement: data-show="${showExpr}" on <${tag}${cls}>`);
|
|
2145
2145
|
}
|
|
2146
2146
|
let initialized = false;
|
|
2147
|
-
const dispose =
|
|
2147
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2148
2148
|
const visible = !!evaluate();
|
|
2149
2149
|
if (_debug) dbg(`data-show effect: "${showExpr}" \u2192 ${visible}`);
|
|
2150
2150
|
applyShowVisibility(el, visible, transition, !initialized);
|
|
@@ -2163,7 +2163,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2163
2163
|
const parent = el.parentNode;
|
|
2164
2164
|
let inserted = true;
|
|
2165
2165
|
let initialized = false;
|
|
2166
|
-
const dispose =
|
|
2166
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2167
2167
|
const show = !!evaluate();
|
|
2168
2168
|
if (show && !inserted) {
|
|
2169
2169
|
clearTransitionState(el);
|
|
@@ -2214,7 +2214,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2214
2214
|
if (getter && setter) {
|
|
2215
2215
|
const input = el;
|
|
2216
2216
|
const tag = input.tagName;
|
|
2217
|
-
const dispose =
|
|
2217
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2218
2218
|
const val = getter();
|
|
2219
2219
|
const type = input.type;
|
|
2220
2220
|
if (type === "checkbox") {
|
|
@@ -2302,14 +2302,14 @@ function bindElement(el, scope, disposers) {
|
|
|
2302
2302
|
} else if (name.startsWith("data-class:")) {
|
|
2303
2303
|
const cls = name.slice(11);
|
|
2304
2304
|
const evaluate = buildEvaluator(attr.value, scope);
|
|
2305
|
-
const dispose =
|
|
2305
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2306
2306
|
el.classList.toggle(cls, !!evaluate());
|
|
2307
2307
|
});
|
|
2308
2308
|
disposers.push(dispose);
|
|
2309
2309
|
} else if (name.startsWith("data-bind:")) {
|
|
2310
2310
|
const attrName = name.slice(10);
|
|
2311
2311
|
const evaluate = buildEvaluator(attr.value, scope);
|
|
2312
|
-
const dispose =
|
|
2312
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2313
2313
|
const val = evaluate();
|
|
2314
2314
|
if (val == null || val === false) {
|
|
2315
2315
|
el.removeAttribute(attrName);
|
|
@@ -2337,7 +2337,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2337
2337
|
if (saved !== null) setter(JSON.parse(saved));
|
|
2338
2338
|
} catch {
|
|
2339
2339
|
}
|
|
2340
|
-
const dispose =
|
|
2340
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2341
2341
|
try {
|
|
2342
2342
|
localStorage.setItem(key, JSON.stringify(getter()));
|
|
2343
2343
|
} catch {
|
|
@@ -2424,7 +2424,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2424
2424
|
});
|
|
2425
2425
|
}
|
|
2426
2426
|
} : void 0;
|
|
2427
|
-
const dispose =
|
|
2427
|
+
const dispose = chunkPAKPQRVE_cjs.internalEffect(() => {
|
|
2428
2428
|
const rawItems = evaluate();
|
|
2429
2429
|
if (!Array.isArray(rawItems)) {
|
|
2430
2430
|
for (const n of oldNodes) {
|
|
@@ -2447,7 +2447,7 @@ function bindElement(el, scope, disposers) {
|
|
|
2447
2447
|
const wrapped = rawItems.map((item, i) => ({ __idx: i, __item: item }));
|
|
2448
2448
|
const oldWrapped = oldItems;
|
|
2449
2449
|
const keyFn = keyProp ? (w) => String(w.__item?.[keyProp] ?? "") : (w) => w.__idx;
|
|
2450
|
-
const result =
|
|
2450
|
+
const result = chunkD46EJ3VS_cjs.reconcileList(
|
|
2451
2451
|
el,
|
|
2452
2452
|
oldWrapped,
|
|
2453
2453
|
wrapped,
|
|
@@ -2495,16 +2495,16 @@ function bindElement(el, scope, disposers) {
|
|
|
2495
2495
|
else if (k === "error") errorTarget = v;
|
|
2496
2496
|
else if (k === "poll") interval = parseInt(v ?? "0", 10);
|
|
2497
2497
|
}
|
|
2498
|
-
const [getTarget, setTarget] =
|
|
2498
|
+
const [getTarget, setTarget] = chunkHAULTMNX_cjs.createSignal(null);
|
|
2499
2499
|
scope.getters[target] = getTarget;
|
|
2500
2500
|
scope.setters[target] = setTarget;
|
|
2501
2501
|
if (loadingTarget) {
|
|
2502
|
-
const [gl, sl] =
|
|
2502
|
+
const [gl, sl] = chunkHAULTMNX_cjs.createSignal(false);
|
|
2503
2503
|
scope.getters[loadingTarget] = gl;
|
|
2504
2504
|
scope.setters[loadingTarget] = sl;
|
|
2505
2505
|
}
|
|
2506
2506
|
if (errorTarget) {
|
|
2507
|
-
const [ge, se] =
|
|
2507
|
+
const [ge, se] = chunkHAULTMNX_cjs.createSignal(null);
|
|
2508
2508
|
scope.getters[errorTarget] = ge;
|
|
2509
2509
|
scope.setters[errorTarget] = se;
|
|
2510
2510
|
}
|
|
@@ -2833,7 +2833,7 @@ function getScopes() {
|
|
|
2833
2833
|
function setScopeValue(element, key, value) {
|
|
2834
2834
|
const scope = element.__formaScope;
|
|
2835
2835
|
if (!scope?.setters[key]) return;
|
|
2836
|
-
|
|
2836
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
2837
2837
|
scope.setters[key](value);
|
|
2838
2838
|
});
|
|
2839
2839
|
}
|
|
@@ -2842,7 +2842,7 @@ function resetScope(element) {
|
|
|
2842
2842
|
const initialJSON = element.__formaInitialState;
|
|
2843
2843
|
if (!scope || !initialJSON) return;
|
|
2844
2844
|
const initial = parseState(initialJSON);
|
|
2845
|
-
|
|
2845
|
+
chunkPAKPQRVE_cjs.batch(() => {
|
|
2846
2846
|
for (const [key, val] of Object.entries(initial)) {
|
|
2847
2847
|
scope.setters[key]?.(val);
|
|
2848
2848
|
}
|
|
@@ -2872,7 +2872,7 @@ function getReconciler() {
|
|
|
2872
2872
|
}
|
|
2873
2873
|
}
|
|
2874
2874
|
},
|
|
2875
|
-
batch:
|
|
2875
|
+
batch: chunkPAKPQRVE_cjs.batch
|
|
2876
2876
|
});
|
|
2877
2877
|
}
|
|
2878
2878
|
return _reconciler;
|
package/dist/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { reconcileList } from './chunk-
|
|
1
|
+
import { reconcileList } from './chunk-3U2IQIKB.js';
|
|
2
2
|
import { isEventHandlerAttr, isUrlAttr, isDangerousUrl } from './chunk-2QQBKQIF.js';
|
|
3
|
-
import { internalEffect, batch } from './chunk-
|
|
4
|
-
import { createSignal, createComputed } from './chunk-
|
|
3
|
+
import { internalEffect, batch } from './chunk-F5QUU76U.js';
|
|
4
|
+
import { createSignal, createComputed } from './chunk-FAIWRZKJ.js';
|
|
5
5
|
|
|
6
6
|
// src/dom/reconcile.ts
|
|
7
7
|
function getBindTargets(el) {
|
package/dist/server.cjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunkPAKPQRVE_cjs = require('./chunk-PAKPQRVE.cjs');
|
|
4
|
+
var chunkHAULTMNX_cjs = require('./chunk-HAULTMNX.cjs');
|
|
5
5
|
|
|
6
6
|
// src/server/action.ts
|
|
7
7
|
function createAction(serverFn, options) {
|
|
8
|
-
const [pending, setPending] =
|
|
9
|
-
const [error, setError] =
|
|
8
|
+
const [pending, setPending] = chunkHAULTMNX_cjs.createSignal(false);
|
|
9
|
+
const [error, setError] = chunkHAULTMNX_cjs.createSignal(void 0);
|
|
10
10
|
const action = async (...args) => {
|
|
11
11
|
setPending(true);
|
|
12
12
|
setError(void 0);
|
|
13
13
|
if (options?.optimistic) {
|
|
14
14
|
try {
|
|
15
|
-
|
|
15
|
+
chunkPAKPQRVE_cjs.batch(() => options.optimistic(...args));
|
|
16
16
|
} catch {
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
try {
|
|
20
20
|
const result = await serverFn(...args);
|
|
21
21
|
if (options?.onSuccess) {
|
|
22
|
-
|
|
22
|
+
chunkPAKPQRVE_cjs.batch(() => options.onSuccess(result, ...args));
|
|
23
23
|
}
|
|
24
24
|
if (options?.invalidates) {
|
|
25
25
|
for (const resource of options.invalidates) {
|
|
@@ -31,7 +31,7 @@ function createAction(serverFn, options) {
|
|
|
31
31
|
} catch (err) {
|
|
32
32
|
if (options?.onError) {
|
|
33
33
|
try {
|
|
34
|
-
|
|
34
|
+
chunkPAKPQRVE_cjs.batch(() => options.onError(err, ...args));
|
|
35
35
|
} catch {
|
|
36
36
|
}
|
|
37
37
|
}
|
package/dist/server.d.cts
CHANGED
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { batch } from './chunk-
|
|
2
|
-
import { createSignal } from './chunk-
|
|
1
|
+
import { batch } from './chunk-F5QUU76U.js';
|
|
2
|
+
import { createSignal } from './chunk-FAIWRZKJ.js';
|
|
3
3
|
|
|
4
4
|
// src/server/action.ts
|
|
5
5
|
function createAction(serverFn, options) {
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forma Reactive - Signal
|
|
3
|
+
*
|
|
4
|
+
* Fine-grained reactive primitive backed by alien-signals.
|
|
5
|
+
* API: createSignal returns [getter, setter] tuple following SolidJS conventions.
|
|
6
|
+
*
|
|
7
|
+
* TC39 Signals equivalent: Signal.State
|
|
8
|
+
*/
|
|
9
|
+
/** Debug name of a signal getter, if one was provided (dev-only). */
|
|
10
|
+
declare function getSignalName(fn: unknown): string | undefined;
|
|
1
11
|
/** A function that reads the current value of a signal. */
|
|
2
12
|
type SignalGetter<T> = () => T;
|
|
3
13
|
/** A function that updates a signal — accepts a value or an updater function. */
|
|
@@ -23,6 +33,24 @@ interface SignalOptions<T> {
|
|
|
23
33
|
*/
|
|
24
34
|
equals?: (prev: T, next: T) => boolean;
|
|
25
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a value so the setter treats it as a literal value, not a functional updater.
|
|
38
|
+
*
|
|
39
|
+
* When `T` is itself a function type, passing a function to the setter is
|
|
40
|
+
* ambiguous -- it looks like a functional update (`prev => next`). Use
|
|
41
|
+
* `value()` to disambiguate:
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* const [getFn, setFn] = createSignal<() => void>(() => console.log('a'));
|
|
45
|
+
*
|
|
46
|
+
* // BUG: interpreted as a functional update -- calls the arrow with prev
|
|
47
|
+
* // setFn(() => console.log('b'));
|
|
48
|
+
*
|
|
49
|
+
* // Correct: wraps in a thunk so the setter stores it as-is
|
|
50
|
+
* setFn(value(() => console.log('b')));
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function value<T>(v: T): () => T;
|
|
26
54
|
/**
|
|
27
55
|
* Create a reactive signal.
|
|
28
56
|
*
|
|
@@ -44,4 +72,4 @@ interface SignalOptions<T> {
|
|
|
44
72
|
*/
|
|
45
73
|
declare function createSignal<T>(initialValue: T, options?: SignalOptions<T>): [get: SignalGetter<T>, set: SignalSetter<T>];
|
|
46
74
|
|
|
47
|
-
export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c };
|
|
75
|
+
export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c, getSignalName as g, value as v };
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forma Reactive - Signal
|
|
3
|
+
*
|
|
4
|
+
* Fine-grained reactive primitive backed by alien-signals.
|
|
5
|
+
* API: createSignal returns [getter, setter] tuple following SolidJS conventions.
|
|
6
|
+
*
|
|
7
|
+
* TC39 Signals equivalent: Signal.State
|
|
8
|
+
*/
|
|
9
|
+
/** Debug name of a signal getter, if one was provided (dev-only). */
|
|
10
|
+
declare function getSignalName(fn: unknown): string | undefined;
|
|
1
11
|
/** A function that reads the current value of a signal. */
|
|
2
12
|
type SignalGetter<T> = () => T;
|
|
3
13
|
/** A function that updates a signal — accepts a value or an updater function. */
|
|
@@ -23,6 +33,24 @@ interface SignalOptions<T> {
|
|
|
23
33
|
*/
|
|
24
34
|
equals?: (prev: T, next: T) => boolean;
|
|
25
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a value so the setter treats it as a literal value, not a functional updater.
|
|
38
|
+
*
|
|
39
|
+
* When `T` is itself a function type, passing a function to the setter is
|
|
40
|
+
* ambiguous -- it looks like a functional update (`prev => next`). Use
|
|
41
|
+
* `value()` to disambiguate:
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* const [getFn, setFn] = createSignal<() => void>(() => console.log('a'));
|
|
45
|
+
*
|
|
46
|
+
* // BUG: interpreted as a functional update -- calls the arrow with prev
|
|
47
|
+
* // setFn(() => console.log('b'));
|
|
48
|
+
*
|
|
49
|
+
* // Correct: wraps in a thunk so the setter stores it as-is
|
|
50
|
+
* setFn(value(() => console.log('b')));
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function value<T>(v: T): () => T;
|
|
26
54
|
/**
|
|
27
55
|
* Create a reactive signal.
|
|
28
56
|
*
|
|
@@ -44,4 +72,4 @@ interface SignalOptions<T> {
|
|
|
44
72
|
*/
|
|
45
73
|
declare function createSignal<T>(initialValue: T, options?: SignalOptions<T>): [get: SignalGetter<T>, set: SignalSetter<T>];
|
|
46
74
|
|
|
47
|
-
export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c };
|
|
75
|
+
export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c, getSignalName as g, value as v };
|
package/dist/storage.cjs
CHANGED
|
@@ -26,7 +26,10 @@ function createLocalStorage(key, options) {
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
remove() {
|
|
29
|
-
|
|
29
|
+
try {
|
|
30
|
+
localStorage.removeItem(key);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
30
33
|
}
|
|
31
34
|
};
|
|
32
35
|
}
|
|
@@ -57,37 +60,70 @@ function createSessionStorage(key, options) {
|
|
|
57
60
|
}
|
|
58
61
|
},
|
|
59
62
|
remove() {
|
|
60
|
-
|
|
63
|
+
try {
|
|
64
|
+
sessionStorage.removeItem(key);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
61
67
|
}
|
|
62
68
|
};
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
// src/storage/indexed.ts
|
|
66
72
|
var dbCache = /* @__PURE__ */ new Map();
|
|
73
|
+
var openLocks = /* @__PURE__ */ new Map();
|
|
67
74
|
function openDB(dbName, storeName) {
|
|
68
75
|
const cacheKey = `${dbName}::${storeName}`;
|
|
69
76
|
const cached = dbCache.get(cacheKey);
|
|
70
77
|
if (cached) return cached;
|
|
71
|
-
const
|
|
78
|
+
const prevLock = openLocks.get(dbName) ?? Promise.resolve();
|
|
79
|
+
const promise = prevLock.then(() => doOpen(dbName, storeName, cacheKey), () => doOpen(dbName, storeName, cacheKey));
|
|
80
|
+
dbCache.set(cacheKey, promise);
|
|
81
|
+
openLocks.set(dbName, promise.then(() => void 0, () => void 0));
|
|
82
|
+
promise.catch(() => {
|
|
83
|
+
dbCache.delete(cacheKey);
|
|
84
|
+
});
|
|
85
|
+
return promise;
|
|
86
|
+
}
|
|
87
|
+
function doOpen(dbName, storeName, cacheKey) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const wire = (db) => {
|
|
90
|
+
db.onversionchange = () => {
|
|
91
|
+
db.close();
|
|
92
|
+
dbCache.delete(cacheKey);
|
|
93
|
+
};
|
|
94
|
+
db.onclose = () => {
|
|
95
|
+
dbCache.delete(cacheKey);
|
|
96
|
+
};
|
|
97
|
+
resolve(db);
|
|
98
|
+
};
|
|
72
99
|
const probe = indexedDB.open(dbName);
|
|
73
100
|
probe.onerror = () => reject(probe.error);
|
|
74
101
|
probe.onsuccess = () => {
|
|
75
102
|
const db = probe.result;
|
|
76
103
|
if (db.objectStoreNames.contains(storeName)) {
|
|
77
|
-
|
|
104
|
+
wire(db);
|
|
78
105
|
return;
|
|
79
106
|
}
|
|
80
107
|
const nextVersion = db.version + 1;
|
|
81
108
|
db.close();
|
|
82
109
|
const upgrade = indexedDB.open(dbName, nextVersion);
|
|
83
110
|
upgrade.onerror = () => reject(upgrade.error);
|
|
111
|
+
upgrade.onblocked = () => reject(upgrade.error ?? new DOMException("Upgrade blocked: another connection is open", "AbortError"));
|
|
84
112
|
upgrade.onupgradeneeded = () => {
|
|
85
113
|
const upgradedDB = upgrade.result;
|
|
86
114
|
if (!upgradedDB.objectStoreNames.contains(storeName)) {
|
|
87
115
|
upgradedDB.createObjectStore(storeName);
|
|
88
116
|
}
|
|
89
117
|
};
|
|
90
|
-
upgrade.onsuccess = () =>
|
|
118
|
+
upgrade.onsuccess = () => {
|
|
119
|
+
const upgradedDB = upgrade.result;
|
|
120
|
+
if (!upgradedDB.objectStoreNames.contains(storeName)) {
|
|
121
|
+
upgradedDB.close();
|
|
122
|
+
reject(new DOMException(`Object store "${storeName}" was not created`, "NotFoundError"));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
wire(upgradedDB);
|
|
126
|
+
};
|
|
91
127
|
};
|
|
92
128
|
probe.onupgradeneeded = () => {
|
|
93
129
|
const db = probe.result;
|
|
@@ -96,23 +132,37 @@ function openDB(dbName, storeName) {
|
|
|
96
132
|
}
|
|
97
133
|
};
|
|
98
134
|
});
|
|
99
|
-
dbCache.set(cacheKey, promise);
|
|
100
|
-
promise.catch(() => {
|
|
101
|
-
dbCache.delete(cacheKey);
|
|
102
|
-
});
|
|
103
|
-
return promise;
|
|
104
135
|
}
|
|
105
136
|
function createIndexedDB(dbName, storeName = "default") {
|
|
137
|
+
const cacheKey = `${dbName}::${storeName}`;
|
|
106
138
|
function withStore(mode, fn) {
|
|
107
139
|
return openDB(dbName, storeName).then(
|
|
108
140
|
(db) => new Promise((resolve, reject) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
141
|
+
let request;
|
|
142
|
+
let tx;
|
|
143
|
+
try {
|
|
144
|
+
tx = db.transaction(storeName, mode);
|
|
145
|
+
request = fn(tx.objectStore(storeName));
|
|
146
|
+
} catch (err) {
|
|
147
|
+
reject(err);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let result;
|
|
151
|
+
request.onsuccess = () => {
|
|
152
|
+
result = request.result;
|
|
153
|
+
if (mode === "readonly") resolve(result);
|
|
154
|
+
};
|
|
155
|
+
tx.oncomplete = () => resolve(result);
|
|
156
|
+
tx.onerror = () => reject(tx.error ?? request.error);
|
|
157
|
+
tx.onabort = () => reject(tx.error ?? request.error ?? new DOMException("Transaction aborted", "AbortError"));
|
|
114
158
|
})
|
|
115
|
-
)
|
|
159
|
+
).catch((err) => {
|
|
160
|
+
const name = err?.name;
|
|
161
|
+
if (name === "InvalidStateError" || name === "AbortError" || name === "NotFoundError") {
|
|
162
|
+
dbCache.delete(cacheKey);
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
});
|
|
116
166
|
}
|
|
117
167
|
return {
|
|
118
168
|
get(key) {
|
package/dist/storage.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/storage/local.ts","../src/storage/session.ts","../src/storage/indexed.ts"],"names":[],"mappings":"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","file":"storage.cjs","sourcesContent":["/**\n * Forma Storage - Local\n *\n * Typed localStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed localStorage wrapper for the given key.\n *\n * ```ts\n * const store = createLocalStorage<{ name: string }>('user');\n * store.set({ name: 'Alice' });\n * store.get(); // { name: 'Alice' }\n * store.remove();\n * ```\n */\nexport function createLocalStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = localStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n localStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n localStorage.removeItem(key);\n },\n };\n}\n","/**\n * Forma Storage - Session\n *\n * Typed sessionStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed sessionStorage wrapper for the given key.\n *\n * ```ts\n * const store = createSessionStorage<{ token: string }>('auth');\n * store.set({ token: 'abc123' });\n * store.get(); // { token: 'abc123' }\n * store.remove();\n * ```\n */\nexport function createSessionStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = sessionStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n sessionStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n sessionStorage.removeItem(key);\n },\n };\n}\n","/**\n * Forma Storage - IndexedDB\n *\n * Simplified IndexedDB wrapper with lazy connection and promise-based API.\n * Zero dependencies — native browser APIs only.\n */\n\nexport interface IDBStore<T> {\n get(key: string): Promise<T | undefined>;\n set(key: string, value: T): Promise<void>;\n delete(key: string): Promise<void>;\n getAll(): Promise<T[]>;\n keys(): Promise<string[]>;\n clear(): Promise<void>;\n}\n\n/** Cached database connections keyed by \"dbName::storeName\". */\nconst dbCache = new Map<string, Promise<IDBDatabase>>();\n\n/**\n * Open (or retrieve a cached) IndexedDB database, ensuring the\n * requested object store exists.\n */\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n const cacheKey = `${dbName}::${storeName}`;\n const cached = dbCache.get(cacheKey);\n if (cached) return cached;\n\n const promise = new Promise<IDBDatabase>((resolve, reject) => {\n // First, try opening at the current (or default) version.\n const probe = indexedDB.open(dbName);\n\n probe.onerror = () => reject(probe.error);\n\n probe.onsuccess = () => {\n const db = probe.result;\n if (db.objectStoreNames.contains(storeName)) {\n resolve(db);\n return;\n }\n // Store doesn't exist yet — close and reopen with a version bump.\n const nextVersion = db.version + 1;\n db.close();\n\n const upgrade = indexedDB.open(dbName, nextVersion);\n upgrade.onerror = () => reject(upgrade.error);\n upgrade.onupgradeneeded = () => {\n const upgradedDB = upgrade.result;\n if (!upgradedDB.objectStoreNames.contains(storeName)) {\n upgradedDB.createObjectStore(storeName);\n }\n };\n upgrade.onsuccess = () => resolve(upgrade.result);\n };\n\n probe.onupgradeneeded = () => {\n const db = probe.result;\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName);\n }\n };\n });\n\n dbCache.set(cacheKey, promise);\n\n // If the open fails, remove from cache so the next call retries.\n promise.catch(() => {\n dbCache.delete(cacheKey);\n });\n\n return promise;\n}\n\n/**\n * Create a simplified IndexedDB store.\n *\n * ```ts\n * const store = createIndexedDB<User>('myApp', 'users');\n * await store.set('u1', { name: 'Alice' });\n * const user = await store.get('u1'); // { name: 'Alice' }\n * ```\n */\nexport function createIndexedDB<T>(\n dbName: string,\n storeName: string = 'default',\n): IDBStore<T> {\n /** Helper: run a single read/write transaction and return the request result. */\n function withStore<R>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<R>,\n ): Promise<R> {\n return openDB(dbName, storeName).then(\n (db) =>\n new Promise<R>((resolve, reject) => {\n const tx = db.transaction(storeName, mode);\n const store = tx.objectStore(storeName);\n const request = fn(store);\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n }),\n );\n }\n\n return {\n get(key: string): Promise<T | undefined> {\n return withStore('readonly', (store) => store.get(key)) as Promise<T | undefined>;\n },\n\n set(key: string, value: T): Promise<void> {\n return withStore('readwrite', (store) => store.put(value, key)).then(\n () => undefined,\n );\n },\n\n delete(key: string): Promise<void> {\n return withStore('readwrite', (store) => store.delete(key)).then(\n () => undefined,\n );\n },\n\n getAll(): Promise<T[]> {\n return withStore('readonly', (store) => store.getAll()) as Promise<T[]>;\n },\n\n keys(): Promise<string[]> {\n return withStore('readonly', (store) => store.getAllKeys()).then(\n (keys) => keys.map(String),\n );\n },\n\n clear(): Promise<void> {\n return withStore('readwrite', (store) => store.clear()).then(\n () => undefined,\n );\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/storage/local.ts","../src/storage/session.ts","../src/storage/indexed.ts"],"names":[],"mappings":"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","file":"storage.cjs","sourcesContent":["/**\n * Forma Storage - Local\n *\n * Typed localStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed localStorage wrapper for the given key.\n *\n * ```ts\n * const store = createLocalStorage<{ name: string }>('user');\n * store.set({ name: 'Alice' });\n * store.get(); // { name: 'Alice' }\n * store.remove();\n * ```\n */\nexport function createLocalStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = localStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n localStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n try {\n localStorage.removeItem(key);\n } catch { /* storage disabled — silently ignore, matching get/set */ }\n },\n };\n}\n","/**\n * Forma Storage - Session\n *\n * Typed sessionStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed sessionStorage wrapper for the given key.\n *\n * ```ts\n * const store = createSessionStorage<{ token: string }>('auth');\n * store.set({ token: 'abc123' });\n * store.get(); // { token: 'abc123' }\n * store.remove();\n * ```\n */\nexport function createSessionStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = sessionStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n sessionStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n try {\n sessionStorage.removeItem(key);\n } catch { /* storage disabled — silently ignore, matching get/set */ }\n },\n };\n}\n","/**\n * Forma Storage - IndexedDB\n *\n * Simplified IndexedDB wrapper with lazy connection and promise-based API.\n * Zero dependencies — native browser APIs only.\n */\n\nexport interface IDBStore<T> {\n get(key: string): Promise<T | undefined>;\n set(key: string, value: T): Promise<void>;\n delete(key: string): Promise<void>;\n getAll(): Promise<T[]>;\n keys(): Promise<string[]>;\n clear(): Promise<void>;\n}\n\n/** Cached database connections keyed by \"dbName::storeName\". */\nconst dbCache = new Map<string, Promise<IDBDatabase>>();\n\n/**\n * Per-database open serialization. Concurrent createIndexedDB(db, storeA/B/...)\n * calls each bump db.version+1; without serialization they compute the version\n * from a stale probe and race, so some stores are never created and their\n * connections are permanently wedged. Chaining opens for the same db name makes\n * each see the previous open's committed version.\n */\nconst openLocks = new Map<string, Promise<unknown>>();\n\n/**\n * Open (or retrieve a cached) IndexedDB database, ensuring the\n * requested object store exists.\n */\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n const cacheKey = `${dbName}::${storeName}`;\n const cached = dbCache.get(cacheKey);\n if (cached) return cached;\n\n // Serialize this open behind any in-flight open for the same db name.\n const prevLock = openLocks.get(dbName) ?? Promise.resolve();\n const promise = prevLock.then(() => doOpen(dbName, storeName, cacheKey), () => doOpen(dbName, storeName, cacheKey));\n\n dbCache.set(cacheKey, promise);\n // Advance the lock (never rejects, so the chain keeps flowing).\n openLocks.set(dbName, promise.then(() => undefined, () => undefined));\n\n // If the open fails, remove from cache so the next call retries.\n promise.catch(() => {\n dbCache.delete(cacheKey);\n });\n\n return promise;\n}\n\nfunction doOpen(dbName: string, storeName: string, cacheKey: string): Promise<IDBDatabase> {\n return new Promise<IDBDatabase>((resolve, reject) => {\n // Wire a resolved connection so it closes + evicts on a version change\n // (another tab/store bumping the version) or a forced close. Without this a\n // later bump blocks forever and a stale connection is never dropped.\n const wire = (db: IDBDatabase) => {\n db.onversionchange = () => { db.close(); dbCache.delete(cacheKey); };\n db.onclose = () => { dbCache.delete(cacheKey); };\n resolve(db);\n };\n\n // First, try opening at the current (or default) version.\n const probe = indexedDB.open(dbName);\n\n probe.onerror = () => reject(probe.error);\n\n probe.onsuccess = () => {\n const db = probe.result;\n if (db.objectStoreNames.contains(storeName)) {\n wire(db);\n return;\n }\n // Store doesn't exist yet — close and reopen with a version bump. Opens\n // are serialized per db, so this version is current, not stale.\n const nextVersion = db.version + 1;\n db.close();\n\n const upgrade = indexedDB.open(dbName, nextVersion);\n upgrade.onerror = () => reject(upgrade.error);\n // Backstop: if another connection blocks the bump, settle (reject) so the\n // promise does not leak; the next call retries.\n upgrade.onblocked = () =>\n reject(upgrade.error ?? new DOMException('Upgrade blocked: another connection is open', 'AbortError'));\n upgrade.onupgradeneeded = () => {\n const upgradedDB = upgrade.result;\n if (!upgradedDB.objectStoreNames.contains(storeName)) {\n upgradedDB.createObjectStore(storeName);\n }\n };\n upgrade.onsuccess = () => {\n const upgradedDB = upgrade.result;\n if (!upgradedDB.objectStoreNames.contains(storeName)) {\n // Should not happen with serialized opens; reject (do not cache a\n // store-less connection) so the next call retries cleanly.\n upgradedDB.close();\n reject(new DOMException(`Object store \"${storeName}\" was not created`, 'NotFoundError'));\n return;\n }\n wire(upgradedDB);\n };\n };\n\n probe.onupgradeneeded = () => {\n const db = probe.result;\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName);\n }\n };\n });\n}\n\n/**\n * Create a simplified IndexedDB store.\n *\n * ```ts\n * const store = createIndexedDB<User>('myApp', 'users');\n * await store.set('u1', { name: 'Alice' });\n * const user = await store.get('u1'); // { name: 'Alice' }\n * ```\n */\nexport function createIndexedDB<T>(\n dbName: string,\n storeName: string = 'default',\n): IDBStore<T> {\n const cacheKey = `${dbName}::${storeName}`;\n\n /** Helper: run a single read/write transaction and return the request result. */\n function withStore<R>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<R>,\n ): Promise<R> {\n return openDB(dbName, storeName).then(\n (db) =>\n new Promise<R>((resolve, reject) => {\n let request: IDBRequest<R>;\n let tx: IDBTransaction;\n try {\n tx = db.transaction(storeName, mode);\n request = fn(tx.objectStore(storeName));\n } catch (err) {\n // A transaction on a closing connection throws synchronously.\n reject(err);\n return;\n }\n let result: R;\n request.onsuccess = () => {\n result = request.result;\n // Reads are durable at request success; writes only at commit.\n if (mode === 'readonly') resolve(result);\n };\n // A readwrite value is only durable once the transaction COMMITS —\n // resolving on request.onsuccess would falsely report success for a\n // write that later aborts (e.g. QuotaExceededError at commit).\n tx.oncomplete = () => resolve(result);\n tx.onerror = () => reject(tx.error ?? request.error);\n tx.onabort = () => reject(tx.error ?? request.error ?? new DOMException('Transaction aborted', 'AbortError'));\n }),\n ).catch((err: unknown) => {\n // Evict a broken/closing/store-less connection so the next call reopens\n // cleanly (NotFoundError = the cached connection lacks the store).\n const name = (err as { name?: string } | null)?.name;\n if (name === 'InvalidStateError' || name === 'AbortError' || name === 'NotFoundError') {\n dbCache.delete(cacheKey);\n }\n throw err;\n });\n }\n\n return {\n get(key: string): Promise<T | undefined> {\n return withStore('readonly', (store) => store.get(key)) as Promise<T | undefined>;\n },\n\n set(key: string, value: T): Promise<void> {\n return withStore('readwrite', (store) => store.put(value, key)).then(\n () => undefined,\n );\n },\n\n delete(key: string): Promise<void> {\n return withStore('readwrite', (store) => store.delete(key)).then(\n () => undefined,\n );\n },\n\n getAll(): Promise<T[]> {\n return withStore('readonly', (store) => store.getAll()) as Promise<T[]>;\n },\n\n keys(): Promise<string[]> {\n return withStore('readonly', (store) => store.getAllKeys()).then(\n (keys) => keys.map(String),\n );\n },\n\n clear(): Promise<void> {\n return withStore('readwrite', (store) => store.clear()).then(\n () => undefined,\n );\n },\n };\n}\n"]}
|