@hardlydifficult/state-tracker 2.0.8 → 2.0.10
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 +129 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,13 +34,33 @@ store.set({ requestCount: 0, lastActiveAt: new Date().toISOString() }); // full
|
|
|
34
34
|
await store.saveAsync(); // force immediate save
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## Key Sanitization
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
StateTracker enforces strict key sanitization to prevent path traversal and invalid characters. Keys must match `/^[A-Za-z0-9_-]+$/`.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
```typescript
|
|
42
|
+
new StateTracker({ key: "../evil", default: 0 }); // throws: "invalid characters"
|
|
43
|
+
new StateTracker({ key: "foo/bar", default: 0 }); // throws: "invalid characters"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Persistence & Graceful Degradation
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
If storage is unavailable (e.g., due to permissions or read-only filesystem), StateTracker automatically falls back to in-memory mode without throwing errors.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const tracker = new StateTracker({
|
|
52
|
+
key: "failing",
|
|
53
|
+
default: { x: 1 },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await tracker.loadAsync(); // fails silently on unreadable dir
|
|
57
|
+
console.log(tracker.isPersistent); // false
|
|
58
|
+
console.log(tracker.state); // uses in-memory default
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Sync API: `load()` and `save()`
|
|
62
|
+
|
|
63
|
+
V1-compatible synchronous operations for environments where async is not preferred or available.
|
|
44
64
|
|
|
45
65
|
```typescript
|
|
46
66
|
import { StateTracker } from "@hardlydifficult/state-tracker";
|
|
@@ -54,9 +74,9 @@ const count = store.load(); // returns current state
|
|
|
54
74
|
store.save(count + 1); // writes entire state atomically
|
|
55
75
|
```
|
|
56
76
|
|
|
57
|
-
|
|
77
|
+
## Async API: `loadAsync()` and `saveAsync()`
|
|
58
78
|
|
|
59
|
-
|
|
79
|
+
Async operations that gracefully degrade to in-memory mode on errors.
|
|
60
80
|
|
|
61
81
|
```typescript
|
|
62
82
|
const store = new StateTracker<AppState>({
|
|
@@ -71,15 +91,107 @@ store.set({ version: 2 });
|
|
|
71
91
|
await store.saveAsync(); // Force immediate save
|
|
72
92
|
```
|
|
73
93
|
|
|
94
|
+
## State Manipulation
|
|
95
|
+
|
|
96
|
+
| Method | Description | Example |
|
|
97
|
+
|--------|-------------|---------|
|
|
98
|
+
| `set(newState)` | Replace entire state, triggers auto-save | `tracker.set({ count: 5 })` |
|
|
99
|
+
| `update(changes)` | Shallow merge (object state only), triggers auto-save | `tracker.update({ count: 5 })` |
|
|
100
|
+
| `reset()` | Restore to default, triggers auto-save | `tracker.reset()` |
|
|
101
|
+
| `state` (getter) | Read-only current state | `tracker.state` |
|
|
102
|
+
| `isPersistent` (getter) | Whether storage is available | `tracker.isPersistent` |
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const tracker = new StateTracker({
|
|
106
|
+
key: "manip",
|
|
107
|
+
default: { a: 1, b: 2 },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
tracker.set({ a: 10, b: 20 }); // replaces all
|
|
111
|
+
tracker.update({ b: 200 }); // merges: { a: 10, b: 200 }
|
|
112
|
+
tracker.reset(); // back to { a: 1, b: 2 }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `update()` on Primitive Types
|
|
116
|
+
|
|
117
|
+
Calling `update()` on a primitive or array state throws:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const primitive = new StateTracker<number>({ key: "num", default: 0 });
|
|
121
|
+
primitive.update(100 as never); // throws: "update() can only be used when state is a non-array object"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Auto-Save with Debouncing
|
|
125
|
+
|
|
126
|
+
Configure automatic debounced saving via `autoSaveMs`. Any state change (`set`, `update`, `reset`) will schedule a save after the delay; rapid changes only trigger one save.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const tracker = new StateTracker({
|
|
130
|
+
key: "auto",
|
|
131
|
+
default: { count: 0 },
|
|
132
|
+
autoSaveMs: 500,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
tracker.set({ count: 1 }); // schedules save in 500ms
|
|
136
|
+
tracker.set({ count: 2 }); // cancels previous, schedules new save
|
|
137
|
+
tracker.set({ count: 3 }); // cancels again, only this save triggers after 500ms
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Event Logging
|
|
141
|
+
|
|
142
|
+
Optionally receive events for debugging, monitoring, or diagnostics.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const tracker = new StateTracker({
|
|
146
|
+
key: "events",
|
|
147
|
+
default: { x: 1 },
|
|
148
|
+
onEvent: (event) => {
|
|
149
|
+
console.log(`[${event.level}] ${event.message}`, event.context);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await tracker.loadAsync();
|
|
154
|
+
// Outputs: [info] No existing state file, using defaults { path: ".../x.json" }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Event Type Reference
|
|
158
|
+
|
|
159
|
+
| Field | Type | Description |
|
|
160
|
+
|-------|------|-------------|
|
|
161
|
+
| `level` | `"debug" \| "info" \| "warn" \| "error"` | Severity level |
|
|
162
|
+
| `message` | `string` | Human-readable message |
|
|
163
|
+
| `context`? | `Record<string, unknown>` | Additional metadata (e.g., `path`, `error`) |
|
|
164
|
+
|
|
165
|
+
## File Format
|
|
166
|
+
|
|
167
|
+
StateTracker uses an envelope format for storage:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"value": <your-state>,
|
|
172
|
+
"lastUpdated": "2025-04-05T12:34:56.789Z"
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Migration
|
|
177
|
+
|
|
178
|
+
Legacy raw JSON objects (without envelope) are automatically loaded and merged with defaults, then rewritten in envelope format on next `saveAsync()`.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// Old format: { "count": 10 }
|
|
182
|
+
await tracker.loadAsync(); // merges with defaults
|
|
183
|
+
await tracker.saveAsync(); // writes: { "value": { "count": 10 }, "lastUpdated": "..." }
|
|
184
|
+
```
|
|
185
|
+
|
|
74
186
|
## Options
|
|
75
187
|
|
|
76
|
-
| Option | Type | Description |
|
|
77
|
-
|
|
78
|
-
| `key` | `string` | Unique identifier
|
|
79
|
-
| `default` | `T` | Default value
|
|
80
|
-
| `stateDirectory
|
|
81
|
-
| `autoSaveMs
|
|
82
|
-
| `onEvent
|
|
188
|
+
| Option | Type | Default | Description |
|
|
189
|
+
|--------|------|---------|-------------|
|
|
190
|
+
| `key` | `string` | — | Unique identifier (alphanumeric, hyphens, underscores only) |
|
|
191
|
+
| `default` | `T` | — | Default value used on missing/corrupt storage |
|
|
192
|
+
| `stateDirectory`? | `string` | `~/.app-state` | Custom directory for state files |
|
|
193
|
+
| `autoSaveMs`? | `number` | `0` | Debounce delay for auto-save (ms); `0` disables |
|
|
194
|
+
| `onEvent`? | `(e: StateTrackerEvent) => void` | — | Event callback for logging |
|
|
83
195
|
|
|
84
196
|
## Properties
|
|
85
197
|
|
|
@@ -101,50 +213,14 @@ await store.saveAsync(); // Force immediate save
|
|
|
101
213
|
| `reset()` | Restore to defaults, triggers auto-save |
|
|
102
214
|
| `getFilePath()` | Returns the full path to the state file |
|
|
103
215
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
For object state types, `update()` merges partial changes:
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
const store = new StateTracker<{ count: number; name: string }>({
|
|
110
|
-
key: "demo",
|
|
111
|
-
default: { count: 0, name: "initial" },
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
store.update({ count: 42 });
|
|
115
|
-
console.log(store.state); // { count: 42, name: "initial" }
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Calling `update()` on a primitive state throws:
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
const primitive = new StateTracker<number>({ key: "num", default: 0 });
|
|
122
|
-
primitive.update(100 as never); // throws: "update() can only be used when state is a non-array object"
|
|
123
|
-
```
|
|
216
|
+
## Environment Variable
|
|
124
217
|
|
|
125
|
-
|
|
218
|
+
- `STATE_TRACKER_DIR`: Overrides default state directory (`~/.app-state`)
|
|
126
219
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
const store = new StateTracker<AppState>({
|
|
131
|
-
key: "app",
|
|
132
|
-
default: { version: 1 },
|
|
133
|
-
onEvent: ({ level, message, context }) => {
|
|
134
|
-
console.log(`[${level}] ${message}`, context);
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
await store.loadAsync();
|
|
139
|
-
// Example output: [info] No existing state file, using defaults { path: ".../app.json" }
|
|
140
|
-
|
|
141
|
-
store.set({ version: 2 });
|
|
142
|
-
await store.saveAsync();
|
|
143
|
-
// Example output: [debug] Saved state to disk { path: ".../app.json" }
|
|
220
|
+
```bash
|
|
221
|
+
STATE_TRACKER_DIR=/custom/path npm start
|
|
144
222
|
```
|
|
145
223
|
|
|
146
|
-
Event levels: `"debug"`, `"info"`, `"warn"`, `"error"`
|
|
147
|
-
|
|
148
224
|
## Features
|
|
149
225
|
|
|
150
226
|
- **Type inference** from the default value
|
|
@@ -154,20 +230,6 @@ Event levels: `"debug"`, `"info"`, `"warn"`, `"error"`
|
|
|
154
230
|
- **Auto-save** — debounced saves after state mutations
|
|
155
231
|
- **Legacy format support** — reads both v1 envelope format and legacy PersistentStore formats
|
|
156
232
|
|
|
157
|
-
## Legacy Format Migration
|
|
158
|
-
|
|
159
|
-
The library transparently handles migration from legacy formats:
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
// If disk contains legacy format: { count: 42 }
|
|
163
|
-
// Load merges with defaults: { count: 42, extra: true }
|
|
164
|
-
await store.loadAsync();
|
|
165
|
-
|
|
166
|
-
// Subsequent save writes new envelope format:
|
|
167
|
-
// { value: { count: 42, extra: true }, lastUpdated: "2025-01-01T..." }
|
|
168
|
-
await store.saveAsync();
|
|
169
|
-
```
|
|
170
|
-
|
|
171
233
|
## Exported Types
|
|
172
234
|
|
|
173
235
|
The package also exports the following types for advanced usage:
|