@weasel-js/storage 0.0.0-stage → 1.9.3
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/LICENSE +21 -0
- package/README.md +17 -2
- package/dist/index.d.ts +139 -0
- package/dist/index.js +500 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/storage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Keyed storage behind one async interface, `StorageAdapter`, with adapters for
|
|
4
|
+
IndexedDB, `localStorage`, `sessionStorage`, the URL hash and memory.
|
|
5
|
+
`openRecords` loads every record under a prefix into a `RecordCache`: reads
|
|
6
|
+
are synchronous from memory, writes land in memory at once and reach storage
|
|
7
|
+
on a debounce, and changes other tabs make arrive as `remote` changes.
|
|
8
|
+
|
|
9
|
+
Adapters that can read synchronously (`localStorage`, `sessionStorage`,
|
|
10
|
+
memory) also implement `listSync`, and `openRecordsSync` opens a cache from
|
|
11
|
+
one with no `await`.
|
|
12
|
+
|
|
13
|
+
`defaultStorage()` is IndexedDB under the database `'weasel'`, or
|
|
14
|
+
`localStorage`, with a warning, where IndexedDB will not open; it chooses once
|
|
15
|
+
per page. `fallbackStorage(preferred, fallback, label)` makes the same choice
|
|
16
|
+
between any two adapters, and `createDefaultStorage(preferred, label)` builds a
|
|
17
|
+
`defaultStorage` of your own over a different preferred adapter, such as
|
|
18
|
+
IndexedDB under another database.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** A change someone else made to one record: its new value, or `undefined`
|
|
2
|
+
* when it was deleted. */
|
|
3
|
+
type StorageChange = [key: string, value: unknown];
|
|
4
|
+
/** Asynchronous keyed storage of structured-clone values, so IndexedDB, the
|
|
5
|
+
* URL, memory or a server can all back one. */
|
|
6
|
+
interface StorageAdapter {
|
|
7
|
+
/** `undefined` when the key is absent. */
|
|
8
|
+
get(key: string): Promise<unknown>;
|
|
9
|
+
list(prefix: string): Promise<[string, unknown][]>;
|
|
10
|
+
/** Every record under `prefix`, read without waiting. Only substrates that
|
|
11
|
+
* can answer synchronously implement it. */
|
|
12
|
+
listSync?(prefix: string): [string, unknown][];
|
|
13
|
+
/** Rejects when the value did not land. */
|
|
14
|
+
set(key: string, value: unknown): Promise<void>;
|
|
15
|
+
delete(key: string): Promise<void>;
|
|
16
|
+
/** Reports writes under `prefix` made by anyone but this adapter — another
|
|
17
|
+
* tab, another instance, a server. Omit when the substrate cannot tell. */
|
|
18
|
+
subscribe?(prefix: string, on: (changes: StorageChange[]) => void): () => void;
|
|
19
|
+
}
|
|
20
|
+
/** An adapter `openRecordsSync` can open. */
|
|
21
|
+
type SyncStorageAdapter = StorageAdapter & Required<Pick<StorageAdapter, 'listSync'>>;
|
|
22
|
+
|
|
23
|
+
/** `preferred` if it opens, otherwise `fallback` — with a warning. */
|
|
24
|
+
declare function fallbackStorage(preferred: StorageAdapter, fallback?: StorageAdapter, label?: string): Promise<StorageAdapter>;
|
|
25
|
+
/** A page-wide default: `preferred`, or localStorage where it will not open,
|
|
26
|
+
* chosen on first call and kept until `resetDefaultStorage` (tests only). */
|
|
27
|
+
declare function createDefaultStorage(preferred: StorageAdapter, label?: string): {
|
|
28
|
+
defaultStorage: () => Promise<StorageAdapter>;
|
|
29
|
+
resetDefaultStorage: () => void;
|
|
30
|
+
};
|
|
31
|
+
/** IndexedDB under the default database, 'weasel', or localStorage where it
|
|
32
|
+
* will not open. Resolved once per page. */
|
|
33
|
+
declare const defaultStorage: () => Promise<StorageAdapter>;
|
|
34
|
+
|
|
35
|
+
/** Options for `createIndexedDbAdapter`. */
|
|
36
|
+
interface IndexedDbAdapterOptions {
|
|
37
|
+
/** Default `'weasel'`. */
|
|
38
|
+
database?: string;
|
|
39
|
+
/** Default `'records'`. One object store per database: a store added to a
|
|
40
|
+
* database that already exists is not created. */
|
|
41
|
+
store?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Persist to IndexedDB, which stores structured-clone values natively — typed
|
|
44
|
+
* arrays, `Map`s, `Blob`s — and holds far more than localStorage. Other tabs
|
|
45
|
+
* hear writes through a `BroadcastChannel`. The database opens on first use. */
|
|
46
|
+
declare function createIndexedDbAdapter({ database, store, }?: IndexedDbAdapterOptions): StorageAdapter;
|
|
47
|
+
/** IndexedDB under the default database and store. */
|
|
48
|
+
declare const indexedDbAdapter: StorageAdapter;
|
|
49
|
+
|
|
50
|
+
/** An in-memory store, discarded on reload. Values are copied in and out, as a
|
|
51
|
+
* real substrate would. Adapters built over one shared `backing` hear each
|
|
52
|
+
* other's writes — two tabs, for a test. */
|
|
53
|
+
declare function createMemoryAdapter(backing?: Map<string, unknown>): SyncStorageAdapter;
|
|
54
|
+
/** Persists nothing and reads back nothing. */
|
|
55
|
+
declare const noneAdapter: SyncStorageAdapter;
|
|
56
|
+
|
|
57
|
+
/** Persist into the URL fragment, so the page's link carries its state and can
|
|
58
|
+
* be shared or bookmarked. Changes to the fragment made elsewhere — a pasted
|
|
59
|
+
* link, back and forward — arrive through `hashchange`. */
|
|
60
|
+
declare const urlHashAdapter: StorageAdapter;
|
|
61
|
+
|
|
62
|
+
/** Persist to `localStorage`, one JSON value per record. Survives a reload and
|
|
63
|
+
* reaches other tabs through the `storage` event. */
|
|
64
|
+
declare const localStorageAdapter: SyncStorageAdapter;
|
|
65
|
+
/** Persist to `sessionStorage` — survives a reload but not a new tab. */
|
|
66
|
+
declare const sessionStorageAdapter: SyncStorageAdapter;
|
|
67
|
+
|
|
68
|
+
/** One record changing, as a cache listener hears it. `value` is `undefined`
|
|
69
|
+
* for a delete. `local` changes were made through this cache; `remote` ones
|
|
70
|
+
* came from another writer. */
|
|
71
|
+
interface RecordChange {
|
|
72
|
+
name: string;
|
|
73
|
+
value: unknown;
|
|
74
|
+
origin: 'local' | 'remote';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Every record under one prefix, held in memory: the only thing that talks to
|
|
78
|
+
* a `StorageAdapter`. Names are keys with the prefix removed.
|
|
79
|
+
*
|
|
80
|
+
* Writes land in memory at once and reach storage on a debounce. The newest
|
|
81
|
+
* write to a record wins: another writer's change to a record with a write
|
|
82
|
+
* still queued here is ignored, since the queued write lands after it.
|
|
83
|
+
*/
|
|
84
|
+
interface RecordCache {
|
|
85
|
+
readonly prefix: string;
|
|
86
|
+
/** False once writing is off — the records could not be read, or the
|
|
87
|
+
* opener marked them read-only — so nothing here can overwrite them. */
|
|
88
|
+
readonly writable: boolean;
|
|
89
|
+
has(name: string): boolean;
|
|
90
|
+
get(name: string): unknown;
|
|
91
|
+
/** Every record whose name starts with `namePrefix`. */
|
|
92
|
+
entries(namePrefix?: string): [string, unknown][];
|
|
93
|
+
set(name: string, value: unknown): void;
|
|
94
|
+
delete(name: string): void;
|
|
95
|
+
subscribe(listener: (changes: RecordChange[]) => void): () => void;
|
|
96
|
+
/** Send queued writes now. Resolves true when every queued write landed or
|
|
97
|
+
* none was queued; false when a write threw (its value stays in memory) or
|
|
98
|
+
* writing is off and writes were queued. */
|
|
99
|
+
flush(): Promise<boolean>;
|
|
100
|
+
/** Send queued writes and stop hearing other writers. */
|
|
101
|
+
close(): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
/** The parts of a cache only its opener uses. */
|
|
104
|
+
interface OwnedRecordCache extends RecordCache {
|
|
105
|
+
/** Hold these records without writing them back. */
|
|
106
|
+
seed(entries: Iterable<[string, unknown]>): void;
|
|
107
|
+
stopWriting(): void;
|
|
108
|
+
}
|
|
109
|
+
/** Options for `createRecordCache` and `openRecords`. */
|
|
110
|
+
interface RecordCacheOptions {
|
|
111
|
+
storage: StorageAdapter;
|
|
112
|
+
prefix: string;
|
|
113
|
+
/** Trailing debounce on writes. Default 300 ms. */
|
|
114
|
+
debounceMs?: number;
|
|
115
|
+
/** The longest a write waits under continuous change. Default 1000 ms. */
|
|
116
|
+
maxWaitMs?: number;
|
|
117
|
+
}
|
|
118
|
+
/** A cache holding `initial`, with nothing read from storage. */
|
|
119
|
+
declare function createRecordCache(options: RecordCacheOptions & {
|
|
120
|
+
initial?: Iterable<[string, unknown]>;
|
|
121
|
+
writable?: boolean;
|
|
122
|
+
}): OwnedRecordCache;
|
|
123
|
+
/** Read every record under `prefix` and hold them. A failed read opens the
|
|
124
|
+
* cache empty with writing off, so it never overwrites what it could not
|
|
125
|
+
* read. */
|
|
126
|
+
declare function openRecords(options: RecordCacheOptions): Promise<OwnedRecordCache>;
|
|
127
|
+
/** `openRecords` for an adapter that reads synchronously: the cache is ready
|
|
128
|
+
* on return. A failed read opens it empty with writing off. */
|
|
129
|
+
declare function openRecordsSync(options: RecordCacheOptions & {
|
|
130
|
+
storage: SyncStorageAdapter;
|
|
131
|
+
}): OwnedRecordCache;
|
|
132
|
+
|
|
133
|
+
/** Encode a string for the URL fragment. */
|
|
134
|
+
declare function encodeUrlHash(value: string): string;
|
|
135
|
+
/** Decode a URL fragment written by `encodeUrlHash`, or `null` if it is
|
|
136
|
+
* malformed. */
|
|
137
|
+
declare function decodeUrlHash(hash: string): string | null;
|
|
138
|
+
|
|
139
|
+
export { type IndexedDbAdapterOptions, type OwnedRecordCache, type RecordCache, type RecordCacheOptions, type RecordChange, type StorageAdapter, type StorageChange, type SyncStorageAdapter, createDefaultStorage, createIndexedDbAdapter, createMemoryAdapter, createRecordCache, decodeUrlHash, defaultStorage, encodeUrlHash, fallbackStorage, indexedDbAdapter, localStorageAdapter, noneAdapter, openRecords, openRecordsSync, sessionStorageAdapter, urlHashAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// src/indexedDb.ts
|
|
2
|
+
function createIndexedDbAdapter({
|
|
3
|
+
database = "weasel",
|
|
4
|
+
store = "records"
|
|
5
|
+
} = {}) {
|
|
6
|
+
let opened = null;
|
|
7
|
+
const open = () => {
|
|
8
|
+
opened ??= new Promise((resolve, reject) => {
|
|
9
|
+
if (typeof indexedDB === "undefined") {
|
|
10
|
+
reject(new Error("[storage] IndexedDB is not available here"));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const request = indexedDB.open(database, 1);
|
|
14
|
+
request.onupgradeneeded = () => {
|
|
15
|
+
if (!request.result.objectStoreNames.contains(store)) {
|
|
16
|
+
request.result.createObjectStore(store);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
request.onsuccess = () => {
|
|
20
|
+
const db = request.result;
|
|
21
|
+
if (!db.objectStoreNames.contains(store)) {
|
|
22
|
+
reject(new Error(`[storage] IndexedDB database "${database}" has no store "${store}"`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
resolve(db);
|
|
26
|
+
};
|
|
27
|
+
request.onerror = () => reject(request.error);
|
|
28
|
+
request.onblocked = () => reject(new Error(`[storage] IndexedDB "${database}" is blocked`));
|
|
29
|
+
});
|
|
30
|
+
return opened;
|
|
31
|
+
};
|
|
32
|
+
const run = (mode, body) => open().then(
|
|
33
|
+
(db) => new Promise((resolve, reject) => {
|
|
34
|
+
const tx = db.transaction(store, mode);
|
|
35
|
+
const result = body(tx.objectStore(store));
|
|
36
|
+
tx.oncomplete = () => resolve(result());
|
|
37
|
+
tx.onerror = () => reject(tx.error);
|
|
38
|
+
tx.onabort = () => reject(tx.error);
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
let channel = null;
|
|
42
|
+
const getChannel = () => {
|
|
43
|
+
if (typeof BroadcastChannel === "undefined") return null;
|
|
44
|
+
if (!channel) {
|
|
45
|
+
channel = new BroadcastChannel(`weasel-storage:${database}:${store}`);
|
|
46
|
+
channel.unref?.();
|
|
47
|
+
}
|
|
48
|
+
return channel;
|
|
49
|
+
};
|
|
50
|
+
const announce = (changes) => {
|
|
51
|
+
getChannel()?.postMessage(changes);
|
|
52
|
+
};
|
|
53
|
+
const adapter = {
|
|
54
|
+
get: (key) => run("readonly", (s) => {
|
|
55
|
+
const request = s.get(key);
|
|
56
|
+
return () => request.result;
|
|
57
|
+
}),
|
|
58
|
+
list: (prefix) => run("readonly", (s) => {
|
|
59
|
+
const range = IDBKeyRange.bound(prefix, `${prefix}\uFFFF`);
|
|
60
|
+
const keys = s.getAllKeys(range);
|
|
61
|
+
const values = s.getAll(range);
|
|
62
|
+
return () => keys.result.map((key, i) => [String(key), values.result[i]]);
|
|
63
|
+
}),
|
|
64
|
+
set: async (key, value) => {
|
|
65
|
+
await run("readwrite", (s) => {
|
|
66
|
+
s.put(value, key);
|
|
67
|
+
return () => void 0;
|
|
68
|
+
});
|
|
69
|
+
announce([[key, value]]);
|
|
70
|
+
},
|
|
71
|
+
delete: async (key) => {
|
|
72
|
+
await run("readwrite", (s) => {
|
|
73
|
+
s.delete(key);
|
|
74
|
+
return () => void 0;
|
|
75
|
+
});
|
|
76
|
+
announce([[key, void 0]]);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
80
|
+
adapter.subscribe = (prefix, on) => {
|
|
81
|
+
const ch = getChannel();
|
|
82
|
+
if (!ch) return () => {
|
|
83
|
+
};
|
|
84
|
+
const handler = (e) => {
|
|
85
|
+
const mine = e.data.filter(([key]) => key.startsWith(prefix));
|
|
86
|
+
if (mine.length > 0) on(mine);
|
|
87
|
+
};
|
|
88
|
+
ch.addEventListener("message", handler);
|
|
89
|
+
return () => ch.removeEventListener("message", handler);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return adapter;
|
|
93
|
+
}
|
|
94
|
+
var indexedDbAdapter = createIndexedDbAdapter();
|
|
95
|
+
|
|
96
|
+
// src/json.ts
|
|
97
|
+
function parse(raw) {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(raw);
|
|
100
|
+
} catch {
|
|
101
|
+
return raw;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function isJsonSafe(value, seen = /* @__PURE__ */ new Set()) {
|
|
105
|
+
if (value === null) return true;
|
|
106
|
+
switch (typeof value) {
|
|
107
|
+
case "string":
|
|
108
|
+
case "boolean":
|
|
109
|
+
return true;
|
|
110
|
+
case "number":
|
|
111
|
+
return Number.isFinite(value);
|
|
112
|
+
case "object": {
|
|
113
|
+
if (seen.has(value)) return false;
|
|
114
|
+
seen.add(value);
|
|
115
|
+
const proto = Object.getPrototypeOf(value);
|
|
116
|
+
const ok = Array.isArray(value) ? value.every((v) => isJsonSafe(v, seen)) : (proto === Object.prototype || proto === null) && Object.values(value).every((v) => v === void 0 || isJsonSafe(v, seen));
|
|
117
|
+
seen.delete(value);
|
|
118
|
+
return ok;
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/webStorage.ts
|
|
126
|
+
function webStorageAdapter(area, name, crossTab) {
|
|
127
|
+
const warned = /* @__PURE__ */ new Set();
|
|
128
|
+
const listNow = (prefix) => {
|
|
129
|
+
const store = area();
|
|
130
|
+
const out = [];
|
|
131
|
+
for (let i = 0; i < store.length; i++) {
|
|
132
|
+
const key = store.key(i);
|
|
133
|
+
if (key === null || !key.startsWith(prefix)) continue;
|
|
134
|
+
const raw = store.getItem(key);
|
|
135
|
+
if (raw !== null) out.push([key, parse(raw)]);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
};
|
|
139
|
+
const adapter = {
|
|
140
|
+
get: async (key) => {
|
|
141
|
+
const raw = area().getItem(key);
|
|
142
|
+
return raw === null ? void 0 : parse(raw);
|
|
143
|
+
},
|
|
144
|
+
list: async (prefix) => listNow(prefix),
|
|
145
|
+
listSync: listNow,
|
|
146
|
+
set: async (key, value) => {
|
|
147
|
+
if (!warned.has(key) && !isJsonSafe(value)) {
|
|
148
|
+
warned.add(key);
|
|
149
|
+
console.warn(
|
|
150
|
+
`[storage] "${key}" holds a value JSON cannot keep exactly; ${name} stores what JSON keeps. Use IndexedDB for binary or non-JSON state.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
area().setItem(key, JSON.stringify(value));
|
|
154
|
+
},
|
|
155
|
+
delete: async (key) => {
|
|
156
|
+
area().removeItem(key);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
if (crossTab) {
|
|
160
|
+
adapter.subscribe = (prefix, on) => {
|
|
161
|
+
if (typeof window === "undefined") return () => {
|
|
162
|
+
};
|
|
163
|
+
const handler = (e) => {
|
|
164
|
+
if (e.key === null || !e.key.startsWith(prefix)) return;
|
|
165
|
+
if (e.storageArea !== area()) return;
|
|
166
|
+
on([[e.key, e.newValue === null ? void 0 : parse(e.newValue)]]);
|
|
167
|
+
};
|
|
168
|
+
window.addEventListener("storage", handler);
|
|
169
|
+
return () => window.removeEventListener("storage", handler);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return adapter;
|
|
173
|
+
}
|
|
174
|
+
var localStorageAdapter = webStorageAdapter(
|
|
175
|
+
() => localStorage,
|
|
176
|
+
"localStorage",
|
|
177
|
+
true
|
|
178
|
+
);
|
|
179
|
+
var sessionStorageAdapter = webStorageAdapter(
|
|
180
|
+
() => sessionStorage,
|
|
181
|
+
"sessionStorage",
|
|
182
|
+
false
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// src/defaultStorage.ts
|
|
186
|
+
async function fallbackStorage(preferred, fallback = localStorageAdapter, label = "preferred storage") {
|
|
187
|
+
try {
|
|
188
|
+
await preferred.list(" ");
|
|
189
|
+
return preferred;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.warn(`[storage] ${label} would not open; using the fallback instead`, error);
|
|
192
|
+
return fallback;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function createDefaultStorage(preferred, label) {
|
|
196
|
+
let resolved = null;
|
|
197
|
+
return {
|
|
198
|
+
defaultStorage: () => resolved ??= fallbackStorage(preferred, localStorageAdapter, label),
|
|
199
|
+
resetDefaultStorage: () => {
|
|
200
|
+
resolved = null;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
var { defaultStorage, resetDefaultStorage } = createDefaultStorage(indexedDbAdapter, "IndexedDB");
|
|
205
|
+
|
|
206
|
+
// src/memory.ts
|
|
207
|
+
var memorySubscribers = /* @__PURE__ */ new WeakMap();
|
|
208
|
+
function createMemoryAdapter(backing = /* @__PURE__ */ new Map()) {
|
|
209
|
+
let subscribers = memorySubscribers.get(backing);
|
|
210
|
+
if (!subscribers) {
|
|
211
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
212
|
+
memorySubscribers.set(backing, subscribers);
|
|
213
|
+
}
|
|
214
|
+
const peers = subscribers;
|
|
215
|
+
const notify = (key, value) => {
|
|
216
|
+
for (const sub of peers) {
|
|
217
|
+
if (sub.owner === adapter || !key.startsWith(sub.prefix)) continue;
|
|
218
|
+
const copy = value === void 0 ? void 0 : structuredClone(value);
|
|
219
|
+
queueMicrotask(() => sub.on([[key, copy]]));
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const listNow = (prefix) => [...backing].filter(([key]) => key.startsWith(prefix)).map(([key, value]) => [key, structuredClone(value)]);
|
|
223
|
+
const adapter = {
|
|
224
|
+
get: async (key) => backing.has(key) ? structuredClone(backing.get(key)) : void 0,
|
|
225
|
+
list: async (prefix) => listNow(prefix),
|
|
226
|
+
listSync: listNow,
|
|
227
|
+
set: async (key, value) => {
|
|
228
|
+
const copy = structuredClone(value);
|
|
229
|
+
backing.set(key, copy);
|
|
230
|
+
notify(key, copy);
|
|
231
|
+
},
|
|
232
|
+
delete: async (key) => {
|
|
233
|
+
backing.delete(key);
|
|
234
|
+
notify(key, void 0);
|
|
235
|
+
},
|
|
236
|
+
subscribe: (prefix, on) => {
|
|
237
|
+
const sub = { owner: adapter, prefix, on };
|
|
238
|
+
peers.add(sub);
|
|
239
|
+
return () => {
|
|
240
|
+
peers.delete(sub);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
return adapter;
|
|
245
|
+
}
|
|
246
|
+
var noneAdapter = {
|
|
247
|
+
get: async () => void 0,
|
|
248
|
+
list: async () => [],
|
|
249
|
+
listSync: () => [],
|
|
250
|
+
set: async () => {
|
|
251
|
+
},
|
|
252
|
+
delete: async () => {
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/urlHash.ts
|
|
257
|
+
function encodeUrlHash(value) {
|
|
258
|
+
return btoa(encodeURIComponent(value));
|
|
259
|
+
}
|
|
260
|
+
function decodeUrlHash(hash) {
|
|
261
|
+
if (!hash) return null;
|
|
262
|
+
try {
|
|
263
|
+
return decodeURIComponent(atob(hash));
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/urlHashAdapter.ts
|
|
270
|
+
var URL_HASH_GUARD = typeof window !== "undefined";
|
|
271
|
+
function readHashMap() {
|
|
272
|
+
if (!URL_HASH_GUARD) return {};
|
|
273
|
+
const raw = decodeUrlHash(window.location.hash.replace(/^#/, ""));
|
|
274
|
+
if (!raw) return {};
|
|
275
|
+
try {
|
|
276
|
+
return JSON.parse(raw);
|
|
277
|
+
} catch {
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
var hashSubscribers = /* @__PURE__ */ new Set();
|
|
282
|
+
function writeHashMap(map) {
|
|
283
|
+
if (!URL_HASH_GUARD) return;
|
|
284
|
+
const encoded = encodeUrlHash(JSON.stringify(map));
|
|
285
|
+
window.history.replaceState(null, "", `#${encoded}`);
|
|
286
|
+
for (const sub of hashSubscribers) sub.last = { ...map };
|
|
287
|
+
}
|
|
288
|
+
var urlHashAdapter = {
|
|
289
|
+
get: async (key) => {
|
|
290
|
+
const raw = readHashMap()[key];
|
|
291
|
+
return raw === void 0 ? void 0 : parse(raw);
|
|
292
|
+
},
|
|
293
|
+
list: async (prefix) => Object.entries(readHashMap()).filter(([key]) => key.startsWith(prefix)).map(([key, raw]) => [key, parse(raw)]),
|
|
294
|
+
set: async (key, value) => {
|
|
295
|
+
const map = readHashMap();
|
|
296
|
+
map[key] = JSON.stringify(value);
|
|
297
|
+
writeHashMap(map);
|
|
298
|
+
},
|
|
299
|
+
delete: async (key) => {
|
|
300
|
+
const map = readHashMap();
|
|
301
|
+
delete map[key];
|
|
302
|
+
writeHashMap(map);
|
|
303
|
+
},
|
|
304
|
+
subscribe: (prefix, on) => {
|
|
305
|
+
if (!URL_HASH_GUARD) return () => {
|
|
306
|
+
};
|
|
307
|
+
const sub = { last: readHashMap() };
|
|
308
|
+
hashSubscribers.add(sub);
|
|
309
|
+
const handler = () => {
|
|
310
|
+
const next = readHashMap();
|
|
311
|
+
const changes = [];
|
|
312
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(sub.last), ...Object.keys(next)])) {
|
|
313
|
+
if (!key.startsWith(prefix) || sub.last[key] === next[key]) continue;
|
|
314
|
+
const raw = next[key];
|
|
315
|
+
changes.push([key, raw === void 0 ? void 0 : parse(raw)]);
|
|
316
|
+
}
|
|
317
|
+
sub.last = next;
|
|
318
|
+
if (changes.length > 0) on(changes);
|
|
319
|
+
};
|
|
320
|
+
window.addEventListener("hashchange", handler);
|
|
321
|
+
return () => {
|
|
322
|
+
hashSubscribers.delete(sub);
|
|
323
|
+
window.removeEventListener("hashchange", handler);
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// src/records.ts
|
|
329
|
+
var DELETED = /* @__PURE__ */ Symbol("deleted");
|
|
330
|
+
function createRecordCache(options) {
|
|
331
|
+
const { storage, prefix } = options;
|
|
332
|
+
const debounceMs = options.debounceMs ?? 300;
|
|
333
|
+
const maxWaitMs = options.maxWaitMs ?? 1e3;
|
|
334
|
+
const values = new Map(options.initial ?? []);
|
|
335
|
+
const queued = /* @__PURE__ */ new Map();
|
|
336
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
337
|
+
let writable = options.writable ?? true;
|
|
338
|
+
let closed = false;
|
|
339
|
+
let timer = null;
|
|
340
|
+
let firstQueuedAt = null;
|
|
341
|
+
const emit = (changes) => {
|
|
342
|
+
for (const listener of [...listeners]) listener(changes);
|
|
343
|
+
};
|
|
344
|
+
const schedule = () => {
|
|
345
|
+
if (!writable || closed) return;
|
|
346
|
+
const now = Date.now();
|
|
347
|
+
firstQueuedAt ??= now;
|
|
348
|
+
if (timer) clearTimeout(timer);
|
|
349
|
+
const wait = Math.max(0, Math.min(debounceMs, firstQueuedAt + maxWaitMs - now));
|
|
350
|
+
timer = setTimeout(() => {
|
|
351
|
+
timer = null;
|
|
352
|
+
void flush();
|
|
353
|
+
}, wait);
|
|
354
|
+
};
|
|
355
|
+
async function flush() {
|
|
356
|
+
if (timer) clearTimeout(timer);
|
|
357
|
+
timer = null;
|
|
358
|
+
firstQueuedAt = null;
|
|
359
|
+
if (queued.size === 0) return true;
|
|
360
|
+
if (!writable) return false;
|
|
361
|
+
const batch = [...queued];
|
|
362
|
+
queued.clear();
|
|
363
|
+
const landed = await Promise.all(
|
|
364
|
+
batch.map(async ([name, value]) => {
|
|
365
|
+
const key = prefix + name;
|
|
366
|
+
try {
|
|
367
|
+
if (value === DELETED) await storage.delete(key);
|
|
368
|
+
else await storage.set(key, value);
|
|
369
|
+
return true;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.warn(`[storage] could not write "${key}"; keeping it in memory`, error);
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
);
|
|
376
|
+
return landed.every(Boolean);
|
|
377
|
+
}
|
|
378
|
+
const applyRemote = (changes) => {
|
|
379
|
+
const applied = [];
|
|
380
|
+
for (const [key, value] of changes) {
|
|
381
|
+
if (!key.startsWith(prefix)) continue;
|
|
382
|
+
const name = key.slice(prefix.length);
|
|
383
|
+
if (queued.has(name)) continue;
|
|
384
|
+
if (value === void 0) {
|
|
385
|
+
if (!values.has(name)) continue;
|
|
386
|
+
values.delete(name);
|
|
387
|
+
} else {
|
|
388
|
+
values.set(name, value);
|
|
389
|
+
}
|
|
390
|
+
applied.push({ name, value, origin: "remote" });
|
|
391
|
+
}
|
|
392
|
+
if (applied.length > 0) emit(applied);
|
|
393
|
+
};
|
|
394
|
+
const unsubscribe = writable ? storage.subscribe?.(prefix, applyRemote) : void 0;
|
|
395
|
+
const onPageHide = () => {
|
|
396
|
+
void flush();
|
|
397
|
+
};
|
|
398
|
+
const onVisibility = () => {
|
|
399
|
+
if (document.visibilityState === "hidden") void flush();
|
|
400
|
+
};
|
|
401
|
+
if (typeof window !== "undefined") {
|
|
402
|
+
window.addEventListener("pagehide", onPageHide);
|
|
403
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
404
|
+
}
|
|
405
|
+
const cache = {
|
|
406
|
+
prefix,
|
|
407
|
+
get writable() {
|
|
408
|
+
return writable;
|
|
409
|
+
},
|
|
410
|
+
has: (name) => values.has(name),
|
|
411
|
+
get: (name) => values.get(name),
|
|
412
|
+
entries: (namePrefix = "") => [...values].filter(([name]) => name.startsWith(namePrefix)),
|
|
413
|
+
set: (name, value) => {
|
|
414
|
+
values.set(name, value);
|
|
415
|
+
queued.set(name, value);
|
|
416
|
+
emit([{ name, value, origin: "local" }]);
|
|
417
|
+
schedule();
|
|
418
|
+
},
|
|
419
|
+
delete: (name) => {
|
|
420
|
+
if (!values.has(name) && !queued.has(name)) return;
|
|
421
|
+
values.delete(name);
|
|
422
|
+
queued.set(name, DELETED);
|
|
423
|
+
emit([{ name, value: void 0, origin: "local" }]);
|
|
424
|
+
schedule();
|
|
425
|
+
},
|
|
426
|
+
subscribe: (listener) => {
|
|
427
|
+
listeners.add(listener);
|
|
428
|
+
return () => {
|
|
429
|
+
listeners.delete(listener);
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
flush,
|
|
433
|
+
close: async () => {
|
|
434
|
+
if (closed) return;
|
|
435
|
+
await flush();
|
|
436
|
+
closed = true;
|
|
437
|
+
unsubscribe?.();
|
|
438
|
+
if (typeof window !== "undefined") {
|
|
439
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
440
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
seed: (entries) => {
|
|
444
|
+
for (const [name, value] of entries) values.set(name, value);
|
|
445
|
+
},
|
|
446
|
+
stopWriting: () => {
|
|
447
|
+
writable = false;
|
|
448
|
+
queued.clear();
|
|
449
|
+
if (timer) clearTimeout(timer);
|
|
450
|
+
timer = null;
|
|
451
|
+
unsubscribe?.();
|
|
452
|
+
},
|
|
453
|
+
applyRemote
|
|
454
|
+
};
|
|
455
|
+
return cache;
|
|
456
|
+
}
|
|
457
|
+
function stripPrefix(prefix, rows) {
|
|
458
|
+
return rows.map(([key, value]) => [key.slice(prefix.length), value]);
|
|
459
|
+
}
|
|
460
|
+
function unreadable(options, error) {
|
|
461
|
+
console.warn(
|
|
462
|
+
`[storage] could not read "${options.prefix}"; opening empty and not persisting`,
|
|
463
|
+
error
|
|
464
|
+
);
|
|
465
|
+
return createRecordCache({ ...options, writable: false });
|
|
466
|
+
}
|
|
467
|
+
async function openRecords(options) {
|
|
468
|
+
const early = [];
|
|
469
|
+
const stopEarly = options.storage.subscribe?.(options.prefix, (c) => early.push(...c));
|
|
470
|
+
let listed;
|
|
471
|
+
try {
|
|
472
|
+
listed = await options.storage.list(options.prefix);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
stopEarly?.();
|
|
475
|
+
return unreadable(options, error);
|
|
476
|
+
}
|
|
477
|
+
const cache = createRecordCache({
|
|
478
|
+
...options,
|
|
479
|
+
initial: stripPrefix(options.prefix, listed)
|
|
480
|
+
});
|
|
481
|
+
stopEarly?.();
|
|
482
|
+
cache.applyRemote(early);
|
|
483
|
+
return cache;
|
|
484
|
+
}
|
|
485
|
+
function openRecordsSync(options) {
|
|
486
|
+
let listed;
|
|
487
|
+
try {
|
|
488
|
+
listed = options.storage.listSync(options.prefix);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
return unreadable(options, error);
|
|
491
|
+
}
|
|
492
|
+
return createRecordCache({
|
|
493
|
+
...options,
|
|
494
|
+
initial: stripPrefix(options.prefix, listed)
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export { createDefaultStorage, createIndexedDbAdapter, createMemoryAdapter, createRecordCache, decodeUrlHash, defaultStorage, encodeUrlHash, fallbackStorage, indexedDbAdapter, localStorageAdapter, noneAdapter, openRecords, openRecordsSync, sessionStorageAdapter, urlHashAdapter };
|
|
499
|
+
//# sourceMappingURL=index.js.map
|
|
500
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/indexedDb.ts","../src/json.ts","../src/webStorage.ts","../src/defaultStorage.ts","../src/memory.ts","../src/urlHash.ts","../src/urlHashAdapter.ts","../src/records.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { StorageAdapter, StorageChange } from './types';\n\n/** Options for `createIndexedDbAdapter`. */\nexport interface IndexedDbAdapterOptions {\n /** Default `'weasel'`. */\n database?: string;\n /** Default `'records'`. One object store per database: a store added to a\n * database that already exists is not created. */\n store?: string;\n}\n\ninterface NodeChannel extends BroadcastChannel {\n unref?: () => void;\n}\n\n/** Persist to IndexedDB, which stores structured-clone values natively — typed\n * arrays, `Map`s, `Blob`s — and holds far more than localStorage. Other tabs\n * hear writes through a `BroadcastChannel`. The database opens on first use. */\nexport function createIndexedDbAdapter({\n database = 'weasel',\n store = 'records',\n}: IndexedDbAdapterOptions = {}): StorageAdapter {\n let opened: Promise<IDBDatabase> | null = null;\n const open = (): Promise<IDBDatabase> => {\n opened ??= new Promise((resolve, reject) => {\n if (typeof indexedDB === 'undefined') {\n reject(new Error('[storage] IndexedDB is not available here'));\n return;\n }\n const request = indexedDB.open(database, 1);\n request.onupgradeneeded = () => {\n if (!request.result.objectStoreNames.contains(store)) {\n request.result.createObjectStore(store);\n }\n };\n request.onsuccess = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(store)) {\n reject(new Error(`[storage] IndexedDB database \"${database}\" has no store \"${store}\"`));\n return;\n }\n resolve(db);\n };\n request.onerror = () => reject(request.error);\n request.onblocked = () => reject(new Error(`[storage] IndexedDB \"${database}\" is blocked`));\n });\n return opened;\n };\n\n const run = <T>(mode: IDBTransactionMode, body: (s: IDBObjectStore) => () => T): Promise<T> =>\n open().then(\n (db) =>\n new Promise<T>((resolve, reject) => {\n const tx = db.transaction(store, mode);\n const result = body(tx.objectStore(store));\n tx.oncomplete = () => resolve(result());\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n }),\n );\n\n let channel: NodeChannel | null = null;\n const getChannel = (): NodeChannel | null => {\n if (typeof BroadcastChannel === 'undefined') return null;\n if (!channel) {\n channel = new BroadcastChannel(`weasel-storage:${database}:${store}`) as NodeChannel;\n // Node keeps its event loop alive for an open channel; a browser has no unref.\n channel.unref?.();\n }\n return channel;\n };\n const announce = (changes: StorageChange[]): void => {\n getChannel()?.postMessage(changes);\n };\n\n const adapter: StorageAdapter = {\n get: (key) =>\n run('readonly', (s) => {\n const request = s.get(key);\n return () => request.result as unknown;\n }),\n list: (prefix) =>\n run('readonly', (s) => {\n const range = IDBKeyRange.bound(prefix, `${prefix}`);\n const keys = s.getAllKeys(range);\n const values = s.getAll(range);\n return () =>\n keys.result.map((key, i): [string, unknown] => [String(key), values.result[i]]);\n }),\n set: async (key, value) => {\n await run('readwrite', (s) => {\n s.put(value, key);\n return () => undefined;\n });\n announce([[key, value]]);\n },\n delete: async (key) => {\n await run('readwrite', (s) => {\n s.delete(key);\n return () => undefined;\n });\n announce([[key, undefined]]);\n },\n };\n if (typeof BroadcastChannel !== 'undefined') {\n adapter.subscribe = (prefix, on) => {\n const ch = getChannel();\n if (!ch) return () => {};\n const handler = (e: MessageEvent<StorageChange[]>): void => {\n const mine = e.data.filter(([key]) => key.startsWith(prefix));\n if (mine.length > 0) on(mine);\n };\n ch.addEventListener('message', handler);\n return () => ch.removeEventListener('message', handler);\n };\n }\n return adapter;\n}\n\n/** IndexedDB under the default database and store. */\nexport const indexedDbAdapter: StorageAdapter = createIndexedDbAdapter();\n","/** Raw strings written before records existed come back as the string they\n * are. */\nexport function parse(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/** Whether `JSON.stringify` would keep `value` exactly. An object key holding\n * `undefined` counts as kept: JSON drops the key, and reading back an absent\n * key yields the same `undefined`. */\nexport function isJsonSafe(value: unknown, seen: Set<object> = new Set()): boolean {\n if (value === null) return true;\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return true;\n case 'number':\n return Number.isFinite(value);\n case 'object': {\n if (seen.has(value)) return false;\n seen.add(value);\n const proto = Object.getPrototypeOf(value);\n const ok = Array.isArray(value)\n ? value.every((v) => isJsonSafe(v, seen))\n : (proto === Object.prototype || proto === null) &&\n Object.values(value).every((v) => v === undefined || isJsonSafe(v, seen));\n seen.delete(value);\n return ok;\n }\n default:\n return false;\n }\n}\n","import { isJsonSafe, parse } from './json';\nimport type { SyncStorageAdapter } from './types';\n\nfunction webStorageAdapter(area: () => Storage, name: string, crossTab: boolean): SyncStorageAdapter {\n const warned = new Set<string>();\n const listNow = (prefix: string): [string, unknown][] => {\n const store = area();\n const out: [string, unknown][] = [];\n for (let i = 0; i < store.length; i++) {\n const key = store.key(i);\n if (key === null || !key.startsWith(prefix)) continue;\n const raw = store.getItem(key);\n if (raw !== null) out.push([key, parse(raw)]);\n }\n return out;\n };\n const adapter: SyncStorageAdapter = {\n get: async (key) => {\n const raw = area().getItem(key);\n return raw === null ? undefined : parse(raw);\n },\n list: async (prefix) => listNow(prefix),\n listSync: listNow,\n set: async (key, value) => {\n if (!warned.has(key) && !isJsonSafe(value)) {\n warned.add(key);\n console.warn(\n `[storage] \"${key}\" holds a value JSON cannot keep exactly; ${name} stores what JSON keeps. Use IndexedDB for binary or non-JSON state.`,\n );\n }\n area().setItem(key, JSON.stringify(value));\n },\n delete: async (key) => {\n area().removeItem(key);\n },\n };\n if (crossTab) {\n adapter.subscribe = (prefix, on) => {\n if (typeof window === 'undefined') return () => {};\n const handler = (e: StorageEvent): void => {\n if (e.key === null || !e.key.startsWith(prefix)) return;\n if (e.storageArea !== area()) return;\n on([[e.key, e.newValue === null ? undefined : parse(e.newValue)]]);\n };\n window.addEventListener('storage', handler);\n return () => window.removeEventListener('storage', handler);\n };\n }\n return adapter;\n}\n\n/** Persist to `localStorage`, one JSON value per record. Survives a reload and\n * reaches other tabs through the `storage` event. */\nexport const localStorageAdapter: SyncStorageAdapter = webStorageAdapter(\n () => localStorage,\n 'localStorage',\n true,\n);\n\n/** Persist to `sessionStorage` — survives a reload but not a new tab. */\nexport const sessionStorageAdapter: SyncStorageAdapter = webStorageAdapter(\n () => sessionStorage,\n 'sessionStorage',\n false,\n);\n","import { indexedDbAdapter } from './indexedDb';\nimport type { StorageAdapter } from './types';\nimport { localStorageAdapter } from './webStorage';\n\n/** `preferred` if it opens, otherwise `fallback` — with a warning. */\nexport async function fallbackStorage(\n preferred: StorageAdapter,\n fallback: StorageAdapter = localStorageAdapter,\n label = 'preferred storage',\n): Promise<StorageAdapter> {\n try {\n await preferred.list(' ');\n return preferred;\n } catch (error) {\n console.warn(`[storage] ${label} would not open; using the fallback instead`, error);\n return fallback;\n }\n}\n\n/** A page-wide default: `preferred`, or localStorage where it will not open,\n * chosen on first call and kept until `resetDefaultStorage` (tests only). */\nexport function createDefaultStorage(\n preferred: StorageAdapter,\n label?: string,\n): { defaultStorage: () => Promise<StorageAdapter>; resetDefaultStorage: () => void } {\n let resolved: Promise<StorageAdapter> | null = null;\n return {\n defaultStorage: () => (resolved ??= fallbackStorage(preferred, localStorageAdapter, label)),\n resetDefaultStorage: () => {\n resolved = null;\n },\n };\n}\n\n/** IndexedDB under the default database, 'weasel', or localStorage where it\n * will not open. Resolved once per page. */\nexport const { defaultStorage, resetDefaultStorage } = createDefaultStorage(indexedDbAdapter, 'IndexedDB');\n","import type { StorageAdapter, StorageChange, SyncStorageAdapter } from './types';\n\ninterface MemorySubscriber {\n owner: StorageAdapter;\n prefix: string;\n on: (changes: StorageChange[]) => void;\n}\n\nconst memorySubscribers = new WeakMap<Map<string, unknown>, Set<MemorySubscriber>>();\n\n/** An in-memory store, discarded on reload. Values are copied in and out, as a\n * real substrate would. Adapters built over one shared `backing` hear each\n * other's writes — two tabs, for a test. */\nexport function createMemoryAdapter(backing: Map<string, unknown> = new Map()): SyncStorageAdapter {\n let subscribers = memorySubscribers.get(backing);\n if (!subscribers) {\n subscribers = new Set();\n memorySubscribers.set(backing, subscribers);\n }\n const peers = subscribers;\n const notify = (key: string, value: unknown): void => {\n for (const sub of peers) {\n if (sub.owner === adapter || !key.startsWith(sub.prefix)) continue;\n const copy = value === undefined ? undefined : structuredClone(value);\n queueMicrotask(() => sub.on([[key, copy]]));\n }\n };\n const listNow = (prefix: string): [string, unknown][] =>\n [...backing]\n .filter(([key]) => key.startsWith(prefix))\n .map(([key, value]): [string, unknown] => [key, structuredClone(value)]);\n const adapter: SyncStorageAdapter = {\n get: async (key) => (backing.has(key) ? structuredClone(backing.get(key)) : undefined),\n list: async (prefix) => listNow(prefix),\n listSync: listNow,\n set: async (key, value) => {\n const copy = structuredClone(value);\n backing.set(key, copy);\n notify(key, copy);\n },\n delete: async (key) => {\n backing.delete(key);\n notify(key, undefined);\n },\n subscribe: (prefix, on) => {\n const sub = { owner: adapter, prefix, on };\n peers.add(sub);\n return () => {\n peers.delete(sub);\n };\n },\n };\n return adapter;\n}\n\n/** Persists nothing and reads back nothing. */\nexport const noneAdapter: SyncStorageAdapter = {\n get: async () => undefined,\n list: async () => [],\n listSync: () => [],\n set: async () => {},\n delete: async () => {},\n};\n","/** Encode a string for the URL fragment. */\nexport function encodeUrlHash(value: string): string {\n return btoa(encodeURIComponent(value));\n}\n\n/** Decode a URL fragment written by `encodeUrlHash`, or `null` if it is\n * malformed. */\nexport function decodeUrlHash(hash: string): string | null {\n if (!hash) return null;\n try {\n return decodeURIComponent(atob(hash));\n } catch {\n return null;\n }\n}\n","import { parse } from './json';\nimport type { StorageAdapter, StorageChange } from './types';\nimport { decodeUrlHash, encodeUrlHash } from './urlHash';\n\nconst URL_HASH_GUARD = typeof window !== 'undefined';\n\nfunction readHashMap(): Record<string, string> {\n if (!URL_HASH_GUARD) return {};\n const raw = decodeUrlHash(window.location.hash.replace(/^#/, ''));\n if (!raw) return {};\n try {\n return JSON.parse(raw) as Record<string, string>;\n } catch {\n return {};\n }\n}\n\n/** What each live subscription last saw, refreshed on this page's own writes\n * so `hashchange` reports only somebody else's. */\nconst hashSubscribers = new Set<{ last: Record<string, string> }>();\n\nfunction writeHashMap(map: Record<string, string>): void {\n if (!URL_HASH_GUARD) return;\n const encoded = encodeUrlHash(JSON.stringify(map));\n window.history.replaceState(null, '', `#${encoded}`);\n for (const sub of hashSubscribers) sub.last = { ...map };\n}\n\n/** Persist into the URL fragment, so the page's link carries its state and can\n * be shared or bookmarked. Changes to the fragment made elsewhere — a pasted\n * link, back and forward — arrive through `hashchange`. */\nexport const urlHashAdapter: StorageAdapter = {\n get: async (key) => {\n const raw = readHashMap()[key];\n return raw === undefined ? undefined : parse(raw);\n },\n list: async (prefix) =>\n Object.entries(readHashMap())\n .filter(([key]) => key.startsWith(prefix))\n .map(([key, raw]): [string, unknown] => [key, parse(raw)]),\n set: async (key, value) => {\n const map = readHashMap();\n map[key] = JSON.stringify(value);\n writeHashMap(map);\n },\n delete: async (key) => {\n const map = readHashMap();\n delete map[key];\n writeHashMap(map);\n },\n subscribe: (prefix, on) => {\n if (!URL_HASH_GUARD) return () => {};\n const sub = { last: readHashMap() };\n hashSubscribers.add(sub);\n const handler = (): void => {\n const next = readHashMap();\n const changes: StorageChange[] = [];\n for (const key of new Set([...Object.keys(sub.last), ...Object.keys(next)])) {\n if (!key.startsWith(prefix) || sub.last[key] === next[key]) continue;\n const raw = next[key];\n changes.push([key, raw === undefined ? undefined : parse(raw)]);\n }\n sub.last = next;\n if (changes.length > 0) on(changes);\n };\n window.addEventListener('hashchange', handler);\n return () => {\n hashSubscribers.delete(sub);\n window.removeEventListener('hashchange', handler);\n };\n },\n};\n","import type { StorageAdapter, StorageChange, SyncStorageAdapter } from './types';\n\n/** One record changing, as a cache listener hears it. `value` is `undefined`\n * for a delete. `local` changes were made through this cache; `remote` ones\n * came from another writer. */\nexport interface RecordChange {\n name: string;\n value: unknown;\n origin: 'local' | 'remote';\n}\n\n/**\n * Every record under one prefix, held in memory: the only thing that talks to\n * a `StorageAdapter`. Names are keys with the prefix removed.\n *\n * Writes land in memory at once and reach storage on a debounce. The newest\n * write to a record wins: another writer's change to a record with a write\n * still queued here is ignored, since the queued write lands after it.\n */\nexport interface RecordCache {\n readonly prefix: string;\n /** False once writing is off — the records could not be read, or the\n * opener marked them read-only — so nothing here can overwrite them. */\n readonly writable: boolean;\n has(name: string): boolean;\n get(name: string): unknown;\n /** Every record whose name starts with `namePrefix`. */\n entries(namePrefix?: string): [string, unknown][];\n set(name: string, value: unknown): void;\n delete(name: string): void;\n subscribe(listener: (changes: RecordChange[]) => void): () => void;\n /** Send queued writes now. Resolves true when every queued write landed or\n * none was queued; false when a write threw (its value stays in memory) or\n * writing is off and writes were queued. */\n flush(): Promise<boolean>;\n /** Send queued writes and stop hearing other writers. */\n close(): Promise<void>;\n}\n\n/** The parts of a cache only its opener uses. */\nexport interface OwnedRecordCache extends RecordCache {\n /** Hold these records without writing them back. */\n seed(entries: Iterable<[string, unknown]>): void;\n stopWriting(): void;\n}\n\n/** Options for `createRecordCache` and `openRecords`. */\nexport interface RecordCacheOptions {\n storage: StorageAdapter;\n prefix: string;\n /** Trailing debounce on writes. Default 300 ms. */\n debounceMs?: number;\n /** The longest a write waits under continuous change. Default 1000 ms. */\n maxWaitMs?: number;\n}\n\nconst DELETED = Symbol('deleted');\n\n/** A cache holding `initial`, with nothing read from storage. */\nexport function createRecordCache(\n options: RecordCacheOptions & { initial?: Iterable<[string, unknown]>; writable?: boolean },\n): OwnedRecordCache {\n const { storage, prefix } = options;\n const debounceMs = options.debounceMs ?? 300;\n const maxWaitMs = options.maxWaitMs ?? 1000;\n const values = new Map<string, unknown>(options.initial ?? []);\n const queued = new Map<string, unknown>();\n const listeners = new Set<(changes: RecordChange[]) => void>();\n let writable = options.writable ?? true;\n let closed = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let firstQueuedAt: number | null = null;\n\n const emit = (changes: RecordChange[]): void => {\n for (const listener of [...listeners]) listener(changes);\n };\n\n const schedule = (): void => {\n if (!writable || closed) return;\n const now = Date.now();\n firstQueuedAt ??= now;\n if (timer) clearTimeout(timer);\n const wait = Math.max(0, Math.min(debounceMs, firstQueuedAt + maxWaitMs - now));\n timer = setTimeout(() => {\n timer = null;\n void flush();\n }, wait);\n };\n\n async function flush(): Promise<boolean> {\n if (timer) clearTimeout(timer);\n timer = null;\n firstQueuedAt = null;\n if (queued.size === 0) return true;\n if (!writable) return false;\n const batch = [...queued];\n queued.clear();\n const landed = await Promise.all(\n batch.map(async ([name, value]) => {\n const key = prefix + name;\n try {\n if (value === DELETED) await storage.delete(key);\n else await storage.set(key, value);\n return true;\n } catch (error) {\n console.warn(`[storage] could not write \"${key}\"; keeping it in memory`, error);\n return false;\n }\n }),\n );\n return landed.every(Boolean);\n }\n\n const applyRemote = (changes: StorageChange[]): void => {\n const applied: RecordChange[] = [];\n for (const [key, value] of changes) {\n if (!key.startsWith(prefix)) continue;\n const name = key.slice(prefix.length);\n if (queued.has(name)) continue;\n if (value === undefined) {\n if (!values.has(name)) continue;\n values.delete(name);\n } else {\n values.set(name, value);\n }\n applied.push({ name, value, origin: 'remote' });\n }\n if (applied.length > 0) emit(applied);\n };\n\n const unsubscribe = writable ? storage.subscribe?.(prefix, applyRemote) : undefined;\n\n const onPageHide = (): void => {\n void flush();\n };\n const onVisibility = (): void => {\n if (document.visibilityState === 'hidden') void flush();\n };\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', onPageHide);\n document.addEventListener('visibilitychange', onVisibility);\n }\n\n const cache: OwnedRecordCache & { applyRemote: typeof applyRemote } = {\n prefix,\n get writable() {\n return writable;\n },\n has: (name) => values.has(name),\n get: (name) => values.get(name),\n entries: (namePrefix = '') => [...values].filter(([name]) => name.startsWith(namePrefix)),\n set: (name, value) => {\n values.set(name, value);\n queued.set(name, value);\n emit([{ name, value, origin: 'local' }]);\n schedule();\n },\n delete: (name) => {\n if (!values.has(name) && !queued.has(name)) return;\n values.delete(name);\n queued.set(name, DELETED);\n emit([{ name, value: undefined, origin: 'local' }]);\n schedule();\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n flush,\n close: async () => {\n if (closed) return;\n await flush();\n closed = true;\n unsubscribe?.();\n if (typeof window !== 'undefined') {\n window.removeEventListener('pagehide', onPageHide);\n document.removeEventListener('visibilitychange', onVisibility);\n }\n },\n seed: (entries) => {\n for (const [name, value] of entries) values.set(name, value);\n },\n stopWriting: () => {\n writable = false;\n queued.clear();\n if (timer) clearTimeout(timer);\n timer = null;\n unsubscribe?.();\n },\n applyRemote,\n };\n return cache;\n}\n\nfunction stripPrefix(prefix: string, rows: [string, unknown][]): [string, unknown][] {\n return rows.map(([key, value]): [string, unknown] => [key.slice(prefix.length), value]);\n}\n\nfunction unreadable(options: RecordCacheOptions, error: unknown): OwnedRecordCache {\n console.warn(\n `[storage] could not read \"${options.prefix}\"; opening empty and not persisting`,\n error,\n );\n return createRecordCache({ ...options, writable: false });\n}\n\n/** Read every record under `prefix` and hold them. A failed read opens the\n * cache empty with writing off, so it never overwrites what it could not\n * read. */\nexport async function openRecords(options: RecordCacheOptions): Promise<OwnedRecordCache> {\n // Listen before listing, so a change landing between the two is not lost.\n const early: StorageChange[] = [];\n const stopEarly = options.storage.subscribe?.(options.prefix, (c) => early.push(...c));\n let listed: [string, unknown][];\n try {\n listed = await options.storage.list(options.prefix);\n } catch (error) {\n stopEarly?.();\n return unreadable(options, error);\n }\n const cache = createRecordCache({\n ...options,\n initial: stripPrefix(options.prefix, listed),\n });\n stopEarly?.();\n (cache as unknown as { applyRemote: (c: StorageChange[]) => void }).applyRemote(early);\n return cache;\n}\n\n/** `openRecords` for an adapter that reads synchronously: the cache is ready\n * on return. A failed read opens it empty with writing off. */\nexport function openRecordsSync(\n options: RecordCacheOptions & { storage: SyncStorageAdapter },\n): OwnedRecordCache {\n let listed: [string, unknown][];\n try {\n listed = options.storage.listSync(options.prefix);\n } catch (error) {\n return unreadable(options, error);\n }\n return createRecordCache({\n ...options,\n initial: stripPrefix(options.prefix, listed),\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/storage",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.9.3",
|
|
4
|
+
"description": "Keyed async storage behind one adapter interface — IndexedDB, localStorage, the URL hash, memory — and an in-memory record cache that writes behind and hears other tabs. No React, no weasel deps.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"author": "orochi235",
|
|
19
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
23
|
+
"directory": "packages/storage"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"provenance": true
|
|
42
|
+
}
|
|
43
|
+
}
|