@hardkas/core 0.2.2-alpha.1 → 0.4.0-alpha
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/index.d.ts +121 -1
- package/dist/index.js +320 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -286,6 +286,126 @@ declare function maskSecrets(data: any): any;
|
|
|
286
286
|
*/
|
|
287
287
|
declare function redactSecret(value: string): string;
|
|
288
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Options for atomic file writing.
|
|
291
|
+
*/
|
|
292
|
+
interface WriteFileAtomicOptions {
|
|
293
|
+
/** Encoding for string data (default: utf-8) */
|
|
294
|
+
encoding?: BufferEncoding;
|
|
295
|
+
/** File mode (permissions) */
|
|
296
|
+
mode?: number;
|
|
297
|
+
/** If true, calls fsync on the parent directory (Linux/macOS) */
|
|
298
|
+
fsyncParent?: boolean;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Writes a file atomically using the temp-file-and-rename pattern.
|
|
302
|
+
* Ensures that either the entire file is written or no changes are made.
|
|
303
|
+
*
|
|
304
|
+
* Pattern:
|
|
305
|
+
* 1. Write data to a temporary file in the same directory.
|
|
306
|
+
* 2. fsync the temporary file to ensure data is on disk.
|
|
307
|
+
* 3. Close the temporary file.
|
|
308
|
+
* 4. Rename the temporary file to the target path (atomic operation).
|
|
309
|
+
* 5. Optional: fsync the parent directory to ensure metadata is on disk.
|
|
310
|
+
*/
|
|
311
|
+
declare function writeFileAtomic(targetPath: string, data: string | Buffer, options?: WriteFileAtomicOptions): Promise<void>;
|
|
312
|
+
/**
|
|
313
|
+
* Synchronous version of writeFileAtomic.
|
|
314
|
+
*/
|
|
315
|
+
declare function writeFileAtomicSync(targetPath: string, data: string | Buffer, options?: WriteFileAtomicOptions): void;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* HardKAS Corruption Taxonomy & Issue Reporting
|
|
319
|
+
*/
|
|
320
|
+
type CorruptionCode = "ARTIFACT_JSON_INVALID" | "ARTIFACT_SCHEMA_MISSING" | "ARTIFACT_SCHEMA_INVALID" | "ARTIFACT_HASH_MISMATCH" | "ARTIFACT_ID_INVALID" | "ARTIFACT_LINEAGE_INVALID" | "EVENT_JSON_INVALID" | "EVENT_SCHEMA_INVALID" | "EVENT_LINE_CORRUPT" | "STORE_STALE" | "STORE_CORRUPT" | "STORE_REBUILD_REQUIRED" | "DUPLICATE_ARTIFACT" | "DUPLICATE_EVENT" | "SEMANTIC_DIVERGENCE" | "REPLAY_PARTIAL" | "REPLAY_UNSUPPORTED_CHECK" | "LOCK_HELD" | "LOCK_TIMEOUT" | "STALE_LOCK" | "LOCK_RELEASE_FAILED" | "LOCK_OWNER_MISMATCH" | "LOCK_METADATA_INVALID" | "STORE_MIGRATION_REQUIRED" | "STORE_MIGRATION_FAILED" | "STORE_MIGRATION_CHECKSUM_MISMATCH" | "STORE_SCHEMA_UNSUPPORTED" | "STORE_LEGACY_BOOTSTRAPPED";
|
|
321
|
+
type CorruptionSeverity = "warning" | "error";
|
|
322
|
+
interface CorruptionIssue {
|
|
323
|
+
readonly code: CorruptionCode;
|
|
324
|
+
readonly severity: CorruptionSeverity;
|
|
325
|
+
readonly message: string;
|
|
326
|
+
readonly path?: string;
|
|
327
|
+
readonly lineNumber?: number;
|
|
328
|
+
readonly artifactId?: string;
|
|
329
|
+
readonly contentHash?: string;
|
|
330
|
+
readonly suggestion?: string;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Formats a corruption issue for human-readable output.
|
|
334
|
+
*/
|
|
335
|
+
declare function formatCorruptionIssue(issue: CorruptionIssue): string;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* HardKAS Lock Metadata schema v1
|
|
339
|
+
*/
|
|
340
|
+
interface LockMetadata {
|
|
341
|
+
schema: "hardkas.lock.v1";
|
|
342
|
+
name: string;
|
|
343
|
+
pid: number;
|
|
344
|
+
command: string;
|
|
345
|
+
cwd: string;
|
|
346
|
+
hostname: string;
|
|
347
|
+
createdAt: string;
|
|
348
|
+
expiresAt: string | null;
|
|
349
|
+
}
|
|
350
|
+
interface LockHandle {
|
|
351
|
+
readonly path: string;
|
|
352
|
+
readonly metadata: LockMetadata;
|
|
353
|
+
release(): Promise<void>;
|
|
354
|
+
}
|
|
355
|
+
interface AcquireLockArgs {
|
|
356
|
+
rootDir: string;
|
|
357
|
+
name: string;
|
|
358
|
+
command?: string;
|
|
359
|
+
staleMs?: number;
|
|
360
|
+
wait?: boolean;
|
|
361
|
+
timeoutMs?: number;
|
|
362
|
+
pollMs?: number;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Deterministic lock ordering to avoid deadlocks.
|
|
366
|
+
* workspace > node > accounts > artifacts > events > query-store
|
|
367
|
+
*/
|
|
368
|
+
declare const LOCK_ORDER: string[];
|
|
369
|
+
/**
|
|
370
|
+
* Acquires a named lock for the workspace.
|
|
371
|
+
*/
|
|
372
|
+
declare function acquireLock(args: AcquireLockArgs): Promise<LockHandle>;
|
|
373
|
+
/**
|
|
374
|
+
* Helper to run a task with a single lock.
|
|
375
|
+
*/
|
|
376
|
+
declare function withLock<T>(args: AcquireLockArgs, fn: (handle: LockHandle) => Promise<T>): Promise<T>;
|
|
377
|
+
/**
|
|
378
|
+
* Helper to run a task with multiple locks in deterministic order.
|
|
379
|
+
*/
|
|
380
|
+
declare function withLocks<T>(rootDir: string, names: string[], fn: () => Promise<T>, options?: {
|
|
381
|
+
command?: string;
|
|
382
|
+
wait?: boolean;
|
|
383
|
+
timeoutMs?: number;
|
|
384
|
+
}): Promise<T>;
|
|
385
|
+
/**
|
|
386
|
+
* Checks if a process is alive.
|
|
387
|
+
*/
|
|
388
|
+
declare function isProcessAlive(pid: number): boolean;
|
|
389
|
+
/**
|
|
390
|
+
* Lists all active locks in the workspace.
|
|
391
|
+
*/
|
|
392
|
+
declare function listLocks(rootDir: string): Array<{
|
|
393
|
+
name: string;
|
|
394
|
+
metadata: LockMetadata;
|
|
395
|
+
path: string;
|
|
396
|
+
isAlive: boolean;
|
|
397
|
+
}>;
|
|
398
|
+
/**
|
|
399
|
+
* Safely clears a lock if criteria are met.
|
|
400
|
+
*/
|
|
401
|
+
declare function clearLock(rootDir: string, name: string, options?: {
|
|
402
|
+
force?: boolean;
|
|
403
|
+
ifDead?: boolean;
|
|
404
|
+
}): {
|
|
405
|
+
cleared: boolean;
|
|
406
|
+
reason?: string;
|
|
407
|
+
};
|
|
408
|
+
|
|
289
409
|
declare const SOMPI_PER_KAS = 100000000n;
|
|
290
410
|
declare const kaspaNetworkIdSchema: z.ZodEnum<["mainnet", "testnet-10", "testnet-11", "testnet-12", "simnet", "simnet-1", "devnet"]>;
|
|
291
411
|
type NetworkId = Brand<z.infer<typeof kaspaNetworkIdSchema>, "NetworkId">;
|
|
@@ -376,4 +496,4 @@ declare function parseHardkasConfig(input: unknown): HardkasConfig;
|
|
|
376
496
|
declare function parseKasToSompi(input: string): bigint;
|
|
377
497
|
declare function formatSompi(amountSompi: bigint): string;
|
|
378
498
|
|
|
379
|
-
export { type ArtifactId, type ArtifactType, ArtifactTypeSchema, type Brand, type Branded, type ContentHash, type CoreEvent, type CoreEventListener, type CorrelationId, type DaaScore, type EventDomain, type EventEnvelope, type EventId, type EventKind, type EventPayloadByKind, type EventSequence, type ExecutionMode, ExecutionModeSchema, type HardkasConfig, HardkasError, type InvariantDomain, type InvariantSeverity, InvariantViolationError, type KaspaAddress, type LineageId, type NetworkId, NetworkIdSchema, type RpcEndpointId, SOMPI_PER_KAS, type StampedEvent, type TxId, type UnknownEventPayload, type WorkflowId, artifactTypeSchema, asArtifactId, asContentHash, asCorrelationId, asDaaScore, asEventId, asEventSequence, asKaspaAddress, asLineageId, asNetworkId, asRpcEndpointId, asTxId, asWorkflowId, coreEvents, createEventEnvelope, executionModeSchema, formatSompi, hardkasConfigSchema, kaspaNetworkIdSchema, maskSecrets, parseHardkasConfig, parseKasToSompi, redactSecret, validateEventEnvelope };
|
|
499
|
+
export { type AcquireLockArgs, type ArtifactId, type ArtifactType, ArtifactTypeSchema, type Brand, type Branded, type ContentHash, type CoreEvent, type CoreEventListener, type CorrelationId, type CorruptionCode, type CorruptionIssue, type CorruptionSeverity, type DaaScore, type EventDomain, type EventEnvelope, type EventId, type EventKind, type EventPayloadByKind, type EventSequence, type ExecutionMode, ExecutionModeSchema, type HardkasConfig, HardkasError, type InvariantDomain, type InvariantSeverity, InvariantViolationError, type KaspaAddress, LOCK_ORDER, type LineageId, type LockHandle, type LockMetadata, type NetworkId, NetworkIdSchema, type RpcEndpointId, SOMPI_PER_KAS, type StampedEvent, type TxId, type UnknownEventPayload, type WorkflowId, type WriteFileAtomicOptions, acquireLock, artifactTypeSchema, asArtifactId, asContentHash, asCorrelationId, asDaaScore, asEventId, asEventSequence, asKaspaAddress, asLineageId, asNetworkId, asRpcEndpointId, asTxId, asWorkflowId, clearLock, coreEvents, createEventEnvelope, executionModeSchema, formatCorruptionIssue, formatSompi, hardkasConfigSchema, isProcessAlive, kaspaNetworkIdSchema, listLocks, maskSecrets, parseHardkasConfig, parseKasToSompi, redactSecret, validateEventEnvelope, withLock, withLocks, writeFileAtomic, writeFileAtomicSync };
|
package/dist/index.js
CHANGED
|
@@ -111,6 +111,315 @@ function redactSecret(value) {
|
|
|
111
111
|
return `${value.slice(0, 6)}...${value.slice(-4)}`;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// src/fs.ts
|
|
115
|
+
import fs from "fs";
|
|
116
|
+
import path from "path";
|
|
117
|
+
async function writeFileAtomic(targetPath, data, options = {}) {
|
|
118
|
+
const dir = path.dirname(targetPath);
|
|
119
|
+
const base = path.basename(targetPath);
|
|
120
|
+
const tempPath = path.join(dir, `.tmp.${base}.${Math.random().toString(36).slice(2)}`);
|
|
121
|
+
let fd = null;
|
|
122
|
+
try {
|
|
123
|
+
if (!fs.existsSync(dir)) {
|
|
124
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
fd = fs.openSync(tempPath, "w", options.mode);
|
|
127
|
+
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
128
|
+
fs.writeSync(fd, buffer, 0, buffer.length);
|
|
129
|
+
fs.fsyncSync(fd);
|
|
130
|
+
fs.closeSync(fd);
|
|
131
|
+
fd = null;
|
|
132
|
+
let attempts = 0;
|
|
133
|
+
const maxAttempts = process.platform === "win32" ? 5 : 1;
|
|
134
|
+
while (attempts < maxAttempts) {
|
|
135
|
+
try {
|
|
136
|
+
fs.renameSync(tempPath, targetPath);
|
|
137
|
+
break;
|
|
138
|
+
} catch (e) {
|
|
139
|
+
attempts++;
|
|
140
|
+
if (attempts >= maxAttempts) throw e;
|
|
141
|
+
if (e.code === "EPERM" || e.code === "EBUSY") {
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 10 * attempts));
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (options.fsyncParent && process.platform !== "win32") {
|
|
149
|
+
let dirFd = null;
|
|
150
|
+
try {
|
|
151
|
+
dirFd = fs.openSync(dir, "r");
|
|
152
|
+
fs.fsyncSync(dirFd);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
} finally {
|
|
155
|
+
if (dirFd !== null) fs.closeSync(dirFd);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
throw new HardkasError(
|
|
160
|
+
"IO_ERROR",
|
|
161
|
+
`Failed to write file atomically: ${targetPath}`,
|
|
162
|
+
{ cause: err }
|
|
163
|
+
);
|
|
164
|
+
} finally {
|
|
165
|
+
if (fs.existsSync(tempPath)) {
|
|
166
|
+
try {
|
|
167
|
+
fs.unlinkSync(tempPath);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (fd !== null) {
|
|
172
|
+
try {
|
|
173
|
+
fs.closeSync(fd);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function writeFileAtomicSync(targetPath, data, options = {}) {
|
|
180
|
+
const dir = path.dirname(targetPath);
|
|
181
|
+
const base = path.basename(targetPath);
|
|
182
|
+
const tempPath = path.join(dir, `.tmp.${base}.${Math.random().toString(36).slice(2)}`);
|
|
183
|
+
let fd = null;
|
|
184
|
+
try {
|
|
185
|
+
if (!fs.existsSync(dir)) {
|
|
186
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
fd = fs.openSync(tempPath, "w", options.mode);
|
|
189
|
+
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
190
|
+
fs.writeSync(fd, buffer, 0, buffer.length);
|
|
191
|
+
fs.fsyncSync(fd);
|
|
192
|
+
fs.closeSync(fd);
|
|
193
|
+
fd = null;
|
|
194
|
+
let attempts = 0;
|
|
195
|
+
const maxAttempts = process.platform === "win32" ? 5 : 1;
|
|
196
|
+
while (attempts < maxAttempts) {
|
|
197
|
+
try {
|
|
198
|
+
fs.renameSync(tempPath, targetPath);
|
|
199
|
+
break;
|
|
200
|
+
} catch (e) {
|
|
201
|
+
attempts++;
|
|
202
|
+
if (attempts >= maxAttempts) throw e;
|
|
203
|
+
if (e.code === "EPERM" || e.code === "EBUSY") {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
throw e;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (options.fsyncParent && process.platform !== "win32") {
|
|
210
|
+
let dirFd = null;
|
|
211
|
+
try {
|
|
212
|
+
dirFd = fs.openSync(dir, "r");
|
|
213
|
+
fs.fsyncSync(dirFd);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
} finally {
|
|
216
|
+
if (dirFd !== null) fs.closeSync(dirFd);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
throw new HardkasError(
|
|
221
|
+
"IO_ERROR",
|
|
222
|
+
`Failed to write file atomically (sync): ${targetPath}`,
|
|
223
|
+
{ cause: err }
|
|
224
|
+
);
|
|
225
|
+
} finally {
|
|
226
|
+
if (fs.existsSync(tempPath)) {
|
|
227
|
+
try {
|
|
228
|
+
fs.unlinkSync(tempPath);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (fd !== null) {
|
|
233
|
+
try {
|
|
234
|
+
fs.closeSync(fd);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/corruption.ts
|
|
242
|
+
function formatCorruptionIssue(issue) {
|
|
243
|
+
const parts = [];
|
|
244
|
+
const icon = issue.severity === "error" ? "\u274C" : "\u26A0\uFE0F";
|
|
245
|
+
parts.push(`${icon} [${issue.code}] ${issue.message}`);
|
|
246
|
+
if (issue.path) {
|
|
247
|
+
const loc = issue.lineNumber ? `${issue.path}:${issue.lineNumber}` : issue.path;
|
|
248
|
+
parts.push(` Location: ${loc}`);
|
|
249
|
+
}
|
|
250
|
+
if (issue.artifactId) {
|
|
251
|
+
parts.push(` Artifact: ${issue.artifactId}`);
|
|
252
|
+
}
|
|
253
|
+
if (issue.suggestion) {
|
|
254
|
+
parts.push(` Suggestion: ${issue.suggestion}`);
|
|
255
|
+
}
|
|
256
|
+
return parts.join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/lock.ts
|
|
260
|
+
import fs2 from "fs";
|
|
261
|
+
import path2 from "path";
|
|
262
|
+
import os from "os";
|
|
263
|
+
var LOCK_ORDER = [
|
|
264
|
+
"workspace",
|
|
265
|
+
"node",
|
|
266
|
+
"accounts",
|
|
267
|
+
"artifacts",
|
|
268
|
+
"events",
|
|
269
|
+
"query-store"
|
|
270
|
+
];
|
|
271
|
+
async function acquireLock(args) {
|
|
272
|
+
const lockDir = path2.join(args.rootDir, ".hardkas", "locks");
|
|
273
|
+
const lockPath = path2.join(lockDir, `${args.name}.lock`);
|
|
274
|
+
const timeoutMs = args.timeoutMs ?? 3e4;
|
|
275
|
+
const pollMs = args.pollMs ?? 250;
|
|
276
|
+
const start = Date.now();
|
|
277
|
+
if (!fs2.existsSync(lockDir)) {
|
|
278
|
+
fs2.mkdirSync(lockDir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
while (true) {
|
|
281
|
+
try {
|
|
282
|
+
const metadata = {
|
|
283
|
+
schema: "hardkas.lock.v1",
|
|
284
|
+
name: args.name,
|
|
285
|
+
pid: process.pid,
|
|
286
|
+
command: args.command || process.argv.join(" "),
|
|
287
|
+
cwd: process.cwd(),
|
|
288
|
+
hostname: os.hostname(),
|
|
289
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
290
|
+
expiresAt: null
|
|
291
|
+
};
|
|
292
|
+
const fd = fs2.openSync(lockPath, "wx");
|
|
293
|
+
fs2.writeSync(fd, JSON.stringify(metadata, null, 2));
|
|
294
|
+
fs2.closeSync(fd);
|
|
295
|
+
return {
|
|
296
|
+
path: lockPath,
|
|
297
|
+
metadata,
|
|
298
|
+
release: async () => {
|
|
299
|
+
if (fs2.existsSync(lockPath)) {
|
|
300
|
+
try {
|
|
301
|
+
const current = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
|
|
302
|
+
if (current.pid === process.pid) {
|
|
303
|
+
fs2.unlinkSync(lockPath);
|
|
304
|
+
}
|
|
305
|
+
} catch (e) {
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
} catch (e) {
|
|
311
|
+
if (e.code === "EEXIST") {
|
|
312
|
+
let existingMetadata = null;
|
|
313
|
+
try {
|
|
314
|
+
existingMetadata = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
|
|
315
|
+
} catch (err) {
|
|
316
|
+
throw new HardkasError("LOCK_METADATA_INVALID", `Lock file at ${lockPath} is corrupted.`, { cause: err });
|
|
317
|
+
}
|
|
318
|
+
if (existingMetadata) {
|
|
319
|
+
const isAlive = isProcessAlive(existingMetadata.pid);
|
|
320
|
+
if (!isAlive) {
|
|
321
|
+
throw new HardkasError(
|
|
322
|
+
"STALE_LOCK",
|
|
323
|
+
`Workspace is locked by a dead process (PID: ${existingMetadata.pid}).`,
|
|
324
|
+
{ cause: existingMetadata }
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (args.wait && Date.now() - start < timeoutMs) {
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
throw new HardkasError(
|
|
332
|
+
args.wait ? "LOCK_TIMEOUT" : "LOCK_HELD",
|
|
333
|
+
`Workspace is locked by another HardKAS process (PID: ${existingMetadata.pid}).`,
|
|
334
|
+
{ cause: existingMetadata }
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
throw e;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function withLock(args, fn) {
|
|
343
|
+
const handle = await acquireLock(args);
|
|
344
|
+
try {
|
|
345
|
+
return await fn(handle);
|
|
346
|
+
} finally {
|
|
347
|
+
await handle.release();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function withLocks(rootDir, names, fn, options = {}) {
|
|
351
|
+
const sortedNames = [...names].sort((a, b) => {
|
|
352
|
+
const idxA = LOCK_ORDER.indexOf(a);
|
|
353
|
+
const idxB = LOCK_ORDER.indexOf(b);
|
|
354
|
+
return idxA - idxB;
|
|
355
|
+
});
|
|
356
|
+
const handles = [];
|
|
357
|
+
try {
|
|
358
|
+
for (const name of sortedNames) {
|
|
359
|
+
handles.push(await acquireLock({ rootDir, name, ...options }));
|
|
360
|
+
}
|
|
361
|
+
return await fn();
|
|
362
|
+
} finally {
|
|
363
|
+
for (const handle of handles.reverse()) {
|
|
364
|
+
await handle.release();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function isProcessAlive(pid) {
|
|
369
|
+
try {
|
|
370
|
+
process.kill(pid, 0);
|
|
371
|
+
return true;
|
|
372
|
+
} catch (e) {
|
|
373
|
+
return e.code !== "ESRCH";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function listLocks(rootDir) {
|
|
377
|
+
const lockDir = path2.join(rootDir, ".hardkas", "locks");
|
|
378
|
+
if (!fs2.existsSync(lockDir)) return [];
|
|
379
|
+
const files = fs2.readdirSync(lockDir).filter((f) => f.endsWith(".lock"));
|
|
380
|
+
const result = [];
|
|
381
|
+
for (const file of files) {
|
|
382
|
+
const lockPath = path2.join(lockDir, file);
|
|
383
|
+
try {
|
|
384
|
+
const metadata = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
|
|
385
|
+
result.push({
|
|
386
|
+
name: path2.basename(file, ".lock"),
|
|
387
|
+
metadata,
|
|
388
|
+
path: lockPath,
|
|
389
|
+
isAlive: metadata.hostname === os.hostname() ? isProcessAlive(metadata.pid) : true
|
|
390
|
+
// Assume alive if remote
|
|
391
|
+
});
|
|
392
|
+
} catch (e) {
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
function clearLock(rootDir, name, options = {}) {
|
|
398
|
+
const lockDir = path2.join(rootDir, ".hardkas", "locks");
|
|
399
|
+
const lockPath = path2.join(lockDir, `${name}.lock`);
|
|
400
|
+
if (!fs2.existsSync(lockPath)) return { cleared: false, reason: "Lock not found" };
|
|
401
|
+
let metadata;
|
|
402
|
+
try {
|
|
403
|
+
metadata = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
|
|
404
|
+
} catch (e) {
|
|
405
|
+
if (options.force) {
|
|
406
|
+
fs2.unlinkSync(lockPath);
|
|
407
|
+
return { cleared: true };
|
|
408
|
+
}
|
|
409
|
+
return { cleared: false, reason: "Corrupt metadata (use --force to clear)" };
|
|
410
|
+
}
|
|
411
|
+
const isLocal = metadata.hostname === os.hostname();
|
|
412
|
+
const isAlive = isLocal ? isProcessAlive(metadata.pid) : true;
|
|
413
|
+
if (options.ifDead) {
|
|
414
|
+
if (!isLocal) return { cleared: false, reason: "Cannot verify liveness of remote lock (host: " + metadata.hostname + ")" };
|
|
415
|
+
if (isAlive) return { cleared: false, reason: `Process (PID: ${metadata.pid}) is still alive` };
|
|
416
|
+
} else if (!options.force) {
|
|
417
|
+
return { cleared: false, reason: "Lock is potentially active. Use --force or --if-dead." };
|
|
418
|
+
}
|
|
419
|
+
fs2.unlinkSync(lockPath);
|
|
420
|
+
return { cleared: true };
|
|
421
|
+
}
|
|
422
|
+
|
|
114
423
|
// src/index.ts
|
|
115
424
|
var SOMPI_PER_KAS = 100000000n;
|
|
116
425
|
var kaspaNetworkIdSchema = z.enum([
|
|
@@ -205,8 +514,10 @@ export {
|
|
|
205
514
|
ExecutionModeSchema,
|
|
206
515
|
HardkasError,
|
|
207
516
|
InvariantViolationError,
|
|
517
|
+
LOCK_ORDER,
|
|
208
518
|
NetworkIdSchema,
|
|
209
519
|
SOMPI_PER_KAS,
|
|
520
|
+
acquireLock,
|
|
210
521
|
artifactTypeSchema,
|
|
211
522
|
asArtifactId,
|
|
212
523
|
asContentHash,
|
|
@@ -220,15 +531,23 @@ export {
|
|
|
220
531
|
asRpcEndpointId,
|
|
221
532
|
asTxId,
|
|
222
533
|
asWorkflowId,
|
|
534
|
+
clearLock,
|
|
223
535
|
coreEvents,
|
|
224
536
|
createEventEnvelope,
|
|
225
537
|
executionModeSchema,
|
|
538
|
+
formatCorruptionIssue,
|
|
226
539
|
formatSompi,
|
|
227
540
|
hardkasConfigSchema,
|
|
541
|
+
isProcessAlive,
|
|
228
542
|
kaspaNetworkIdSchema,
|
|
543
|
+
listLocks,
|
|
229
544
|
maskSecrets,
|
|
230
545
|
parseHardkasConfig,
|
|
231
546
|
parseKasToSompi,
|
|
232
547
|
redactSecret,
|
|
233
|
-
validateEventEnvelope
|
|
548
|
+
validateEventEnvelope,
|
|
549
|
+
withLock,
|
|
550
|
+
withLocks,
|
|
551
|
+
writeFileAtomic,
|
|
552
|
+
writeFileAtomicSync
|
|
234
553
|
};
|