@appydave/core 0.1.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 +23 -0
- package/dist/atomic-write.d.ts +33 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/atomic-write.js +63 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +129 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +56 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +160 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +30 -0
- package/dist/logger.js.map +1 -0
- package/dist/serial-queue.d.ts +30 -0
- package/dist/serial-queue.d.ts.map +1 -0
- package/dist/serial-queue.js +49 -0
- package/dist/serial-queue.js.map +1 -0
- package/dist/store.d.ts +32 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +51 -0
- package/dist/store.js.map +1 -0
- package/package.json +85 -0
- package/src/atomic-write.ts +81 -0
- package/src/config.ts +186 -0
- package/src/index.ts +40 -0
- package/src/lifecycle.ts +206 -0
- package/src/logger.ts +52 -0
- package/src/serial-queue.ts +53 -0
- package/src/store.ts +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @appydave/core
|
|
2
|
+
|
|
3
|
+
The shared, product-prefix-free foundation reused by every AppyDave boilerplate
|
|
4
|
+
(AppyStack · AppySentinel · AppyTron).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm i @appydave/core
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Primitives
|
|
11
|
+
|
|
12
|
+
| Export | What it does |
|
|
13
|
+
|--------|--------------|
|
|
14
|
+
| `createLifecycle()` | start/stop hook harness (signals, ordered hooks, health) |
|
|
15
|
+
| `createConfigLoader()` | defaults → file → env, Zod-validated, reloadable |
|
|
16
|
+
| `createLogger({ name })` | Pino structured logging |
|
|
17
|
+
| `atomicWrite(path, content)` | torn-write-proof file write (`string \| Uint8Array`) |
|
|
18
|
+
| `SerialQueue` | ordered, one-at-a-time task queue |
|
|
19
|
+
| `createStore({ path, defaults })` | local-first JSON store (atomic + serialised) |
|
|
20
|
+
|
|
21
|
+
No product-specific, web, or Electron code — those live in the consuming boilerplate.
|
|
22
|
+
|
|
23
|
+
MIT · David Cruwys · [github.com/appydave/foundation](https://github.com/appydave/foundation)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic file write helper.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: write to a sibling temp file, then `rename` into place. On POSIX
|
|
5
|
+
* filesystems (and APFS / NTFS via Node's promisified fs.rename) this is
|
|
6
|
+
* atomic at the directory-entry level — readers either see the previous
|
|
7
|
+
* version or the new one, never a torn write.
|
|
8
|
+
*
|
|
9
|
+
* Essential for crash safety with flat-file stores (JSONL indexes, registry
|
|
10
|
+
* snapshots, config files).
|
|
11
|
+
*/
|
|
12
|
+
export interface AtomicWriteOptions {
|
|
13
|
+
/** File mode (e.g. 0o600). Defaults to system default. */
|
|
14
|
+
mode?: number;
|
|
15
|
+
/** Encoding for string content. Defaults to 'utf8'. */
|
|
16
|
+
encoding?: BufferEncoding;
|
|
17
|
+
/**
|
|
18
|
+
* fsync the file before rename. Slower but stronger crash guarantee.
|
|
19
|
+
* Defaults to false.
|
|
20
|
+
*/
|
|
21
|
+
fsync?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Atomically write content to `path`.
|
|
25
|
+
*
|
|
26
|
+
* Behaviour:
|
|
27
|
+
* 1. Write to `<path>.<random>.tmp` in the same directory.
|
|
28
|
+
* 2. Optionally fsync.
|
|
29
|
+
* 3. `rename` over the destination.
|
|
30
|
+
* 4. On any error, the temp file is removed on a best-effort basis.
|
|
31
|
+
*/
|
|
32
|
+
export declare function atomicWrite(path: string, content: string | Uint8Array, options?: AtomicWriteOptions): Promise<void>;
|
|
33
|
+
//# sourceMappingURL=atomic-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic-write.d.ts","sourceRoot":"","sources":["../src/atomic-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAuCf"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic file write helper.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: write to a sibling temp file, then `rename` into place. On POSIX
|
|
5
|
+
* filesystems (and APFS / NTFS via Node's promisified fs.rename) this is
|
|
6
|
+
* atomic at the directory-entry level — readers either see the previous
|
|
7
|
+
* version or the new one, never a torn write.
|
|
8
|
+
*
|
|
9
|
+
* Essential for crash safety with flat-file stores (JSONL indexes, registry
|
|
10
|
+
* snapshots, config files).
|
|
11
|
+
*/
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import { dirname, basename, join } from 'node:path';
|
|
14
|
+
import { randomBytes } from 'node:crypto';
|
|
15
|
+
/**
|
|
16
|
+
* Atomically write content to `path`.
|
|
17
|
+
*
|
|
18
|
+
* Behaviour:
|
|
19
|
+
* 1. Write to `<path>.<random>.tmp` in the same directory.
|
|
20
|
+
* 2. Optionally fsync.
|
|
21
|
+
* 3. `rename` over the destination.
|
|
22
|
+
* 4. On any error, the temp file is removed on a best-effort basis.
|
|
23
|
+
*/
|
|
24
|
+
export async function atomicWrite(path, content, options = {}) {
|
|
25
|
+
const dir = dirname(path);
|
|
26
|
+
const base = basename(path);
|
|
27
|
+
const suffix = randomBytes(6).toString('hex');
|
|
28
|
+
const tmp = join(dir, `.${base}.${suffix}.tmp`);
|
|
29
|
+
let handle;
|
|
30
|
+
try {
|
|
31
|
+
handle = await fs.open(tmp, 'w', options.mode);
|
|
32
|
+
if (typeof content === 'string') {
|
|
33
|
+
await handle.writeFile(content, { encoding: options.encoding ?? 'utf8' });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
await handle.writeFile(content);
|
|
37
|
+
}
|
|
38
|
+
if (options.fsync) {
|
|
39
|
+
await handle.sync();
|
|
40
|
+
}
|
|
41
|
+
await handle.close();
|
|
42
|
+
handle = undefined;
|
|
43
|
+
await fs.rename(tmp, path);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (handle) {
|
|
47
|
+
try {
|
|
48
|
+
await handle.close();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* ignore */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
await fs.unlink(tmp);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* best-effort cleanup */
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=atomic-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic-write.js","sourceRoot":"","sources":["../src/atomic-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAc1C;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAY,EACZ,OAA4B,EAC5B,UAA8B,EAAE;IAEhC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,IAAI,MAAM,MAAM,CAAC,CAAC;IAEhD,IAAI,MAAiC,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAE/C,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,MAAM,EAAE,CAAC,CAAC;QAC5E,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAClC,CAAC;QAED,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACtB,CAAC;QAED,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,GAAG,SAAS,CAAC;QAEnB,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;YAAC,MAAM,CAAC;gBACP,YAAY;YACd,CAAC;QACH,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hierarchical config loader: built-in defaults → env vars → config file.
|
|
3
|
+
*
|
|
4
|
+
* Validated through a Zod schema. Optional file-watch reloads via SIGHUP or
|
|
5
|
+
* a chokidar watcher driven externally.
|
|
6
|
+
*
|
|
7
|
+
* Spec §5.4.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
export interface ConfigLoaderOptions<T> {
|
|
11
|
+
/** Zod schema describing the merged config. */
|
|
12
|
+
schema: z.ZodType<T>;
|
|
13
|
+
/** Static defaults applied first. */
|
|
14
|
+
defaults?: Partial<T>;
|
|
15
|
+
/**
|
|
16
|
+
* Map of env var names → object paths to layer in.
|
|
17
|
+
*
|
|
18
|
+
* Example: `{ LOG_LEVEL: 'logger.level' }` reads `process.env.LOG_LEVEL`
|
|
19
|
+
* and sets `config.logger.level`. Values are strings; coerce inside the
|
|
20
|
+
* Zod schema (via `z.coerce.number()` etc).
|
|
21
|
+
*/
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
/** Optional path to a JSON config file. Missing file is not an error. */
|
|
24
|
+
filePath?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Custom env source (defaults to process.env). Useful for tests.
|
|
27
|
+
*/
|
|
28
|
+
envSource?: NodeJS.ProcessEnv;
|
|
29
|
+
}
|
|
30
|
+
export interface ConfigLoader<T> {
|
|
31
|
+
/** Load + validate the merged config. Throws on schema errors. */
|
|
32
|
+
load(): Promise<T>;
|
|
33
|
+
/**
|
|
34
|
+
* Re-read the config file and re-merge env. Returns the new config.
|
|
35
|
+
* Subscribers registered via `onChange` are notified.
|
|
36
|
+
*/
|
|
37
|
+
reload(): Promise<T>;
|
|
38
|
+
/** Register a change handler. Returns an unsubscribe function. */
|
|
39
|
+
onChange(handler: (config: T) => void): () => void;
|
|
40
|
+
/** Most recently loaded config, if any. */
|
|
41
|
+
current(): T | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a hierarchical config loader.
|
|
45
|
+
*
|
|
46
|
+
* Layering order (later overrides earlier):
|
|
47
|
+
* 1. `options.defaults`
|
|
48
|
+
* 2. JSON file at `options.filePath` (if present)
|
|
49
|
+
* 3. environment variables mapped via `options.env`
|
|
50
|
+
*
|
|
51
|
+
* Validation runs once at the end against `options.schema`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createConfigLoader<T>(options: ConfigLoaderOptions<T>): ConfigLoader<T>;
|
|
54
|
+
export { z };
|
|
55
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,+CAA+C;IAC/C,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAErB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAEtB;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE7B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,kEAAkE;IAClE,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAEnB;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAErB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAEnD,2CAA2C;IAC3C,OAAO,IAAI,CAAC,GAAG,SAAS,CAAC;CAC1B;AA4DD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CA6DtF;AAED,OAAO,EAAE,CAAC,EAAE,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hierarchical config loader: built-in defaults → env vars → config file.
|
|
3
|
+
*
|
|
4
|
+
* Validated through a Zod schema. Optional file-watch reloads via SIGHUP or
|
|
5
|
+
* a chokidar watcher driven externally.
|
|
6
|
+
*
|
|
7
|
+
* Spec §5.4.
|
|
8
|
+
*/
|
|
9
|
+
import { promises as fs } from 'node:fs';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
function setPath(target, path, value) {
|
|
12
|
+
const segments = path.split('.');
|
|
13
|
+
let node = target;
|
|
14
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
15
|
+
const seg = segments[i];
|
|
16
|
+
const next = node[seg];
|
|
17
|
+
if (next === undefined || next === null || typeof next !== 'object') {
|
|
18
|
+
const fresh = {};
|
|
19
|
+
node[seg] = fresh;
|
|
20
|
+
node = fresh;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
node = next;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
node[segments[segments.length - 1]] = value;
|
|
27
|
+
}
|
|
28
|
+
/** Deep merge — plain objects only, arrays replace. */
|
|
29
|
+
function deepMerge(base, overlay) {
|
|
30
|
+
const out = { ...base };
|
|
31
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
32
|
+
const existing = out[key];
|
|
33
|
+
if (existing &&
|
|
34
|
+
typeof existing === 'object' &&
|
|
35
|
+
!Array.isArray(existing) &&
|
|
36
|
+
value &&
|
|
37
|
+
typeof value === 'object' &&
|
|
38
|
+
!Array.isArray(value)) {
|
|
39
|
+
out[key] = deepMerge(existing, value);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
out[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
async function readJsonFile(path) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = await fs.readFile(path, 'utf8');
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`config file ${path} must contain a JSON object`);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err.code === 'ENOENT')
|
|
58
|
+
return undefined;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a hierarchical config loader.
|
|
64
|
+
*
|
|
65
|
+
* Layering order (later overrides earlier):
|
|
66
|
+
* 1. `options.defaults`
|
|
67
|
+
* 2. JSON file at `options.filePath` (if present)
|
|
68
|
+
* 3. environment variables mapped via `options.env`
|
|
69
|
+
*
|
|
70
|
+
* Validation runs once at the end against `options.schema`.
|
|
71
|
+
*/
|
|
72
|
+
export function createConfigLoader(options) {
|
|
73
|
+
const handlers = new Set();
|
|
74
|
+
let lastConfig;
|
|
75
|
+
const envSource = options.envSource ?? process.env;
|
|
76
|
+
const buildOnce = async () => {
|
|
77
|
+
let merged = (options.defaults ?? {});
|
|
78
|
+
if (options.filePath) {
|
|
79
|
+
const fileLayer = await readJsonFile(options.filePath);
|
|
80
|
+
if (fileLayer)
|
|
81
|
+
merged = deepMerge(merged, fileLayer);
|
|
82
|
+
}
|
|
83
|
+
if (options.env) {
|
|
84
|
+
const envOverlay = {};
|
|
85
|
+
for (const [varName, dottedPath] of Object.entries(options.env)) {
|
|
86
|
+
const raw = envSource[varName];
|
|
87
|
+
if (raw !== undefined) {
|
|
88
|
+
setPath(envOverlay, dottedPath, raw);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (Object.keys(envOverlay).length > 0) {
|
|
92
|
+
merged = deepMerge(merged, envOverlay);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return options.schema.parse(merged);
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
async load() {
|
|
99
|
+
const cfg = await buildOnce();
|
|
100
|
+
lastConfig = cfg;
|
|
101
|
+
return cfg;
|
|
102
|
+
},
|
|
103
|
+
async reload() {
|
|
104
|
+
const cfg = await buildOnce();
|
|
105
|
+
lastConfig = cfg;
|
|
106
|
+
for (const handler of handlers) {
|
|
107
|
+
try {
|
|
108
|
+
handler(cfg);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.error('[appysentinel] config onChange handler error', err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return cfg;
|
|
116
|
+
},
|
|
117
|
+
onChange(handler) {
|
|
118
|
+
handlers.add(handler);
|
|
119
|
+
return () => {
|
|
120
|
+
handlers.delete(handler);
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
current() {
|
|
124
|
+
return lastConfig;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export { z };
|
|
129
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @appydave/core — shared AppyDave foundation primitives.
|
|
3
|
+
*
|
|
4
|
+
* Cross-cutting building blocks reused by every AppyDave boilerplate
|
|
5
|
+
* (AppyStack, AppySentinel, AppyTron): process lifecycle, config loading,
|
|
6
|
+
* structured logging, atomic file writes, a serial task queue, and a
|
|
7
|
+
* local-first JSON store. No product-specific or Electron code lives here.
|
|
8
|
+
*/
|
|
9
|
+
export { type Lifecycle, type LifecycleStatus, type StopReason, type HealthReport, type StartHook, type StopHook, type ReloadHook, type CreateLifecycleOptions, createLifecycle, } from './lifecycle.js';
|
|
10
|
+
export { type ConfigLoader, type ConfigLoaderOptions, createConfigLoader, z, } from './config.js';
|
|
11
|
+
export { createLogger, type Logger, type LogLevel, type CreateLoggerOptions, } from './logger.js';
|
|
12
|
+
export { atomicWrite, type AtomicWriteOptions } from './atomic-write.js';
|
|
13
|
+
export { SerialQueue } from './serial-queue.js';
|
|
14
|
+
export { createStore, type Store, type StoreOptions } from './store.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,sBAAsB,EAC3B,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,kBAAkB,EAClB,CAAC,GACF,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEzE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @appydave/core — shared AppyDave foundation primitives.
|
|
3
|
+
*
|
|
4
|
+
* Cross-cutting building blocks reused by every AppyDave boilerplate
|
|
5
|
+
* (AppyStack, AppySentinel, AppyTron): process lifecycle, config loading,
|
|
6
|
+
* structured logging, atomic file writes, a serial task queue, and a
|
|
7
|
+
* local-first JSON store. No product-specific or Electron code lives here.
|
|
8
|
+
*/
|
|
9
|
+
export { createLifecycle, } from './lifecycle.js';
|
|
10
|
+
export { createConfigLoader, z, } from './config.js';
|
|
11
|
+
export { createLogger, } from './logger.js';
|
|
12
|
+
export { atomicWrite } from './atomic-write.js';
|
|
13
|
+
export { SerialQueue } from './serial-queue.js';
|
|
14
|
+
export { createStore } from './store.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EASL,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAGL,kBAAkB,EAClB,CAAC,GACF,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,YAAY,GAIb,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,WAAW,EAA2B,MAAM,mBAAmB,CAAC;AAEzE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,WAAW,EAAiC,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle harness — start, stop, reload, health.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates graceful shutdown across the Sentinel:
|
|
5
|
+
* - Registers SIGINT / SIGTERM / SIGHUP listeners on first `start()`.
|
|
6
|
+
* - SIGINT/SIGTERM trigger `stop(reason)` exactly once.
|
|
7
|
+
* - SIGHUP triggers `reload()`.
|
|
8
|
+
* - Hooks fire in registration order on start, reverse order on stop.
|
|
9
|
+
* - `health()` is a synchronous snapshot suitable for `/health` endpoints.
|
|
10
|
+
*
|
|
11
|
+
* Spec §5.3.
|
|
12
|
+
*/
|
|
13
|
+
export type StopReason = 'sigint' | 'sigterm' | 'reload' | 'fatal' | 'manual';
|
|
14
|
+
export type LifecycleStatus = 'idle' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
|
|
15
|
+
export interface HealthReport {
|
|
16
|
+
status: LifecycleStatus;
|
|
17
|
+
startedAt?: string;
|
|
18
|
+
stoppedAt?: string;
|
|
19
|
+
uptimeMs?: number;
|
|
20
|
+
hooks: {
|
|
21
|
+
start: number;
|
|
22
|
+
stop: number;
|
|
23
|
+
reload: number;
|
|
24
|
+
};
|
|
25
|
+
lastError?: string;
|
|
26
|
+
}
|
|
27
|
+
export type StartHook = () => void | Promise<void>;
|
|
28
|
+
export type StopHook = (reason: StopReason) => void | Promise<void>;
|
|
29
|
+
export type ReloadHook = () => void | Promise<void>;
|
|
30
|
+
export interface Lifecycle {
|
|
31
|
+
start(): Promise<void>;
|
|
32
|
+
stop(reason?: StopReason): Promise<void>;
|
|
33
|
+
reload(): Promise<void>;
|
|
34
|
+
health(): HealthReport;
|
|
35
|
+
/** Register a hook that runs during `start()`. Returns an unsubscribe fn. */
|
|
36
|
+
onStart(hook: StartHook): () => void;
|
|
37
|
+
/** Register a hook that runs during `stop()`. Reverse-order. */
|
|
38
|
+
onStop(hook: StopHook): () => void;
|
|
39
|
+
/** Register a hook that runs during `reload()` / SIGHUP. */
|
|
40
|
+
onReload(hook: ReloadHook): () => void;
|
|
41
|
+
}
|
|
42
|
+
export interface CreateLifecycleOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Whether to wire OS signal handlers (SIGINT/SIGTERM/SIGHUP) automatically.
|
|
45
|
+
* Defaults to true in production. Set false in tests.
|
|
46
|
+
*/
|
|
47
|
+
installSignalHandlers?: boolean;
|
|
48
|
+
/** Optional log hook. */
|
|
49
|
+
log?: (level: 'info' | 'warn' | 'error', msg: string, meta?: Record<string, unknown>) => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build a Lifecycle. The returned object is independent — multiple Sentinels
|
|
53
|
+
* in the same process each get their own (though OS signals will fan out).
|
|
54
|
+
*/
|
|
55
|
+
export declare function createLifecycle(options?: CreateLifecycleOptions): Lifecycle;
|
|
56
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE9E,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElG,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,eAAe,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AACnD,MAAM,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AACpE,MAAM,MAAM,UAAU,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpD,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,IAAI,YAAY,CAAC;IAEvB,6EAA6E;IAC7E,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,IAAI,CAAC;IACrC,gEAAgE;IAChE,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC;IACnC,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,yBAAyB;IACzB,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC/F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,SAAS,CAmJ/E"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle harness — start, stop, reload, health.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates graceful shutdown across the Sentinel:
|
|
5
|
+
* - Registers SIGINT / SIGTERM / SIGHUP listeners on first `start()`.
|
|
6
|
+
* - SIGINT/SIGTERM trigger `stop(reason)` exactly once.
|
|
7
|
+
* - SIGHUP triggers `reload()`.
|
|
8
|
+
* - Hooks fire in registration order on start, reverse order on stop.
|
|
9
|
+
* - `health()` is a synchronous snapshot suitable for `/health` endpoints.
|
|
10
|
+
*
|
|
11
|
+
* Spec §5.3.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Build a Lifecycle. The returned object is independent — multiple Sentinels
|
|
15
|
+
* in the same process each get their own (though OS signals will fan out).
|
|
16
|
+
*/
|
|
17
|
+
export function createLifecycle(options = {}) {
|
|
18
|
+
const startHooks = [];
|
|
19
|
+
const stopHooks = [];
|
|
20
|
+
const reloadHooks = [];
|
|
21
|
+
const installSignalHandlers = options.installSignalHandlers ?? true;
|
|
22
|
+
const log = options.log ??
|
|
23
|
+
(() => {
|
|
24
|
+
/* default: silent */
|
|
25
|
+
});
|
|
26
|
+
let status = 'idle';
|
|
27
|
+
let startedAt;
|
|
28
|
+
let stoppedAt;
|
|
29
|
+
let lastError;
|
|
30
|
+
let stopPromise;
|
|
31
|
+
let signalHandlersInstalled = false;
|
|
32
|
+
const installHandlers = (lifecycle) => {
|
|
33
|
+
if (signalHandlersInstalled || !installSignalHandlers)
|
|
34
|
+
return;
|
|
35
|
+
signalHandlersInstalled = true;
|
|
36
|
+
const handle = (reason) => () => {
|
|
37
|
+
log('info', `lifecycle: received signal`, { reason });
|
|
38
|
+
void lifecycle.stop(reason).then(() => {
|
|
39
|
+
// Once everything has shut down, exit cleanly so the process doesn't hang.
|
|
40
|
+
process.exit(0);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
process.once('SIGINT', handle('sigint'));
|
|
44
|
+
process.once('SIGTERM', handle('sigterm'));
|
|
45
|
+
process.on('SIGHUP', () => {
|
|
46
|
+
log('info', 'lifecycle: received SIGHUP, reloading');
|
|
47
|
+
void lifecycle.reload().catch((err) => {
|
|
48
|
+
log('error', 'lifecycle: reload failed', { err: String(err) });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const lifecycle = {
|
|
53
|
+
async start() {
|
|
54
|
+
if (status === 'starting' || status === 'running')
|
|
55
|
+
return;
|
|
56
|
+
status = 'starting';
|
|
57
|
+
lastError = undefined;
|
|
58
|
+
startedAt = new Date();
|
|
59
|
+
stoppedAt = undefined;
|
|
60
|
+
try {
|
|
61
|
+
for (const hook of startHooks) {
|
|
62
|
+
await hook();
|
|
63
|
+
}
|
|
64
|
+
status = 'running';
|
|
65
|
+
installHandlers(lifecycle);
|
|
66
|
+
log('info', 'lifecycle: started');
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
status = 'failed';
|
|
70
|
+
lastError = String(err);
|
|
71
|
+
log('error', 'lifecycle: start failed', { err: lastError });
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
async stop(reason = 'manual') {
|
|
76
|
+
if (status === 'stopped' || status === 'idle')
|
|
77
|
+
return;
|
|
78
|
+
if (stopPromise)
|
|
79
|
+
return stopPromise;
|
|
80
|
+
status = 'stopping';
|
|
81
|
+
log('info', 'lifecycle: stopping', { reason });
|
|
82
|
+
stopPromise = (async () => {
|
|
83
|
+
// Reverse order — last-in, first-out.
|
|
84
|
+
for (let i = stopHooks.length - 1; i >= 0; i -= 1) {
|
|
85
|
+
const hook = stopHooks[i];
|
|
86
|
+
try {
|
|
87
|
+
await hook(reason);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
log('error', 'lifecycle: stop hook threw', { err: String(err) });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
status = 'stopped';
|
|
94
|
+
stoppedAt = new Date();
|
|
95
|
+
log('info', 'lifecycle: stopped');
|
|
96
|
+
})();
|
|
97
|
+
return stopPromise;
|
|
98
|
+
},
|
|
99
|
+
async reload() {
|
|
100
|
+
if (status !== 'running') {
|
|
101
|
+
log('warn', 'lifecycle: reload requested but not running', { status });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
log('info', 'lifecycle: reloading');
|
|
105
|
+
for (const hook of reloadHooks) {
|
|
106
|
+
try {
|
|
107
|
+
await hook();
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
log('error', 'lifecycle: reload hook threw', { err: String(err) });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
health() {
|
|
115
|
+
const report = {
|
|
116
|
+
status,
|
|
117
|
+
hooks: {
|
|
118
|
+
start: startHooks.length,
|
|
119
|
+
stop: stopHooks.length,
|
|
120
|
+
reload: reloadHooks.length,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
if (startedAt) {
|
|
124
|
+
report.startedAt = startedAt.toISOString();
|
|
125
|
+
report.uptimeMs = Date.now() - startedAt.getTime();
|
|
126
|
+
}
|
|
127
|
+
if (stoppedAt)
|
|
128
|
+
report.stoppedAt = stoppedAt.toISOString();
|
|
129
|
+
if (lastError)
|
|
130
|
+
report.lastError = lastError;
|
|
131
|
+
return report;
|
|
132
|
+
},
|
|
133
|
+
onStart(hook) {
|
|
134
|
+
startHooks.push(hook);
|
|
135
|
+
return () => {
|
|
136
|
+
const idx = startHooks.indexOf(hook);
|
|
137
|
+
if (idx >= 0)
|
|
138
|
+
startHooks.splice(idx, 1);
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
onStop(hook) {
|
|
142
|
+
stopHooks.push(hook);
|
|
143
|
+
return () => {
|
|
144
|
+
const idx = stopHooks.indexOf(hook);
|
|
145
|
+
if (idx >= 0)
|
|
146
|
+
stopHooks.splice(idx, 1);
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
onReload(hook) {
|
|
150
|
+
reloadHooks.push(hook);
|
|
151
|
+
return () => {
|
|
152
|
+
const idx = reloadHooks.indexOf(hook);
|
|
153
|
+
if (idx >= 0)
|
|
154
|
+
reloadHooks.splice(idx, 1);
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
return lifecycle;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"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"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pino logger factory with child-logger support.
|
|
3
|
+
*
|
|
4
|
+
* Sentinels emit structured logs. Pino is fast, JSON-by-default, and supports
|
|
5
|
+
* child loggers without context-passing ceremony.
|
|
6
|
+
*/
|
|
7
|
+
import { type Logger as PinoLogger } from 'pino';
|
|
8
|
+
export type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
|
|
9
|
+
export interface CreateLoggerOptions {
|
|
10
|
+
/** Component name used as the root binding (default: 'sentinel'). */
|
|
11
|
+
name?: string;
|
|
12
|
+
/** Log level. Reads $LOG_LEVEL or defaults to 'info'. */
|
|
13
|
+
level?: LogLevel;
|
|
14
|
+
/**
|
|
15
|
+
* If true, route through pino-pretty for human-readable dev output.
|
|
16
|
+
* Caller must install `pino-pretty` themselves.
|
|
17
|
+
*/
|
|
18
|
+
pretty?: boolean;
|
|
19
|
+
/** Extra bindings included on every log line. */
|
|
20
|
+
bindings?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/** A small re-export type so consumers don't need to depend on pino directly. */
|
|
23
|
+
export type Logger = PinoLogger;
|
|
24
|
+
/**
|
|
25
|
+
* Create a configured Pino logger. Convenient defaults; opts in to pretty
|
|
26
|
+
* printing only when explicitly requested.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createLogger(options?: CreateLoggerOptions): Logger;
|
|
29
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAa,EAAE,KAAK,MAAM,IAAI,UAAU,EAAsB,MAAM,MAAM,CAAC;AAE3E,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1F,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,iFAAiF;AACjF,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC;AAEhC;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAmBtE"}
|