@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.
- package/README.md +114 -13
- 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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
//
|
|
23
|
-
tracker.
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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 |
|