@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 CHANGED
@@ -1,481 +1,518 @@
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
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` は [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md)(Core / Shell / Binding Contract)アーキテクチャに従います:
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
+ 現在のストレージの値を表し、CSBC のメインサーフェスです:
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` は CSBC アーキテクチャに従います。
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
+ 宣言は wc-bindable インターフェースモデルの全体に従い、3 つの独立したサーフェスを持ちます:
272
+
273
+ - **`properties`** — `bind()` が購読する観測可能な出力(`value`, `loading`, `error`, および Shell の `trigger`)
274
+ - **`inputs`** 設定可能なサーフェス(`key`, `type` など)。ツール・コード生成・リモートプロキシが読む宣言的メタデータ
275
+ - **`commands`** — 呼び出し可能なメソッド(`load`, `save`, `remove`)。`@wcstack/state` のようなバインディングシステムが名前で呼び出せる
276
+
277
+ プロトコル上、コアの `bind()` が解釈するのは `properties` のみです。`inputs` / `commands`(および `attribute` / `async` ヒント)は記述的なメタデータであり、暗黙の双方向データフローを生成しません。
278
+
279
+ ### Core (`StorageCore`)
280
+
281
+ `StorageCore` は任意のランタイムが購読できるバインド可能な状態に加え、ポータブルな入力 / コマンドサーフェスを宣言します:
282
+
283
+ ```typescript
284
+ static wcBindable = {
285
+ protocol: "wc-bindable",
286
+ version: 1,
287
+ properties: [
288
+ { name: "value", event: "wcs-storage:value-changed",
289
+ getter: (e) => e.detail },
290
+ { name: "loading", event: "wcs-storage:loading-changed" },
291
+ { name: "error", event: "wcs-storage:error" },
292
+ ],
293
+ inputs: [
294
+ { name: "key" },
295
+ { name: "type" },
296
+ ],
297
+ commands: [
298
+ { name: "load" },
299
+ { name: "save" },
300
+ { name: "remove" },
301
+ ],
302
+ };
303
+ ```
304
+
305
+ ヘッドレス利用では `core.load()` / `core.save(value)` を直接呼び出します — `trigger` は不要です。
306
+
307
+ ### Shell (`<wcs-storage>`)
308
+
309
+ Shell Core の宣言を `trigger` 出力と DOM 駆動の入力サーフェスで拡張します。`commands`(`load` / `save` / `remove`)は Core からそのまま継承されます:
310
+
311
+ ```typescript
312
+ static wcBindable = {
313
+ ...StorageCore.wcBindable,
314
+ properties: [
315
+ ...StorageCore.wcBindable.properties,
316
+ { name: "trigger", event: "wcs-storage:trigger-changed" },
317
+ ],
318
+ inputs: [
319
+ { name: "key" },
320
+ { name: "type" },
321
+ { name: "value" },
322
+ { name: "manual" },
323
+ { name: "trigger" },
324
+ ],
325
+ };
326
+ ```
327
+
328
+ Shell の inputs は意図的に `attribute` ヒントを持ちません: `key` / `type` / `manual` のセッターは既に各属性へ反映するため、`inputs[].attribute` を反映するバインディングシステムだと属性を二重に設定してしまうからです。
329
+
330
+ ## TypeScript
331
+
332
+ ```typescript
333
+ import type {
334
+ WcsStorageError, WcsStorageCoreValues, WcsStorageValues, StorageType
335
+ } from "@wcstack/storage";
336
+ ```
337
+
338
+ ```typescript
339
+ type StorageType = "local" | "session";
340
+
341
+ // ストレージ操作エラー
342
+ interface WcsStorageError {
343
+ operation: "load" | "save" | "remove";
344
+ message: string;
345
+ }
346
+
347
+ // Core(ヘッドレス)— 3 つの状態プロパティ
348
+ interface WcsStorageCoreValues<T = unknown> {
349
+ value: T;
350
+ loading: boolean;
351
+ error: WcsStorageError | Error | null;
352
+ }
353
+
354
+ // Shell(<wcs-storage>)— Core を拡張し trigger を追加
355
+ interface WcsStorageValues<T = unknown> extends WcsStorageCoreValues<T> {
356
+ trigger: boolean;
357
+ }
358
+ ```
359
+
360
+ ## なぜ `@wcstack/state` とうまく連携するのか
361
+
362
+ `@wcstack/state` UI と状態の唯一の契約としてパス文字列を使います。
363
+ `<wcs-storage>` はこのモデルに自然に適合します:
364
+
365
+ - `<wcs-storage>` が接続時にストレージから自動読み込み
366
+ - `value` が状態パスにバインドされ、UI に反映
367
+ - ユーザーが UI を操作すると状態が変わり、自動的にストレージに書き戻し
368
+ - リロードしても状態が復元される
369
+
370
+ 永続化が通常の状態更新と同じように見えるようになります。
371
+
372
+ ## フレームワーク連携
373
+
374
+ `<wcs-storage>` は CSBC + `wc-bindable-protocol` なので、`@wc-bindable/*` の薄いアダプタを通じて任意のフレームワークで動作します。
375
+
376
+ ### React
377
+
378
+ ```tsx
379
+ import { useWcBindable } from "@wc-bindable/react";
380
+ import type { WcsStorageValues } from "@wcstack/storage";
381
+
382
+ interface Settings { theme: string; lang: string; }
383
+
384
+ function SettingsPanel() {
385
+ const [ref, { value: settings, loading, error }] =
386
+ useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
387
+
388
+ return (
389
+ <>
390
+ <wcs-storage ref={ref} key="app-settings" />
391
+ {loading && <p>読み込み中...</p>}
392
+ {settings && <p>テーマ: {settings.theme}</p>}
393
+ </>
394
+ );
395
+ }
396
+ ```
397
+
398
+ ### Vue
399
+
400
+ ```vue
401
+ <script setup lang="ts">
402
+ import { useWcBindable } from "@wc-bindable/vue";
403
+ import type { WcsStorageValues } from "@wcstack/storage";
404
+
405
+ interface Settings { theme: string; lang: string; }
406
+
407
+ const { ref, values } = useWcBindable<HTMLElement, WcsStorageValues<Settings>>();
408
+ </script>
409
+
410
+ <template>
411
+ <wcs-storage :ref="ref" key="app-settings" />
412
+ <p v-if="values.loading">読み込み中...</p>
413
+ <p v-else-if="values.value">テーマ: {{ values.value.theme }}</p>
414
+ </template>
415
+ ```
416
+
417
+ ### Svelte
418
+
419
+ ```svelte
420
+ <script>
421
+ import { wcBindable } from "@wc-bindable/svelte";
422
+
423
+ let settings = $state(null);
424
+ let loading = $state(false);
425
+ </script>
426
+
427
+ <wcs-storage key="app-settings"
428
+ use:wcBindable={{ onUpdate: (name, v) => {
429
+ if (name === "value") settings = v;
430
+ if (name === "loading") loading = v;
431
+ }}} />
432
+
433
+ {#if loading}
434
+ <p>読み込み中...</p>
435
+ {:else if settings}
436
+ <p>テーマ: {settings.theme}</p>
437
+ {/if}
438
+ ```
439
+
440
+ ### Solid
441
+
442
+ ```tsx
443
+ import { createWcBindable } from "@wc-bindable/solid";
444
+ import type { WcsStorageValues } from "@wcstack/storage";
445
+
446
+ interface Settings { theme: string; lang: string; }
447
+
448
+ function SettingsPanel() {
449
+ const [values, directive] = createWcBindable<WcsStorageValues<Settings>>();
450
+
451
+ return (
452
+ <>
453
+ <wcs-storage ref={directive} key="app-settings" />
454
+ <Show when={!values.loading} fallback={<p>読み込み中...</p>}>
455
+ <p>テーマ: {values.value?.theme}</p>
456
+ </Show>
457
+ </>
458
+ );
459
+ }
460
+ ```
461
+
462
+ ### Vanilla — `bind()` を直接利用
463
+
464
+ ```javascript
465
+ import { bind } from "@wc-bindable/core";
466
+
467
+ const storageEl = document.querySelector("wcs-storage");
468
+
469
+ bind(storageEl, (name, value) => {
470
+ console.log(`${name} changed:`, value);
471
+ });
472
+ ```
473
+
474
+ ## オプションの DOM トリガー
475
+
476
+ `autoTrigger` が有効(デフォルト)の場合、`data-storagetarget` 属性を持つ要素のクリックで対応する `<wcs-storage>` `save()` が実行されます:
477
+
478
+ ```html
479
+ <button data-storagetarget="settings-store">設定を保存</button>
480
+ <wcs-storage id="settings-store" key="settings" manual
481
+ data-wcs="value: settings"></wcs-storage>
482
+ ```
483
+
484
+ ## 設定
485
+
486
+ ```javascript
487
+ import { bootstrapStorage } from "@wcstack/storage";
488
+
489
+ bootstrapStorage({
490
+ autoTrigger: true,
491
+ triggerAttribute: "data-storagetarget",
492
+ tagNames: {
493
+ storage: "wcs-storage",
494
+ },
495
+ });
496
+ ```
497
+
498
+ ## 設計メモ
499
+
500
+ - `value`、`loading`、`error` は **出力ステート**
501
+ - `key`、`type`、`trigger` は **入力 / コマンドサーフェス**
502
+ - `trigger` は意図的に単方向: `true` を書き込むと保存、リセットで完了を通知。内部の `save()` が失敗した場合(例: `key` 未設定)でも `trigger` は `false` へ復帰し完了イベントも発火するため、`true` で固着しない。
503
+ - `value` セッターは `manual` でない場合に自動保存を行う
504
+ - **`value` セッター vs `save()` / `trigger`**: `value` への代入(非 manual)は *代入された引数* を保存する(ライトスルー)。一方 `save()` と `trigger` は *現在の `value`*(直前の `load()` や他タブからの `storage` イベントで更新されうる)を保存する。このため `trigger`/`save()` は他タブ由来の値を書き戻す可能性がある。
505
+ - **`manual` モードでの `value`**: `manual` では `value` セッターは値を**ステージング**する(ストレージへは書き込まない)。`el.value = x` で読み取り値は更新される(`el.value === x`)が、ストレージには触れず、実際の書き込みは `save()` / `trigger` でのみ行われる。これにより `value: …` + `trigger: …` のバインディング対が機能する — バインドされた値がステージングされ、トリガー時にコミットされる。
506
+ - **非 manual の `value` 経路にはエコーガードを置かない**: 同値の `value-changed` 再発火をスキップするのは*ステージング*経路(`manual` モードで使う Core の `value` セッター)のみ。主経路である非 manual の `value` セッター → `save()` は意図的にライトスルーであり、代入値が現在値と等しくても毎回保存し `value-changed` を再発火する。これは仕様である — 上記のライトスルー契約を保つ必要があり、同一タブでは `storage` イベントが再発火しないためフィードバックループは生じない。`data-wcs="value: x"` の双方向バインディングでもエコーされる `value-changed` は無害で、`@wcstack/state` 側がラウンドトリップを重複排除する。
507
+ - **`save` コマンドのアリティ**: ヘッドレスな Core は `save(value)`、Shell は `save()`(現在値を保存)。どちらも同じ `commands` 名 `save` に現れるが、プロトコルの `commands` メタデータは記述的でアリティを持たないため、これは契約上の差異でありプロトコル違反ではない。
508
+ - **不正な `type`**: `"session"` 以外の `type` 属性はすべて `"local"` として扱う。不正値(例: `type="foo"`)は例外を投げず暗黙に `local` へフォールバックする。
509
+ - **実行時の `type` 変更**: 接続後に `type` 属性を変更すると以降の操作で使うストレージ領域は切り替わるが、新しい領域から**自動で再ロードはしない**(非 manual で自動再ロードするのは `key` 変更時のみ)。新領域の値が必要なら明示的に `load()` を呼ぶこと。
510
+ - **`error` の形状**: ストレージ失敗時、`error` には失敗した操作(`load` / `save` / `remove`)を示す `WcsStorageError`(`{ operation, message }`)が設定される。`key is required`(key 未設定での操作呼び出し)は `error` ではなく同期的に throw される。したがって実際には `error` は常に `WcsStorageError` か `null` のいずれかになる。より広い `WcsStorageError | Error | null` 型は前方互換性と兄弟パッケージとの一貫性のために維持している。
511
+ - JSON 自動シリアライズにより、オブジェクト / 配列 / プリミティブを透過的に扱える
512
+ - `null` / `undefined` の保存はストレージからのキー削除として扱われる
513
+ - `storage` イベントによるクロスタブ同期は localStorage でのみ動作。Shell は接続時(および再 attach 時)に監視を現在の `key` / `type` へ結びつけるため、自動ロードが走らない `manual` モードでもクロスタブ同期が機能する。接続後に `key` 属性を変更した場合も常に Core の key を再同期するため、`manual` モードや key を空にした場合でもクロスタブ同期は新しい key を追従する。クロスタブ更新が成功すると残存していた `error` もクリアされる(`load()` / `save()` / `remove()` が成功時に冒頭で error を null にするのと同様)。これにより、過去の失敗で残った error と新鮮な value が共存しない。
514
+ - `manual` は保存タイミングを明示的に制御したい場合に有用
515
+
516
+ ## ライセンス
517
+
518
+ MIT