hzl-core 1.6.0 → 1.7.1
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/dist/__tests__/backup/backup-restore.test.js +13 -12
- package/dist/__tests__/backup/backup-restore.test.js.map +1 -1
- package/dist/__tests__/backup/import-export.test.js +41 -39
- package/dist/__tests__/backup/import-export.test.js.map +1 -1
- package/dist/__tests__/concurrency/stress.test.js +2 -2
- package/dist/__tests__/concurrency/stress.test.js.map +1 -1
- package/dist/__tests__/concurrency/worker.js +3 -2
- package/dist/__tests__/concurrency/worker.js.map +1 -1
- package/dist/__tests__/migrations/upgrade.test.js +132 -117
- package/dist/__tests__/migrations/upgrade.test.js.map +1 -1
- package/dist/__tests__/projections/rebuild-equivalence.test.js +2 -2
- package/dist/__tests__/projections/rebuild-equivalence.test.js.map +1 -1
- package/dist/__tests__/properties/invariants.test.js +2 -2
- package/dist/__tests__/properties/invariants.test.js.map +1 -1
- package/dist/db/__tests__/append-only.test.d.ts +2 -0
- package/dist/db/__tests__/append-only.test.d.ts.map +1 -0
- package/dist/db/__tests__/append-only.test.js +46 -0
- package/dist/db/__tests__/append-only.test.js.map +1 -0
- package/dist/db/__tests__/datastore.test.d.ts +2 -0
- package/dist/db/__tests__/datastore.test.d.ts.map +1 -0
- package/dist/db/__tests__/datastore.test.js +68 -0
- package/dist/db/__tests__/datastore.test.js.map +1 -0
- package/dist/db/__tests__/libsql-compat.test.d.ts +2 -0
- package/dist/db/__tests__/libsql-compat.test.d.ts.map +1 -0
- package/dist/db/__tests__/libsql-compat.test.js +56 -0
- package/dist/db/__tests__/libsql-compat.test.js.map +1 -0
- package/dist/db/__tests__/lock.test.d.ts +2 -0
- package/dist/db/__tests__/lock.test.d.ts.map +1 -0
- package/dist/db/__tests__/lock.test.js +77 -0
- package/dist/db/__tests__/lock.test.js.map +1 -0
- package/dist/db/__tests__/meta.test.d.ts +2 -0
- package/dist/db/__tests__/meta.test.d.ts.map +1 -0
- package/dist/db/__tests__/meta.test.js +50 -0
- package/dist/db/__tests__/meta.test.js.map +1 -0
- package/dist/db/__tests__/migrations.test.d.ts +2 -0
- package/dist/db/__tests__/migrations.test.d.ts.map +1 -0
- package/dist/db/__tests__/migrations.test.js +57 -0
- package/dist/db/__tests__/migrations.test.js.map +1 -0
- package/dist/db/__tests__/sync-policy.test.d.ts +2 -0
- package/dist/db/__tests__/sync-policy.test.d.ts.map +1 -0
- package/dist/db/__tests__/sync-policy.test.js +118 -0
- package/dist/db/__tests__/sync-policy.test.js.map +1 -0
- package/dist/db/__tests__/types.test.d.ts +2 -0
- package/dist/db/__tests__/types.test.d.ts.map +1 -0
- package/dist/db/__tests__/types.test.js +91 -0
- package/dist/db/__tests__/types.test.js.map +1 -0
- package/dist/db/datastore.d.ts +16 -0
- package/dist/db/datastore.d.ts.map +1 -0
- package/dist/db/datastore.js +159 -0
- package/dist/db/datastore.js.map +1 -0
- package/dist/db/lock.d.ts +38 -0
- package/dist/db/lock.d.ts.map +1 -0
- package/dist/db/lock.js +114 -0
- package/dist/db/lock.js.map +1 -0
- package/dist/db/meta.d.ts +28 -0
- package/dist/db/meta.d.ts.map +1 -0
- package/dist/db/meta.js +95 -0
- package/dist/db/meta.js.map +1 -0
- package/dist/db/migrations.d.ts +23 -3
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +69 -99
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/migrations.test.js +40 -105
- package/dist/db/migrations.test.js.map +1 -1
- package/dist/db/schema.d.ts +2 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +52 -10
- package/dist/db/schema.js.map +1 -1
- package/dist/db/sync-policy.d.ts +21 -0
- package/dist/db/sync-policy.d.ts.map +1 -0
- package/dist/db/sync-policy.js +62 -0
- package/dist/db/sync-policy.js.map +1 -0
- package/dist/db/test-utils.d.ts +33 -0
- package/dist/db/test-utils.d.ts.map +1 -0
- package/dist/db/test-utils.js +58 -0
- package/dist/db/test-utils.js.map +1 -0
- package/dist/db/{connection.d.ts → transaction.d.ts} +2 -4
- package/dist/db/transaction.d.ts.map +1 -0
- package/dist/db/{connection.js → transaction.js} +1 -23
- package/dist/db/transaction.js.map +1 -0
- package/dist/db/types.d.ts +199 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +43 -0
- package/dist/db/types.js.map +1 -0
- package/dist/events/store.d.ts +1 -1
- package/dist/events/store.d.ts.map +1 -1
- package/dist/events/store.test.js +2 -4
- package/dist/events/store.test.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +13 -11
- package/dist/index.test.js.map +1 -1
- package/dist/projections/comments-checkpoints.d.ts +1 -1
- package/dist/projections/comments-checkpoints.d.ts.map +1 -1
- package/dist/projections/comments-checkpoints.test.js +3 -4
- package/dist/projections/comments-checkpoints.test.js.map +1 -1
- package/dist/projections/dependencies.d.ts +1 -1
- package/dist/projections/dependencies.d.ts.map +1 -1
- package/dist/projections/dependencies.test.js +3 -4
- package/dist/projections/dependencies.test.js.map +1 -1
- package/dist/projections/engine.d.ts +9 -2
- package/dist/projections/engine.d.ts.map +1 -1
- package/dist/projections/engine.js +22 -6
- package/dist/projections/engine.js.map +1 -1
- package/dist/projections/engine.test.js +3 -4
- package/dist/projections/engine.test.js.map +1 -1
- package/dist/projections/projects.d.ts +1 -1
- package/dist/projections/projects.d.ts.map +1 -1
- package/dist/projections/projects.test.js +2 -20
- package/dist/projections/projects.test.js.map +1 -1
- package/dist/projections/rebuild.d.ts +1 -1
- package/dist/projections/rebuild.d.ts.map +1 -1
- package/dist/projections/rebuild.test.js +3 -4
- package/dist/projections/rebuild.test.js.map +1 -1
- package/dist/projections/search.d.ts +1 -1
- package/dist/projections/search.d.ts.map +1 -1
- package/dist/projections/search.test.js +3 -4
- package/dist/projections/search.test.js.map +1 -1
- package/dist/projections/tags.d.ts +1 -1
- package/dist/projections/tags.d.ts.map +1 -1
- package/dist/projections/tags.test.js +3 -4
- package/dist/projections/tags.test.js.map +1 -1
- package/dist/projections/tasks-current.d.ts +1 -1
- package/dist/projections/tasks-current.d.ts.map +1 -1
- package/dist/projections/tasks-current.test.js +3 -4
- package/dist/projections/tasks-current.test.js.map +1 -1
- package/dist/projections/types.d.ts +1 -1
- package/dist/projections/types.d.ts.map +1 -1
- package/dist/services/backup-service.d.ts +2 -2
- package/dist/services/backup-service.d.ts.map +1 -1
- package/dist/services/backup-service.js +13 -3
- package/dist/services/backup-service.js.map +1 -1
- package/dist/services/project-service.d.ts +1 -1
- package/dist/services/project-service.d.ts.map +1 -1
- package/dist/services/project-service.js +1 -1
- package/dist/services/project-service.js.map +1 -1
- package/dist/services/project-service.test.js +2 -13
- package/dist/services/project-service.test.js.map +1 -1
- package/dist/services/search-service.d.ts +1 -1
- package/dist/services/search-service.d.ts.map +1 -1
- package/dist/services/search-service.test.js +3 -4
- package/dist/services/search-service.test.js.map +1 -1
- package/dist/services/task-service.d.ts +1 -1
- package/dist/services/task-service.d.ts.map +1 -1
- package/dist/services/task-service.js +1 -1
- package/dist/services/task-service.js.map +1 -1
- package/dist/services/task-service.test.js +3 -4
- package/dist/services/task-service.test.js.map +1 -1
- package/dist/services/validation-service.d.ts +1 -1
- package/dist/services/validation-service.d.ts.map +1 -1
- package/dist/services/validation-service.test.js +2 -4
- package/dist/services/validation-service.test.js.map +1 -1
- package/package.json +4 -4
- package/dist/db/connection.d.ts.map +0 -1
- package/dist/db/connection.js.map +0 -1
- package/dist/db/connection.test.d.ts +0 -2
- package/dist/db/connection.test.d.ts.map +0 -1
- package/dist/db/connection.test.js +0 -63
- package/dist/db/connection.test.js.map +0 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Database from 'libsql';
|
|
2
|
+
import type { DbConfig, SyncStats } from './types.js';
|
|
3
|
+
export type ConnectionMode = 'local-only' | 'remote-replica' | 'offline-sync' | 'remote-only';
|
|
4
|
+
export interface Datastore {
|
|
5
|
+
eventsDb: Database.Database;
|
|
6
|
+
cacheDb: Database.Database;
|
|
7
|
+
mode: ConnectionMode;
|
|
8
|
+
syncUrl?: string;
|
|
9
|
+
instanceId: string;
|
|
10
|
+
deviceId: string;
|
|
11
|
+
syncAttempts: number[];
|
|
12
|
+
sync(): Promise<SyncStats>;
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createDatastore(config: DbConfig): Datastore;
|
|
16
|
+
//# sourceMappingURL=datastore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"datastore.d.ts","sourceRoot":"","sources":["../../src/db/datastore.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,QAAQ,CAAC;AAG9B,OAAO,KAAK,EAAE,QAAQ,EAAc,SAAS,EAAE,MAAM,YAAY,CAAC;AAKlE,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,gBAAgB,GAAG,cAAc,GAAG,aAAa,CAAC;AAE9F,MAAM,WAAW,SAAS;IACtB,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAC5B,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAC3B,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC;IAC3B,KAAK,IAAI,IAAI,CAAC;CACjB;AAiBD,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAuK3D"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import Database from 'libsql';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { EVENTS_SCHEMA_V2, CACHE_SCHEMA_V1, PRAGMAS } from './schema.js';
|
|
5
|
+
import { generateId } from '../utils/id.js';
|
|
6
|
+
import { setInstanceId, getInstanceId, setDeviceId, getDeviceId, setLastSyncAttemptAt, setLastSyncAt, setLastSyncError, clearLastSyncError, clearDirtySince } from './meta.js';
|
|
7
|
+
function ensureDirectory(filePath) {
|
|
8
|
+
const dir = path.dirname(filePath);
|
|
9
|
+
if (!fs.existsSync(dir)) {
|
|
10
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function determineMode(config) {
|
|
14
|
+
const syncUrl = config.events?.syncUrl;
|
|
15
|
+
if (!syncUrl)
|
|
16
|
+
return 'local-only';
|
|
17
|
+
const syncMode = config.events?.syncMode ?? 'offline';
|
|
18
|
+
return syncMode === 'replica' ? 'remote-replica' : 'offline-sync';
|
|
19
|
+
}
|
|
20
|
+
export function createDatastore(config) {
|
|
21
|
+
const eventsPath = config.events?.path ?? ':memory:';
|
|
22
|
+
const cachePath = config.cache?.path ?? ':memory:';
|
|
23
|
+
const mode = determineMode(config);
|
|
24
|
+
// Ensure directories exist
|
|
25
|
+
if (eventsPath !== ':memory:')
|
|
26
|
+
ensureDirectory(eventsPath);
|
|
27
|
+
if (cachePath !== ':memory:')
|
|
28
|
+
ensureDirectory(cachePath);
|
|
29
|
+
const eventsOpts = {
|
|
30
|
+
syncUrl: config.events?.syncUrl,
|
|
31
|
+
authToken: config.events?.authToken,
|
|
32
|
+
encryptionKey: config.events?.encryptionKey,
|
|
33
|
+
timeout: config.timeoutSec,
|
|
34
|
+
// Use configured syncPeriod, default to 0 (disabled) for CLI use cases
|
|
35
|
+
syncPeriod: config.syncPeriod ?? 0,
|
|
36
|
+
};
|
|
37
|
+
// Create connections
|
|
38
|
+
const eventsDb = new Database(eventsPath, eventsOpts);
|
|
39
|
+
const cacheDb = new Database(cachePath, { timeout: config.timeoutSec });
|
|
40
|
+
// Set pragmas
|
|
41
|
+
cacheDb.exec(PRAGMAS);
|
|
42
|
+
try {
|
|
43
|
+
eventsDb.exec(PRAGMAS);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// In sync mode (embedded replica), some pragmas like journal_mode might be restricted
|
|
47
|
+
// or handled by the engine. We ignore 'Sqlite3UnsupportedStatement' in this context.
|
|
48
|
+
// The library throws generic Error with message or name equivalent to the code.
|
|
49
|
+
const errorObj = err;
|
|
50
|
+
const isUnsupported = errorObj.code === 'Sqlite3UnsupportedStatement' ||
|
|
51
|
+
errorObj.message === 'Sqlite3UnsupportedStatement' ||
|
|
52
|
+
String(err).includes('Sqlite3UnsupportedStatement');
|
|
53
|
+
if (!isUnsupported) {
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Initialize schemas
|
|
58
|
+
eventsDb.exec(EVENTS_SCHEMA_V2);
|
|
59
|
+
cacheDb.exec(CACHE_SCHEMA_V1);
|
|
60
|
+
// Ensure instance ID exists (generate if new database)
|
|
61
|
+
let instanceId = getInstanceId(eventsDb);
|
|
62
|
+
if (!instanceId) {
|
|
63
|
+
instanceId = generateId();
|
|
64
|
+
setInstanceId(eventsDb, instanceId);
|
|
65
|
+
}
|
|
66
|
+
// Ensure device ID exists (generate if new device)
|
|
67
|
+
let deviceId = getDeviceId(cacheDb);
|
|
68
|
+
if (!deviceId) {
|
|
69
|
+
deviceId = generateId();
|
|
70
|
+
setDeviceId(cacheDb, deviceId);
|
|
71
|
+
}
|
|
72
|
+
const datastore = {
|
|
73
|
+
eventsDb,
|
|
74
|
+
cacheDb,
|
|
75
|
+
mode,
|
|
76
|
+
syncUrl: config.events?.syncUrl,
|
|
77
|
+
instanceId,
|
|
78
|
+
deviceId,
|
|
79
|
+
// Rate limiting state
|
|
80
|
+
syncAttempts: [],
|
|
81
|
+
async sync() {
|
|
82
|
+
if (mode === 'local-only') {
|
|
83
|
+
return { attempted: false, success: true };
|
|
84
|
+
}
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const maxAttempts = config.sync?.maxSyncAttemptsPerMinute ?? 10;
|
|
87
|
+
const syncTimeoutMs = config.sync?.syncTimeoutMs ?? 30000;
|
|
88
|
+
// Rate limiting: track attempts in the last minute
|
|
89
|
+
this.syncAttempts = this.syncAttempts.filter(t => now - t < 60000);
|
|
90
|
+
if (this.syncAttempts.length >= maxAttempts) {
|
|
91
|
+
return {
|
|
92
|
+
attempted: false,
|
|
93
|
+
success: false,
|
|
94
|
+
error: `Rate limited: ${maxAttempts} sync attempts per minute exceeded`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
this.syncAttempts.push(now);
|
|
98
|
+
// Update sync attempt timestamp in local meta
|
|
99
|
+
setLastSyncAttemptAt(cacheDb, now);
|
|
100
|
+
try {
|
|
101
|
+
// Wrap sync() with timeout using AbortController pattern
|
|
102
|
+
const syncPromise = new Promise((resolve, reject) => {
|
|
103
|
+
const timeout = setTimeout(() => {
|
|
104
|
+
reject(new Error(`Sync timed out after ${syncTimeoutMs}ms`));
|
|
105
|
+
}, syncTimeoutMs);
|
|
106
|
+
try {
|
|
107
|
+
// libsql sync() is synchronous in Node.js binding? No, it returns void or result?
|
|
108
|
+
// Wait, better-sqlite3 style sync might be synchronous, but libsql over HTTP is likely async?
|
|
109
|
+
// The libsql type defs say sync() returns void... wait.
|
|
110
|
+
// Let's assume sync() is synchronous for now as per better-sqlite3-libsql, OR verify.
|
|
111
|
+
// The 'libsql' package (which is @libsql/client usually, but here we are using 'libsql' npm package which is the better-sqlite3 fork)
|
|
112
|
+
// The 'libsql' npm package's sync() IS synchronous usually if it's embedded replica.
|
|
113
|
+
// But if it blocks, we might need to be careful.
|
|
114
|
+
// Actually, looking at docs, db.sync() returns nothing in the better-sqlite3 compatible binding?
|
|
115
|
+
// Let's assume it works synchronously.
|
|
116
|
+
eventsDb.sync();
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
// It doesn't return stats in the binding usually.
|
|
119
|
+
resolve({ frames_synced: 0, frame_no: 0 }); // Mock result since binding might not return it
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// If it's truly synchronous, the promise wrapper above is silly but harmless.
|
|
127
|
+
// If it's async (which Turso sync implies), then we await it.
|
|
128
|
+
// But 'libsql' (better-sqlite3 fork) sync() blocks.
|
|
129
|
+
await syncPromise;
|
|
130
|
+
// On success: update sync metadata
|
|
131
|
+
const syncTime = Date.now();
|
|
132
|
+
setLastSyncAt(cacheDb, syncTime);
|
|
133
|
+
clearLastSyncError(cacheDb);
|
|
134
|
+
clearDirtySince(cacheDb);
|
|
135
|
+
return {
|
|
136
|
+
attempted: true,
|
|
137
|
+
success: true,
|
|
138
|
+
framesSynced: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// On failure: record error
|
|
143
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
144
|
+
setLastSyncError(cacheDb, errorMessage);
|
|
145
|
+
return {
|
|
146
|
+
attempted: true,
|
|
147
|
+
success: false,
|
|
148
|
+
error: errorMessage,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
close() {
|
|
153
|
+
eventsDb.close();
|
|
154
|
+
cacheDb.close();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
return datastore;
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=datastore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"datastore.js","sourceRoot":"","sources":["../../src/db/datastore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface LockMetadata {
|
|
2
|
+
pid: number;
|
|
3
|
+
hostname: string;
|
|
4
|
+
startedAt: number;
|
|
5
|
+
command?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface LockGuard {
|
|
9
|
+
release(): void;
|
|
10
|
+
staleLockCleared: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface LockOptions {
|
|
13
|
+
command?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class DatabaseLock {
|
|
17
|
+
private lockPath;
|
|
18
|
+
private options;
|
|
19
|
+
constructor(lockPath: string, options?: LockOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Read current lock metadata if lock file exists
|
|
22
|
+
*/
|
|
23
|
+
readMetadata(): LockMetadata | null;
|
|
24
|
+
/**
|
|
25
|
+
* Check if lock is stale (held by dead process)
|
|
26
|
+
*/
|
|
27
|
+
isStale(): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Clear the lock file
|
|
30
|
+
*/
|
|
31
|
+
clear(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Acquire the lock with timeout.
|
|
34
|
+
* Uses exponential backoff starting at 5ms for fast CLI responsiveness.
|
|
35
|
+
*/
|
|
36
|
+
acquire(timeoutMs: number): Promise<LockGuard>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/db/lock.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACtB,OAAO,IAAI,IAAI,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD,qBAAa,YAAY;IACrB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAc;gBAEjB,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB;IAKvD;;OAEG;IACH,YAAY,IAAI,YAAY,GAAG,IAAI;IAYnC;;OAEG;IACH,OAAO,IAAI,OAAO;IAYlB;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;;OAGG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;CAmDvD"}
|
package/dist/db/lock.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
function isPidRunning(pid) {
|
|
4
|
+
try {
|
|
5
|
+
// Sending signal 0 checks if process exists without killing it
|
|
6
|
+
process.kill(pid, 0);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
const error = err;
|
|
11
|
+
// EPERM means process exists but we lack permission to signal it
|
|
12
|
+
// ESRCH means process does not exist
|
|
13
|
+
if (error.code === 'EPERM') {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class DatabaseLock {
|
|
20
|
+
lockPath;
|
|
21
|
+
options;
|
|
22
|
+
constructor(lockPath, options = {}) {
|
|
23
|
+
this.lockPath = lockPath;
|
|
24
|
+
this.options = options;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read current lock metadata if lock file exists
|
|
28
|
+
*/
|
|
29
|
+
readMetadata() {
|
|
30
|
+
if (!fs.existsSync(this.lockPath)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(this.lockPath, 'utf-8');
|
|
35
|
+
return JSON.parse(content);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if lock is stale (held by dead process)
|
|
43
|
+
*/
|
|
44
|
+
isStale() {
|
|
45
|
+
const metadata = this.readMetadata();
|
|
46
|
+
if (!metadata)
|
|
47
|
+
return false;
|
|
48
|
+
// Only consider stale if on same hostname (can't check PIDs across machines)
|
|
49
|
+
if (metadata.hostname !== os.hostname()) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return !isPidRunning(metadata.pid);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Clear the lock file
|
|
56
|
+
*/
|
|
57
|
+
clear() {
|
|
58
|
+
if (fs.existsSync(this.lockPath)) {
|
|
59
|
+
fs.unlinkSync(this.lockPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Acquire the lock with timeout.
|
|
64
|
+
* Uses exponential backoff starting at 5ms for fast CLI responsiveness.
|
|
65
|
+
*/
|
|
66
|
+
async acquire(timeoutMs) {
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
let staleLockCleared = false;
|
|
69
|
+
let attempt = 0;
|
|
70
|
+
const BASE_DELAY_MS = 5;
|
|
71
|
+
const MAX_DELAY_MS = 100;
|
|
72
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
73
|
+
// Check for stale lock
|
|
74
|
+
if (this.isStale()) {
|
|
75
|
+
this.clear();
|
|
76
|
+
staleLockCleared = true;
|
|
77
|
+
}
|
|
78
|
+
// Try to create lock file exclusively
|
|
79
|
+
try {
|
|
80
|
+
const metadata = {
|
|
81
|
+
pid: process.pid,
|
|
82
|
+
hostname: os.hostname(),
|
|
83
|
+
startedAt: Date.now(),
|
|
84
|
+
command: this.options.command,
|
|
85
|
+
version: this.options.version,
|
|
86
|
+
};
|
|
87
|
+
// O_EXCL ensures atomic creation - fails if file exists
|
|
88
|
+
const fd = fs.openSync(this.lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
89
|
+
fs.writeSync(fd, JSON.stringify(metadata, null, 2));
|
|
90
|
+
fs.closeSync(fd);
|
|
91
|
+
return {
|
|
92
|
+
release: () => this.clear(),
|
|
93
|
+
staleLockCleared,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const error = err;
|
|
98
|
+
if (error.code !== 'EEXIST') {
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
// Lock exists, wait with exponential backoff (5ms, 10ms, 20ms, 40ms, 80ms, 100ms max)
|
|
102
|
+
const delay = Math.min(BASE_DELAY_MS * Math.pow(2, attempt), MAX_DELAY_MS);
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
104
|
+
attempt++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const metadata = this.readMetadata();
|
|
108
|
+
const holder = metadata
|
|
109
|
+
? `PID ${metadata.pid} (${metadata.command ?? 'unknown'}) since ${new Date(metadata.startedAt).toISOString()}`
|
|
110
|
+
: 'unknown process';
|
|
111
|
+
throw new Error(`Lock is held by ${holder}. Timeout after ${timeoutMs}ms.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/db/lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type Database from 'libsql';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for hzl_global_meta table (stored in events.db, synced)
|
|
4
|
+
* Contains immutable dataset identity.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createGlobalMetaSchema(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Schema for hzl_local_meta table (stored in cache.db, local-only)
|
|
9
|
+
* Contains per-device sync bookkeeping.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createLocalMetaSchema(): string;
|
|
12
|
+
export declare function getInstanceId(db: Database.Database): string | null;
|
|
13
|
+
export declare function setInstanceId(db: Database.Database, instanceId: string): void;
|
|
14
|
+
export declare function getDeviceId(db: Database.Database): string | null;
|
|
15
|
+
export declare function setDeviceId(db: Database.Database, deviceId: string): void;
|
|
16
|
+
export declare function getDirtySince(db: Database.Database): number | null;
|
|
17
|
+
export declare function setDirtySince(db: Database.Database, timestamp: number): void;
|
|
18
|
+
export declare function clearDirtySince(db: Database.Database): void;
|
|
19
|
+
export declare function getLastSyncAt(db: Database.Database): number | null;
|
|
20
|
+
export declare function setLastSyncAt(db: Database.Database, timestamp: number): void;
|
|
21
|
+
export declare function getLastSyncError(db: Database.Database): string | null;
|
|
22
|
+
export declare function setLastSyncError(db: Database.Database, error: string): void;
|
|
23
|
+
export declare function clearLastSyncError(db: Database.Database): void;
|
|
24
|
+
export declare function getLastSyncFrameNo(db: Database.Database): number | null;
|
|
25
|
+
export declare function setLastSyncFrameNo(db: Database.Database, frameNo: number): void;
|
|
26
|
+
export declare function getLastSyncAttemptAt(db: Database.Database): number | null;
|
|
27
|
+
export declare function setLastSyncAttemptAt(db: Database.Database, timestamp: number): void;
|
|
28
|
+
//# sourceMappingURL=meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["../../src/db/meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAO/C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAO9C;AAcD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAO7E;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAEzE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAE5E;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAE3D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAE5E;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGrE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE3E;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAE9D;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGvE;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAE/E;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGzE;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAEnF"}
|
package/dist/db/meta.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema for hzl_global_meta table (stored in events.db, synced)
|
|
3
|
+
* Contains immutable dataset identity.
|
|
4
|
+
*/
|
|
5
|
+
export function createGlobalMetaSchema() {
|
|
6
|
+
return `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS hzl_global_meta (
|
|
8
|
+
key TEXT PRIMARY KEY,
|
|
9
|
+
value TEXT NOT NULL
|
|
10
|
+
);
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Schema for hzl_local_meta table (stored in cache.db, local-only)
|
|
15
|
+
* Contains per-device sync bookkeeping.
|
|
16
|
+
*/
|
|
17
|
+
export function createLocalMetaSchema() {
|
|
18
|
+
return `
|
|
19
|
+
CREATE TABLE IF NOT EXISTS hzl_local_meta (
|
|
20
|
+
key TEXT PRIMARY KEY,
|
|
21
|
+
value TEXT NOT NULL
|
|
22
|
+
);
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
// Global meta keys
|
|
26
|
+
const INSTANCE_ID_KEY = 'hzl_instance_id';
|
|
27
|
+
const CREATED_AT_KEY = 'created_at_ms';
|
|
28
|
+
// Local meta keys
|
|
29
|
+
const DEVICE_ID_KEY = 'device_id';
|
|
30
|
+
const DIRTY_SINCE_KEY = 'dirty_since_ms';
|
|
31
|
+
const LAST_SYNC_AT_KEY = 'last_sync_at_ms';
|
|
32
|
+
const LAST_SYNC_ATTEMPT_KEY = 'last_sync_attempt_at_ms';
|
|
33
|
+
const LAST_SYNC_ERROR_KEY = 'last_sync_error';
|
|
34
|
+
const LAST_SYNC_FRAME_KEY = 'last_sync_frame_no';
|
|
35
|
+
export function getInstanceId(db) {
|
|
36
|
+
const row = db.prepare('SELECT value FROM hzl_global_meta WHERE key = ?').get(INSTANCE_ID_KEY);
|
|
37
|
+
return row?.value ?? null;
|
|
38
|
+
}
|
|
39
|
+
export function setInstanceId(db, instanceId) {
|
|
40
|
+
const existing = getInstanceId(db);
|
|
41
|
+
if (existing !== null) {
|
|
42
|
+
throw new Error(`Instance ID already set to ${existing}. Cannot overwrite.`);
|
|
43
|
+
}
|
|
44
|
+
db.prepare('INSERT INTO hzl_global_meta (key, value) VALUES (?, ?)').run(INSTANCE_ID_KEY, instanceId);
|
|
45
|
+
db.prepare('INSERT INTO hzl_global_meta (key, value) VALUES (?, ?)').run(CREATED_AT_KEY, Date.now().toString());
|
|
46
|
+
}
|
|
47
|
+
export function getDeviceId(db) {
|
|
48
|
+
const row = db.prepare('SELECT value FROM hzl_local_meta WHERE key = ?').get(DEVICE_ID_KEY);
|
|
49
|
+
return row?.value ?? null;
|
|
50
|
+
}
|
|
51
|
+
export function setDeviceId(db, deviceId) {
|
|
52
|
+
db.prepare('INSERT OR REPLACE INTO hzl_local_meta (key, value) VALUES (?, ?)').run(DEVICE_ID_KEY, deviceId);
|
|
53
|
+
}
|
|
54
|
+
export function getDirtySince(db) {
|
|
55
|
+
const row = db.prepare('SELECT value FROM hzl_local_meta WHERE key = ?').get(DIRTY_SINCE_KEY);
|
|
56
|
+
return row ? parseInt(row.value, 10) : null;
|
|
57
|
+
}
|
|
58
|
+
export function setDirtySince(db, timestamp) {
|
|
59
|
+
db.prepare('INSERT OR REPLACE INTO hzl_local_meta (key, value) VALUES (?, ?)').run(DIRTY_SINCE_KEY, timestamp.toString());
|
|
60
|
+
}
|
|
61
|
+
export function clearDirtySince(db) {
|
|
62
|
+
db.prepare('DELETE FROM hzl_local_meta WHERE key = ?').run(DIRTY_SINCE_KEY);
|
|
63
|
+
}
|
|
64
|
+
export function getLastSyncAt(db) {
|
|
65
|
+
const row = db.prepare('SELECT value FROM hzl_local_meta WHERE key = ?').get(LAST_SYNC_AT_KEY);
|
|
66
|
+
return row ? parseInt(row.value, 10) : null;
|
|
67
|
+
}
|
|
68
|
+
export function setLastSyncAt(db, timestamp) {
|
|
69
|
+
db.prepare('INSERT OR REPLACE INTO hzl_local_meta (key, value) VALUES (?, ?)').run(LAST_SYNC_AT_KEY, timestamp.toString());
|
|
70
|
+
}
|
|
71
|
+
export function getLastSyncError(db) {
|
|
72
|
+
const row = db.prepare('SELECT value FROM hzl_local_meta WHERE key = ?').get(LAST_SYNC_ERROR_KEY);
|
|
73
|
+
return row?.value ?? null;
|
|
74
|
+
}
|
|
75
|
+
export function setLastSyncError(db, error) {
|
|
76
|
+
db.prepare('INSERT OR REPLACE INTO hzl_local_meta (key, value) VALUES (?, ?)').run(LAST_SYNC_ERROR_KEY, error);
|
|
77
|
+
}
|
|
78
|
+
export function clearLastSyncError(db) {
|
|
79
|
+
db.prepare('DELETE FROM hzl_local_meta WHERE key = ?').run(LAST_SYNC_ERROR_KEY);
|
|
80
|
+
}
|
|
81
|
+
export function getLastSyncFrameNo(db) {
|
|
82
|
+
const row = db.prepare('SELECT value FROM hzl_local_meta WHERE key = ?').get(LAST_SYNC_FRAME_KEY);
|
|
83
|
+
return row ? parseInt(row.value, 10) : null;
|
|
84
|
+
}
|
|
85
|
+
export function setLastSyncFrameNo(db, frameNo) {
|
|
86
|
+
db.prepare('INSERT OR REPLACE INTO hzl_local_meta (key, value) VALUES (?, ?)').run(LAST_SYNC_FRAME_KEY, frameNo.toString());
|
|
87
|
+
}
|
|
88
|
+
export function getLastSyncAttemptAt(db) {
|
|
89
|
+
const row = db.prepare('SELECT value FROM hzl_local_meta WHERE key = ?').get(LAST_SYNC_ATTEMPT_KEY);
|
|
90
|
+
return row ? parseInt(row.value, 10) : null;
|
|
91
|
+
}
|
|
92
|
+
export function setLastSyncAttemptAt(db, timestamp) {
|
|
93
|
+
db.prepare('INSERT OR REPLACE INTO hzl_local_meta (key, value) VALUES (?, ?)').run(LAST_SYNC_ATTEMPT_KEY, timestamp.toString());
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/db/meta.ts"],"names":[],"mappings":"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"}
|
package/dist/db/migrations.d.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
import type Database from '
|
|
2
|
-
export
|
|
3
|
-
|
|
1
|
+
import type Database from 'libsql';
|
|
2
|
+
export interface Migration {
|
|
3
|
+
id: string;
|
|
4
|
+
up: string;
|
|
5
|
+
down?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface MigrationResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
applied: string[];
|
|
10
|
+
skipped: string[];
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class MigrationError extends Error {
|
|
14
|
+
readonly failedMigration: string;
|
|
15
|
+
readonly rolledBack: string[];
|
|
16
|
+
constructor(message: string, failedMigration: string, rolledBack: string[]);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run migrations with atomic rollback on failure.
|
|
20
|
+
* All pending migrations are run in a single transaction.
|
|
21
|
+
* If any migration fails, ALL changes are rolled back.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runMigrationsWithRollback(db: Database.Database, migrations: Migration[]): MigrationResult;
|
|
4
24
|
//# sourceMappingURL=migrations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/db/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/db/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAGnC,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,cAAe,SAAQ,KAAK;aAGrB,eAAe,EAAE,MAAM;aACvB,UAAU,EAAE,MAAM,EAAE;gBAFpC,OAAO,EAAE,MAAM,EACC,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,EAAE;CAKvC;AAqBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,UAAU,EAAE,SAAS,EAAE,GACtB,eAAe,CA4DjB"}
|