@hardlydifficult/state-tracker 1.0.0

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 ADDED
@@ -0,0 +1,37 @@
1
+ # @hardlydifficult/state-tracker
2
+
3
+ File-based state persistence for recovery across restarts.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @hardlydifficult/state-tracker
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { StateTracker } from '@hardlydifficult/state-tracker';
15
+
16
+ const tracker = new StateTracker({ key: 'last-sync', default: 0 });
17
+
18
+ // Load persisted value (or default on first run)
19
+ const lastSync = tracker.load();
20
+
21
+ // Save updated value (atomic write)
22
+ tracker.save(Date.now());
23
+ ```
24
+
25
+ ## Options
26
+
27
+ | Option | Description |
28
+ |--------|-------------|
29
+ | `key` | Unique identifier for the state file (alphanumeric, hyphens, underscores) |
30
+ | `default` | Default value returned when no state exists (also sets the type) |
31
+ | `stateDirectory?` | Custom directory for state files (default: `~/.app-state` or `STATE_TRACKER_DIR` env) |
32
+
33
+ ## Features
34
+
35
+ - **Type inference** from the default value
36
+ - **Atomic writes** via temp file + rename to prevent corruption
37
+ - **Key sanitization** to prevent path traversal
@@ -0,0 +1,16 @@
1
+ export interface StateTrackerOptions<T> {
2
+ key: string;
3
+ default: T;
4
+ stateDirectory?: string;
5
+ }
6
+ export declare class StateTracker<T> {
7
+ private readonly filePath;
8
+ private readonly defaultValue;
9
+ private static sanitizeKey;
10
+ private static getDefaultStateDirectory;
11
+ constructor(options: StateTrackerOptions<T>);
12
+ load(): T;
13
+ save(value: T): void;
14
+ getFilePath(): string;
15
+ }
16
+ //# sourceMappingURL=StateTracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StateTracker.d.ts","sourceRoot":"","sources":["../src/StateTracker.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,CAAC,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,YAAY,CAAC,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAI;IAEjC,OAAO,CAAC,MAAM,CAAC,WAAW;IAa1B,OAAO,CAAC,MAAM,CAAC,wBAAwB;gBAQ3B,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAY3C,IAAI,IAAI,CAAC;IAiBT,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAUpB,WAAW,IAAI,MAAM;CAGtB"}
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.StateTracker = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ class StateTracker {
41
+ filePath;
42
+ defaultValue;
43
+ static sanitizeKey(key) {
44
+ const trimmed = key.trim();
45
+ if (trimmed === "") {
46
+ throw new Error("StateTracker key must be a non-empty string");
47
+ }
48
+ if (!/^[A-Za-z0-9_-]+$/.test(trimmed)) {
49
+ throw new Error("StateTracker key contains invalid characters (only alphanumeric, hyphens, and underscores allowed)");
50
+ }
51
+ return trimmed;
52
+ }
53
+ static getDefaultStateDirectory() {
54
+ const envDir = process.env.STATE_TRACKER_DIR;
55
+ if (envDir !== undefined && envDir !== "") {
56
+ return envDir;
57
+ }
58
+ return path.join(os.homedir(), ".app-state");
59
+ }
60
+ constructor(options) {
61
+ const sanitizedKey = StateTracker.sanitizeKey(options.key);
62
+ this.defaultValue = options.default;
63
+ const stateDirectory = options.stateDirectory ?? StateTracker.getDefaultStateDirectory();
64
+ if (!fs.existsSync(stateDirectory)) {
65
+ fs.mkdirSync(stateDirectory, { recursive: true });
66
+ }
67
+ this.filePath = path.join(stateDirectory, `${sanitizedKey}.json`);
68
+ }
69
+ load() {
70
+ if (!fs.existsSync(this.filePath)) {
71
+ return this.defaultValue;
72
+ }
73
+ try {
74
+ const data = fs.readFileSync(this.filePath, "utf-8");
75
+ const state = JSON.parse(data);
76
+ const { value } = state;
77
+ if (value === undefined) {
78
+ return this.defaultValue;
79
+ }
80
+ return value;
81
+ }
82
+ catch {
83
+ return this.defaultValue;
84
+ }
85
+ }
86
+ save(value) {
87
+ const state = {
88
+ value,
89
+ lastUpdated: new Date().toISOString(),
90
+ };
91
+ const tempFilePath = `${this.filePath}.tmp`;
92
+ fs.writeFileSync(tempFilePath, JSON.stringify(state, null, 2), "utf-8");
93
+ fs.renameSync(tempFilePath, this.filePath);
94
+ }
95
+ getFilePath() {
96
+ return this.filePath;
97
+ }
98
+ }
99
+ exports.StateTracker = StateTracker;
100
+ //# sourceMappingURL=StateTracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StateTracker.js","sourceRoot":"","sources":["../src/StateTracker.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AACzB,uCAAyB;AACzB,2CAA6B;AAQ7B,MAAa,YAAY;IACN,QAAQ,CAAS;IACjB,YAAY,CAAI;IAEzB,MAAM,CAAC,WAAW,CAAC,GAAW;QACpC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,KAAK,CACb,oGAAoG,CACrG,CAAC;QACJ,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,MAAM,CAAC,wBAAwB;QACrC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QAC7C,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;YAC1C,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;IAC/C,CAAC;IAED,YAAY,OAA+B;QACzC,MAAM,YAAY,GAAG,YAAY,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC3D,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;QACpC,MAAM,cAAc,GAClB,OAAO,CAAC,cAAc,IAAI,YAAY,CAAC,wBAAwB,EAAE,CAAC;QAEpE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;YACnC,EAAE,CAAC,SAAS,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,YAAY,OAAO,CAAC,CAAC;IACpE,CAAC;IAED,IAAI;QACF,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC,YAAY,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;YAC1D,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;YACxB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,IAAI,CAAC,YAAY,CAAC;YAC3B,CAAC;YACD,OAAO,KAAU,CAAC;QACpB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,YAAY,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAQ;QACX,MAAM,KAAK,GAA4B;YACrC,KAAK;YACL,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACtC,CAAC;QACF,MAAM,YAAY,GAAG,GAAG,IAAI,CAAC,QAAQ,MAAM,CAAC;QAC5C,EAAE,CAAC,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QACxE,EAAE,CAAC,UAAU,CAAC,YAAY,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;CACF;AAnED,oCAmEC"}
@@ -0,0 +1,2 @@
1
+ export { StateTracker, type StateTrackerOptions } from "./StateTracker";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StateTracker = void 0;
4
+ var StateTracker_1 = require("./StateTracker");
5
+ Object.defineProperty(exports, "StateTracker", { enumerable: true, get: function () { return StateTracker_1.StateTracker; } });
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,+CAAwE;AAA/D,4GAAA,YAAY,OAAA"}
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@hardlydifficult/state-tracker",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest run --coverage",
14
+ "lint": "tsc --noEmit",
15
+ "clean": "rm -rf dist"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "20.19.31",
19
+ "typescript": "5.8.3",
20
+ "vitest": "1.6.1"
21
+ },
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ }
25
+ }