@fluixi/core 1.0.0-alpha.91 → 1.0.0-alpha.93
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/cdn/core.global.js +2 -2
- package/dist/src/cdn/{chunk-MDJLC3PE.mjs → chunk-7XM7Q5VR.mjs} +271 -229
- package/dist/src/cdn/core.cjs +360 -253
- package/dist/src/cdn/core.mjs +92 -20
- package/dist/src/cdn/{router-JYFZP3EZ.mjs → router-VOY43YNM.mjs} +7 -3
- package/dist/src/index.cjs +547 -232
- package/dist/src/index.mjs +547 -223
- package/dist/src/lib/client/chunk-QT3RFKH6.mjs +53 -0
- package/dist/src/lib/client/index.cjs +310 -176
- package/dist/src/lib/client/index.d.ts.map +1 -1
- package/dist/src/lib/client/index.js +33 -18
- package/dist/src/lib/client/index.mjs +86 -15
- package/dist/src/lib/client/{router-I66EJO2Y.mjs → router-ADTS55OP.mjs} +244 -99
- package/dist/src/lib/context/context.cjs +35 -72
- package/dist/src/lib/context/context.d.ts +45 -291
- package/dist/src/lib/context/context.d.ts.map +1 -1
- package/dist/src/lib/context/context.js +65 -397
- package/dist/src/lib/context/context.mjs +38 -73
- package/dist/src/lib/context/index.cjs +35 -72
- package/dist/src/lib/context/index.d.ts +2 -2
- package/dist/src/lib/context/index.d.ts.map +1 -1
- package/dist/src/lib/context/index.js +1 -5
- package/dist/src/lib/context/index.mjs +38 -73
- package/dist/src/lib/core.cjs +546 -231
- package/dist/src/lib/core.d.ts +3 -1
- package/dist/src/lib/core.d.ts.map +1 -1
- package/dist/src/lib/core.js +5 -3
- package/dist/src/lib/core.mjs +546 -222
- package/dist/src/lib/env/index.cjs +1 -17
- package/dist/src/lib/env/index.d.ts +2 -14
- package/dist/src/lib/env/index.d.ts.map +1 -1
- package/dist/src/lib/env/index.js +2 -26
- package/dist/src/lib/env/index.mjs +1 -17
- package/dist/src/lib/i18n/index.cjs +17 -2
- package/dist/src/lib/i18n/index.d.ts +10 -0
- package/dist/src/lib/i18n/index.d.ts.map +1 -1
- package/dist/src/lib/i18n/index.js +17 -1
- package/dist/src/lib/i18n/index.mjs +17 -2
- package/dist/src/lib/index.cjs +64 -99
- package/dist/src/lib/index.mjs +43 -76
- package/dist/src/lib/isomorphic.cjs +9 -4
- package/dist/src/lib/isomorphic.d.ts +9 -2
- package/dist/src/lib/isomorphic.d.ts.map +1 -1
- package/dist/src/lib/isomorphic.js +11 -4
- package/dist/src/lib/isomorphic.mjs +9 -4
- package/dist/src/lib/jsx-runtime/index.cjs +1 -1
- package/dist/src/lib/jsx-runtime/index.mjs +1 -1
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.cjs +1 -1
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.mjs +1 -1
- package/dist/src/lib/plugins/fluixi-routes-plugin.cjs +2 -2
- package/dist/src/lib/plugins/fluixi-routes-plugin.mjs +2 -2
- package/dist/src/lib/plugins/index.cjs +3 -3
- package/dist/src/lib/plugins/index.mjs +3 -3
- package/dist/src/lib/plugins/route-codegen.cjs +2 -2
- package/dist/src/lib/plugins/route-codegen.js +1 -1
- package/dist/src/lib/plugins/route-codegen.mjs +2 -2
- package/dist/src/lib/render/index.cjs +1 -1
- package/dist/src/lib/render/index.mjs +1 -1
- package/dist/src/lib/render/versions.cjs +1 -1
- package/dist/src/lib/render/versions.mjs +1 -1
- package/dist/src/lib/router/data.cjs +113 -115
- package/dist/src/lib/router/data.d.ts +63 -10
- package/dist/src/lib/router/data.d.ts.map +1 -1
- package/dist/src/lib/router/data.js +158 -68
- package/dist/src/lib/router/data.mjs +112 -112
- package/dist/src/lib/router/index.cjs +260 -163
- package/dist/src/lib/router/index.d.ts +22 -2
- package/dist/src/lib/router/index.d.ts.map +1 -1
- package/dist/src/lib/router/index.js +165 -22
- package/dist/src/lib/router/index.mjs +244 -143
- package/dist/src/lib/server/index.cjs +1 -1
- package/dist/src/lib/server/index.mjs +1 -1
- package/dist/src/lib/storage/cache.cjs +51 -0
- package/dist/src/lib/storage/cache.d.ts +18 -0
- package/dist/src/lib/storage/cache.d.ts.map +1 -0
- package/dist/src/lib/storage/cache.js +42 -0
- package/dist/src/lib/storage/cache.mjs +30 -0
- package/dist/src/lib/storage/index.cjs +246 -0
- package/dist/src/lib/storage/index.d.ts +83 -0
- package/dist/src/lib/storage/index.d.ts.map +1 -0
- package/dist/src/lib/storage/index.js +179 -0
- package/dist/src/lib/storage/index.mjs +231 -0
- package/dist/src/lib/storage/persisted.cjs +91 -0
- package/dist/src/lib/storage/persisted.d.ts +53 -0
- package/dist/src/lib/storage/persisted.d.ts.map +1 -0
- package/dist/src/lib/storage/persisted.js +75 -0
- package/dist/src/lib/storage/persisted.mjs +70 -0
- package/dist/src/lib/theme/color-scheme.cjs +281 -0
- package/dist/src/lib/theme/color-scheme.d.ts +61 -0
- package/dist/src/lib/theme/color-scheme.d.ts.map +1 -0
- package/dist/src/lib/theme/color-scheme.js +120 -0
- package/dist/src/lib/theme/color-scheme.mjs +265 -0
- package/dist/src/lib/theme/index.cjs +194 -37
- package/dist/src/lib/theme/index.d.ts +23 -11
- package/dist/src/lib/theme/index.d.ts.map +1 -1
- package/dist/src/lib/theme/index.js +73 -32
- package/dist/src/lib/theme/index.mjs +197 -35
- package/dist/src/lib/theme/provider.cjs +346 -0
- package/dist/src/lib/theme/provider.d.ts +34 -0
- package/dist/src/lib/theme/provider.d.ts.map +1 -0
- package/dist/src/lib/theme/provider.js +60 -0
- package/dist/src/lib/theme/provider.mjs +332 -0
- package/dist/src/routes.cjs +1 -1
- package/dist/src/routes.mjs +1 -1
- package/dist/src/utils.cjs +1 -1
- package/dist/src/utils.mjs +1 -1
- package/dist/src/version.generated.cjs +1 -1
- package/dist/src/version.generated.d.ts +1 -1
- package/dist/src/version.generated.js +1 -1
- package/dist/src/version.generated.mjs +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +18 -11
- package/dist/src/lib/client/chunk-SKXDXBGV.mjs +0 -133
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `cachedResource` cache kept in a storage, so fetched data outlives the page: session
|
|
3
|
+
* storage for the tab, local storage across visits. Values pass through the storage's
|
|
4
|
+
* serializer, so they must be plain data. In-flight fetches stay in memory.
|
|
5
|
+
*
|
|
6
|
+
* Give it its own namespace, since `clearCachedResources()` clears that storage, and a `ttl`
|
|
7
|
+
* so entries do not pile up after the resource that wrote them is gone.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const cache = storageCache(useStorage({ scope: 'session', namespace: 'cache' }), { ttl: 10 * 60_000 });
|
|
11
|
+
* const orgs = cachedResource(() => api.get('/orgs'), { name: 'orgs', ttl: 60_000, cache });
|
|
12
|
+
*/
|
|
13
|
+
export function storageCache(storage, options = {}) {
|
|
14
|
+
const inflight = new Map();
|
|
15
|
+
return {
|
|
16
|
+
get(key) {
|
|
17
|
+
try {
|
|
18
|
+
const entry = storage.getEntry(key);
|
|
19
|
+
return entry ? { value: entry.value, timestamp: entry.createdAt } : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Unreadable or blocked: a miss, and the resource fetches.
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
set(key, entry) {
|
|
27
|
+
try {
|
|
28
|
+
storage.set(key, entry.value, options.ttl !== undefined ? { ttl: options.ttl } : undefined);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* quota, or a value the serializer refuses: kept for this page only by the fetch itself */
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
delete: (key) => storage.remove(key),
|
|
35
|
+
keys: () => storage.keys(),
|
|
36
|
+
clear() {
|
|
37
|
+
storage.clear();
|
|
38
|
+
inflight.clear();
|
|
39
|
+
},
|
|
40
|
+
inflight,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/lib/storage/cache.ts
|
|
2
|
+
function storageCache(storage, options = {}) {
|
|
3
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
4
|
+
return {
|
|
5
|
+
get(key) {
|
|
6
|
+
try {
|
|
7
|
+
const entry = storage.getEntry(key);
|
|
8
|
+
return entry ? { value: entry.value, timestamp: entry.createdAt } : void 0;
|
|
9
|
+
} catch {
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
set(key, entry) {
|
|
14
|
+
try {
|
|
15
|
+
storage.set(key, entry.value, options.ttl !== void 0 ? { ttl: options.ttl } : void 0);
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
delete: (key) => storage.remove(key),
|
|
20
|
+
keys: () => storage.keys(),
|
|
21
|
+
clear() {
|
|
22
|
+
storage.clear();
|
|
23
|
+
inflight.clear();
|
|
24
|
+
},
|
|
25
|
+
inflight
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
storageCache
|
|
30
|
+
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/*! @fluixi/core v1.0.0-alpha.93 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/lib/storage/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
StorageProvider: () => StorageProvider,
|
|
25
|
+
hydrateStorage: () => hydrateStorage,
|
|
26
|
+
persisted: () => persisted,
|
|
27
|
+
storageCache: () => storageCache,
|
|
28
|
+
useAsyncStorage: () => useAsyncStorage,
|
|
29
|
+
useStorage: () => useStorage
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
var import_dom = require("@fluixi/dom");
|
|
33
|
+
var import_signal2 = require("@fluixi/reactive/signal");
|
|
34
|
+
var import_storage = require("@fluixi/storage");
|
|
35
|
+
var import_browser = require("@fluixi/storage/browser");
|
|
36
|
+
var import_server = require("@fluixi/storage/server");
|
|
37
|
+
|
|
38
|
+
// src/lib/storage/persisted.ts
|
|
39
|
+
var import_signal = require("@fluixi/reactive/signal");
|
|
40
|
+
var isAsync = (storage) => "encryption" in storage;
|
|
41
|
+
function persisted(storage, key, options = {}) {
|
|
42
|
+
const fallback = options.fallback ?? null;
|
|
43
|
+
const name = options.name ?? `storage:${storage.keyPrefix}${key}`;
|
|
44
|
+
const state = (0, import_signal.signal)(isAsync(storage) ? fallback : storage.get(key) ?? fallback, { name });
|
|
45
|
+
const put = (value) => state.set(() => value);
|
|
46
|
+
const resolve = (next) => typeof next === "function" ? next((0, import_signal.untrack)(state)) : next;
|
|
47
|
+
const writeOptions = (extra) => ({
|
|
48
|
+
...options.ttl !== void 0 ? { ttl: options.ttl } : {},
|
|
49
|
+
...options.encrypt ? { encrypt: true } : {},
|
|
50
|
+
...extra
|
|
51
|
+
});
|
|
52
|
+
if (!isAsync(storage)) {
|
|
53
|
+
const off2 = storage.subscribe(key, (event) => {
|
|
54
|
+
if (event.type !== "set") return put(fallback);
|
|
55
|
+
if (event.value !== void 0) put(event.value);
|
|
56
|
+
});
|
|
57
|
+
if ((0, import_signal.getOwner)()) (0, import_signal.onCleanup)(off2);
|
|
58
|
+
return Object.assign(() => state(), {
|
|
59
|
+
set(next, extra) {
|
|
60
|
+
const value = resolve(next);
|
|
61
|
+
storage.set(key, value, writeOptions(extra));
|
|
62
|
+
put(value);
|
|
63
|
+
},
|
|
64
|
+
remove() {
|
|
65
|
+
storage.remove(key);
|
|
66
|
+
put(fallback);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
let written = false;
|
|
71
|
+
const reread = () => storage.get(key).then(
|
|
72
|
+
(value) => put(value ?? fallback),
|
|
73
|
+
() => void 0
|
|
74
|
+
);
|
|
75
|
+
const off = storage.subscribe(key, (event) => {
|
|
76
|
+
if (event.type !== "set") return put(fallback);
|
|
77
|
+
if (event.value !== void 0) put(event.value);
|
|
78
|
+
else void reread();
|
|
79
|
+
});
|
|
80
|
+
if ((0, import_signal.getOwner)()) (0, import_signal.onCleanup)(off);
|
|
81
|
+
const ready = storage.get(key).then((value) => {
|
|
82
|
+
if (!written) put(value ?? fallback);
|
|
83
|
+
});
|
|
84
|
+
return Object.assign(() => state(), {
|
|
85
|
+
ready,
|
|
86
|
+
async set(next, extra) {
|
|
87
|
+
const previous = (0, import_signal.untrack)(state);
|
|
88
|
+
const value = resolve(next);
|
|
89
|
+
written = true;
|
|
90
|
+
put(value);
|
|
91
|
+
try {
|
|
92
|
+
await storage.set(key, value, writeOptions(extra));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
put(previous);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
async remove() {
|
|
99
|
+
written = true;
|
|
100
|
+
await storage.remove(key);
|
|
101
|
+
put(fallback);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/lib/storage/cache.ts
|
|
107
|
+
function storageCache(storage, options = {}) {
|
|
108
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
109
|
+
return {
|
|
110
|
+
get(key) {
|
|
111
|
+
try {
|
|
112
|
+
const entry = storage.getEntry(key);
|
|
113
|
+
return entry ? { value: entry.value, timestamp: entry.createdAt } : void 0;
|
|
114
|
+
} catch {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
set(key, entry) {
|
|
119
|
+
try {
|
|
120
|
+
storage.set(key, entry.value, options.ttl !== void 0 ? { ttl: options.ttl } : void 0);
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
delete: (key) => storage.remove(key),
|
|
125
|
+
keys: () => storage.keys(),
|
|
126
|
+
clear() {
|
|
127
|
+
storage.clear();
|
|
128
|
+
inflight.clear();
|
|
129
|
+
},
|
|
130
|
+
inflight
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/lib/storage/index.ts
|
|
135
|
+
var StorageContext = (0, import_signal2.createContext)(void 0, "storage");
|
|
136
|
+
var AsyncStorageContext = (0, import_signal2.createContext)(void 0, "asyncStorage");
|
|
137
|
+
var isAsyncStorage = (storage) => "encryption" in storage;
|
|
138
|
+
function StorageProvider(props) {
|
|
139
|
+
const context = isAsyncStorage(props.storage) ? AsyncStorageContext : StorageContext;
|
|
140
|
+
return context.Provider({
|
|
141
|
+
value: props.storage,
|
|
142
|
+
get children() {
|
|
143
|
+
return props.children;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
var isBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
148
|
+
var perRequest = /* @__PURE__ */ new WeakMap();
|
|
149
|
+
var perPage = /* @__PURE__ */ new Map();
|
|
150
|
+
function cached(scope, id, make) {
|
|
151
|
+
let adapter = scope.get(id);
|
|
152
|
+
if (!adapter) scope.set(id, adapter = make());
|
|
153
|
+
return adapter;
|
|
154
|
+
}
|
|
155
|
+
function requestRequired(scope) {
|
|
156
|
+
throw new import_storage.StorageUnavailableError(
|
|
157
|
+
`useStorage({ scope: '${scope}' }) on the server needs a request: call it during a render, a loader or a server function, or provide a storage with <StorageProvider>.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function adapterFor(scope, cookie = {}) {
|
|
161
|
+
const ctx = (0, import_dom.getRequestContext)();
|
|
162
|
+
const options = { httpOnly: false, ...cookie };
|
|
163
|
+
switch (scope) {
|
|
164
|
+
case "local":
|
|
165
|
+
return (0, import_browser.localStorageAdapter)();
|
|
166
|
+
case "session":
|
|
167
|
+
return (0, import_browser.sessionStorageAdapter)();
|
|
168
|
+
case "request":
|
|
169
|
+
if (isBrowser()) return cached(perPage, "request", import_storage.memoryStorage);
|
|
170
|
+
if (!ctx) requestRequired(scope);
|
|
171
|
+
return cached(perRequest.get(ctx) ?? perRequest.set(ctx, /* @__PURE__ */ new Map()).get(ctx), "request", import_server.requestStorage);
|
|
172
|
+
case "cookie": {
|
|
173
|
+
const id = `cookie:${JSON.stringify(options)}`;
|
|
174
|
+
if (isBrowser()) {
|
|
175
|
+
const { httpOnly: _ignored, ...browser } = options;
|
|
176
|
+
return cached(perPage, id, () => (0, import_browser.documentCookieAdapter)(browser));
|
|
177
|
+
}
|
|
178
|
+
if (!ctx) requestRequired(scope);
|
|
179
|
+
const raw = ctx.event.request;
|
|
180
|
+
const target = raw && typeof raw === "object" ? raw : void 0;
|
|
181
|
+
return cached(
|
|
182
|
+
perRequest.get(ctx) ?? perRequest.set(ctx, /* @__PURE__ */ new Map()).get(ctx),
|
|
183
|
+
id,
|
|
184
|
+
() => (0, import_server.cookieStorage)({
|
|
185
|
+
...options,
|
|
186
|
+
request: raw && typeof raw === "object" && ("headers" in raw || "getHeader" in raw) ? (0, import_server.toStorageRequest)(raw) : void 0,
|
|
187
|
+
response: {
|
|
188
|
+
appendHeader(name, value) {
|
|
189
|
+
if (!(0, import_dom.appendResponseHeader)(name, value, target)) {
|
|
190
|
+
throw new import_storage.StorageUnavailableError("The response headers were already sent, so this cookie cannot reach the browser. Write it before the page head goes out: in middleware, a loader, a server function or the render before its first chunk.");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function useStorage(options = {}) {
|
|
200
|
+
const { scope = "auto", cookie, ...storageOptions } = options;
|
|
201
|
+
if (scope === "auto") {
|
|
202
|
+
const provided = (0, import_signal2.useContext)(StorageContext);
|
|
203
|
+
if (provided) return options.namespace ? provided.namespace(options.namespace) : provided;
|
|
204
|
+
}
|
|
205
|
+
return (0, import_storage.createSyncStorage)({ ...storageOptions, adapter: resolveAdapter(scope, cookie) });
|
|
206
|
+
}
|
|
207
|
+
function useAsyncStorage(options = {}) {
|
|
208
|
+
const { scope = "auto", cookie, ...storageOptions } = options;
|
|
209
|
+
if (scope === "auto") {
|
|
210
|
+
const provided = (0, import_signal2.useContext)(AsyncStorageContext);
|
|
211
|
+
if (provided) return options.namespace ? provided.namespace(options.namespace) : provided;
|
|
212
|
+
}
|
|
213
|
+
return (0, import_storage.createAsyncStorage)({ ...storageOptions, adapter: resolveAdapter(scope, cookie) });
|
|
214
|
+
}
|
|
215
|
+
function resolveAdapter(scope, cookie) {
|
|
216
|
+
if (scope !== "auto") return adapterFor(scope, cookie);
|
|
217
|
+
if (isBrowser()) return adapterFor("local");
|
|
218
|
+
if (!(0, import_dom.getRequestContext)()) requestRequired(scope);
|
|
219
|
+
return adapterFor("request");
|
|
220
|
+
}
|
|
221
|
+
var SLOT = "__storage:";
|
|
222
|
+
var seeded = /* @__PURE__ */ new Set();
|
|
223
|
+
function hydrateStorage(storage, keys) {
|
|
224
|
+
const slot = SLOT + storage.keyPrefix;
|
|
225
|
+
if (!isBrowser()) {
|
|
226
|
+
const ctx = (0, import_dom.getRequestContext)();
|
|
227
|
+
if (!ctx) return;
|
|
228
|
+
const entries2 = ctx.data.get(slot) ?? {};
|
|
229
|
+
for (const key of keys) {
|
|
230
|
+
const entry = storage.getEntry(key);
|
|
231
|
+
if (entry) entries2[key] = entry;
|
|
232
|
+
}
|
|
233
|
+
ctx.data.set(slot, entries2);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const entries = (0, import_dom.getServerData)()?.[slot];
|
|
237
|
+
if (!entries) return;
|
|
238
|
+
const fresh = {};
|
|
239
|
+
for (const key of keys) {
|
|
240
|
+
const id = slot + key;
|
|
241
|
+
if (seeded.has(id) || !entries[key]) continue;
|
|
242
|
+
seeded.add(id);
|
|
243
|
+
fresh[key] = entries[key];
|
|
244
|
+
}
|
|
245
|
+
storage.restore(fresh, { overwrite: true });
|
|
246
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type AsyncTypedStorage, type EncryptionOptions, type StorageKey, type StorageOptions, type StorageSchema, type TypedStorage } from '@fluixi/storage';
|
|
2
|
+
import type { CookieOptions } from '@fluixi/utils/cookie';
|
|
3
|
+
/**
|
|
4
|
+
* Where `useStorage` keeps values.
|
|
5
|
+
*
|
|
6
|
+
* - `auto`: a storage from `<StorageProvider>` when there is one; else localStorage in the
|
|
7
|
+
* browser and the request's own storage during a server render.
|
|
8
|
+
* - `local`, `session`: the browser's storages. Reading them on a server throws.
|
|
9
|
+
* - `request`: values for one server request, or for the page's lifetime in the browser.
|
|
10
|
+
* - `cookie`: the request's cookies on the server, `document.cookie` in the browser. Both sides
|
|
11
|
+
* read the same value, which is what server-rendered preferences such as a theme need.
|
|
12
|
+
*/
|
|
13
|
+
export type UseStorageScope = 'auto' | 'local' | 'session' | 'request' | 'cookie';
|
|
14
|
+
export interface UseStorageCookieOptions extends CookieOptions {
|
|
15
|
+
/** Largest `name=value` in bytes. Defaults to 4096. */
|
|
16
|
+
maxSize?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface UseStorageOptions<S extends StorageSchema = StorageSchema> extends Omit<StorageOptions<S>, 'adapter'> {
|
|
19
|
+
scope?: UseStorageScope;
|
|
20
|
+
/**
|
|
21
|
+
* Cookie attributes for `scope: 'cookie'`. `httpOnly` defaults to false here, since the browser
|
|
22
|
+
* half has to read the cookie; set it to true for a value only the server reads.
|
|
23
|
+
*/
|
|
24
|
+
cookie?: UseStorageCookieOptions;
|
|
25
|
+
}
|
|
26
|
+
export interface UseAsyncStorageOptions<S extends StorageSchema = StorageSchema> extends UseStorageOptions<S> {
|
|
27
|
+
/** Per-item encryption: `set(key, value, { encrypt: true })`, reads decrypt on their own. */
|
|
28
|
+
encryption?: EncryptionOptions;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Hands `storage` to every `useStorage()` below that asks for `scope: 'auto'` (the default), or
|
|
32
|
+
* to every `useAsyncStorage()` when it is an asynchronous one: a test's memory storage, an
|
|
33
|
+
* encrypted or app-specific one.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* <StorageProvider storage={createStorage({ adapter: memoryStorage() })}>
|
|
37
|
+
* <Settings />
|
|
38
|
+
* </StorageProvider>
|
|
39
|
+
*/
|
|
40
|
+
export declare function StorageProvider(props: {
|
|
41
|
+
storage: TypedStorage<any> | AsyncTypedStorage<any>;
|
|
42
|
+
children?: unknown;
|
|
43
|
+
}): any;
|
|
44
|
+
/**
|
|
45
|
+
* A typed storage for the place this code runs. Call it in a component, a loader or a server
|
|
46
|
+
* function; the scope decides the backend (see `UseStorageScope`) and the rest of the options
|
|
47
|
+
* are those of `createStorage`.
|
|
48
|
+
*
|
|
49
|
+
* Server-side state never lives in a module: each request gets its own adapters, so two
|
|
50
|
+
* visitors rendering at once never see each other's values.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const prefs = useStorage<{ theme: 'light' | 'dark' }>({ scope: 'cookie', namespace: 'ui', cookie: { maxAge: 31536000 } });
|
|
54
|
+
* const theme = prefs.get('theme') ?? 'light';
|
|
55
|
+
*/
|
|
56
|
+
export declare function useStorage<S extends StorageSchema = StorageSchema>(options?: UseStorageOptions<S>): TypedStorage<S>;
|
|
57
|
+
/**
|
|
58
|
+
* `useStorage` with every operation awaited, and the one that can encrypt. Same scopes, same
|
|
59
|
+
* per-request isolation; with `scope: 'auto'` it takes an asynchronous storage from
|
|
60
|
+
* `<StorageProvider>` first.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* const vault = useAsyncStorage<{ token: string }>({ namespace: 'auth', encryption: { keyProvider } });
|
|
64
|
+
* await vault.set('token', token, { encrypt: true });
|
|
65
|
+
*/
|
|
66
|
+
export declare function useAsyncStorage<S extends StorageSchema = StorageSchema>(options?: UseAsyncStorageOptions<S>): AsyncTypedStorage<S>;
|
|
67
|
+
/**
|
|
68
|
+
* Carries `keys` from the server render to the browser. On the server it puts their values,
|
|
69
|
+
* with expiry and version, in the page data; in the browser, the first call for a key writes
|
|
70
|
+
* the server's value into `storage`, and later calls leave the browser's own value alone.
|
|
71
|
+
*
|
|
72
|
+
* Meant for request-scoped values the page needs again: call it with the same keys on both
|
|
73
|
+
* sides. The page data is readable by anyone who sees the page, so never hydrate a secret,
|
|
74
|
+
* and an encrypted value throws here rather than travel in clear.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const scratch = useStorage<{ draft: Draft }>({ scope: 'request', namespace: 'editor' });
|
|
78
|
+
* hydrateStorage(scratch, ['draft']);
|
|
79
|
+
*/
|
|
80
|
+
export declare function hydrateStorage<S extends StorageSchema>(storage: TypedStorage<S>, keys: StorageKey<S>[]): void;
|
|
81
|
+
export { persisted, type Persisted, type AsyncPersisted, type PersistedOptions } from './persisted.js';
|
|
82
|
+
export { storageCache } from './cache.js';
|
|
83
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/storage/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAKL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EAGtB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElF,MAAM,WAAW,uBAAwB,SAAQ,aAAa;IAC5D,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,CAAE,SAAQ,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC;IACpH,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,uBAAuB,CAAC;CAClC;AAED,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,CAAE,SAAQ,iBAAiB,CAAC,CAAC,CAAC;IAC3G,6FAA6F;IAC7F,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAOD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,OAAO,EAAE,YAAY,CAAC,GAAG,CAAC,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,GAAG,CAQvH;AA6DD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,EAAE,OAAO,GAAE,iBAAiB,CAAC,CAAC,CAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAOvH;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,EAAE,OAAO,GAAE,sBAAsB,CAAC,CAAC,CAAM,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAOtI;AAYD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,aAAa,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAuB7G;AAED,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvG,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage inside a Fluixi app: `useStorage` picks the backend for where the code runs, so one
|
|
3
|
+
* component reads the same value on the server and in the browser. The storage itself is
|
|
4
|
+
* `@fluixi/storage`; this module only binds it to the request, the owner tree and the page data.
|
|
5
|
+
*/
|
|
6
|
+
import { appendResponseHeader, getRequestContext, getServerData } from '@fluixi/dom';
|
|
7
|
+
import { createContext, useContext } from '@fluixi/reactive/signal';
|
|
8
|
+
import { createAsyncStorage, createSyncStorage, memoryStorage, StorageUnavailableError, } from '@fluixi/storage';
|
|
9
|
+
import { documentCookieAdapter, localStorageAdapter, sessionStorageAdapter } from '@fluixi/storage/browser';
|
|
10
|
+
import { cookieStorage, requestStorage, toStorageRequest } from '@fluixi/storage/server';
|
|
11
|
+
const StorageContext = createContext(undefined, 'storage');
|
|
12
|
+
const AsyncStorageContext = createContext(undefined, 'asyncStorage');
|
|
13
|
+
const isAsyncStorage = (storage) => 'encryption' in storage;
|
|
14
|
+
/**
|
|
15
|
+
* Hands `storage` to every `useStorage()` below that asks for `scope: 'auto'` (the default), or
|
|
16
|
+
* to every `useAsyncStorage()` when it is an asynchronous one: a test's memory storage, an
|
|
17
|
+
* encrypted or app-specific one.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* <StorageProvider storage={createStorage({ adapter: memoryStorage() })}>
|
|
21
|
+
* <Settings />
|
|
22
|
+
* </StorageProvider>
|
|
23
|
+
*/
|
|
24
|
+
export function StorageProvider(props) {
|
|
25
|
+
const context = isAsyncStorage(props.storage) ? AsyncStorageContext : StorageContext;
|
|
26
|
+
return context.Provider({
|
|
27
|
+
value: props.storage,
|
|
28
|
+
get children() {
|
|
29
|
+
return props.children;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
34
|
+
// One adapter per request and kind, so a cookie written early in a render reads back later in
|
|
35
|
+
// the same render. Weakly held: they go with the request.
|
|
36
|
+
const perRequest = new WeakMap();
|
|
37
|
+
// In the browser there is one user and one page, so the page is the scope.
|
|
38
|
+
const perPage = new Map();
|
|
39
|
+
function cached(scope, id, make) {
|
|
40
|
+
let adapter = scope.get(id);
|
|
41
|
+
if (!adapter)
|
|
42
|
+
scope.set(id, (adapter = make()));
|
|
43
|
+
return adapter;
|
|
44
|
+
}
|
|
45
|
+
function requestRequired(scope) {
|
|
46
|
+
throw new StorageUnavailableError(`useStorage({ scope: '${scope}' }) on the server needs a request: call it during a render, a loader or a server function, or provide a storage with <StorageProvider>.`);
|
|
47
|
+
}
|
|
48
|
+
function adapterFor(scope, cookie = {}) {
|
|
49
|
+
const ctx = getRequestContext();
|
|
50
|
+
const options = { httpOnly: false, ...cookie };
|
|
51
|
+
switch (scope) {
|
|
52
|
+
case 'local':
|
|
53
|
+
return localStorageAdapter();
|
|
54
|
+
case 'session':
|
|
55
|
+
return sessionStorageAdapter();
|
|
56
|
+
case 'request':
|
|
57
|
+
if (isBrowser())
|
|
58
|
+
return cached(perPage, 'request', memoryStorage);
|
|
59
|
+
if (!ctx)
|
|
60
|
+
requestRequired(scope);
|
|
61
|
+
return cached(perRequest.get(ctx) ?? perRequest.set(ctx, new Map()).get(ctx), 'request', requestStorage);
|
|
62
|
+
case 'cookie': {
|
|
63
|
+
const id = `cookie:${JSON.stringify(options)}`;
|
|
64
|
+
if (isBrowser()) {
|
|
65
|
+
const { httpOnly: _ignored, ...browser } = options;
|
|
66
|
+
return cached(perPage, id, () => documentCookieAdapter(browser));
|
|
67
|
+
}
|
|
68
|
+
if (!ctx)
|
|
69
|
+
requestRequired(scope);
|
|
70
|
+
const raw = ctx.event.request;
|
|
71
|
+
// Bound now: an async write resumes after awaits, where the ambient request may be gone.
|
|
72
|
+
const target = (raw && typeof raw === 'object' ? raw : undefined);
|
|
73
|
+
return cached(perRequest.get(ctx) ?? perRequest.set(ctx, new Map()).get(ctx), id, () => cookieStorage({
|
|
74
|
+
...options,
|
|
75
|
+
request: raw && typeof raw === 'object' && ('headers' in raw || 'getHeader' in raw) ? toStorageRequest(raw) : undefined,
|
|
76
|
+
response: {
|
|
77
|
+
appendHeader(name, value) {
|
|
78
|
+
if (!appendResponseHeader(name, value, target)) {
|
|
79
|
+
throw new StorageUnavailableError('The response headers were already sent, so this cookie cannot reach the browser. Write it before the page head goes out: in middleware, a loader, a server function or the render before its first chunk.');
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A typed storage for the place this code runs. Call it in a component, a loader or a server
|
|
89
|
+
* function; the scope decides the backend (see `UseStorageScope`) and the rest of the options
|
|
90
|
+
* are those of `createStorage`.
|
|
91
|
+
*
|
|
92
|
+
* Server-side state never lives in a module: each request gets its own adapters, so two
|
|
93
|
+
* visitors rendering at once never see each other's values.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const prefs = useStorage<{ theme: 'light' | 'dark' }>({ scope: 'cookie', namespace: 'ui', cookie: { maxAge: 31536000 } });
|
|
97
|
+
* const theme = prefs.get('theme') ?? 'light';
|
|
98
|
+
*/
|
|
99
|
+
export function useStorage(options = {}) {
|
|
100
|
+
const { scope = 'auto', cookie, ...storageOptions } = options;
|
|
101
|
+
if (scope === 'auto') {
|
|
102
|
+
const provided = useContext(StorageContext);
|
|
103
|
+
if (provided)
|
|
104
|
+
return (options.namespace ? provided.namespace(options.namespace) : provided);
|
|
105
|
+
}
|
|
106
|
+
return createSyncStorage({ ...storageOptions, adapter: resolveAdapter(scope, cookie) });
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `useStorage` with every operation awaited, and the one that can encrypt. Same scopes, same
|
|
110
|
+
* per-request isolation; with `scope: 'auto'` it takes an asynchronous storage from
|
|
111
|
+
* `<StorageProvider>` first.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* const vault = useAsyncStorage<{ token: string }>({ namespace: 'auth', encryption: { keyProvider } });
|
|
115
|
+
* await vault.set('token', token, { encrypt: true });
|
|
116
|
+
*/
|
|
117
|
+
export function useAsyncStorage(options = {}) {
|
|
118
|
+
const { scope = 'auto', cookie, ...storageOptions } = options;
|
|
119
|
+
if (scope === 'auto') {
|
|
120
|
+
const provided = useContext(AsyncStorageContext);
|
|
121
|
+
if (provided)
|
|
122
|
+
return (options.namespace ? provided.namespace(options.namespace) : provided);
|
|
123
|
+
}
|
|
124
|
+
return createAsyncStorage({ ...storageOptions, adapter: resolveAdapter(scope, cookie) });
|
|
125
|
+
}
|
|
126
|
+
function resolveAdapter(scope, cookie) {
|
|
127
|
+
if (scope !== 'auto')
|
|
128
|
+
return adapterFor(scope, cookie);
|
|
129
|
+
if (isBrowser())
|
|
130
|
+
return adapterFor('local');
|
|
131
|
+
if (!getRequestContext())
|
|
132
|
+
requestRequired(scope);
|
|
133
|
+
return adapterFor('request');
|
|
134
|
+
}
|
|
135
|
+
const SLOT = '__storage:';
|
|
136
|
+
const seeded = new Set();
|
|
137
|
+
/**
|
|
138
|
+
* Carries `keys` from the server render to the browser. On the server it puts their values,
|
|
139
|
+
* with expiry and version, in the page data; in the browser, the first call for a key writes
|
|
140
|
+
* the server's value into `storage`, and later calls leave the browser's own value alone.
|
|
141
|
+
*
|
|
142
|
+
* Meant for request-scoped values the page needs again: call it with the same keys on both
|
|
143
|
+
* sides. The page data is readable by anyone who sees the page, so never hydrate a secret,
|
|
144
|
+
* and an encrypted value throws here rather than travel in clear.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* const scratch = useStorage<{ draft: Draft }>({ scope: 'request', namespace: 'editor' });
|
|
148
|
+
* hydrateStorage(scratch, ['draft']);
|
|
149
|
+
*/
|
|
150
|
+
export function hydrateStorage(storage, keys) {
|
|
151
|
+
const slot = SLOT + storage.keyPrefix;
|
|
152
|
+
if (!isBrowser()) {
|
|
153
|
+
const ctx = getRequestContext();
|
|
154
|
+
if (!ctx)
|
|
155
|
+
return;
|
|
156
|
+
const entries = ctx.data.get(slot) ?? {};
|
|
157
|
+
for (const key of keys) {
|
|
158
|
+
const entry = storage.getEntry(key);
|
|
159
|
+
if (entry)
|
|
160
|
+
entries[key] = entry;
|
|
161
|
+
}
|
|
162
|
+
ctx.data.set(slot, entries);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const entries = getServerData()?.[slot];
|
|
166
|
+
if (!entries)
|
|
167
|
+
return;
|
|
168
|
+
const fresh = {};
|
|
169
|
+
for (const key of keys) {
|
|
170
|
+
const id = slot + key;
|
|
171
|
+
if (seeded.has(id) || !entries[key])
|
|
172
|
+
continue;
|
|
173
|
+
seeded.add(id);
|
|
174
|
+
fresh[key] = entries[key];
|
|
175
|
+
}
|
|
176
|
+
storage.restore(fresh, { overwrite: true });
|
|
177
|
+
}
|
|
178
|
+
export { persisted } from './persisted.js';
|
|
179
|
+
export { storageCache } from './cache.js';
|