@hardlydifficult/state-tracker 2.0.13 → 2.0.14
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 +126 -275
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/state-tracker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript state tracker with atomic JSON persistence, auto-save debouncing, typed migrations, and graceful fallback.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -13,358 +13,210 @@ npm install @hardlydifficult/state-tracker
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { StateTracker } from "@hardlydifficult/state-tracker";
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
version: string;
|
|
18
|
-
theme: "light" | "dark";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const tracker = new StateTracker<AppConfig>({
|
|
16
|
+
const tracker = new StateTracker({
|
|
22
17
|
key: "app-config",
|
|
23
|
-
default: {
|
|
24
|
-
autoSaveMs:
|
|
18
|
+
default: { theme: "light", notifications: true },
|
|
19
|
+
autoSaveMs: 500,
|
|
25
20
|
});
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
tracker.
|
|
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.
|
|
37
|
-
|
|
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) |
|
|
46
|
-
|
|
47
|
-
#### Async vs Sync Persistence
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
// Async (recommended for production)
|
|
51
|
-
await tracker.loadAsync();
|
|
52
|
-
await tracker.saveAsync();
|
|
22
|
+
tracker.set({ theme: "dark" }); // saves debounced
|
|
23
|
+
console.log(tracker.state); // { theme: "dark", notifications: true }
|
|
53
24
|
|
|
54
|
-
|
|
55
|
-
tracker.load();
|
|
56
|
-
tracker.save({ theme: "dark" });
|
|
25
|
+
tracker.reset(); // restores default
|
|
57
26
|
```
|
|
58
27
|
|
|
59
|
-
|
|
28
|
+
## Core State Management
|
|
60
29
|
|
|
61
|
-
|
|
30
|
+
The `StateTracker` class provides type-safe state persistence with automatic atomic writes and in-memory caching.
|
|
62
31
|
|
|
63
|
-
|
|
64
|
-
tracker.set("__proto__", { malicious: true }); // ignored
|
|
65
|
-
tracker.set("normalKey", "value"); // works as expected
|
|
66
|
-
```
|
|
32
|
+
### Constructor Options
|
|
67
33
|
|
|
68
|
-
|
|
34
|
+
| Option | Type | Default | Description |
|
|
35
|
+
|--------|------|---------|-------------|
|
|
36
|
+
| `key` | `string` | — | Unique identifier; must contain only alphanumeric characters, hyphens, underscores |
|
|
37
|
+
| `default` | `T` | — | Initial state used if no saved state is found |
|
|
38
|
+
| `stateDirectory?` | `string` | `~/.app-state` | Override storage directory (or `STATE_TRACKER_DIR` env var) |
|
|
39
|
+
| `autoSaveMs?` | `number` | `0` | Debounce delay (ms) after state changes |
|
|
40
|
+
| `onEvent?` | `(event) => void` | — | Optional callback for debug/info/warn/error events |
|
|
69
41
|
|
|
70
|
-
|
|
42
|
+
### Instance Properties
|
|
71
43
|
|
|
72
|
-
|
|
73
|
-
|
|
44
|
+
- `state: Readonly<T>` — Current in-memory state (read-only)
|
|
45
|
+
- `isPersistent: boolean` — Whether disk storage is available
|
|
46
|
+
- `getFilePath(): string` — Full path to the JSON file
|
|
74
47
|
|
|
75
|
-
|
|
76
|
-
theme: "light" | "dark";
|
|
77
|
-
}
|
|
48
|
+
### Instance Methods
|
|
78
49
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
50
|
+
| Method | Signature | Description |
|
|
51
|
+
|--------|-----------|-------------|
|
|
52
|
+
| `load()` / `loadSync()` | `(): T` | Sync load (uses defaults on error) |
|
|
53
|
+
| `save(value)` | `(value: T): void` | Sync save to disk |
|
|
54
|
+
| `set(newState)` | `(newState: T): void` | Replace state, triggers auto-save |
|
|
55
|
+
| `update(changes)` | `(changes: Partial<T>): void` | Shallow merge (object state only) |
|
|
56
|
+
| `reset()` | `(): void` | Restore to default value |
|
|
83
57
|
|
|
84
|
-
|
|
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
|
-
});
|
|
58
|
+
### Async Persistence API
|
|
99
59
|
|
|
100
|
-
|
|
101
|
-
key: "config",
|
|
102
|
-
default: { version: "1.1.0", theme: "dark" },
|
|
103
|
-
});
|
|
60
|
+
Async methods support graceful degradation when file system access fails.
|
|
104
61
|
|
|
105
|
-
|
|
106
|
-
```
|
|
62
|
+
#### Methods
|
|
107
63
|
|
|
108
|
-
|
|
64
|
+
- `loadAsync(): Promise<void>` — Loads state from disk; sets `isPersistent` to `false` on failure
|
|
65
|
+
- `saveAsync(): Promise<void>` — Atomic async save using temp file + rename
|
|
66
|
+
- Both are idempotent: subsequent calls after first `loadAsync` are no-ops
|
|
109
67
|
|
|
110
|
-
|
|
68
|
+
#### Example
|
|
111
69
|
|
|
112
70
|
```typescript
|
|
113
|
-
const tracker = new StateTracker
|
|
114
|
-
key: "
|
|
115
|
-
default: {
|
|
71
|
+
const tracker = new StateTracker({
|
|
72
|
+
key: "settings",
|
|
73
|
+
default: { fontSize: 14 },
|
|
74
|
+
stateDirectory: "/app/state",
|
|
116
75
|
});
|
|
117
76
|
|
|
118
77
|
await tracker.loadAsync();
|
|
119
|
-
|
|
120
|
-
console.warn("Running in-memory mode");
|
|
121
|
-
}
|
|
122
|
-
```
|
|
78
|
+
console.log(tracker.isPersistent); // true (or false if directory not writable)
|
|
123
79
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
### Constructor Options
|
|
129
|
-
|
|
130
|
-
| Option | Type | Default | Description |
|
|
131
|
-
|--------|------|---------|-------------|
|
|
132
|
-
| `key` | `string` | — | Unique identifier for the state file (alphanumeric, hyphens, underscores only) |
|
|
133
|
-
| `default` | `T` | — | Default state value used if no persisted state exists |
|
|
134
|
-
| `stateDirectory` | `string` | `~/.app-state` or `$STATE_TRACKER_DIR` | Directory to store state files |
|
|
135
|
-
| `autoSaveMs` | `number` | `0` | Debounce delay (ms) for auto-save after state changes |
|
|
136
|
-
| `migration` | `Migration<T>` | — | **Deprecated:** Use `defineStateMigration` with `loadOrDefault` instead |
|
|
137
|
-
| `onEvent` | `(event: StateTrackerEvent) => void` | `undefined` | Callback for internal events (debug/info/warn/error) |
|
|
80
|
+
tracker.set({ fontSize: 16 });
|
|
81
|
+
await tracker.saveAsync();
|
|
82
|
+
```
|
|
138
83
|
|
|
139
|
-
|
|
84
|
+
## Auto-Save with Debouncing
|
|
140
85
|
|
|
141
|
-
|
|
142
|
-
- **`isPersistent: boolean`** — Indicates whether disk persistence is available (set after `loadAsync()`).
|
|
86
|
+
Set `autoSaveMs` in the constructor to debounce writes after state changes via `set()` or `update()`.
|
|
143
87
|
|
|
144
88
|
```typescript
|
|
145
89
|
const tracker = new StateTracker({
|
|
146
|
-
key: "
|
|
147
|
-
default: 0,
|
|
148
|
-
|
|
90
|
+
key: "editor",
|
|
91
|
+
default: { text: "", cursor: 0 },
|
|
92
|
+
autoSaveMs: 300,
|
|
149
93
|
});
|
|
150
94
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.log(tracker.isPersistent); // true if disk write succeeded
|
|
95
|
+
tracker.set({ text: "Hello" }); // will auto-save after 300ms
|
|
96
|
+
tracker.update({ cursor: 5 }); // cancels pending auto-save, re-schedules
|
|
154
97
|
```
|
|
155
98
|
|
|
156
|
-
|
|
99
|
+
- `save()` and `saveAsync()` cancel pending auto-saves and write immediately
|
|
100
|
+
- If `autoSaveMs <= 0`, no debounced saves are scheduled
|
|
101
|
+
- On persistent storage failure, auto-save is disabled until `loadAsync` succeeds
|
|
102
|
+
|
|
103
|
+
## Typed Migrations
|
|
104
|
+
|
|
105
|
+
Support legacy state formats with typed migrations.
|
|
157
106
|
|
|
158
|
-
|
|
159
|
-
Synchronous state load from disk. Returns the current state (default if missing or corrupted).
|
|
107
|
+
### Migration Interface
|
|
160
108
|
|
|
161
109
|
```typescript
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
110
|
+
interface StateTrackerMigration<TCurrent, TLegacy = unknown> {
|
|
111
|
+
name?: string;
|
|
112
|
+
isLegacy(input: unknown): input is TLegacy;
|
|
113
|
+
migrate(legacy: TLegacy): TCurrent;
|
|
114
|
+
}
|
|
167
115
|
```
|
|
168
116
|
|
|
169
|
-
|
|
170
|
-
Explicit "safe load" convenience. Behaves like `load()` and supports typed legacy migrations.
|
|
117
|
+
### Helper
|
|
171
118
|
|
|
172
|
-
|
|
173
|
-
import { defineStateMigration, StateTracker } from "@hardlydifficult/state-tracker";
|
|
119
|
+
- `defineStateMigration<TCurrent, TLegacy>(migration)` — Type-safe migration builder
|
|
174
120
|
|
|
175
|
-
|
|
176
|
-
offset: number;
|
|
177
|
-
completedIds: string[];
|
|
178
|
-
}
|
|
121
|
+
### Usage
|
|
179
122
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
123
|
+
```typescript
|
|
124
|
+
interface LegacyState { offset: number; completedIds: string[] }
|
|
125
|
+
interface CurrentState { cursor: number; done: string[] }
|
|
184
126
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
>({
|
|
189
|
-
name: "sync-state-v0",
|
|
190
|
-
isLegacy(input): input is LegacySyncState {
|
|
127
|
+
const migration = defineStateMigration<CurrentState, LegacyState>({
|
|
128
|
+
name: "cursor-migration",
|
|
129
|
+
isLegacy(input): input is LegacyState {
|
|
191
130
|
return (
|
|
192
|
-
input
|
|
131
|
+
input &&
|
|
193
132
|
typeof input === "object" &&
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
Array.isArray((input as Record<string, unknown>).completedIds)
|
|
133
|
+
"offset" in input &&
|
|
134
|
+
"completedIds" in input
|
|
197
135
|
);
|
|
198
136
|
},
|
|
199
137
|
migrate(legacy) {
|
|
200
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
cursor: legacy.offset,
|
|
140
|
+
done: legacy.completedIds,
|
|
141
|
+
};
|
|
201
142
|
},
|
|
202
143
|
});
|
|
203
144
|
|
|
204
|
-
const state = tracker.loadOrDefault({ migrations: [legacyMigration] });
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
#### `save(value: T): void`
|
|
208
|
-
Synchronous atomic save using temp file + rename.
|
|
209
|
-
|
|
210
|
-
```typescript
|
|
211
|
-
tracker.save({ version: 2 });
|
|
212
|
-
// File is updated atomically; previous state preserved if crash occurs mid-write
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
#### `saveWithMeta(value: T, meta?: Record<string, unknown>): void`
|
|
216
|
-
Synchronous atomic save with optional metadata in the envelope.
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
tracker.saveWithMeta(
|
|
220
|
-
{ version: 3 },
|
|
221
|
-
{ source: "sync-script", reason: "manual-run" }
|
|
222
|
-
);
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
#### `loadAsync(): Promise<void>`
|
|
226
|
-
Async state load with graceful degradation. Sets `isPersistent = false` on failure instead of throwing.
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
145
|
const tracker = new StateTracker({
|
|
230
|
-
key: "
|
|
231
|
-
default: {
|
|
146
|
+
key: "tasks",
|
|
147
|
+
default: { cursor: 0, done: [] } as CurrentState,
|
|
232
148
|
});
|
|
233
149
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
150
|
+
// Load with migration
|
|
151
|
+
const value = tracker.loadOrDefault({ migrations: [migration] });
|
|
152
|
+
// Legacy { offset: 3, completedIds: ["a", "b"] } → { cursor: 3, done: ["a", "b"] }
|
|
238
153
|
```
|
|
239
154
|
|
|
240
|
-
|
|
241
|
-
Async atomic save (temp file + rename). Cancels any pending auto-save before writing.
|
|
155
|
+
### Envelope Format
|
|
242
156
|
|
|
243
|
-
|
|
244
|
-
tracker.set({ darkMode: true });
|
|
245
|
-
await tracker.saveAsync(); // Immediate save, bypassing debounce
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### State Mutations
|
|
249
|
-
|
|
250
|
-
#### `set(newState: T): void`
|
|
251
|
-
Replace entire state and schedule auto-save.
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
tracker.set({ darkMode: true, theme: "midnight" });
|
|
255
|
-
// Auto-saves after configured delay (if autoSaveMs > 0)
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
#### `update(changes: Partial<T>): void`
|
|
259
|
-
Shallow merge for object types and schedule auto-save.
|
|
157
|
+
Saved state uses a JSON envelope:
|
|
260
158
|
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
#### `reset(): void`
|
|
268
|
-
Restore state to the default value and schedule auto-save.
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
tracker.reset(); // Reverts to default state
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"value": { /* your state */ },
|
|
162
|
+
"lastUpdated": "2025-04-05T12:34:56.789Z",
|
|
163
|
+
"meta": { /* optional metadata */ }
|
|
164
|
+
}
|
|
272
165
|
```
|
|
273
166
|
|
|
274
|
-
|
|
167
|
+
- `load()` and `loadAsync()` extract `value` and merge missing keys from `default`
|
|
168
|
+
- Supports raw legacy objects (without envelope) with default-merge
|
|
275
169
|
|
|
276
|
-
|
|
277
|
-
Returns the full path to the state file.
|
|
170
|
+
## Event Logging
|
|
278
171
|
|
|
279
|
-
|
|
280
|
-
console.log(tracker.getFilePath()); // "/home/user/.app-state/counter.json"
|
|
281
|
-
```
|
|
172
|
+
Optionally track runtime events via the `onEvent` callback.
|
|
282
173
|
|
|
283
|
-
|
|
174
|
+
### Event Types
|
|
284
175
|
|
|
285
|
-
|
|
176
|
+
- `debug`: Internal behavior (e.g., auto-save completion)
|
|
177
|
+
- `info`: Startup/loading (e.g., no existing state, directory creation)
|
|
178
|
+
- `warn`: recoverable issues (e.g., storage unavailable, migration failure)
|
|
179
|
+
- `error`: unrecoverable errors (e.g., disk write failure)
|
|
286
180
|
|
|
287
|
-
|
|
288
|
-
|-------|-------------|
|
|
289
|
-
| `debug` | Low-level operations (e.g., save completion) |
|
|
290
|
-
| `info` | Normal operations (e.g., file read/write) |
|
|
291
|
-
| `warn` | Recoverable failures (e.g., disk I/O errors) |
|
|
292
|
-
| `error` | Non-recoverable failures (e.g., permission issues) |
|
|
181
|
+
### Example
|
|
293
182
|
|
|
294
183
|
```typescript
|
|
295
184
|
const tracker = new StateTracker({
|
|
296
|
-
key: "
|
|
185
|
+
key: "my-state",
|
|
297
186
|
default: {},
|
|
298
187
|
onEvent: (event) => {
|
|
299
|
-
|
|
188
|
+
if (event.level === "error") console.error(event.message, event.context);
|
|
300
189
|
},
|
|
301
190
|
});
|
|
302
191
|
|
|
303
192
|
await tracker.loadAsync();
|
|
304
|
-
//
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
## Persistence Format
|
|
308
|
-
|
|
309
|
-
### v2 (Envelope) Format
|
|
310
|
-
```json
|
|
311
|
-
{
|
|
312
|
-
"value": { "theme": "dark", "notifications": true },
|
|
313
|
-
"lastUpdated": "2024-05-01T12:00:00.000Z",
|
|
314
|
-
"meta": { "source": "sync-script" }
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### Legacy (PersistentStore) Migration
|
|
319
|
-
The tracker automatically detects and merges legacy raw JSON objects with defaults on load.
|
|
320
|
-
|
|
321
|
-
```typescript
|
|
322
|
-
// If disk contains: { "count": 42 }
|
|
323
|
-
// And default is: { "count": 0, "name": "default" }
|
|
324
|
-
// Loaded state becomes: { "count": 42, "name": "default" }
|
|
193
|
+
// emits info: "No existing state file" or "Loaded state from disk"
|
|
325
194
|
```
|
|
326
195
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
For custom legacy formats, use typed migrations with `defineStateMigration(...)`
|
|
330
|
-
and pass them to `loadOrDefault({ migrations })`.
|
|
331
|
-
|
|
332
|
-
## Auto-Save Behavior
|
|
196
|
+
## Appendix: Key Sanitization
|
|
333
197
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
1. `set()` or `update()` triggers a timer.
|
|
337
|
-
2. Subsequent changes within the window reset the timer.
|
|
338
|
-
3. After `autoSaveMs` ms of inactivity, the state is saved.
|
|
198
|
+
Keys must match `^[A-Za-z0-9_-]+$`. Invalid keys throw at construction:
|
|
339
199
|
|
|
340
200
|
```typescript
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
default: { x: 0 },
|
|
344
|
-
autoSaveMs: 500,
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
tracker.set({ x: 1 });
|
|
348
|
-
tracker.set({ x: 2 }); // Timer resets
|
|
349
|
-
await new Promise(r => setTimeout(r, 100));
|
|
350
|
-
tracker.set({ x: 3 }); // Timer resets again
|
|
201
|
+
new StateTracker({ key: "../evil", default: 0 });
|
|
202
|
+
// Error: StateTracker key contains invalid characters
|
|
351
203
|
|
|
352
|
-
|
|
204
|
+
new StateTracker({ key: "", default: 0 });
|
|
205
|
+
// Error: StateTracker key must be a non-empty string
|
|
353
206
|
```
|
|
354
207
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
## Types
|
|
208
|
+
State files are written as `<key>.json` inside the state directory.
|
|
358
209
|
|
|
359
|
-
|
|
360
|
-
export type StateTrackerEventLevel = "debug" | "info" | "warn" | "error";
|
|
210
|
+
## Exported API
|
|
361
211
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
212
|
+
- `StateTracker<T>` — Main persistence class
|
|
213
|
+
- `defineStateMigration<TCurrent, TLegacy>` — Migration builder
|
|
214
|
+
- `StateTrackerOptions<T>` — Constructor options
|
|
215
|
+
- `StateTrackerEvent` — Event payload
|
|
216
|
+
- `StateTrackerEventLevel` — `"debug" \| "info" \| "warn" \| "error"`
|
|
217
|
+
- `StateTrackerLoadOrDefaultOptions<T>` — Options for `loadOrDefault`
|
|
218
|
+
- `StateTrackerMigration<TCurrent, TLegacy>` — Migration definition
|
|
219
|
+
- `StateTrackerSaveMeta` — Optional metadata in save envelope
|
|
368
220
|
|
|
369
221
|
## Environment Variable
|
|
370
222
|
|
|
@@ -383,13 +235,12 @@ STATE_TRACKER_DIR=/custom/path npm start
|
|
|
383
235
|
- **Auto-save** — debounced saves after state mutations
|
|
384
236
|
- **Legacy format support** — reads both v1 envelope format and legacy PersistentStore formats
|
|
385
237
|
- **Typed migration helper** — declarative migration rules for old JSON shapes
|
|
386
|
-
- **Optional save metadata** — annotate saved state with `saveWithMeta(...)`
|
|
387
238
|
- **API consistency** — all operations work seamlessly across sync/async modes
|
|
388
239
|
|
|
389
|
-
##
|
|
240
|
+
## Platform Behavior
|
|
390
241
|
|
|
391
242
|
| Environment | Persistence | Fallback Behavior |
|
|
392
243
|
|-------------|-------------|-------------------|
|
|
393
|
-
| Node.js
|
|
394
|
-
| Browser
|
|
395
|
-
| Bun/Deno
|
|
244
|
+
| Node.js | ✅ File system access | Falls back to memory on errors |
|
|
245
|
+
| Browser | ❌ No file system access | Always in-memory only |
|
|
246
|
+
| Bun/Deno | ⚠️ Experimental support | Depends on environment capabilities |
|