@wcstack/storage 1.9.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +481 -481
- package/README.md +478 -478
- package/dist/auto.js +3 -3
- package/dist/auto.min.js +3 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +72 -72
package/README.md
CHANGED
|
@@ -1,478 +1,478 @@
|
|
|
1
|
-
# @wcstack/storage
|
|
2
|
-
|
|
3
|
-
`@wcstack/storage` is a headless storage component for the wcstack ecosystem.
|
|
4
|
-
|
|
5
|
-
It is not a visual UI widget.
|
|
6
|
-
It is an **I/O node** that connects browser storage (localStorage / sessionStorage) to reactive state.
|
|
7
|
-
|
|
8
|
-
When combined with `@wcstack/state`, `<wcs-storage>` can be bound directly through a path contract:
|
|
9
|
-
|
|
10
|
-
- **Input / Command Surface**: `key`, `type`, `trigger`
|
|
11
|
-
- **Output State Surface**: `value`, `loading`, `error`
|
|
12
|
-
|
|
13
|
-
This means you can express browser storage persistence declaratively in HTML, without writing `localStorage.getItem()`, `JSON.parse()`, or serialization glue code in the UI layer.
|
|
14
|
-
|
|
15
|
-
`@wcstack/storage` follows the [HAWC](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/docs/articles/HAWC.md) architecture:
|
|
16
|
-
|
|
17
|
-
- **Core** (`StorageCore`) handles storage read/write and cross-tab sync
|
|
18
|
-
- **Shell** (`<wcs-storage>`) connects that state to the DOM
|
|
19
|
-
- Frameworks and binding systems consume it via the [wc-bindable-protocol](https://github.com/wc-bindable-protocol/wc-bindable-protocol)
|
|
20
|
-
|
|
21
|
-
## Why this exists
|
|
22
|
-
|
|
23
|
-
Frontend applications frequently use localStorage / sessionStorage for persisting user settings and session data. Yet the glue code — reading, JSON parsing, saving, error handling — follows the same pattern every time.
|
|
24
|
-
|
|
25
|
-
`@wcstack/storage` moves that glue code into a reusable component and exposes the stored value as bindable state.
|
|
26
|
-
|
|
27
|
-
The flow with `@wcstack/state`:
|
|
28
|
-
|
|
29
|
-
1. `<wcs-storage>` auto-loads from storage on connection
|
|
30
|
-
2. `value` is bound to the UI via `data-wcs`
|
|
31
|
-
3. State changes are automatically written back to storage
|
|
32
|
-
4. Changes from other tabs are automatically detected
|
|
33
|
-
|
|
34
|
-
Persistence becomes a **state transition**, not imperative glue code.
|
|
35
|
-
|
|
36
|
-
## Install
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npm install @wcstack/storage
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Quick Start
|
|
43
|
-
|
|
44
|
-
### 1. Primitive value auto-save
|
|
45
|
-
|
|
46
|
-
Primitive values (strings, numbers, booleans) work with just a `value` binding for two-way persistence.
|
|
47
|
-
|
|
48
|
-
```html
|
|
49
|
-
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
50
|
-
<script type="module" src="https://esm.run/@wcstack/storage/auto"></script>
|
|
51
|
-
|
|
52
|
-
<wcs-state>
|
|
53
|
-
<script type="module">
|
|
54
|
-
export default { username: "" };
|
|
55
|
-
</script>
|
|
56
|
-
</wcs-state>
|
|
57
|
-
|
|
58
|
-
<wcs-storage key="username" data-wcs="value: username"></wcs-storage>
|
|
59
|
-
|
|
60
|
-
<input data-wcs="value: username" placeholder="Username">
|
|
61
|
-
<p>Saved: <span data-wcs="textContent: username"></span></p>
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
This is the default mode:
|
|
65
|
-
|
|
66
|
-
- Set a `key` to auto-load on connection
|
|
67
|
-
- Bind to `value` for two-way persistence
|
|
68
|
-
- Optionally bind `loading` and `error` as well
|
|
69
|
-
|
|
70
|
-
### 2. Persisting objects with `$trackDependency`
|
|
71
|
-
|
|
72
|
-
When sub-properties of an object (e.g. `settings.theme`) change, the parent path `settings` binding does **not** fire.
|
|
73
|
-
This is because `@wcstack/state`'s dependency walk is **parent → child only**.
|
|
74
|
-
|
|
75
|
-
In this case, use `$trackDependency` to explicitly list the sub-properties to watch, and save via `trigger`:
|
|
76
|
-
|
|
77
|
-
```html
|
|
78
|
-
<wcs-state>
|
|
79
|
-
<script type="module">
|
|
80
|
-
export default defineState({
|
|
81
|
-
settings: { theme: "light", lang: "en" },
|
|
82
|
-
|
|
83
|
-
get settingsChanged() {
|
|
84
|
-
this.$trackDependency("settings.theme");
|
|
85
|
-
this.$trackDependency("settings.lang");
|
|
86
|
-
return true;
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
</script>
|
|
90
|
-
</wcs-state>
|
|
91
|
-
|
|
92
|
-
<wcs-storage key="app-settings" manual
|
|
93
|
-
data-wcs="value: settings; trigger: settingsChanged">
|
|
94
|
-
</wcs-storage>
|
|
95
|
-
|
|
96
|
-
<select data-wcs="value: settings.theme">
|
|
97
|
-
<option value="light">Light</option>
|
|
98
|
-
<option value="dark">Dark</option>
|
|
99
|
-
</select>
|
|
100
|
-
|
|
101
|
-
<select data-wcs="value: settings.lang">
|
|
102
|
-
<option value="en">English</option>
|
|
103
|
-
<option value="ja">日本語</option>
|
|
104
|
-
</select>
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
**Flow:**
|
|
108
|
-
|
|
109
|
-
1. User changes theme → `settings.theme` updates
|
|
110
|
-
2. Dynamic dependency triggers `settingsChanged` re-evaluation → returns `true`
|
|
111
|
-
3. `trigger: settingsChanged` binding fires → `save()` executes
|
|
112
|
-
4. The entire `settings` object is saved to localStorage
|
|
113
|
-
|
|
114
|
-
### 3. Using sessionStorage
|
|
115
|
-
|
|
116
|
-
Use `type="session"` for sessionStorage:
|
|
117
|
-
|
|
118
|
-
```html
|
|
119
|
-
<wcs-state>
|
|
120
|
-
<script type="module">
|
|
121
|
-
export default { sessionData: null };
|
|
122
|
-
</script>
|
|
123
|
-
</wcs-state>
|
|
124
|
-
|
|
125
|
-
<wcs-storage key="session-data" type="session"
|
|
126
|
-
data-wcs="value: sessionData">
|
|
127
|
-
</wcs-storage>
|
|
128
|
-
|
|
129
|
-
<p data-wcs="textContent: sessionData"></p>
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### 4. Cross-tab sync
|
|
133
|
-
|
|
134
|
-
localStorage changes are automatically detected from other tabs:
|
|
135
|
-
|
|
136
|
-
```html
|
|
137
|
-
<wcs-state>
|
|
138
|
-
<script type="module">
|
|
139
|
-
export default { sharedCounter: 0 };
|
|
140
|
-
</script>
|
|
141
|
-
</wcs-state>
|
|
142
|
-
|
|
143
|
-
<wcs-storage key="shared-counter"
|
|
144
|
-
data-wcs="value: sharedCounter">
|
|
145
|
-
</wcs-storage>
|
|
146
|
-
|
|
147
|
-
<!-- Changes from other tabs update this value automatically -->
|
|
148
|
-
<p data-wcs="textContent: sharedCounter"></p>
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
> **Note**: The `storage` event only fires for changes made in other tabs of the same origin. Since sessionStorage is not shared across tabs, cross-tab sync only works with localStorage.
|
|
152
|
-
|
|
153
|
-
## State Surface vs Command Surface
|
|
154
|
-
|
|
155
|
-
`<wcs-storage>` exposes two kinds of properties.
|
|
156
|
-
|
|
157
|
-
### Output State (bindable state)
|
|
158
|
-
|
|
159
|
-
Represents the current storage value and is the HAWC main surface:
|
|
160
|
-
|
|
161
|
-
| Property | Type | Description |
|
|
162
|
-
|----------|------|-------------|
|
|
163
|
-
| `value` | `any` | Value stored in storage |
|
|
164
|
-
| `loading` | `boolean` | `true` during read/write |
|
|
165
|
-
| `error` | `WcsStorageError \| Error \| null` | Storage operation error |
|
|
166
|
-
|
|
167
|
-
### Input / Command Surface
|
|
168
|
-
|
|
169
|
-
Controls storage operations from HTML, JS, or `@wcstack/state` bindings:
|
|
170
|
-
|
|
171
|
-
| Property | Type | Description |
|
|
172
|
-
|----------|------|-------------|
|
|
173
|
-
| `key` | `string` | Storage key |
|
|
174
|
-
| `type` | `"local" \| "session"` | Storage type |
|
|
175
|
-
| `value` | `any` | Setting this auto-saves (when not `manual`) |
|
|
176
|
-
| `trigger` | `boolean` | One-way save trigger |
|
|
177
|
-
| `manual` | `boolean` | Disables auto-load and auto-save |
|
|
178
|
-
|
|
179
|
-
## Architecture
|
|
180
|
-
|
|
181
|
-
`@wcstack/storage` follows the HAWC architecture.
|
|
182
|
-
|
|
183
|
-
### Core: `StorageCore`
|
|
184
|
-
|
|
185
|
-
`StorageCore` is a pure `EventTarget` class. It encapsulates:
|
|
186
|
-
|
|
187
|
-
- Storage read, write, and remove
|
|
188
|
-
- Automatic JSON serialization / deserialization
|
|
189
|
-
- Cross-tab sync via `storage` event
|
|
190
|
-
- `wc-bindable-protocol` declaration
|
|
191
|
-
|
|
192
|
-
It works headlessly in any runtime that supports `EventTarget` and `localStorage` / `sessionStorage`.
|
|
193
|
-
|
|
194
|
-
### Shell: `<wcs-storage>`
|
|
195
|
-
|
|
196
|
-
`<wcs-storage>` is a thin `HTMLElement` wrapper around `StorageCore`. It adds:
|
|
197
|
-
|
|
198
|
-
- Attribute / property mapping
|
|
199
|
-
- DOM lifecycle integration (auto-load on connect, cleanup on disconnect)
|
|
200
|
-
- Auto-save via the `value` setter
|
|
201
|
-
- Declarative execution helpers like `trigger`
|
|
202
|
-
|
|
203
|
-
This separation keeps storage logic portable while enabling natural integration with DOM-based binding systems like `@wcstack/state`.
|
|
204
|
-
|
|
205
|
-
### Target injection
|
|
206
|
-
|
|
207
|
-
Core uses **target injection** to fire events directly on the Shell, avoiding event re-dispatch.
|
|
208
|
-
|
|
209
|
-
## Headless usage (Core only)
|
|
210
|
-
|
|
211
|
-
`StorageCore` can be used standalone without DOM. It declares `static wcBindable`, so you can subscribe to state with `bind()` from `@wc-bindable/core` — the same mechanism used by framework adapters:
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
import { StorageCore } from "@wcstack/storage";
|
|
215
|
-
import { bind } from "@wc-bindable/core";
|
|
216
|
-
|
|
217
|
-
const core = new StorageCore();
|
|
218
|
-
|
|
219
|
-
const unbind = bind(core, (name, value) => {
|
|
220
|
-
console.log(`${name}:`, value);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
core.key = "my-data";
|
|
224
|
-
core.load();
|
|
225
|
-
|
|
226
|
-
unbind();
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Auto JSON serialization
|
|
230
|
-
|
|
231
|
-
`StorageCore` automatically serializes and deserializes based on data type:
|
|
232
|
-
|
|
233
|
-
| Type on save | Format in storage | Type on load |
|
|
234
|
-
|-------------|-------------------|-------------|
|
|
235
|
-
| Object / Array | `JSON.stringify()` result | `JSON.parse()` result |
|
|
236
|
-
| String | As-is | Parsed if valid JSON, otherwise the raw string |
|
|
237
|
-
| Number / boolean | `JSON.stringify()` result | `JSON.parse()` result |
|
|
238
|
-
| `null` / `undefined` | Key removed | `null` |
|
|
239
|
-
|
|
240
|
-
## Element Reference
|
|
241
|
-
|
|
242
|
-
### `<wcs-storage>`
|
|
243
|
-
|
|
244
|
-
| Attribute | Type | Default | Description |
|
|
245
|
-
|-----------|------|---------|-------------|
|
|
246
|
-
| `key` | `string` | — | Storage key |
|
|
247
|
-
| `type` | `"local" \| "session"` | `local` | Storage type |
|
|
248
|
-
| `manual` | `boolean` | `false` | Disables auto-load and auto-save |
|
|
249
|
-
|
|
250
|
-
| Property | Type | Description |
|
|
251
|
-
|----------|------|-------------|
|
|
252
|
-
| `value` | `any` | Storage value (auto-saves on set) |
|
|
253
|
-
| `loading` | `boolean` | `true` during read/write |
|
|
254
|
-
| `error` | `WcsStorageError \| Error \| null` | Error info |
|
|
255
|
-
| `trigger` | `boolean` | Set `true` to execute save |
|
|
256
|
-
| `manual` | `boolean` | Manual mode |
|
|
257
|
-
|
|
258
|
-
| Method | Description |
|
|
259
|
-
|--------|-------------|
|
|
260
|
-
| `load()` | Load value from storage |
|
|
261
|
-
| `save()` | Save current value to storage |
|
|
262
|
-
| `remove()` | Remove key from storage |
|
|
263
|
-
|
|
264
|
-
## wc-bindable-protocol
|
|
265
|
-
|
|
266
|
-
Both `StorageCore` and `<wcs-storage>` conform to the wc-bindable-protocol, enabling interop with any protocol-aware framework or component.
|
|
267
|
-
|
|
268
|
-
### Core (`StorageCore`)
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
static wcBindable = {
|
|
272
|
-
protocol: "wc-bindable",
|
|
273
|
-
version: 1,
|
|
274
|
-
properties: [
|
|
275
|
-
{ name: "value", event: "wcs-storage:value-changed",
|
|
276
|
-
getter: (e) => e.detail },
|
|
277
|
-
{ name: "loading", event: "wcs-storage:loading-changed" },
|
|
278
|
-
{ name: "error", event: "wcs-storage:error" },
|
|
279
|
-
],
|
|
280
|
-
};
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
### Shell (`<wcs-storage>`)
|
|
284
|
-
|
|
285
|
-
The Shell extends the Core declaration, adding trigger support for declarative storage operations from binding systems:
|
|
286
|
-
|
|
287
|
-
```typescript
|
|
288
|
-
static wcBindable = {
|
|
289
|
-
...StorageCore.wcBindable,
|
|
290
|
-
properties: [
|
|
291
|
-
...StorageCore.wcBindable.properties,
|
|
292
|
-
{ name: "trigger", event: "wcs-storage:trigger-changed" },
|
|
293
|
-
],
|
|
294
|
-
};
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
## TypeScript Types
|
|
298
|
-
|
|
299
|
-
```typescript
|
|
300
|
-
import type {
|
|
301
|
-
WcsStorageError, WcsStorageCoreValues, WcsStorageValues, StorageType
|
|
302
|
-
} from "@wcstack/storage";
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
type StorageType = "local" | "session";
|
|
307
|
-
|
|
308
|
-
// Storage operation error
|
|
309
|
-
interface WcsStorageError {
|
|
310
|
-
operation: "load" | "save" | "remove";
|
|
311
|
-
message: string;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Core (headless) — 3 state properties
|
|
315
|
-
interface WcsStorageCoreValues<T = unknown> {
|
|
316
|
-
value: T;
|
|
317
|
-
loading: boolean;
|
|
318
|
-
error: WcsStorageError | Error | null;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Shell (<wcs-storage>) — extends Core with trigger
|
|
322
|
-
interface WcsStorageValues<T = unknown> extends WcsStorageCoreValues<T> {
|
|
323
|
-
trigger: boolean;
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
## Why it works well with `@wcstack/state`
|
|
328
|
-
|
|
329
|
-
`@wcstack/state` uses path strings as the sole contract between UI and state.
|
|
330
|
-
`<wcs-storage>` fits naturally into this model:
|
|
331
|
-
|
|
332
|
-
- `<wcs-storage>` auto-loads from storage on connection
|
|
333
|
-
- `value` is bound to a state path, reflected in the UI
|
|
334
|
-
- User interactions update state, which auto-saves back to storage
|
|
335
|
-
- State survives page reloads
|
|
336
|
-
|
|
337
|
-
Persistence looks just like any other state update.
|
|
338
|
-
|
|
339
|
-
## Framework Integration
|
|
340
|
-
|
|
341
|
-
`<wcs-storage>` is HAWC + `wc-bindable-protocol`, so it works in any framework via thin `@wc-bindable/*` adapters.
|
|
342
|
-
|
|
343
|
-
### React
|
|
344
|
-
|
|
345
|
-
```tsx
|
|
346
|
-
import { useWcBindable } from "@wc-bindable/react";
|
|
347
|
-
import type { WcsStorageValues } from "@wcstack/storage";
|
|
348
|
-
|
|
349
|
-
interface Settings { theme: string; lang: string; }
|
|
350
|
-
|
|
351
|
-
function SettingsPanel() {
|
|
352
|
-
const [ref, { value: settings, loading, error }] =
|
|
353
|
-
useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
|
|
354
|
-
|
|
355
|
-
return (
|
|
356
|
-
<>
|
|
357
|
-
<wcs-storage ref={ref} key="app-settings" />
|
|
358
|
-
{loading && <p>Loading...</p>}
|
|
359
|
-
{settings && <p>Theme: {settings.theme}</p>}
|
|
360
|
-
</>
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
### Vue
|
|
366
|
-
|
|
367
|
-
```vue
|
|
368
|
-
<script setup lang="ts">
|
|
369
|
-
import { useWcBindable } from "@wc-bindable/vue";
|
|
370
|
-
import type { WcsStorageValues } from "@wcstack/storage";
|
|
371
|
-
|
|
372
|
-
interface Settings { theme: string; lang: string; }
|
|
373
|
-
|
|
374
|
-
const { ref, values } = useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
|
|
375
|
-
</script>
|
|
376
|
-
|
|
377
|
-
<template>
|
|
378
|
-
<wcs-storage :ref="ref" key="app-settings" />
|
|
379
|
-
<p v-if="values.loading">Loading...</p>
|
|
380
|
-
<p v-else-if="values.value">Theme: {{ values.value.theme }}</p>
|
|
381
|
-
</template>
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
### Svelte
|
|
385
|
-
|
|
386
|
-
```svelte
|
|
387
|
-
<script>
|
|
388
|
-
import { wcBindable } from "@wc-bindable/svelte";
|
|
389
|
-
|
|
390
|
-
let settings = $state(null);
|
|
391
|
-
let loading = $state(false);
|
|
392
|
-
</script>
|
|
393
|
-
|
|
394
|
-
<wcs-storage key="app-settings"
|
|
395
|
-
use:wcBindable={{ onUpdate: (name, v) => {
|
|
396
|
-
if (name === "value") settings = v;
|
|
397
|
-
if (name === "loading") loading = v;
|
|
398
|
-
}}} />
|
|
399
|
-
|
|
400
|
-
{#if loading}
|
|
401
|
-
<p>Loading...</p>
|
|
402
|
-
{:else if settings}
|
|
403
|
-
<p>Theme: {settings.theme}</p>
|
|
404
|
-
{/if}
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
### Solid
|
|
408
|
-
|
|
409
|
-
```tsx
|
|
410
|
-
import { createWcBindable } from "@wc-bindable/solid";
|
|
411
|
-
import type { WcsStorageValues } from "@wcstack/storage";
|
|
412
|
-
|
|
413
|
-
interface Settings { theme: string; lang: string; }
|
|
414
|
-
|
|
415
|
-
function SettingsPanel() {
|
|
416
|
-
const [values, directive] = createWcBindable<WcsStorageValues<Settings>>();
|
|
417
|
-
|
|
418
|
-
return (
|
|
419
|
-
<>
|
|
420
|
-
<wcs-storage ref={directive} key="app-settings" />
|
|
421
|
-
<Show when={!values.loading} fallback={<p>Loading...</p>}>
|
|
422
|
-
<p>Theme: {values.value?.theme}</p>
|
|
423
|
-
</Show>
|
|
424
|
-
</>
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
### Vanilla — direct `bind()`
|
|
430
|
-
|
|
431
|
-
```javascript
|
|
432
|
-
import { bind } from "@wc-bindable/core";
|
|
433
|
-
|
|
434
|
-
const storageEl = document.querySelector("wcs-storage");
|
|
435
|
-
|
|
436
|
-
bind(storageEl, (name, value) => {
|
|
437
|
-
console.log(`${name} changed:`, value);
|
|
438
|
-
});
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
## Optional DOM Triggering
|
|
442
|
-
|
|
443
|
-
When `autoTrigger` is enabled (default), clicking an element with a `data-storagetarget` attribute executes `save()` on the corresponding `<wcs-storage>`:
|
|
444
|
-
|
|
445
|
-
```html
|
|
446
|
-
<button data-storagetarget="settings-store">Save Settings</button>
|
|
447
|
-
<wcs-storage id="settings-store" key="settings" manual
|
|
448
|
-
data-wcs="value: settings"></wcs-storage>
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
## Configuration
|
|
452
|
-
|
|
453
|
-
```javascript
|
|
454
|
-
import { bootstrapStorage } from "@wcstack/storage";
|
|
455
|
-
|
|
456
|
-
bootstrapStorage({
|
|
457
|
-
autoTrigger: true,
|
|
458
|
-
triggerAttribute: "data-storagetarget",
|
|
459
|
-
tagNames: {
|
|
460
|
-
storage: "wcs-storage",
|
|
461
|
-
},
|
|
462
|
-
});
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
## Design Notes
|
|
466
|
-
|
|
467
|
-
- `value`, `loading`, `error` are **output state**
|
|
468
|
-
- `key`, `type`, `trigger` are **input / command surface**
|
|
469
|
-
- `trigger` is intentionally one-way: writing `true` saves, reset signals completion
|
|
470
|
-
- The `value` setter auto-saves when not in `manual` mode
|
|
471
|
-
- JSON auto-serialization handles objects, arrays, and primitives transparently
|
|
472
|
-
- Saving `null` / `undefined` removes the key from storage
|
|
473
|
-
- Cross-tab sync via `storage` event works only with localStorage
|
|
474
|
-
- `manual` is useful when you want explicit control over save timing
|
|
475
|
-
|
|
476
|
-
## License
|
|
477
|
-
|
|
478
|
-
MIT
|
|
1
|
+
# @wcstack/storage
|
|
2
|
+
|
|
3
|
+
`@wcstack/storage` is a headless storage component for the wcstack ecosystem.
|
|
4
|
+
|
|
5
|
+
It is not a visual UI widget.
|
|
6
|
+
It is an **I/O node** that connects browser storage (localStorage / sessionStorage) to reactive state.
|
|
7
|
+
|
|
8
|
+
When combined with `@wcstack/state`, `<wcs-storage>` can be bound directly through a path contract:
|
|
9
|
+
|
|
10
|
+
- **Input / Command Surface**: `key`, `type`, `trigger`
|
|
11
|
+
- **Output State Surface**: `value`, `loading`, `error`
|
|
12
|
+
|
|
13
|
+
This means you can express browser storage persistence declaratively in HTML, without writing `localStorage.getItem()`, `JSON.parse()`, or serialization glue code in the UI layer.
|
|
14
|
+
|
|
15
|
+
`@wcstack/storage` follows the [HAWC](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/docs/articles/HAWC.md) architecture:
|
|
16
|
+
|
|
17
|
+
- **Core** (`StorageCore`) handles storage read/write and cross-tab sync
|
|
18
|
+
- **Shell** (`<wcs-storage>`) connects that state to the DOM
|
|
19
|
+
- Frameworks and binding systems consume it via the [wc-bindable-protocol](https://github.com/wc-bindable-protocol/wc-bindable-protocol)
|
|
20
|
+
|
|
21
|
+
## Why this exists
|
|
22
|
+
|
|
23
|
+
Frontend applications frequently use localStorage / sessionStorage for persisting user settings and session data. Yet the glue code — reading, JSON parsing, saving, error handling — follows the same pattern every time.
|
|
24
|
+
|
|
25
|
+
`@wcstack/storage` moves that glue code into a reusable component and exposes the stored value as bindable state.
|
|
26
|
+
|
|
27
|
+
The flow with `@wcstack/state`:
|
|
28
|
+
|
|
29
|
+
1. `<wcs-storage>` auto-loads from storage on connection
|
|
30
|
+
2. `value` is bound to the UI via `data-wcs`
|
|
31
|
+
3. State changes are automatically written back to storage
|
|
32
|
+
4. Changes from other tabs are automatically detected
|
|
33
|
+
|
|
34
|
+
Persistence becomes a **state transition**, not imperative glue code.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @wcstack/storage
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### 1. Primitive value auto-save
|
|
45
|
+
|
|
46
|
+
Primitive values (strings, numbers, booleans) work with just a `value` binding for two-way persistence.
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
50
|
+
<script type="module" src="https://esm.run/@wcstack/storage/auto"></script>
|
|
51
|
+
|
|
52
|
+
<wcs-state>
|
|
53
|
+
<script type="module">
|
|
54
|
+
export default { username: "" };
|
|
55
|
+
</script>
|
|
56
|
+
</wcs-state>
|
|
57
|
+
|
|
58
|
+
<wcs-storage key="username" data-wcs="value: username"></wcs-storage>
|
|
59
|
+
|
|
60
|
+
<input data-wcs="value: username" placeholder="Username">
|
|
61
|
+
<p>Saved: <span data-wcs="textContent: username"></span></p>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is the default mode:
|
|
65
|
+
|
|
66
|
+
- Set a `key` to auto-load on connection
|
|
67
|
+
- Bind to `value` for two-way persistence
|
|
68
|
+
- Optionally bind `loading` and `error` as well
|
|
69
|
+
|
|
70
|
+
### 2. Persisting objects with `$trackDependency`
|
|
71
|
+
|
|
72
|
+
When sub-properties of an object (e.g. `settings.theme`) change, the parent path `settings` binding does **not** fire.
|
|
73
|
+
This is because `@wcstack/state`'s dependency walk is **parent → child only**.
|
|
74
|
+
|
|
75
|
+
In this case, use `$trackDependency` to explicitly list the sub-properties to watch, and save via `trigger`:
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<wcs-state>
|
|
79
|
+
<script type="module">
|
|
80
|
+
export default defineState({
|
|
81
|
+
settings: { theme: "light", lang: "en" },
|
|
82
|
+
|
|
83
|
+
get settingsChanged() {
|
|
84
|
+
this.$trackDependency("settings.theme");
|
|
85
|
+
this.$trackDependency("settings.lang");
|
|
86
|
+
return true;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
90
|
+
</wcs-state>
|
|
91
|
+
|
|
92
|
+
<wcs-storage key="app-settings" manual
|
|
93
|
+
data-wcs="value: settings; trigger: settingsChanged">
|
|
94
|
+
</wcs-storage>
|
|
95
|
+
|
|
96
|
+
<select data-wcs="value: settings.theme">
|
|
97
|
+
<option value="light">Light</option>
|
|
98
|
+
<option value="dark">Dark</option>
|
|
99
|
+
</select>
|
|
100
|
+
|
|
101
|
+
<select data-wcs="value: settings.lang">
|
|
102
|
+
<option value="en">English</option>
|
|
103
|
+
<option value="ja">日本語</option>
|
|
104
|
+
</select>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Flow:**
|
|
108
|
+
|
|
109
|
+
1. User changes theme → `settings.theme` updates
|
|
110
|
+
2. Dynamic dependency triggers `settingsChanged` re-evaluation → returns `true`
|
|
111
|
+
3. `trigger: settingsChanged` binding fires → `save()` executes
|
|
112
|
+
4. The entire `settings` object is saved to localStorage
|
|
113
|
+
|
|
114
|
+
### 3. Using sessionStorage
|
|
115
|
+
|
|
116
|
+
Use `type="session"` for sessionStorage:
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<wcs-state>
|
|
120
|
+
<script type="module">
|
|
121
|
+
export default { sessionData: null };
|
|
122
|
+
</script>
|
|
123
|
+
</wcs-state>
|
|
124
|
+
|
|
125
|
+
<wcs-storage key="session-data" type="session"
|
|
126
|
+
data-wcs="value: sessionData">
|
|
127
|
+
</wcs-storage>
|
|
128
|
+
|
|
129
|
+
<p data-wcs="textContent: sessionData"></p>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 4. Cross-tab sync
|
|
133
|
+
|
|
134
|
+
localStorage changes are automatically detected from other tabs:
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<wcs-state>
|
|
138
|
+
<script type="module">
|
|
139
|
+
export default { sharedCounter: 0 };
|
|
140
|
+
</script>
|
|
141
|
+
</wcs-state>
|
|
142
|
+
|
|
143
|
+
<wcs-storage key="shared-counter"
|
|
144
|
+
data-wcs="value: sharedCounter">
|
|
145
|
+
</wcs-storage>
|
|
146
|
+
|
|
147
|
+
<!-- Changes from other tabs update this value automatically -->
|
|
148
|
+
<p data-wcs="textContent: sharedCounter"></p>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
> **Note**: The `storage` event only fires for changes made in other tabs of the same origin. Since sessionStorage is not shared across tabs, cross-tab sync only works with localStorage.
|
|
152
|
+
|
|
153
|
+
## State Surface vs Command Surface
|
|
154
|
+
|
|
155
|
+
`<wcs-storage>` exposes two kinds of properties.
|
|
156
|
+
|
|
157
|
+
### Output State (bindable state)
|
|
158
|
+
|
|
159
|
+
Represents the current storage value and is the HAWC main surface:
|
|
160
|
+
|
|
161
|
+
| Property | Type | Description |
|
|
162
|
+
|----------|------|-------------|
|
|
163
|
+
| `value` | `any` | Value stored in storage |
|
|
164
|
+
| `loading` | `boolean` | `true` during read/write |
|
|
165
|
+
| `error` | `WcsStorageError \| Error \| null` | Storage operation error |
|
|
166
|
+
|
|
167
|
+
### Input / Command Surface
|
|
168
|
+
|
|
169
|
+
Controls storage operations from HTML, JS, or `@wcstack/state` bindings:
|
|
170
|
+
|
|
171
|
+
| Property | Type | Description |
|
|
172
|
+
|----------|------|-------------|
|
|
173
|
+
| `key` | `string` | Storage key |
|
|
174
|
+
| `type` | `"local" \| "session"` | Storage type |
|
|
175
|
+
| `value` | `any` | Setting this auto-saves (when not `manual`) |
|
|
176
|
+
| `trigger` | `boolean` | One-way save trigger |
|
|
177
|
+
| `manual` | `boolean` | Disables auto-load and auto-save |
|
|
178
|
+
|
|
179
|
+
## Architecture
|
|
180
|
+
|
|
181
|
+
`@wcstack/storage` follows the HAWC architecture.
|
|
182
|
+
|
|
183
|
+
### Core: `StorageCore`
|
|
184
|
+
|
|
185
|
+
`StorageCore` is a pure `EventTarget` class. It encapsulates:
|
|
186
|
+
|
|
187
|
+
- Storage read, write, and remove
|
|
188
|
+
- Automatic JSON serialization / deserialization
|
|
189
|
+
- Cross-tab sync via `storage` event
|
|
190
|
+
- `wc-bindable-protocol` declaration
|
|
191
|
+
|
|
192
|
+
It works headlessly in any runtime that supports `EventTarget` and `localStorage` / `sessionStorage`.
|
|
193
|
+
|
|
194
|
+
### Shell: `<wcs-storage>`
|
|
195
|
+
|
|
196
|
+
`<wcs-storage>` is a thin `HTMLElement` wrapper around `StorageCore`. It adds:
|
|
197
|
+
|
|
198
|
+
- Attribute / property mapping
|
|
199
|
+
- DOM lifecycle integration (auto-load on connect, cleanup on disconnect)
|
|
200
|
+
- Auto-save via the `value` setter
|
|
201
|
+
- Declarative execution helpers like `trigger`
|
|
202
|
+
|
|
203
|
+
This separation keeps storage logic portable while enabling natural integration with DOM-based binding systems like `@wcstack/state`.
|
|
204
|
+
|
|
205
|
+
### Target injection
|
|
206
|
+
|
|
207
|
+
Core uses **target injection** to fire events directly on the Shell, avoiding event re-dispatch.
|
|
208
|
+
|
|
209
|
+
## Headless usage (Core only)
|
|
210
|
+
|
|
211
|
+
`StorageCore` can be used standalone without DOM. It declares `static wcBindable`, so you can subscribe to state with `bind()` from `@wc-bindable/core` — the same mechanism used by framework adapters:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { StorageCore } from "@wcstack/storage";
|
|
215
|
+
import { bind } from "@wc-bindable/core";
|
|
216
|
+
|
|
217
|
+
const core = new StorageCore();
|
|
218
|
+
|
|
219
|
+
const unbind = bind(core, (name, value) => {
|
|
220
|
+
console.log(`${name}:`, value);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
core.key = "my-data";
|
|
224
|
+
core.load();
|
|
225
|
+
|
|
226
|
+
unbind();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Auto JSON serialization
|
|
230
|
+
|
|
231
|
+
`StorageCore` automatically serializes and deserializes based on data type:
|
|
232
|
+
|
|
233
|
+
| Type on save | Format in storage | Type on load |
|
|
234
|
+
|-------------|-------------------|-------------|
|
|
235
|
+
| Object / Array | `JSON.stringify()` result | `JSON.parse()` result |
|
|
236
|
+
| String | As-is | Parsed if valid JSON, otherwise the raw string |
|
|
237
|
+
| Number / boolean | `JSON.stringify()` result | `JSON.parse()` result |
|
|
238
|
+
| `null` / `undefined` | Key removed | `null` |
|
|
239
|
+
|
|
240
|
+
## Element Reference
|
|
241
|
+
|
|
242
|
+
### `<wcs-storage>`
|
|
243
|
+
|
|
244
|
+
| Attribute | Type | Default | Description |
|
|
245
|
+
|-----------|------|---------|-------------|
|
|
246
|
+
| `key` | `string` | — | Storage key |
|
|
247
|
+
| `type` | `"local" \| "session"` | `local` | Storage type |
|
|
248
|
+
| `manual` | `boolean` | `false` | Disables auto-load and auto-save |
|
|
249
|
+
|
|
250
|
+
| Property | Type | Description |
|
|
251
|
+
|----------|------|-------------|
|
|
252
|
+
| `value` | `any` | Storage value (auto-saves on set) |
|
|
253
|
+
| `loading` | `boolean` | `true` during read/write |
|
|
254
|
+
| `error` | `WcsStorageError \| Error \| null` | Error info |
|
|
255
|
+
| `trigger` | `boolean` | Set `true` to execute save |
|
|
256
|
+
| `manual` | `boolean` | Manual mode |
|
|
257
|
+
|
|
258
|
+
| Method | Description |
|
|
259
|
+
|--------|-------------|
|
|
260
|
+
| `load()` | Load value from storage |
|
|
261
|
+
| `save()` | Save current value to storage |
|
|
262
|
+
| `remove()` | Remove key from storage |
|
|
263
|
+
|
|
264
|
+
## wc-bindable-protocol
|
|
265
|
+
|
|
266
|
+
Both `StorageCore` and `<wcs-storage>` conform to the wc-bindable-protocol, enabling interop with any protocol-aware framework or component.
|
|
267
|
+
|
|
268
|
+
### Core (`StorageCore`)
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
static wcBindable = {
|
|
272
|
+
protocol: "wc-bindable",
|
|
273
|
+
version: 1,
|
|
274
|
+
properties: [
|
|
275
|
+
{ name: "value", event: "wcs-storage:value-changed",
|
|
276
|
+
getter: (e) => e.detail },
|
|
277
|
+
{ name: "loading", event: "wcs-storage:loading-changed" },
|
|
278
|
+
{ name: "error", event: "wcs-storage:error" },
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Shell (`<wcs-storage>`)
|
|
284
|
+
|
|
285
|
+
The Shell extends the Core declaration, adding trigger support for declarative storage operations from binding systems:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
static wcBindable = {
|
|
289
|
+
...StorageCore.wcBindable,
|
|
290
|
+
properties: [
|
|
291
|
+
...StorageCore.wcBindable.properties,
|
|
292
|
+
{ name: "trigger", event: "wcs-storage:trigger-changed" },
|
|
293
|
+
],
|
|
294
|
+
};
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## TypeScript Types
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
import type {
|
|
301
|
+
WcsStorageError, WcsStorageCoreValues, WcsStorageValues, StorageType
|
|
302
|
+
} from "@wcstack/storage";
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
type StorageType = "local" | "session";
|
|
307
|
+
|
|
308
|
+
// Storage operation error
|
|
309
|
+
interface WcsStorageError {
|
|
310
|
+
operation: "load" | "save" | "remove";
|
|
311
|
+
message: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Core (headless) — 3 state properties
|
|
315
|
+
interface WcsStorageCoreValues<T = unknown> {
|
|
316
|
+
value: T;
|
|
317
|
+
loading: boolean;
|
|
318
|
+
error: WcsStorageError | Error | null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Shell (<wcs-storage>) — extends Core with trigger
|
|
322
|
+
interface WcsStorageValues<T = unknown> extends WcsStorageCoreValues<T> {
|
|
323
|
+
trigger: boolean;
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Why it works well with `@wcstack/state`
|
|
328
|
+
|
|
329
|
+
`@wcstack/state` uses path strings as the sole contract between UI and state.
|
|
330
|
+
`<wcs-storage>` fits naturally into this model:
|
|
331
|
+
|
|
332
|
+
- `<wcs-storage>` auto-loads from storage on connection
|
|
333
|
+
- `value` is bound to a state path, reflected in the UI
|
|
334
|
+
- User interactions update state, which auto-saves back to storage
|
|
335
|
+
- State survives page reloads
|
|
336
|
+
|
|
337
|
+
Persistence looks just like any other state update.
|
|
338
|
+
|
|
339
|
+
## Framework Integration
|
|
340
|
+
|
|
341
|
+
`<wcs-storage>` is HAWC + `wc-bindable-protocol`, so it works in any framework via thin `@wc-bindable/*` adapters.
|
|
342
|
+
|
|
343
|
+
### React
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
import { useWcBindable } from "@wc-bindable/react";
|
|
347
|
+
import type { WcsStorageValues } from "@wcstack/storage";
|
|
348
|
+
|
|
349
|
+
interface Settings { theme: string; lang: string; }
|
|
350
|
+
|
|
351
|
+
function SettingsPanel() {
|
|
352
|
+
const [ref, { value: settings, loading, error }] =
|
|
353
|
+
useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<>
|
|
357
|
+
<wcs-storage ref={ref} key="app-settings" />
|
|
358
|
+
{loading && <p>Loading...</p>}
|
|
359
|
+
{settings && <p>Theme: {settings.theme}</p>}
|
|
360
|
+
</>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Vue
|
|
366
|
+
|
|
367
|
+
```vue
|
|
368
|
+
<script setup lang="ts">
|
|
369
|
+
import { useWcBindable } from "@wc-bindable/vue";
|
|
370
|
+
import type { WcsStorageValues } from "@wcstack/storage";
|
|
371
|
+
|
|
372
|
+
interface Settings { theme: string; lang: string; }
|
|
373
|
+
|
|
374
|
+
const { ref, values } = useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
|
|
375
|
+
</script>
|
|
376
|
+
|
|
377
|
+
<template>
|
|
378
|
+
<wcs-storage :ref="ref" key="app-settings" />
|
|
379
|
+
<p v-if="values.loading">Loading...</p>
|
|
380
|
+
<p v-else-if="values.value">Theme: {{ values.value.theme }}</p>
|
|
381
|
+
</template>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Svelte
|
|
385
|
+
|
|
386
|
+
```svelte
|
|
387
|
+
<script>
|
|
388
|
+
import { wcBindable } from "@wc-bindable/svelte";
|
|
389
|
+
|
|
390
|
+
let settings = $state(null);
|
|
391
|
+
let loading = $state(false);
|
|
392
|
+
</script>
|
|
393
|
+
|
|
394
|
+
<wcs-storage key="app-settings"
|
|
395
|
+
use:wcBindable={{ onUpdate: (name, v) => {
|
|
396
|
+
if (name === "value") settings = v;
|
|
397
|
+
if (name === "loading") loading = v;
|
|
398
|
+
}}} />
|
|
399
|
+
|
|
400
|
+
{#if loading}
|
|
401
|
+
<p>Loading...</p>
|
|
402
|
+
{:else if settings}
|
|
403
|
+
<p>Theme: {settings.theme}</p>
|
|
404
|
+
{/if}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Solid
|
|
408
|
+
|
|
409
|
+
```tsx
|
|
410
|
+
import { createWcBindable } from "@wc-bindable/solid";
|
|
411
|
+
import type { WcsStorageValues } from "@wcstack/storage";
|
|
412
|
+
|
|
413
|
+
interface Settings { theme: string; lang: string; }
|
|
414
|
+
|
|
415
|
+
function SettingsPanel() {
|
|
416
|
+
const [values, directive] = createWcBindable<WcsStorageValues<Settings>>();
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<>
|
|
420
|
+
<wcs-storage ref={directive} key="app-settings" />
|
|
421
|
+
<Show when={!values.loading} fallback={<p>Loading...</p>}>
|
|
422
|
+
<p>Theme: {values.value?.theme}</p>
|
|
423
|
+
</Show>
|
|
424
|
+
</>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Vanilla — direct `bind()`
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
import { bind } from "@wc-bindable/core";
|
|
433
|
+
|
|
434
|
+
const storageEl = document.querySelector("wcs-storage");
|
|
435
|
+
|
|
436
|
+
bind(storageEl, (name, value) => {
|
|
437
|
+
console.log(`${name} changed:`, value);
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Optional DOM Triggering
|
|
442
|
+
|
|
443
|
+
When `autoTrigger` is enabled (default), clicking an element with a `data-storagetarget` attribute executes `save()` on the corresponding `<wcs-storage>`:
|
|
444
|
+
|
|
445
|
+
```html
|
|
446
|
+
<button data-storagetarget="settings-store">Save Settings</button>
|
|
447
|
+
<wcs-storage id="settings-store" key="settings" manual
|
|
448
|
+
data-wcs="value: settings"></wcs-storage>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Configuration
|
|
452
|
+
|
|
453
|
+
```javascript
|
|
454
|
+
import { bootstrapStorage } from "@wcstack/storage";
|
|
455
|
+
|
|
456
|
+
bootstrapStorage({
|
|
457
|
+
autoTrigger: true,
|
|
458
|
+
triggerAttribute: "data-storagetarget",
|
|
459
|
+
tagNames: {
|
|
460
|
+
storage: "wcs-storage",
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Design Notes
|
|
466
|
+
|
|
467
|
+
- `value`, `loading`, `error` are **output state**
|
|
468
|
+
- `key`, `type`, `trigger` are **input / command surface**
|
|
469
|
+
- `trigger` is intentionally one-way: writing `true` saves, reset signals completion
|
|
470
|
+
- The `value` setter auto-saves when not in `manual` mode
|
|
471
|
+
- JSON auto-serialization handles objects, arrays, and primitives transparently
|
|
472
|
+
- Saving `null` / `undefined` removes the key from storage
|
|
473
|
+
- Cross-tab sync via `storage` event works only with localStorage
|
|
474
|
+
- `manual` is useful when you want explicit control over save timing
|
|
475
|
+
|
|
476
|
+
## License
|
|
477
|
+
|
|
478
|
+
MIT
|