@wcstack/storage 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md ADDED
@@ -0,0 +1,481 @@
1
+ # @wcstack/storage
2
+
3
+ `@wcstack/storage` は wcstack エコシステムのためのヘッドレス ストレージ コンポーネントです。
4
+
5
+ 視覚的な UI ウィジェットではありません。
6
+ ブラウザのストレージ(localStorage / sessionStorage)とリアクティブな状態をつなぐ **I/O ノード** です。
7
+
8
+ `@wcstack/state` と組み合わせると、`<wcs-storage>` はパス契約を通じて直接バインドできます:
9
+
10
+ - **入力 / コマンドサーフェス**: `key`, `type`, `trigger`
11
+ - **出力ステートサーフェス**: `value`, `loading`, `error`
12
+
13
+ つまり、ブラウザストレージの永続化を HTML 内で宣言的に表現できます。UI レイヤーに `localStorage.getItem()`、`JSON.parse()`、シリアライズのグルーコードを書く必要はありません。
14
+
15
+ `@wcstack/storage` は [HAWC](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/docs/articles/HAWC.md) アーキテクチャに従います:
16
+
17
+ - **Core** (`StorageCore`) がストレージの読み書き、クロスタブ同期を処理
18
+ - **Shell** (`<wcs-storage>`) がその状態を DOM に接続
19
+ - フレームワークやバインディングシステムは [wc-bindable-protocol](https://github.com/wc-bindable-protocol/wc-bindable-protocol) 経由で利用
20
+
21
+ ## なぜこれが存在するのか
22
+
23
+ フロントエンドアプリケーションでは、ユーザー設定やセッションデータの永続化に localStorage / sessionStorage を頻繁に使います。
24
+ しかし、読み込み、JSON パース、保存、エラー処理のグルーコードは毎回同じようなパターンです。
25
+
26
+ `@wcstack/storage` はそのグルーコードを再利用可能なコンポーネントに移し、ストレージの値をバインド可能な状態として公開します。
27
+
28
+ `@wcstack/state` と組み合わせたフローは:
29
+
30
+ 1. `<wcs-storage>` が接続時にストレージから自動読み込み
31
+ 2. `value` が `data-wcs` で UI にバインド
32
+ 3. 状態が変わると自動的にストレージに書き戻し
33
+ 4. 他のタブでの変更も自動検知
34
+
35
+ 永続化が命令的なグルーコードではなく、**状態遷移**になります。
36
+
37
+ ## インストール
38
+
39
+ ```bash
40
+ npm install @wcstack/storage
41
+ ```
42
+
43
+ ## クイックスタート
44
+
45
+ ### 1. プリミティブ値の自動保存
46
+
47
+ プリミティブ値(文字列、数値、boolean)は `value` バインディングだけで双方向永続化できます。
48
+
49
+ ```html
50
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
51
+ <script type="module" src="https://esm.run/@wcstack/storage/auto"></script>
52
+
53
+ <wcs-state>
54
+ <script type="module">
55
+ export default { username: "" };
56
+ </script>
57
+ </wcs-state>
58
+
59
+ <wcs-storage key="username" data-wcs="value: username"></wcs-storage>
60
+
61
+ <input data-wcs="value: username" placeholder="ユーザー名">
62
+ <p>保存済み: <span data-wcs="textContent: username"></span></p>
63
+ ```
64
+
65
+ これがデフォルトモードです:
66
+
67
+ - `key` を設定すると接続時に自動読み込み
68
+ - `value` にバインドすると双方向永続化
69
+ - 任意で `loading`、`error` もバインド
70
+
71
+ ### 2. オブジェクトの永続化と `$trackDependency`
72
+
73
+ オブジェクトのサブプロパティ(`settings.theme` 等)を変更しても、親パス `settings` へのバインディングは発火しません。
74
+ `@wcstack/state` の依存走査は**親→子方向**のみだからです。
75
+
76
+ この場合は `$trackDependency` で監視したいサブプロパティを明示し、`trigger` 経由で保存します:
77
+
78
+ ```html
79
+ <wcs-state>
80
+ <script type="module">
81
+ export default defineState({
82
+ settings: { theme: "light", lang: "ja" },
83
+
84
+ get settingsChanged() {
85
+ this.$trackDependency("settings.theme");
86
+ this.$trackDependency("settings.lang");
87
+ return true;
88
+ },
89
+ });
90
+ </script>
91
+ </wcs-state>
92
+
93
+ <wcs-storage key="app-settings" manual
94
+ data-wcs="value: settings; trigger: settingsChanged">
95
+ </wcs-storage>
96
+
97
+ <select data-wcs="value: settings.theme">
98
+ <option value="light">ライト</option>
99
+ <option value="dark">ダーク</option>
100
+ </select>
101
+
102
+ <select data-wcs="value: settings.lang">
103
+ <option value="ja">日本語</option>
104
+ <option value="en">English</option>
105
+ </select>
106
+ ```
107
+
108
+ **フロー:**
109
+
110
+ 1. ユーザーがテーマを変更 → `settings.theme` が更新
111
+ 2. 動的依存により `settingsChanged` が再評価 → `true` を返す
112
+ 3. `trigger: settingsChanged` バインディングが発火 → `save()` 実行
113
+ 4. `settings` オブジェクト全体が localStorage に保存
114
+
115
+ ### 3. sessionStorage の使用
116
+
117
+ `type="session"` で sessionStorage を使用します:
118
+
119
+ ```html
120
+ <wcs-state>
121
+ <script type="module">
122
+ export default { sessionData: null };
123
+ </script>
124
+ </wcs-state>
125
+
126
+ <wcs-storage key="session-data" type="session"
127
+ data-wcs="value: sessionData">
128
+ </wcs-storage>
129
+
130
+ <p data-wcs="textContent: sessionData"></p>
131
+ ```
132
+
133
+ ### 4. クロスタブ同期
134
+
135
+ localStorage の変更は、別のタブからの更新も自動的に検知されます:
136
+
137
+ ```html
138
+ <wcs-state>
139
+ <script type="module">
140
+ export default { sharedCounter: 0 };
141
+ </script>
142
+ </wcs-state>
143
+
144
+ <wcs-storage key="shared-counter"
145
+ data-wcs="value: sharedCounter">
146
+ </wcs-storage>
147
+
148
+ <!-- 他のタブで localStorage を変更すると、この値も自動更新される -->
149
+ <p data-wcs="textContent: sharedCounter"></p>
150
+ ```
151
+
152
+ > **注意**: `storage` イベントは同一オリジンの他のタブでの変更時にのみ発火します。sessionStorage はタブ間で共有されないため、クロスタブ同期は localStorage でのみ動作します。
153
+
154
+ ## ステートサーフェス vs コマンドサーフェス
155
+
156
+ `<wcs-storage>` は 2 種類のプロパティを公開します。
157
+
158
+ ### 出力ステート(バインド可能な状態)
159
+
160
+ 現在のストレージの値を表し、HAWC のメインサーフェスです:
161
+
162
+ | プロパティ | 型 | 説明 |
163
+ |------------|------|------|
164
+ | `value` | `any` | ストレージに保存された値 |
165
+ | `loading` | `boolean` | 読み書き中は `true` |
166
+ | `error` | `WcsStorageError \| Error \| null` | ストレージ操作のエラー |
167
+
168
+ ### 入力 / コマンドサーフェス
169
+
170
+ HTML、JS、または `@wcstack/state` バインディングからストレージ操作を制御します:
171
+
172
+ | プロパティ | 型 | 説明 |
173
+ |------------|------|------|
174
+ | `key` | `string` | ストレージキー |
175
+ | `type` | `"local" \| "session"` | ストレージタイプ |
176
+ | `value` | `any` | 設定すると自動保存(`manual` でない場合) |
177
+ | `trigger` | `boolean` | 単方向の保存トリガー |
178
+ | `manual` | `boolean` | 自動読み込み・自動保存を無効化 |
179
+
180
+ ## アーキテクチャ
181
+
182
+ `@wcstack/storage` は HAWC アーキテクチャに従います。
183
+
184
+ ### Core: `StorageCore`
185
+
186
+ `StorageCore` は純粋な `EventTarget` クラスです。
187
+ 以下を内包します:
188
+
189
+ - ストレージの読み込み・保存・削除
190
+ - JSON の自動シリアライズ / デシリアライズ
191
+ - クロスタブ同期(`storage` イベント監視)
192
+ - `wc-bindable-protocol` 宣言
193
+
194
+ `EventTarget` と `localStorage` / `sessionStorage` をサポートする任意のランタイムでヘッドレスに動作します。
195
+
196
+ ### Shell: `<wcs-storage>`
197
+
198
+ `<wcs-storage>` は `StorageCore` の薄い `HTMLElement` ラッパーです。
199
+ 以下を追加します:
200
+
201
+ - 属性 / プロパティマッピング
202
+ - DOM ライフサイクル統合(接続時に自動読み込み、切断時にクリーンアップ)
203
+ - `value` セッター経由の自動保存
204
+ - `trigger` などの宣言的実行ヘルパー
205
+
206
+ この分離により、ストレージロジックのポータビリティを保ちながら、`@wcstack/state` のような DOM ベースのバインディングシステムとの自然な連携を可能にしています。
207
+
208
+ ### Target injection
209
+
210
+ Core は **target injection** により Shell 上で直接イベントを発火するため、イベントの再ディスパッチは不要です。
211
+
212
+ ## ヘッドレス利用(Core 単体)
213
+
214
+ `StorageCore` は DOM なしで単体利用できます。`static wcBindable` を宣言しているため、`@wc-bindable/core` の `bind()` で状態をサブスクライブできます — フレームワークアダプタと同じ仕組みです:
215
+
216
+ ```typescript
217
+ import { StorageCore } from "@wcstack/storage";
218
+ import { bind } from "@wc-bindable/core";
219
+
220
+ const core = new StorageCore();
221
+
222
+ const unbind = bind(core, (name, value) => {
223
+ console.log(`${name}:`, value);
224
+ });
225
+
226
+ core.key = "my-data";
227
+ core.load();
228
+
229
+ unbind();
230
+ ```
231
+
232
+ ### JSON 自動シリアライズ
233
+
234
+ `StorageCore` はデータ型に応じて自動的にシリアライズ / デシリアライズを行います:
235
+
236
+ | 保存時の型 | ストレージ上の形式 | 読み込み時の型 |
237
+ |-----------|-------------------|--------------|
238
+ | オブジェクト / 配列 | `JSON.stringify()` 結果 | `JSON.parse()` 結果 |
239
+ | 文字列 | そのまま | JSON パース成功時はパース結果、失敗時はそのまま文字列 |
240
+ | 数値 / boolean | `JSON.stringify()` 結果 | `JSON.parse()` 結果 |
241
+ | `null` / `undefined` | キーを削除 | `null` |
242
+
243
+ ## 要素一覧
244
+
245
+ ### `<wcs-storage>`
246
+
247
+ | 属性 | 型 | デフォルト | 説明 |
248
+ |------|------|------------|------|
249
+ | `key` | `string` | — | ストレージキー |
250
+ | `type` | `"local" \| "session"` | `local` | ストレージタイプ |
251
+ | `manual` | `boolean` | `false` | 自動読み込み・自動保存を無効化 |
252
+
253
+ | プロパティ | 型 | 説明 |
254
+ |------------|------|------|
255
+ | `value` | `any` | ストレージの値(設定すると自動保存) |
256
+ | `loading` | `boolean` | 読み書き中は `true` |
257
+ | `error` | `WcsStorageError \| Error \| null` | エラー情報 |
258
+ | `trigger` | `boolean` | `true` を設定すると save を実行 |
259
+ | `manual` | `boolean` | 自動モードの無効化 |
260
+
261
+ | メソッド | 説明 |
262
+ |----------|------|
263
+ | `load()` | ストレージから値を読み込み |
264
+ | `save()` | 現在の値をストレージに保存 |
265
+ | `remove()` | ストレージからキーを削除 |
266
+
267
+ ## wc-bindable-protocol
268
+
269
+ `StorageCore` と `<wcs-storage>` はどちらも wc-bindable-protocol に準拠しており、プロトコル対応の任意のフレームワークやコンポーネントと相互運用できます。
270
+
271
+ ### Core (`StorageCore`)
272
+
273
+ ```typescript
274
+ static wcBindable = {
275
+ protocol: "wc-bindable",
276
+ version: 1,
277
+ properties: [
278
+ { name: "value", event: "wcs-storage:value-changed",
279
+ getter: (e) => e.detail },
280
+ { name: "loading", event: "wcs-storage:loading-changed" },
281
+ { name: "error", event: "wcs-storage:error" },
282
+ ],
283
+ };
284
+ ```
285
+
286
+ ### Shell (`<wcs-storage>`)
287
+
288
+ Shell は Core の宣言を拡張し、バインディングシステムから宣言的にストレージ操作を実行できるようにします:
289
+
290
+ ```typescript
291
+ static wcBindable = {
292
+ ...StorageCore.wcBindable,
293
+ properties: [
294
+ ...StorageCore.wcBindable.properties,
295
+ { name: "trigger", event: "wcs-storage:trigger-changed" },
296
+ ],
297
+ };
298
+ ```
299
+
300
+ ## TypeScript 型
301
+
302
+ ```typescript
303
+ import type {
304
+ WcsStorageError, WcsStorageCoreValues, WcsStorageValues, StorageType
305
+ } from "@wcstack/storage";
306
+ ```
307
+
308
+ ```typescript
309
+ type StorageType = "local" | "session";
310
+
311
+ // ストレージ操作エラー
312
+ interface WcsStorageError {
313
+ operation: "load" | "save" | "remove";
314
+ message: string;
315
+ }
316
+
317
+ // Core(ヘッドレス)— 3 つの状態プロパティ
318
+ interface WcsStorageCoreValues<T = unknown> {
319
+ value: T;
320
+ loading: boolean;
321
+ error: WcsStorageError | Error | null;
322
+ }
323
+
324
+ // Shell(<wcs-storage>)— Core を拡張し trigger を追加
325
+ interface WcsStorageValues<T = unknown> extends WcsStorageCoreValues<T> {
326
+ trigger: boolean;
327
+ }
328
+ ```
329
+
330
+ ## なぜ `@wcstack/state` とうまく連携するのか
331
+
332
+ `@wcstack/state` は UI と状態の唯一の契約としてパス文字列を使います。
333
+ `<wcs-storage>` はこのモデルに自然に適合します:
334
+
335
+ - `<wcs-storage>` が接続時にストレージから自動読み込み
336
+ - `value` が状態パスにバインドされ、UI に反映
337
+ - ユーザーが UI を操作すると状態が変わり、自動的にストレージに書き戻し
338
+ - リロードしても状態が復元される
339
+
340
+ 永続化が通常の状態更新と同じように見えるようになります。
341
+
342
+ ## フレームワーク連携
343
+
344
+ `<wcs-storage>` は HAWC + `wc-bindable-protocol` なので、`@wc-bindable/*` の薄いアダプタを通じて任意のフレームワークで動作します。
345
+
346
+ ### React
347
+
348
+ ```tsx
349
+ import { useWcBindable } from "@wc-bindable/react";
350
+ import type { WcsStorageValues } from "@wcstack/storage";
351
+
352
+ interface Settings { theme: string; lang: string; }
353
+
354
+ function SettingsPanel() {
355
+ const [ref, { value: settings, loading, error }] =
356
+ useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
357
+
358
+ return (
359
+ <>
360
+ <wcs-storage ref={ref} key="app-settings" />
361
+ {loading && <p>読み込み中...</p>}
362
+ {settings && <p>テーマ: {settings.theme}</p>}
363
+ </>
364
+ );
365
+ }
366
+ ```
367
+
368
+ ### Vue
369
+
370
+ ```vue
371
+ <script setup lang="ts">
372
+ import { useWcBindable } from "@wc-bindable/vue";
373
+ import type { WcsStorageValues } from "@wcstack/storage";
374
+
375
+ interface Settings { theme: string; lang: string; }
376
+
377
+ const { ref, values } = useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
378
+ </script>
379
+
380
+ <template>
381
+ <wcs-storage :ref="ref" key="app-settings" />
382
+ <p v-if="values.loading">読み込み中...</p>
383
+ <p v-else-if="values.value">テーマ: {{ values.value.theme }}</p>
384
+ </template>
385
+ ```
386
+
387
+ ### Svelte
388
+
389
+ ```svelte
390
+ <script>
391
+ import { wcBindable } from "@wc-bindable/svelte";
392
+
393
+ let settings = $state(null);
394
+ let loading = $state(false);
395
+ </script>
396
+
397
+ <wcs-storage key="app-settings"
398
+ use:wcBindable={{ onUpdate: (name, v) => {
399
+ if (name === "value") settings = v;
400
+ if (name === "loading") loading = v;
401
+ }}} />
402
+
403
+ {#if loading}
404
+ <p>読み込み中...</p>
405
+ {:else if settings}
406
+ <p>テーマ: {settings.theme}</p>
407
+ {/if}
408
+ ```
409
+
410
+ ### Solid
411
+
412
+ ```tsx
413
+ import { createWcBindable } from "@wc-bindable/solid";
414
+ import type { WcsStorageValues } from "@wcstack/storage";
415
+
416
+ interface Settings { theme: string; lang: string; }
417
+
418
+ function SettingsPanel() {
419
+ const [values, directive] = createWcBindable<WcsStorageValues<Settings>>();
420
+
421
+ return (
422
+ <>
423
+ <wcs-storage ref={directive} key="app-settings" />
424
+ <Show when={!values.loading} fallback={<p>読み込み中...</p>}>
425
+ <p>テーマ: {values.value?.theme}</p>
426
+ </Show>
427
+ </>
428
+ );
429
+ }
430
+ ```
431
+
432
+ ### Vanilla — `bind()` を直接利用
433
+
434
+ ```javascript
435
+ import { bind } from "@wc-bindable/core";
436
+
437
+ const storageEl = document.querySelector("wcs-storage");
438
+
439
+ bind(storageEl, (name, value) => {
440
+ console.log(`${name} changed:`, value);
441
+ });
442
+ ```
443
+
444
+ ## オプションの DOM トリガー
445
+
446
+ `autoTrigger` が有効(デフォルト)の場合、`data-storagetarget` 属性を持つ要素のクリックで対応する `<wcs-storage>` の `save()` が実行されます:
447
+
448
+ ```html
449
+ <button data-storagetarget="settings-store">設定を保存</button>
450
+ <wcs-storage id="settings-store" key="settings" manual
451
+ data-wcs="value: settings"></wcs-storage>
452
+ ```
453
+
454
+ ## 設定
455
+
456
+ ```javascript
457
+ import { bootstrapStorage } from "@wcstack/storage";
458
+
459
+ bootstrapStorage({
460
+ autoTrigger: true,
461
+ triggerAttribute: "data-storagetarget",
462
+ tagNames: {
463
+ storage: "wcs-storage",
464
+ },
465
+ });
466
+ ```
467
+
468
+ ## 設計メモ
469
+
470
+ - `value`、`loading`、`error` は **出力ステート**
471
+ - `key`、`type`、`trigger` は **入力 / コマンドサーフェス**
472
+ - `trigger` は意図的に単方向: `true` を書き込むと保存、リセットで完了を通知
473
+ - `value` セッターは `manual` でない場合に自動保存を行う
474
+ - JSON 自動シリアライズにより、オブジェクト / 配列 / プリミティブを透過的に扱える
475
+ - `null` / `undefined` の保存はストレージからのキー削除として扱われる
476
+ - `storage` イベントによるクロスタブ同期は localStorage でのみ動作
477
+ - `manual` は保存タイミングを明示的に制御したい場合に有用
478
+
479
+ ## ライセンス
480
+
481
+ MIT