@hardlydifficult/state-tracker 2.0.12 → 2.0.13

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.
Files changed (2) hide show
  1. package/README.md +114 -13
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hardlydifficult/state-tracker
2
2
 
3
- Atomic JSON state persistence with sync/async APIs, auto-save debouncing, and graceful degradation to in-memory mode for TypeScript.
3
+ Atomic JSON state persistence with sync/async APIs, auto-save debouncing, typed migrations, key sanitization, and graceful fallback to in-memory mode.
4
4
 
5
5
  ## Installation
6
6
 
@@ -13,28 +13,119 @@ npm install @hardlydifficult/state-tracker
13
13
  ```typescript
14
14
  import { StateTracker } from "@hardlydifficult/state-tracker";
15
15
 
16
- const tracker = new StateTracker({
17
- key: "user-settings",
18
- default: { theme: "light", notifications: true },
16
+ interface AppConfig {
17
+ version: string;
18
+ theme: "light" | "dark";
19
+ }
20
+
21
+ const tracker = new StateTracker<AppConfig>({
22
+ key: "app-config",
23
+ default: { version: "1.0.0", theme: "light" },
19
24
  autoSaveMs: 1000,
20
25
  });
21
26
 
22
- // Load persisted state (sync or async)
23
- tracker.load(); // or await tracker.loadAsync();
27
+ await tracker.loadAsync(); // Loads from disk, enables isPersistent tracking
28
+ tracker.update({ theme: "dark" }); // Auto-saves after 1s of inactivity
29
+ console.log(tracker.state); // { version: "1.0.0", theme: "dark" }
30
+ ```
31
+
32
+ ## Core Features
33
+
34
+ ### State Persistence
35
+
36
+ The `StateTracker` class provides atomic JSON-based state persistence with auto-save support.
24
37
 
25
- // Update state and auto-save
26
- tracker.update({ theme: "dark" });
27
- // State is saved automatically after 1 second of inactivity
38
+ | Option | Type | Description |
39
+ |--------|------|-------------|
40
+ | `key` | `string` | Unique identifier for the state file (alphanumeric, hyphens, underscores only) |
41
+ | `default` | `T` | Default state if no persisted data exists |
42
+ | `stateDirectory` | `string` (default: `~/.app-state` or `$STATE_TRACKER_DIR`) | Directory to store state files |
43
+ | `autoSaveMs` | `number` (default: `0`) | Debounce delay in ms for auto-save |
44
+ | `migration` | `Migration<T>` (deprecated) | Optional migration function (use `loadOrDefault()` with `defineStateMigration` instead) |
45
+ | `onEvent` | `(event: StateTrackerEvent) => void` | Callback for internal events (debug/info/warn/error) |
28
46
 
29
- // Read current state
30
- console.log(tracker.state); // { theme: "dark", notifications: true }
47
+ #### Async vs Sync Persistence
48
+
49
+ ```typescript
50
+ // Async (recommended for production)
51
+ await tracker.loadAsync();
52
+ await tracker.saveAsync();
53
+
54
+ // Sync fallback (throws if file access fails)
55
+ tracker.load();
56
+ tracker.save({ theme: "dark" });
57
+ ```
58
+
59
+ ### Key Sanitization
60
+
61
+ Invalid keys (including `__proto__`, `constructor`, `prototype`) are sanitized automatically to prevent prototype pollution and path traversal.
62
+
63
+ ```typescript
64
+ tracker.set("__proto__", { malicious: true }); // ignored
65
+ tracker.set("normalKey", "value"); // works as expected
66
+ ```
67
+
68
+ ### Typed Migrations
69
+
70
+ Support for typed migrations from older state formats using `loadOrDefault()` and `defineStateMigration()`.
71
+
72
+ ```typescript
73
+ import { defineStateMigration } from "@hardlydifficult/state-tracker";
74
+
75
+ interface LegacyConfig {
76
+ theme: "light" | "dark";
77
+ }
78
+
79
+ interface Config {
80
+ version: string;
81
+ theme: "light" | "dark";
82
+ }
83
+
84
+ const legacyMigration = defineStateMigration<Config, LegacyConfig>({
85
+ name: "legacy-config",
86
+ isLegacy(input): input is LegacyConfig {
87
+ return (
88
+ typeof input === "object" &&
89
+ input !== null &&
90
+ !Array.isArray(input) &&
91
+ "theme" in input &&
92
+ !("version" in input)
93
+ );
94
+ },
95
+ migrate(legacy) {
96
+ return { version: "1.0.0", ...legacy };
97
+ },
98
+ });
99
+
100
+ const tracker = new StateTracker<Config>({
101
+ key: "config",
102
+ default: { version: "1.1.0", theme: "dark" },
103
+ });
104
+
105
+ const state = tracker.loadOrDefault({ migrations: [legacyMigration] });
106
+ ```
107
+
108
+ ### Graceful Degradation
109
+
110
+ If persistent storage fails (e.g., due to permissions or path issues), the tracker falls back to in-memory mode without throwing errors.
111
+
112
+ ```typescript
113
+ const tracker = new StateTracker<AppConfig>({
114
+ key: "config",
115
+ default: { version: "1.0.0", theme: "light" },
116
+ });
117
+
118
+ await tracker.loadAsync();
119
+ if (!tracker.isPersistent) {
120
+ console.warn("Running in-memory mode");
121
+ }
31
122
  ```
32
123
 
33
124
  ## State Management
34
125
 
35
126
  The `StateTracker` class provides a robust interface for managing persistent application state.
36
127
 
37
- ### Constructor
128
+ ### Constructor Options
38
129
 
39
130
  | Option | Type | Default | Description |
40
131
  |--------|------|---------|-------------|
@@ -42,6 +133,7 @@ The `StateTracker` class provides a robust interface for managing persistent app
42
133
  | `default` | `T` | — | Default state value used if no persisted state exists |
43
134
  | `stateDirectory` | `string` | `~/.app-state` or `$STATE_TRACKER_DIR` | Directory to store state files |
44
135
  | `autoSaveMs` | `number` | `0` | Debounce delay (ms) for auto-save after state changes |
136
+ | `migration` | `Migration<T>` | — | **Deprecated:** Use `defineStateMigration` with `loadOrDefault` instead |
45
137
  | `onEvent` | `(event: StateTrackerEvent) => void` | `undefined` | Callback for internal events (debug/info/warn/error) |
46
138
 
47
139
  ### State Accessors
@@ -291,4 +383,13 @@ STATE_TRACKER_DIR=/custom/path npm start
291
383
  - **Auto-save** — debounced saves after state mutations
292
384
  - **Legacy format support** — reads both v1 envelope format and legacy PersistentStore formats
293
385
  - **Typed migration helper** — declarative migration rules for old JSON shapes
294
- - **Optional save metadata** — annotate saved state with `saveWithMeta(...)`
386
+ - **Optional save metadata** — annotate saved state with `saveWithMeta(...)`
387
+ - **API consistency** — all operations work seamlessly across sync/async modes
388
+
389
+ ## Appendix: Platform Behavior
390
+
391
+ | Environment | Persistence | Fallback Behavior |
392
+ |-------------|-------------|-------------------|
393
+ | Node.js | ✅ File system access | Falls back to memory on errors |
394
+ | Browser | ❌ No file system access | Always in-memory only |
395
+ | Bun/Deno | ⚠️ Experimental support | Depends on environment capabilities |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/state-tracker",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [