@wcstack/storage 1.9.1 → 1.10.4
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 +518 -481
- package/README.md +515 -478
- package/dist/auto.js +3 -3
- package/dist/auto.min.js +3 -3
- package/dist/index.d.ts +14 -1
- package/dist/index.esm.js +173 -32
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +72 -72
package/README.md
CHANGED
|
@@ -1,478 +1,515 @@
|
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
{ name: "
|
|
293
|
-
],
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
##
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
{
|
|
401
|
-
|
|
402
|
-
{:
|
|
403
|
-
|
|
404
|
-
{
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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 [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md) (Core / Shell / Binding Contract) 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 CSBC 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 CSBC 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
|
+
The declaration follows the full wc-bindable interface model — three independent surfaces:
|
|
269
|
+
|
|
270
|
+
- **`properties`** — observable outputs that `bind()` subscribes to (`value`, `loading`, `error`, and the Shell's `trigger`)
|
|
271
|
+
- **`inputs`** — the settable surface (`key`, `type`, …); declarative metadata that tooling, codegen, and remote proxying read
|
|
272
|
+
- **`commands`** — invocable methods (`load`, `save`, `remove`); a binding system such as `@wcstack/state` can invoke them by name
|
|
273
|
+
|
|
274
|
+
Per the protocol, only `properties` is interpreted by core `bind()`; `inputs` / `commands` (and the `attribute` / `async` hints) are descriptive. They do **not** create implicit two-way data flow.
|
|
275
|
+
|
|
276
|
+
### Core (`StorageCore`)
|
|
277
|
+
|
|
278
|
+
`StorageCore` declares the bindable state any runtime can subscribe to, plus its portable input/command surface:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
static wcBindable = {
|
|
282
|
+
protocol: "wc-bindable",
|
|
283
|
+
version: 1,
|
|
284
|
+
properties: [
|
|
285
|
+
{ name: "value", event: "wcs-storage:value-changed",
|
|
286
|
+
getter: (e) => e.detail },
|
|
287
|
+
{ name: "loading", event: "wcs-storage:loading-changed" },
|
|
288
|
+
{ name: "error", event: "wcs-storage:error" },
|
|
289
|
+
],
|
|
290
|
+
inputs: [
|
|
291
|
+
{ name: "key" },
|
|
292
|
+
{ name: "type" },
|
|
293
|
+
],
|
|
294
|
+
commands: [
|
|
295
|
+
{ name: "load" },
|
|
296
|
+
{ name: "save" },
|
|
297
|
+
{ name: "remove" },
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Headless consumers call `core.load()` / `core.save(value)` directly — no `trigger` needed.
|
|
303
|
+
|
|
304
|
+
### Shell (`<wcs-storage>`)
|
|
305
|
+
|
|
306
|
+
The Shell extends the Core declaration with the `trigger` output and the DOM-driven input surface; `commands` (`load` / `save` / `remove`) are inherited unchanged:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
static wcBindable = {
|
|
310
|
+
...StorageCore.wcBindable,
|
|
311
|
+
properties: [
|
|
312
|
+
...StorageCore.wcBindable.properties,
|
|
313
|
+
{ name: "trigger", event: "wcs-storage:trigger-changed" },
|
|
314
|
+
],
|
|
315
|
+
inputs: [
|
|
316
|
+
{ name: "key" },
|
|
317
|
+
{ name: "type" },
|
|
318
|
+
{ name: "value" },
|
|
319
|
+
{ name: "manual" },
|
|
320
|
+
{ name: "trigger" },
|
|
321
|
+
],
|
|
322
|
+
};
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The Shell's inputs intentionally carry no `attribute` hint: the `key` / `type` / `manual` setters already reflect to their attributes, so a binding system that mirrors `inputs[].attribute` would set the attribute twice.
|
|
326
|
+
|
|
327
|
+
## TypeScript Types
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import type {
|
|
331
|
+
WcsStorageError, WcsStorageCoreValues, WcsStorageValues, StorageType
|
|
332
|
+
} from "@wcstack/storage";
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
type StorageType = "local" | "session";
|
|
337
|
+
|
|
338
|
+
// Storage operation error
|
|
339
|
+
interface WcsStorageError {
|
|
340
|
+
operation: "load" | "save" | "remove";
|
|
341
|
+
message: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Core (headless) — 3 state properties
|
|
345
|
+
interface WcsStorageCoreValues<T = unknown> {
|
|
346
|
+
value: T;
|
|
347
|
+
loading: boolean;
|
|
348
|
+
error: WcsStorageError | Error | null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Shell (<wcs-storage>) — extends Core with trigger
|
|
352
|
+
interface WcsStorageValues<T = unknown> extends WcsStorageCoreValues<T> {
|
|
353
|
+
trigger: boolean;
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Why it works well with `@wcstack/state`
|
|
358
|
+
|
|
359
|
+
`@wcstack/state` uses path strings as the sole contract between UI and state.
|
|
360
|
+
`<wcs-storage>` fits naturally into this model:
|
|
361
|
+
|
|
362
|
+
- `<wcs-storage>` auto-loads from storage on connection
|
|
363
|
+
- `value` is bound to a state path, reflected in the UI
|
|
364
|
+
- User interactions update state, which auto-saves back to storage
|
|
365
|
+
- State survives page reloads
|
|
366
|
+
|
|
367
|
+
Persistence looks just like any other state update.
|
|
368
|
+
|
|
369
|
+
## Framework Integration
|
|
370
|
+
|
|
371
|
+
`<wcs-storage>` is CSBC + `wc-bindable-protocol`, so it works in any framework via thin `@wc-bindable/*` adapters.
|
|
372
|
+
|
|
373
|
+
### React
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
import { useWcBindable } from "@wc-bindable/react";
|
|
377
|
+
import type { WcsStorageValues } from "@wcstack/storage";
|
|
378
|
+
|
|
379
|
+
interface Settings { theme: string; lang: string; }
|
|
380
|
+
|
|
381
|
+
function SettingsPanel() {
|
|
382
|
+
const [ref, { value: settings, loading, error }] =
|
|
383
|
+
useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<>
|
|
387
|
+
<wcs-storage ref={ref} key="app-settings" />
|
|
388
|
+
{loading && <p>Loading...</p>}
|
|
389
|
+
{settings && <p>Theme: {settings.theme}</p>}
|
|
390
|
+
</>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Vue
|
|
396
|
+
|
|
397
|
+
```vue
|
|
398
|
+
<script setup lang="ts">
|
|
399
|
+
import { useWcBindable } from "@wc-bindable/vue";
|
|
400
|
+
import type { WcsStorageValues } from "@wcstack/storage";
|
|
401
|
+
|
|
402
|
+
interface Settings { theme: string; lang: string; }
|
|
403
|
+
|
|
404
|
+
const { ref, values } = useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
|
|
405
|
+
</script>
|
|
406
|
+
|
|
407
|
+
<template>
|
|
408
|
+
<wcs-storage :ref="ref" key="app-settings" />
|
|
409
|
+
<p v-if="values.loading">Loading...</p>
|
|
410
|
+
<p v-else-if="values.value">Theme: {{ values.value.theme }}</p>
|
|
411
|
+
</template>
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Svelte
|
|
415
|
+
|
|
416
|
+
```svelte
|
|
417
|
+
<script>
|
|
418
|
+
import { wcBindable } from "@wc-bindable/svelte";
|
|
419
|
+
|
|
420
|
+
let settings = $state(null);
|
|
421
|
+
let loading = $state(false);
|
|
422
|
+
</script>
|
|
423
|
+
|
|
424
|
+
<wcs-storage key="app-settings"
|
|
425
|
+
use:wcBindable={{ onUpdate: (name, v) => {
|
|
426
|
+
if (name === "value") settings = v;
|
|
427
|
+
if (name === "loading") loading = v;
|
|
428
|
+
}}} />
|
|
429
|
+
|
|
430
|
+
{#if loading}
|
|
431
|
+
<p>Loading...</p>
|
|
432
|
+
{:else if settings}
|
|
433
|
+
<p>Theme: {settings.theme}</p>
|
|
434
|
+
{/if}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Solid
|
|
438
|
+
|
|
439
|
+
```tsx
|
|
440
|
+
import { createWcBindable } from "@wc-bindable/solid";
|
|
441
|
+
import type { WcsStorageValues } from "@wcstack/storage";
|
|
442
|
+
|
|
443
|
+
interface Settings { theme: string; lang: string; }
|
|
444
|
+
|
|
445
|
+
function SettingsPanel() {
|
|
446
|
+
const [values, directive] = createWcBindable<WcsStorageValues<Settings>>();
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<>
|
|
450
|
+
<wcs-storage ref={directive} key="app-settings" />
|
|
451
|
+
<Show when={!values.loading} fallback={<p>Loading...</p>}>
|
|
452
|
+
<p>Theme: {values.value?.theme}</p>
|
|
453
|
+
</Show>
|
|
454
|
+
</>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Vanilla — direct `bind()`
|
|
460
|
+
|
|
461
|
+
```javascript
|
|
462
|
+
import { bind } from "@wc-bindable/core";
|
|
463
|
+
|
|
464
|
+
const storageEl = document.querySelector("wcs-storage");
|
|
465
|
+
|
|
466
|
+
bind(storageEl, (name, value) => {
|
|
467
|
+
console.log(`${name} changed:`, value);
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Optional DOM Triggering
|
|
472
|
+
|
|
473
|
+
When `autoTrigger` is enabled (default), clicking an element with a `data-storagetarget` attribute executes `save()` on the corresponding `<wcs-storage>`:
|
|
474
|
+
|
|
475
|
+
```html
|
|
476
|
+
<button data-storagetarget="settings-store">Save Settings</button>
|
|
477
|
+
<wcs-storage id="settings-store" key="settings" manual
|
|
478
|
+
data-wcs="value: settings"></wcs-storage>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## Configuration
|
|
482
|
+
|
|
483
|
+
```javascript
|
|
484
|
+
import { bootstrapStorage } from "@wcstack/storage";
|
|
485
|
+
|
|
486
|
+
bootstrapStorage({
|
|
487
|
+
autoTrigger: true,
|
|
488
|
+
triggerAttribute: "data-storagetarget",
|
|
489
|
+
tagNames: {
|
|
490
|
+
storage: "wcs-storage",
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Design Notes
|
|
496
|
+
|
|
497
|
+
- `value`, `loading`, `error` are **output state**
|
|
498
|
+
- `key`, `type`, `trigger` are **input / command surface**
|
|
499
|
+
- `trigger` is intentionally one-way: writing `true` saves, reset signals completion. If the underlying `save()` throws (e.g. `key` is unset), `trigger` is still reset to `false` and the completion event still fires, so it never gets stuck in the `true` state.
|
|
500
|
+
- The `value` setter auto-saves when not in `manual` mode
|
|
501
|
+
- **`value` setter vs `save()` / `trigger`**: assigning `value` (non-manual) persists the *assigned* argument (write-through). `save()` and `trigger`, by contrast, persist the *current* `value` — which a prior `load()` or a cross-tab `storage` event may have updated. This means `trigger`/`save()` can write back a value that arrived from another tab.
|
|
502
|
+
- **`value` in `manual` mode**: the `value` setter **stages** the value (no storage write) instead of persisting it. `el.value = x` updates the readable value (`el.value === x`) but does **not** touch storage; the actual write happens only via `save()` / `trigger`. This is what makes the `value: …` + `trigger: …` binding pair work — the bound value is staged, then committed on trigger.
|
|
503
|
+
- **No echo guard on the non-manual `value` path**: only the *staging* path (the Core `value` setter, used in `manual` mode) skips a same-value `value-changed` re-dispatch. The main non-manual path (`value` setter → `save()`) is deliberately write-through: every assignment persists and re-emits `value-changed`, even when the assigned value equals the current one. This is intentional — the write-through contract above must hold, and same-tab `storage` events do not re-fire, so there is no feedback loop. In a `data-wcs="value: x"` two-way binding the echoed `value-changed` is harmless: `@wcstack/state` dedups the round-trip on its side.
|
|
504
|
+
- **`save` command arity**: the headless Core takes `save(value)`, while the Shell exposes `save()` (persists the current value). Both appear under the same `commands` name `save`; the protocol's `commands` metadata is descriptive and arity-less, so this is contractual, not a protocol violation.
|
|
505
|
+
- **Invalid `type`**: any `type` attribute other than `"session"` is treated as `"local"`. An invalid value (e.g. `type="foo"`) silently falls back to `local` rather than throwing.
|
|
506
|
+
- **Runtime `type` change**: changing the `type` attribute after connection updates the Core's storage area for subsequent operations but does **not** re-load from the new area (only `key` changes auto-reload in non-manual mode). Re-load explicitly with `load()` if you need the value from the newly selected area.
|
|
507
|
+
- **`error` shape**: on a storage failure, `error` is set to a `WcsStorageError` (`{ operation, message }`) identifying which operation (`load` / `save` / `remove`) failed. `key is required` (calling an operation with no key) is thrown synchronously, not surfaced via `error`. In practice `error` is therefore always either a `WcsStorageError` or `null`; the wider `WcsStorageError | Error | null` type is kept for forward compatibility and consistency with sibling packages.
|
|
508
|
+
- JSON auto-serialization handles objects, arrays, and primitives transparently
|
|
509
|
+
- Saving `null` / `undefined` removes the key from storage
|
|
510
|
+
- Cross-tab sync via `storage` event works only with localStorage. The Shell binds the watcher to its current `key` / `type` on connect (and re-binds on re-attach), so cross-tab sync works even in `manual` mode where no auto-load runs. Changing the `key` attribute after connection always re-syncs the Core key, so cross-tab sync follows the new key even in `manual` mode or when the key is cleared. A successful cross-tab update also clears any stale `error` (just like `load()` / `save()` / `remove()` do at the start of a successful operation), so a fresh value never coexists with a leftover error from an earlier failure.
|
|
511
|
+
- `manual` is useful when you want explicit control over save timing
|
|
512
|
+
|
|
513
|
+
## License
|
|
514
|
+
|
|
515
|
+
MIT
|