stitchkit 0.77.0 → 0.79.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/dist/agent-runtime-browser.js +53 -581
- package/dist/agent-runtime-coding-tools.js +6 -5
- package/dist/agent-runtime-harness.js +20 -93
- package/dist/agent-runtime-openrouter.js +1 -1
- package/dist/agent-runtime.js +65 -61
- package/dist/application/decisions.d.ts +28 -25
- package/dist/application/decisions.d.ts.map +1 -1
- package/dist/application/kernel.d.ts +2 -0
- package/dist/application/kernel.d.ts.map +1 -1
- package/dist/application/keyspace.d.ts +11 -1
- package/dist/application/keyspace.d.ts.map +1 -1
- package/dist/application/schedule.d.ts.map +1 -1
- package/dist/application/schemas.d.ts +2 -0
- package/dist/application/schemas.d.ts.map +1 -1
- package/dist/application/server-resource.d.ts.map +1 -1
- package/dist/application/watch-hub.d.ts.map +1 -1
- package/dist/application-diagnostic-journal.d.ts +21 -0
- package/dist/application-diagnostic-journal.d.ts.map +1 -0
- package/dist/application-diagnostic-journal.js +632 -0
- package/dist/application-opentelemetry.js +3 -2
- package/dist/application-schemas.js +14 -98
- package/dist/application.d.ts +1 -2
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +155 -882
- package/dist/cli.js +13 -11
- package/dist/contract/index.js +15 -10
- package/dist/files/boundary.d.ts.map +1 -1
- package/dist/files.js +1 -1
- package/dist/{index-7b188kmz.js → index-2qzskm8h.js} +15 -1
- package/dist/{index-k2zczx1g.js → index-2rwwmr7v.js} +3 -3
- package/dist/{index-8eywc9zv.js → index-3gye9wzb.js} +6 -3
- package/dist/index-4fpa9dy2.js +51 -0
- package/dist/{index-7qy2ex0m.js → index-5mtc969c.js} +2 -2
- package/dist/index-5s3zajp8.js +192 -0
- package/dist/{index-3cwck0rm.js → index-6s7n2v50.js} +60 -374
- package/dist/{index-s4c8wy8m.js → index-758zx2ha.js} +11 -90
- package/dist/{index-qyrqwr4c.js → index-77fekveh.js} +4 -1
- package/dist/{index-58jzmnn4.js → index-7ag81v2f.js} +355 -34
- package/dist/index-7gnkny4z.js +326 -0
- package/dist/index-7zbps32p.js +89 -0
- package/dist/index-85vfqd7m.js +135 -0
- package/dist/{index-m668wzyc.js → index-9s6z8vgf.js} +17 -141
- package/dist/{index-bfcpjw20.js → index-actkayzn.js} +7 -3
- package/dist/index-b9aefrqd.js +20 -0
- package/dist/index-dre2ywck.js +48 -0
- package/dist/{index-nemjkxjp.js → index-dwf1f2ap.js} +16 -12
- package/dist/{index-wwst0td5.js → index-dx9xehwt.js} +1 -1
- package/dist/{index-ezmn6ac6.js → index-dxesg10d.js} +4 -4
- package/dist/{index-aczggrty.js → index-dxjjv399.js} +2 -2
- package/dist/{index-jqtsc9mj.js → index-egaabxkz.js} +3 -3
- package/dist/{index-vc1b0b1b.js → index-evcnmsc3.js} +49 -8
- package/dist/{index-nt1mp8km.js → index-fxz0v7vg.js} +84 -84
- package/dist/index-h4pj6fta.js +87 -0
- package/dist/{index-1rxswfbv.js → index-hb5pf3s4.js} +1 -1
- package/dist/{index-t8qyvrvg.js → index-k9ay6v6d.js} +21 -272
- package/dist/index-kzfs85xp.js +9 -0
- package/dist/{index-vbf2p6me.js → index-m9kzax52.js} +91 -89
- package/dist/{index-22by16v6.js → index-mgtxfe6b.js} +1 -1
- package/dist/index-p7hgdyhb.js +22 -0
- package/dist/index-pq95qkbf.js +0 -0
- package/dist/index-t0z5p5w6.js +318 -0
- package/dist/{index-s2rchahr.js → index-vwp69871.js} +13 -11
- package/dist/index-wc4x00bn.js +103 -0
- package/dist/{index-da1aqnhb.js → index-xcetq6j3.js} +8 -6
- package/dist/{index-f6n5n7nz.js → index-y01f4223.js} +33 -126
- package/dist/{index-35z5h2ty.js → index-ywd0y6m1.js} +18 -218
- package/dist/index.js +65 -1588
- package/dist/internal/deadline.d.ts +17 -0
- package/dist/internal/deadline.d.ts.map +1 -0
- package/dist/live.js +15 -89
- package/dist/node.js +17 -15
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/index.js +7 -6
- package/dist/primitives.js +5 -2
- package/dist/react.js +1 -1
- package/dist/remote.js +7 -6
- package/dist/server/event-bus.d.ts.map +1 -1
- package/dist/server/index.js +26 -38
- package/dist/telegram.js +1 -1
- package/dist/testing.js +86 -97
- package/dist/tool-invoker.js +11 -9
- package/dist/tools/async-operation-contract.d.ts +61 -0
- package/dist/tools/async-operation-contract.d.ts.map +1 -0
- package/dist/tools/async-operation.d.ts +2 -47
- package/dist/tools/async-operation.d.ts.map +1 -1
- package/dist/tools/view-file-contract.d.ts +98 -0
- package/dist/tools/view-file-contract.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +4 -83
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools-contract.d.ts +29 -0
- package/dist/tools-contract.d.ts.map +1 -0
- package/dist/tools-contract.js +24 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +59 -95
- package/llms-full.txt +146 -14
- package/package.json +14 -7
- package/dist/index-ksp6e2ye.js +0 -24
- package/dist/index-vj3vvpaa.js +0 -307
package/dist/application.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApplicationAdmissionError,
|
|
3
|
+
ApplicationRestartInputSchema,
|
|
4
|
+
ApplicationRestartOutcomeSchema,
|
|
5
|
+
ApplicationRestartResultSchema,
|
|
6
|
+
ApplicationShutdownBudgetSchema,
|
|
7
|
+
ApplicationShutdownOptionsSchema,
|
|
8
|
+
createApplication
|
|
9
|
+
} from "./index-evcnmsc3.js";
|
|
10
|
+
import {
|
|
11
|
+
defineManagedResource,
|
|
12
|
+
managedResourceDependencyId
|
|
13
|
+
} from "./index-2k4yrqkc.js";
|
|
1
14
|
import {
|
|
2
15
|
ActivityIdSchema,
|
|
3
16
|
ActivityLiveStateSchema,
|
|
@@ -6,41 +19,28 @@ import {
|
|
|
6
19
|
ActivityStageSnapshotSchema,
|
|
7
20
|
ActivityTokenBrand,
|
|
8
21
|
ApplicationSnapshotSinkStatusSchema,
|
|
9
|
-
BoundedChannelPolicySchema,
|
|
10
|
-
BoundedChannelReaderError,
|
|
11
|
-
BoundedChannelSnapshotSchema,
|
|
12
|
-
BoundedChannelStateSchema,
|
|
13
|
-
CreditWindowSnapshotSchema,
|
|
14
22
|
ManagedScheduleDescriptorSchema,
|
|
15
23
|
ManagedScheduleErrorPolicySchema,
|
|
16
24
|
ManagedScheduleOverlapSchema,
|
|
17
25
|
ManagedScheduleStatusSchema,
|
|
18
26
|
createActivityProjection,
|
|
19
27
|
createApplicationSnapshotSink,
|
|
20
|
-
createBoundedChannel,
|
|
21
|
-
createCreditWindow,
|
|
22
28
|
createManagedSchedule
|
|
23
|
-
} from "./index-
|
|
29
|
+
} from "./index-6s7n2v50.js";
|
|
24
30
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
createApplication
|
|
39
|
-
} from "./index-vc1b0b1b.js";
|
|
40
|
-
import {
|
|
41
|
-
defineManagedResource,
|
|
42
|
-
managedResourceDependencyId
|
|
43
|
-
} from "./index-2k4yrqkc.js";
|
|
31
|
+
DiagnosticJournalCloseResultSchema,
|
|
32
|
+
DiagnosticJournalFailurePhaseSchema,
|
|
33
|
+
DiagnosticJournalFrameSchema,
|
|
34
|
+
DiagnosticJournalLimitsSchema,
|
|
35
|
+
DiagnosticJournalLockPolicySchema,
|
|
36
|
+
DiagnosticJournalRefusalReasonSchema,
|
|
37
|
+
DiagnosticJournalStateSchema,
|
|
38
|
+
DiagnosticJournalStatusSchema,
|
|
39
|
+
DiagnosticJournalSubmitResultSchema,
|
|
40
|
+
DiagnosticJournalWaitResultSchema,
|
|
41
|
+
readDiagnosticJournalLockDiagnosis
|
|
42
|
+
} from "./index-wc4x00bn.js";
|
|
43
|
+
import"./index-pq95qkbf.js";
|
|
44
44
|
import {
|
|
45
45
|
ApplicationAdmissionSnapshotSchema,
|
|
46
46
|
ApplicationHealthSchema,
|
|
@@ -53,12 +53,36 @@ import {
|
|
|
53
53
|
ManagedResourceSnapshotSchema,
|
|
54
54
|
ManagedResourceStateSchema,
|
|
55
55
|
projectApplicationStatus
|
|
56
|
-
} from "./index-
|
|
56
|
+
} from "./index-3gye9wzb.js";
|
|
57
|
+
import {
|
|
58
|
+
withDeadline
|
|
59
|
+
} from "./index-b9aefrqd.js";
|
|
57
60
|
import"./index-3z73fh2c.js";
|
|
58
|
-
import"./index-0w9abg87.js";
|
|
59
61
|
import {
|
|
60
|
-
|
|
61
|
-
} from "./index-
|
|
62
|
+
createBoundedSinkManager
|
|
63
|
+
} from "./index-a59da114.js";
|
|
64
|
+
import {
|
|
65
|
+
createBackoff
|
|
66
|
+
} from "./index-h4pj6fta.js";
|
|
67
|
+
import {
|
|
68
|
+
watchKeyString
|
|
69
|
+
} from "./index-dre2ywck.js";
|
|
70
|
+
import"./index-ywd0y6m1.js";
|
|
71
|
+
import {
|
|
72
|
+
BoundedChannelPolicySchema,
|
|
73
|
+
BoundedChannelReaderError,
|
|
74
|
+
BoundedChannelSnapshotSchema,
|
|
75
|
+
BoundedChannelStateSchema,
|
|
76
|
+
CreditWindowSnapshotSchema,
|
|
77
|
+
createBoundedChannel,
|
|
78
|
+
createCreditWindow
|
|
79
|
+
} from "./index-7gnkny4z.js";
|
|
80
|
+
import {
|
|
81
|
+
argumentsDigest,
|
|
82
|
+
stableValue
|
|
83
|
+
} from "./index-a916km3r.js";
|
|
84
|
+
import"./index-0w9abg87.js";
|
|
85
|
+
import"./index-77fekveh.js";
|
|
62
86
|
// src/application/admission.ts
|
|
63
87
|
import { z } from "zod";
|
|
64
88
|
var PositiveSafeIntegerSchema = z.number().int().positive().safe();
|
|
@@ -454,13 +478,19 @@ class DecisionUndecidedError extends Error {
|
|
|
454
478
|
|
|
455
479
|
class DecisionPolicyError extends Error {
|
|
456
480
|
policyId;
|
|
457
|
-
|
|
458
|
-
|
|
481
|
+
trace;
|
|
482
|
+
constructor(policyId, detail, trace = []) {
|
|
483
|
+
super(`Policy "${policyId}" did not answer (${detail}). A policy returns { outcome: "allow" | "deny" | "defer" } within its deadline, and a deny carries a reason.`);
|
|
459
484
|
this.name = "DecisionPolicyError";
|
|
460
485
|
this.policyId = policyId;
|
|
486
|
+
this.trace = trace;
|
|
461
487
|
}
|
|
462
488
|
}
|
|
463
|
-
function createDecisionPipeline(policies) {
|
|
489
|
+
function createDecisionPipeline(policies, config) {
|
|
490
|
+
const { policyTimeoutMs } = config;
|
|
491
|
+
if (!Number.isInteger(policyTimeoutMs) || policyTimeoutMs <= 0) {
|
|
492
|
+
throw new Error("[stitchkit] decision pipeline: policyTimeoutMs must be a positive integer number of milliseconds.");
|
|
493
|
+
}
|
|
464
494
|
const seen = new Set;
|
|
465
495
|
for (const policy of policies) {
|
|
466
496
|
if (policy.id.trim() === "") {
|
|
@@ -476,10 +506,21 @@ function createDecisionPipeline(policies) {
|
|
|
476
506
|
async decide(input) {
|
|
477
507
|
const trace = [];
|
|
478
508
|
for (const policy of policies) {
|
|
479
|
-
|
|
509
|
+
let answer;
|
|
510
|
+
try {
|
|
511
|
+
const outcome = await withDeadline(policy.decide(input), policyTimeoutMs);
|
|
512
|
+
if (!outcome.settled) {
|
|
513
|
+
throw new DecisionPolicyError(policy.id, `did not answer within ${policyTimeoutMs}ms`, trace);
|
|
514
|
+
}
|
|
515
|
+
answer = outcome.value;
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (error instanceof DecisionPolicyError)
|
|
518
|
+
throw error;
|
|
519
|
+
throw new DecisionPolicyError(policy.id, error instanceof Error ? `threw: ${error.message}` : "threw", trace);
|
|
520
|
+
}
|
|
480
521
|
const parsed = PolicyDecisionSchema.safeParse(answer);
|
|
481
522
|
if (!parsed.success) {
|
|
482
|
-
throw new DecisionPolicyError(policy.id, parsed.error.issues[0]?.message ?? "invalid");
|
|
523
|
+
throw new DecisionPolicyError(policy.id, parsed.error.issues[0]?.message ?? "invalid", trace);
|
|
483
524
|
}
|
|
484
525
|
const decision = parsed.data;
|
|
485
526
|
trace.push(decision.outcome === "deny" ? { id: policy.id, outcome: "deny", reason: decision.reason } : { id: policy.id, outcome: decision.outcome });
|
|
@@ -494,728 +535,18 @@ function createDecisionPipeline(policies) {
|
|
|
494
535
|
}
|
|
495
536
|
};
|
|
496
537
|
}
|
|
497
|
-
// src/application/diagnostic-journal.ts
|
|
498
|
-
import { randomUUID } from "node:crypto";
|
|
499
|
-
|
|
500
|
-
// src/application/diagnostic-journal-contract.ts
|
|
501
|
-
import { z as z3 } from "zod";
|
|
502
|
-
var PositiveSafeIntegerSchema2 = z3.number().int().positive().safe();
|
|
503
|
-
var FileModeSchema = z3.number().int().min(0).max(511);
|
|
504
|
-
var DiagnosticJournalLimitsSchema = z3.object({
|
|
505
|
-
maxEventBytes: PositiveSafeIntegerSchema2,
|
|
506
|
-
maxPendingItems: PositiveSafeIntegerSchema2,
|
|
507
|
-
maxPendingBytes: PositiveSafeIntegerSchema2,
|
|
508
|
-
maxFileBytes: PositiveSafeIntegerSchema2,
|
|
509
|
-
maxFiles: PositiveSafeIntegerSchema2
|
|
510
|
-
}).strict().readonly();
|
|
511
|
-
var DiagnosticJournalLockPolicySchema = z3.enum(["refuse", "reclaim-stale"]);
|
|
512
|
-
var DiagnosticJournalStateSchema = z3.enum(["open", "draining", "closed", "failed"]);
|
|
513
|
-
var DiagnosticJournalRefusalReasonSchema = z3.enum([
|
|
514
|
-
"closed",
|
|
515
|
-
"failed",
|
|
516
|
-
"invalid",
|
|
517
|
-
"oversized",
|
|
518
|
-
"item-capacity",
|
|
519
|
-
"byte-capacity"
|
|
520
|
-
]);
|
|
521
|
-
var DiagnosticJournalRefusalCountersSchema = z3.object({
|
|
522
|
-
closed: z3.number().int().nonnegative(),
|
|
523
|
-
failed: z3.number().int().nonnegative(),
|
|
524
|
-
invalid: z3.number().int().nonnegative(),
|
|
525
|
-
oversized: z3.number().int().nonnegative(),
|
|
526
|
-
"item-capacity": z3.number().int().nonnegative(),
|
|
527
|
-
"byte-capacity": z3.number().int().nonnegative()
|
|
528
|
-
}).strict().readonly();
|
|
529
|
-
var DiagnosticJournalFailurePhaseSchema = z3.enum(["write", "rotation", "close"]);
|
|
530
|
-
var DiagnosticJournalStatusSchema = z3.object({
|
|
531
|
-
state: DiagnosticJournalStateSchema,
|
|
532
|
-
epoch: z3.uuid(),
|
|
533
|
-
limits: DiagnosticJournalLimitsSchema,
|
|
534
|
-
lock: z3.object({
|
|
535
|
-
policy: DiagnosticJournalLockPolicySchema,
|
|
536
|
-
reclaimedStale: z3.boolean()
|
|
537
|
-
}).strict().readonly(),
|
|
538
|
-
received: z3.number().int().nonnegative(),
|
|
539
|
-
accepted: z3.number().int().nonnegative(),
|
|
540
|
-
refused: z3.number().int().nonnegative(),
|
|
541
|
-
refusals: DiagnosticJournalRefusalCountersSchema,
|
|
542
|
-
written: z3.number().int().nonnegative(),
|
|
543
|
-
failedRecords: z3.number().int().nonnegative(),
|
|
544
|
-
pendingItems: z3.number().int().nonnegative(),
|
|
545
|
-
pendingBytes: z3.number().int().nonnegative(),
|
|
546
|
-
inFlight: z3.boolean(),
|
|
547
|
-
rotations: z3.number().int().nonnegative(),
|
|
548
|
-
rotationFailures: z3.number().int().nonnegative(),
|
|
549
|
-
partialTails: z3.number().int().nonnegative(),
|
|
550
|
-
currentFileBytes: z3.number().int().nonnegative(),
|
|
551
|
-
retainedFiles: z3.number().int().nonnegative(),
|
|
552
|
-
lastAcceptedSequence: z3.number().int().positive().optional(),
|
|
553
|
-
lastWrittenSequence: z3.number().int().positive().optional(),
|
|
554
|
-
lastSettledSequence: z3.number().int().positive().optional(),
|
|
555
|
-
lastFailure: z3.object({
|
|
556
|
-
phase: DiagnosticJournalFailurePhaseSchema,
|
|
557
|
-
sequence: z3.number().int().positive().optional()
|
|
558
|
-
}).strict().readonly().optional()
|
|
559
|
-
}).strict().readonly();
|
|
560
|
-
var DiagnosticJournalSubmitResultSchema = z3.discriminatedUnion("outcome", [
|
|
561
|
-
z3.object({
|
|
562
|
-
outcome: z3.literal("accepted"),
|
|
563
|
-
epoch: z3.uuid(),
|
|
564
|
-
sequence: z3.number().int().positive()
|
|
565
|
-
}).strict().readonly(),
|
|
566
|
-
z3.object({
|
|
567
|
-
outcome: z3.literal("refused"),
|
|
568
|
-
reason: DiagnosticJournalRefusalReasonSchema
|
|
569
|
-
}).strict().readonly()
|
|
570
|
-
]);
|
|
571
|
-
var DiagnosticJournalWaitResultSchema = z3.object({
|
|
572
|
-
outcome: z3.enum(["settled", "timed-out", "cancelled"]),
|
|
573
|
-
state: DiagnosticJournalStateSchema,
|
|
574
|
-
throughSequence: z3.number().int().nonnegative(),
|
|
575
|
-
settledSequence: z3.number().int().nonnegative()
|
|
576
|
-
}).strict().readonly();
|
|
577
|
-
var DiagnosticJournalCloseResultSchema = z3.object({
|
|
578
|
-
outcome: z3.enum(["closed", "timed-out", "cancelled"]),
|
|
579
|
-
state: DiagnosticJournalStateSchema,
|
|
580
|
-
pendingItems: z3.number().int().nonnegative()
|
|
581
|
-
}).strict().readonly();
|
|
582
|
-
var DiagnosticJournalFrameSchema = z3.object({
|
|
583
|
-
schemaVersion: z3.literal(1),
|
|
584
|
-
epoch: z3.uuid(),
|
|
585
|
-
sequence: z3.number().int().positive(),
|
|
586
|
-
event: z3.json()
|
|
587
|
-
}).strict().readonly();
|
|
588
|
-
function parseDiagnosticJournalMode(mode) {
|
|
589
|
-
return FileModeSchema.parse(mode ?? 384);
|
|
590
|
-
}
|
|
591
|
-
function readDiagnosticJournalLockDiagnosis(error) {
|
|
592
|
-
if (!isRecord(error))
|
|
593
|
-
return;
|
|
594
|
-
const diagnosis = error.journalLock;
|
|
595
|
-
return isRecord(diagnosis) ? diagnosis : undefined;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// src/application/diagnostic-journal-manager.ts
|
|
599
|
-
import { z as z4 } from "zod";
|
|
600
|
-
|
|
601
|
-
// src/application/diagnostic-journal-storage.ts
|
|
602
|
-
import { constants as constants2 } from "node:fs";
|
|
603
|
-
import { lstat, open as open2, readdir, realpath, rename, unlink as unlink2 } from "node:fs/promises";
|
|
604
|
-
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
605
|
-
|
|
606
|
-
// src/application/diagnostic-journal-lock.ts
|
|
607
|
-
import { execFile } from "node:child_process";
|
|
608
|
-
import { constants } from "node:fs";
|
|
609
|
-
import { open, readFile, unlink } from "node:fs/promises";
|
|
610
|
-
import { hostname, platform } from "node:os";
|
|
611
|
-
import { promisify } from "node:util";
|
|
612
|
-
function isExisting(error) {
|
|
613
|
-
return isRecord(error) && error.code === "EEXIST";
|
|
614
|
-
}
|
|
615
|
-
function readOwner(text) {
|
|
616
|
-
let parsed;
|
|
617
|
-
try {
|
|
618
|
-
parsed = JSON.parse(text);
|
|
619
|
-
} catch {
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
if (!isRecord(parsed))
|
|
623
|
-
return;
|
|
624
|
-
const { pid, host, acquiredAt, machine } = parsed;
|
|
625
|
-
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0)
|
|
626
|
-
return;
|
|
627
|
-
if (typeof host !== "string" || host.length === 0)
|
|
628
|
-
return;
|
|
629
|
-
if (typeof acquiredAt !== "string")
|
|
630
|
-
return;
|
|
631
|
-
const identified = typeof machine === "string" && machine.length > 0 ? { machine } : {};
|
|
632
|
-
return { pid, host, acquiredAt, ...identified };
|
|
633
|
-
}
|
|
634
|
-
var detectedIdentity;
|
|
635
|
-
async function firstReadableLine(paths) {
|
|
636
|
-
for (const path of paths) {
|
|
637
|
-
try {
|
|
638
|
-
const text = (await readFile(path, "utf8")).trim();
|
|
639
|
-
if (text.length > 0)
|
|
640
|
-
return text;
|
|
641
|
-
} catch {}
|
|
642
|
-
}
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
var run = promisify(execFile);
|
|
646
|
-
function parsePlatformUuid(dump) {
|
|
647
|
-
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(dump)?.[1] ?? null;
|
|
648
|
-
}
|
|
649
|
-
async function readDarwinPlatformUuid(command = "ioreg") {
|
|
650
|
-
try {
|
|
651
|
-
const { stdout } = await run(command, ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
652
|
-
timeout: 2000,
|
|
653
|
-
maxBuffer: 1 << 20,
|
|
654
|
-
encoding: "utf8"
|
|
655
|
-
});
|
|
656
|
-
return parsePlatformUuid(stdout);
|
|
657
|
-
} catch {
|
|
658
|
-
return null;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
async function machineIdentity(declared) {
|
|
662
|
-
if (declared !== undefined)
|
|
663
|
-
return declared;
|
|
664
|
-
if (detectedIdentity !== undefined)
|
|
665
|
-
return detectedIdentity;
|
|
666
|
-
detectedIdentity = platform() === "darwin" ? await readDarwinPlatformUuid() : await firstReadableLine(["/etc/machine-id", "/var/lib/dbus/machine-id"]);
|
|
667
|
-
return detectedIdentity;
|
|
668
|
-
}
|
|
669
|
-
function attribute(owner, identity) {
|
|
670
|
-
if (identity !== null && owner.machine !== undefined) {
|
|
671
|
-
return owner.machine === identity ? "this-machine" : "another-machine";
|
|
672
|
-
}
|
|
673
|
-
return owner.host === hostname() ? "this-machine" : "unattributable";
|
|
674
|
-
}
|
|
675
|
-
async function probeLiveness(owner) {
|
|
676
|
-
try {
|
|
677
|
-
process.kill(owner.pid, 0);
|
|
678
|
-
} catch (error) {
|
|
679
|
-
return isRecord(error) && error.code === "ESRCH" ? "gone" : "alive";
|
|
680
|
-
}
|
|
681
|
-
return await isZombieProcess(owner.pid) ? "gone" : "alive";
|
|
682
|
-
}
|
|
683
|
-
async function isZombieProcess(pid, seam = {}) {
|
|
684
|
-
const { procRoot = "/proc", psCommand = "ps" } = seam;
|
|
685
|
-
try {
|
|
686
|
-
const stat = await readFile(`${procRoot}/${pid}/stat`, "utf8");
|
|
687
|
-
return stat.slice(stat.lastIndexOf(")") + 1).trimStart().startsWith("Z");
|
|
688
|
-
} catch {}
|
|
689
|
-
try {
|
|
690
|
-
const { stdout } = await run(psCommand, ["-o", "state=", "-p", String(pid)], {
|
|
691
|
-
timeout: 2000,
|
|
692
|
-
maxBuffer: 1 << 16,
|
|
693
|
-
encoding: "utf8"
|
|
694
|
-
});
|
|
695
|
-
return stdout.trim().startsWith("Z");
|
|
696
|
-
} catch {
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
async function diagnose(owner, declaredIdentity) {
|
|
701
|
-
if (!owner)
|
|
702
|
-
return { attribution: "unattributable", liveness: "not-probed", owner: null };
|
|
703
|
-
const attribution = attribute(owner, await machineIdentity(declaredIdentity));
|
|
704
|
-
const record = {
|
|
705
|
-
pid: owner.pid,
|
|
706
|
-
host: owner.host,
|
|
707
|
-
acquiredAt: owner.acquiredAt,
|
|
708
|
-
...owner.machine !== undefined && { machine: owner.machine }
|
|
709
|
-
};
|
|
710
|
-
if (attribution !== "this-machine")
|
|
711
|
-
return { attribution, liveness: "not-probed", owner: record };
|
|
712
|
-
return { attribution, liveness: await probeLiveness(owner), owner: record };
|
|
713
|
-
}
|
|
714
|
-
async function readLockOwner(lockPath) {
|
|
715
|
-
let handle;
|
|
716
|
-
try {
|
|
717
|
-
handle = await open(lockPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
718
|
-
return readOwner(await handle.readFile("utf8"));
|
|
719
|
-
} catch {
|
|
720
|
-
return;
|
|
721
|
-
} finally {
|
|
722
|
-
await handle?.close().catch(() => {
|
|
723
|
-
return;
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
async function createExclusive(lockPath, mode, declaredIdentity) {
|
|
728
|
-
const handle = await open(lockPath, "wx", mode);
|
|
729
|
-
const identity = await machineIdentity(declaredIdentity);
|
|
730
|
-
const owner = {
|
|
731
|
-
pid: process.pid,
|
|
732
|
-
host: hostname(),
|
|
733
|
-
acquiredAt: new Date().toISOString(),
|
|
734
|
-
...identity !== null && { machine: identity }
|
|
735
|
-
};
|
|
736
|
-
await handle.writeFile(`${JSON.stringify(owner)}
|
|
737
|
-
`, "utf8");
|
|
738
|
-
return handle;
|
|
739
|
-
}
|
|
740
|
-
async function acquireDiagnosticJournalLock(lockPath, mode, policy, declaredIdentity) {
|
|
741
|
-
try {
|
|
742
|
-
return {
|
|
743
|
-
handle: await createExclusive(lockPath, mode, declaredIdentity),
|
|
744
|
-
reclaimedStale: false
|
|
745
|
-
};
|
|
746
|
-
} catch (error) {
|
|
747
|
-
if (policy !== "reclaim-stale" || !isExisting(error))
|
|
748
|
-
throw error;
|
|
749
|
-
const owner = await readLockOwner(lockPath);
|
|
750
|
-
const diagnosis = await diagnose(owner, declaredIdentity);
|
|
751
|
-
if (!owner || diagnosis.liveness !== "gone") {
|
|
752
|
-
if (isRecord(error))
|
|
753
|
-
Object.assign(error, { journalLock: diagnosis });
|
|
754
|
-
throw error;
|
|
755
|
-
}
|
|
756
|
-
await unlink(lockPath).catch(() => {
|
|
757
|
-
return;
|
|
758
|
-
});
|
|
759
|
-
return {
|
|
760
|
-
handle: await createExclusive(lockPath, mode, declaredIdentity),
|
|
761
|
-
reclaimedStale: true
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// src/application/diagnostic-journal-storage.ts
|
|
767
|
-
class DiagnosticJournalStorageError extends Error {
|
|
768
|
-
phase;
|
|
769
|
-
constructor(phase, message, options) {
|
|
770
|
-
super(message, options);
|
|
771
|
-
this.phase = phase;
|
|
772
|
-
this.name = "DiagnosticJournalStorageError";
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
function isMissing(error) {
|
|
776
|
-
return isRecord(error) && error.code === "ENOENT";
|
|
777
|
-
}
|
|
778
|
-
async function removeIfPresent(path) {
|
|
779
|
-
try {
|
|
780
|
-
const info = await lstat(path);
|
|
781
|
-
if (info.isSymbolicLink() || !info.isFile()) {
|
|
782
|
-
throw new Error("Diagnostic journal generations must be regular files");
|
|
783
|
-
}
|
|
784
|
-
await unlink2(path);
|
|
785
|
-
} catch (error) {
|
|
786
|
-
if (!isMissing(error))
|
|
787
|
-
throw error;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
async function moveIfPresent(from, to) {
|
|
791
|
-
try {
|
|
792
|
-
const info = await lstat(from);
|
|
793
|
-
if (info.isSymbolicLink() || !info.isFile()) {
|
|
794
|
-
throw new Error("Diagnostic journal generations must be regular files");
|
|
795
|
-
}
|
|
796
|
-
await rename(from, to);
|
|
797
|
-
return true;
|
|
798
|
-
} catch (error) {
|
|
799
|
-
if (isMissing(error))
|
|
800
|
-
return false;
|
|
801
|
-
throw error;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
function generation(path, index) {
|
|
805
|
-
return `${path}.${index}`;
|
|
806
|
-
}
|
|
807
|
-
function generationIndex(name, prefix) {
|
|
808
|
-
if (!name.startsWith(prefix))
|
|
809
|
-
return;
|
|
810
|
-
const suffix = name.slice(prefix.length);
|
|
811
|
-
if (!/^\d+$/.test(suffix))
|
|
812
|
-
return;
|
|
813
|
-
const value = Number(suffix);
|
|
814
|
-
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
815
|
-
}
|
|
816
|
-
async function createRotatingDiagnosticJournalStorage(config) {
|
|
817
|
-
if (!isAbsolute(config.path) || resolve(config.path) !== config.path) {
|
|
818
|
-
throw new TypeError("Diagnostic journal path must be normalized and absolute");
|
|
819
|
-
}
|
|
820
|
-
const parent = await realpath(dirname(config.path));
|
|
821
|
-
const journalPath = resolve(parent, basename(config.path));
|
|
822
|
-
const lockPath = `${journalPath}.lock`;
|
|
823
|
-
const { handle: lock, reclaimedStale } = await acquireDiagnosticJournalLock(lockPath, config.mode, config.lock, config.machineIdentity);
|
|
824
|
-
let handle;
|
|
825
|
-
let currentFileBytes = 0;
|
|
826
|
-
let retainedFiles = 1;
|
|
827
|
-
let rotations = 0;
|
|
828
|
-
let partialTails = 0;
|
|
829
|
-
let closed = false;
|
|
830
|
-
const openCurrent = async () => {
|
|
831
|
-
handle = await open2(journalPath, constants2.O_APPEND | constants2.O_CREAT | constants2.O_RDWR | constants2.O_NOFOLLOW, config.mode);
|
|
832
|
-
const info = await handle.stat();
|
|
833
|
-
if (!info.isFile()) {
|
|
834
|
-
await handle.close();
|
|
835
|
-
handle = undefined;
|
|
836
|
-
throw new Error("Diagnostic journal path must be a regular file");
|
|
837
|
-
}
|
|
838
|
-
currentFileBytes = Number(info.size);
|
|
839
|
-
};
|
|
840
|
-
const rotate = async () => {
|
|
841
|
-
try {
|
|
842
|
-
await handle?.close();
|
|
843
|
-
handle = undefined;
|
|
844
|
-
if (config.maxFiles === 1) {
|
|
845
|
-
await removeIfPresent(journalPath);
|
|
846
|
-
} else {
|
|
847
|
-
await removeIfPresent(generation(journalPath, config.maxFiles - 1));
|
|
848
|
-
for (let index = config.maxFiles - 2;index >= 1; index -= 1) {
|
|
849
|
-
await moveIfPresent(generation(journalPath, index), generation(journalPath, index + 1));
|
|
850
|
-
}
|
|
851
|
-
await moveIfPresent(journalPath, generation(journalPath, 1));
|
|
852
|
-
}
|
|
853
|
-
retainedFiles = Math.min(config.maxFiles, retainedFiles + 1);
|
|
854
|
-
rotations += 1;
|
|
855
|
-
await openCurrent();
|
|
856
|
-
} catch (error) {
|
|
857
|
-
throw new DiagnosticJournalStorageError("rotation", "Diagnostic journal rotation failed", { cause: error });
|
|
858
|
-
}
|
|
859
|
-
};
|
|
860
|
-
try {
|
|
861
|
-
const names = await readdir(parent);
|
|
862
|
-
const prefix = `${basename(journalPath)}.`;
|
|
863
|
-
let existingGenerations = 0;
|
|
864
|
-
for (const name of names) {
|
|
865
|
-
const index = generationIndex(name, prefix);
|
|
866
|
-
if (index === undefined)
|
|
867
|
-
continue;
|
|
868
|
-
if (index >= config.maxFiles) {
|
|
869
|
-
await removeIfPresent(resolve(parent, name));
|
|
870
|
-
} else {
|
|
871
|
-
const info = await lstat(resolve(parent, name));
|
|
872
|
-
if (info.isSymbolicLink() || !info.isFile()) {
|
|
873
|
-
throw new Error("Diagnostic journal generations must be regular files");
|
|
874
|
-
}
|
|
875
|
-
existingGenerations += 1;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
retainedFiles = Math.min(config.maxFiles, existingGenerations + 1);
|
|
879
|
-
await openCurrent();
|
|
880
|
-
if (currentFileBytes > 0) {
|
|
881
|
-
const tail = new Uint8Array(1);
|
|
882
|
-
const read = await handle?.read(tail, 0, 1, currentFileBytes - 1);
|
|
883
|
-
if (read?.bytesRead === 1 && tail[0] !== 10) {
|
|
884
|
-
partialTails += 1;
|
|
885
|
-
await rotate();
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
} catch (error) {
|
|
889
|
-
await handle?.close().catch(() => {
|
|
890
|
-
return;
|
|
891
|
-
});
|
|
892
|
-
await lock.close().catch(() => {
|
|
893
|
-
return;
|
|
894
|
-
});
|
|
895
|
-
await unlink2(lockPath).catch(() => {
|
|
896
|
-
return;
|
|
897
|
-
});
|
|
898
|
-
throw error;
|
|
899
|
-
}
|
|
900
|
-
const snapshot = () => ({
|
|
901
|
-
currentFileBytes,
|
|
902
|
-
retainedFiles,
|
|
903
|
-
rotations,
|
|
904
|
-
partialTails
|
|
905
|
-
});
|
|
906
|
-
return {
|
|
907
|
-
reclaimedStale,
|
|
908
|
-
async append(bytes) {
|
|
909
|
-
if (closed || !handle) {
|
|
910
|
-
throw new DiagnosticJournalStorageError("write", "Diagnostic journal storage is closed");
|
|
911
|
-
}
|
|
912
|
-
let rotated = false;
|
|
913
|
-
if (currentFileBytes > 0 && currentFileBytes + bytes.byteLength > config.maxFileBytes) {
|
|
914
|
-
await rotate();
|
|
915
|
-
rotated = true;
|
|
916
|
-
}
|
|
917
|
-
try {
|
|
918
|
-
await handle.writeFile(bytes);
|
|
919
|
-
currentFileBytes += bytes.byteLength;
|
|
920
|
-
return { ...snapshot(), rotated };
|
|
921
|
-
} catch (error) {
|
|
922
|
-
throw new DiagnosticJournalStorageError("write", "Diagnostic journal write failed", {
|
|
923
|
-
cause: error
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
},
|
|
927
|
-
snapshot,
|
|
928
|
-
async close() {
|
|
929
|
-
if (closed)
|
|
930
|
-
return;
|
|
931
|
-
closed = true;
|
|
932
|
-
let failure;
|
|
933
|
-
try {
|
|
934
|
-
await handle?.close();
|
|
935
|
-
} catch (error) {
|
|
936
|
-
failure = error;
|
|
937
|
-
}
|
|
938
|
-
try {
|
|
939
|
-
await lock.close();
|
|
940
|
-
await unlink2(lockPath);
|
|
941
|
-
} catch (error) {
|
|
942
|
-
failure ??= error;
|
|
943
|
-
}
|
|
944
|
-
if (failure !== undefined) {
|
|
945
|
-
throw new DiagnosticJournalStorageError("close", "Diagnostic journal close failed", {
|
|
946
|
-
cause: failure
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// src/application/diagnostic-journal-manager.ts
|
|
954
|
-
function waitBudget(options) {
|
|
955
|
-
if (options.timeoutMs === undefined)
|
|
956
|
-
return;
|
|
957
|
-
if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
958
|
-
throw new TypeError("Diagnostic journal timeoutMs must be a positive safe integer");
|
|
959
|
-
}
|
|
960
|
-
return options.timeoutMs;
|
|
961
|
-
}
|
|
962
|
-
function createDiagnosticJournalManager(config, storage) {
|
|
963
|
-
const limits = DiagnosticJournalLimitsSchema.parse(config.limits);
|
|
964
|
-
const encoder = new TextEncoder;
|
|
965
|
-
const channel = createBoundedChannel({
|
|
966
|
-
policy: "ordered",
|
|
967
|
-
maxItems: limits.maxPendingItems,
|
|
968
|
-
maxBytes: limits.maxPendingBytes,
|
|
969
|
-
sizeOf: (frame) => frame.bytes.byteLength
|
|
970
|
-
});
|
|
971
|
-
const refusals = {
|
|
972
|
-
closed: 0,
|
|
973
|
-
failed: 0,
|
|
974
|
-
invalid: 0,
|
|
975
|
-
oversized: 0,
|
|
976
|
-
"item-capacity": 0,
|
|
977
|
-
"byte-capacity": 0
|
|
978
|
-
};
|
|
979
|
-
const progressWaiters = new Set;
|
|
980
|
-
let state = "open";
|
|
981
|
-
let received = 0;
|
|
982
|
-
let accepted = 0;
|
|
983
|
-
let refused = 0;
|
|
984
|
-
let written = 0;
|
|
985
|
-
let failedRecords = 0;
|
|
986
|
-
let pendingItems = 0;
|
|
987
|
-
let pendingBytes = 0;
|
|
988
|
-
let inFlight = false;
|
|
989
|
-
let rotationFailures = 0;
|
|
990
|
-
let lastAcceptedSequence = 0;
|
|
991
|
-
let lastWrittenSequence = 0;
|
|
992
|
-
let lastSettledSequence = 0;
|
|
993
|
-
let lastFailure;
|
|
994
|
-
let physicalClosed = false;
|
|
995
|
-
const isFailed = () => state === "failed";
|
|
996
|
-
const notifyProgress = () => {
|
|
997
|
-
for (const waiter of progressWaiters)
|
|
998
|
-
waiter();
|
|
999
|
-
};
|
|
1000
|
-
const reportFailure = (failure) => {
|
|
1001
|
-
if (!config.onFailure)
|
|
1002
|
-
return;
|
|
1003
|
-
Promise.resolve().then(() => config.onFailure?.(failure)).catch(() => {
|
|
1004
|
-
return;
|
|
1005
|
-
});
|
|
1006
|
-
};
|
|
1007
|
-
const markFailure = (error, fallbackPhase, sequence) => {
|
|
1008
|
-
const phase = error instanceof DiagnosticJournalStorageError ? error.phase : fallbackPhase;
|
|
1009
|
-
if (phase === "rotation")
|
|
1010
|
-
rotationFailures += 1;
|
|
1011
|
-
lastFailure = { phase, ...sequence !== undefined && { sequence } };
|
|
1012
|
-
state = "failed";
|
|
1013
|
-
channel.close({ mode: "drain" });
|
|
1014
|
-
reportFailure({ phase, error, ...sequence !== undefined && { sequence } });
|
|
1015
|
-
};
|
|
1016
|
-
const worker = (async () => {
|
|
1017
|
-
for await (const frame of channel) {
|
|
1018
|
-
inFlight = true;
|
|
1019
|
-
if (isFailed()) {
|
|
1020
|
-
failedRecords += 1;
|
|
1021
|
-
} else {
|
|
1022
|
-
try {
|
|
1023
|
-
await storage.append(frame.bytes);
|
|
1024
|
-
written += 1;
|
|
1025
|
-
lastWrittenSequence = frame.sequence;
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
failedRecords += 1;
|
|
1028
|
-
markFailure(error, "write", frame.sequence);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
pendingItems -= 1;
|
|
1032
|
-
pendingBytes -= frame.bytes.byteLength;
|
|
1033
|
-
lastSettledSequence = frame.sequence;
|
|
1034
|
-
inFlight = false;
|
|
1035
|
-
notifyProgress();
|
|
1036
|
-
}
|
|
1037
|
-
try {
|
|
1038
|
-
await storage.close();
|
|
1039
|
-
} catch (error) {
|
|
1040
|
-
markFailure(error, "close");
|
|
1041
|
-
}
|
|
1042
|
-
physicalClosed = true;
|
|
1043
|
-
if (!isFailed())
|
|
1044
|
-
state = "closed";
|
|
1045
|
-
notifyProgress();
|
|
1046
|
-
})();
|
|
1047
|
-
worker.catch((error) => {
|
|
1048
|
-
markFailure(error, "close");
|
|
1049
|
-
physicalClosed = true;
|
|
1050
|
-
notifyProgress();
|
|
1051
|
-
});
|
|
1052
|
-
const refuse = (reason) => {
|
|
1053
|
-
refused += 1;
|
|
1054
|
-
refusals[reason] += 1;
|
|
1055
|
-
return DiagnosticJournalSubmitResultSchema.parse({ outcome: "refused", reason });
|
|
1056
|
-
};
|
|
1057
|
-
const waitUntil = (predicate, options) => {
|
|
1058
|
-
const timeoutMs = waitBudget(options);
|
|
1059
|
-
if (predicate())
|
|
1060
|
-
return Promise.resolve("settled");
|
|
1061
|
-
if (options.signal?.aborted)
|
|
1062
|
-
return Promise.resolve("cancelled");
|
|
1063
|
-
return new Promise((resolve2) => {
|
|
1064
|
-
let settled = false;
|
|
1065
|
-
let timer;
|
|
1066
|
-
const finish = (outcome) => {
|
|
1067
|
-
if (settled)
|
|
1068
|
-
return;
|
|
1069
|
-
settled = true;
|
|
1070
|
-
progressWaiters.delete(onProgress);
|
|
1071
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
1072
|
-
if (timer)
|
|
1073
|
-
clearTimeout(timer);
|
|
1074
|
-
resolve2(outcome);
|
|
1075
|
-
};
|
|
1076
|
-
const onProgress = () => {
|
|
1077
|
-
if (predicate())
|
|
1078
|
-
finish("settled");
|
|
1079
|
-
};
|
|
1080
|
-
const onAbort = () => finish("cancelled");
|
|
1081
|
-
progressWaiters.add(onProgress);
|
|
1082
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1083
|
-
if (timeoutMs !== undefined)
|
|
1084
|
-
timer = setTimeout(() => finish("timed-out"), timeoutMs);
|
|
1085
|
-
onProgress();
|
|
1086
|
-
});
|
|
1087
|
-
};
|
|
1088
|
-
const getStatus = () => {
|
|
1089
|
-
const file = storage.snapshot();
|
|
1090
|
-
return DiagnosticJournalStatusSchema.parse({
|
|
1091
|
-
state,
|
|
1092
|
-
epoch: config.epoch,
|
|
1093
|
-
limits,
|
|
1094
|
-
lock: config.lock,
|
|
1095
|
-
received,
|
|
1096
|
-
accepted,
|
|
1097
|
-
refused,
|
|
1098
|
-
refusals,
|
|
1099
|
-
written,
|
|
1100
|
-
failedRecords,
|
|
1101
|
-
pendingItems,
|
|
1102
|
-
pendingBytes,
|
|
1103
|
-
inFlight,
|
|
1104
|
-
rotations: file.rotations,
|
|
1105
|
-
rotationFailures,
|
|
1106
|
-
partialTails: file.partialTails,
|
|
1107
|
-
currentFileBytes: file.currentFileBytes,
|
|
1108
|
-
retainedFiles: file.retainedFiles,
|
|
1109
|
-
...lastAcceptedSequence > 0 && { lastAcceptedSequence },
|
|
1110
|
-
...lastWrittenSequence > 0 && { lastWrittenSequence },
|
|
1111
|
-
...lastSettledSequence > 0 && { lastSettledSequence },
|
|
1112
|
-
...lastFailure && { lastFailure }
|
|
1113
|
-
});
|
|
1114
|
-
};
|
|
1115
|
-
return {
|
|
1116
|
-
submit(event) {
|
|
1117
|
-
received += 1;
|
|
1118
|
-
if (state === "failed")
|
|
1119
|
-
return refuse("failed");
|
|
1120
|
-
if (state !== "open")
|
|
1121
|
-
return refuse("closed");
|
|
1122
|
-
const parsed = config.eventSchema.safeParse(event);
|
|
1123
|
-
if (!parsed.success)
|
|
1124
|
-
return refuse("invalid");
|
|
1125
|
-
const json = z4.json().safeParse(parsed.data);
|
|
1126
|
-
if (!json.success)
|
|
1127
|
-
return refuse("invalid");
|
|
1128
|
-
const payload = encoder.encode(JSON.stringify(json.data));
|
|
1129
|
-
if (payload.byteLength > limits.maxEventBytes)
|
|
1130
|
-
return refuse("oversized");
|
|
1131
|
-
if (pendingItems >= limits.maxPendingItems)
|
|
1132
|
-
return refuse("item-capacity");
|
|
1133
|
-
const sequence = lastAcceptedSequence + 1;
|
|
1134
|
-
const frame = DiagnosticJournalFrameSchema.parse({
|
|
1135
|
-
schemaVersion: 1,
|
|
1136
|
-
epoch: config.epoch,
|
|
1137
|
-
sequence,
|
|
1138
|
-
event: json.data
|
|
1139
|
-
});
|
|
1140
|
-
const bytes = encoder.encode(`${JSON.stringify(frame)}
|
|
1141
|
-
`);
|
|
1142
|
-
if (bytes.byteLength > limits.maxFileBytes)
|
|
1143
|
-
return refuse("oversized");
|
|
1144
|
-
if (pendingBytes + bytes.byteLength > limits.maxPendingBytes) {
|
|
1145
|
-
return refuse("byte-capacity");
|
|
1146
|
-
}
|
|
1147
|
-
const offered = channel.offer({ sequence, bytes });
|
|
1148
|
-
if (offered.outcome === "refused") {
|
|
1149
|
-
return refuse(offered.reason === "not-open" ? "closed" : "item-capacity");
|
|
1150
|
-
}
|
|
1151
|
-
accepted += 1;
|
|
1152
|
-
pendingItems += 1;
|
|
1153
|
-
pendingBytes += bytes.byteLength;
|
|
1154
|
-
lastAcceptedSequence = sequence;
|
|
1155
|
-
return DiagnosticJournalSubmitResultSchema.parse({
|
|
1156
|
-
outcome: "accepted",
|
|
1157
|
-
epoch: config.epoch,
|
|
1158
|
-
sequence
|
|
1159
|
-
});
|
|
1160
|
-
},
|
|
1161
|
-
async flush(options = {}) {
|
|
1162
|
-
const throughSequence = lastAcceptedSequence;
|
|
1163
|
-
const outcome = await waitUntil(() => lastSettledSequence >= throughSequence, options);
|
|
1164
|
-
return DiagnosticJournalWaitResultSchema.parse({
|
|
1165
|
-
outcome,
|
|
1166
|
-
state,
|
|
1167
|
-
throughSequence,
|
|
1168
|
-
settledSequence: lastSettledSequence
|
|
1169
|
-
});
|
|
1170
|
-
},
|
|
1171
|
-
getStatus,
|
|
1172
|
-
async close(options = {}) {
|
|
1173
|
-
if (state === "open") {
|
|
1174
|
-
state = "draining";
|
|
1175
|
-
channel.close({ mode: "drain" });
|
|
1176
|
-
}
|
|
1177
|
-
const wait = await waitUntil(() => physicalClosed, options);
|
|
1178
|
-
return DiagnosticJournalCloseResultSchema.parse({
|
|
1179
|
-
outcome: wait === "settled" ? "closed" : wait,
|
|
1180
|
-
state,
|
|
1181
|
-
pendingItems
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// src/application/diagnostic-journal.ts
|
|
1188
|
-
async function createDiagnosticJournal(config) {
|
|
1189
|
-
const limits = DiagnosticJournalLimitsSchema.parse(config.limits);
|
|
1190
|
-
const lock = DiagnosticJournalLockPolicySchema.parse(config.lock ?? "refuse");
|
|
1191
|
-
const storage = await createRotatingDiagnosticJournalStorage({
|
|
1192
|
-
path: config.path,
|
|
1193
|
-
maxFileBytes: limits.maxFileBytes,
|
|
1194
|
-
maxFiles: limits.maxFiles,
|
|
1195
|
-
mode: parseDiagnosticJournalMode(config.mode),
|
|
1196
|
-
lock,
|
|
1197
|
-
...config.machineIdentity !== undefined && { machineIdentity: config.machineIdentity }
|
|
1198
|
-
});
|
|
1199
|
-
return createDiagnosticJournalManager({
|
|
1200
|
-
eventSchema: config.eventSchema,
|
|
1201
|
-
epoch: randomUUID(),
|
|
1202
|
-
limits,
|
|
1203
|
-
lock: { policy: lock, reclaimedStale: storage.reclaimedStale },
|
|
1204
|
-
...config.onFailure && { onFailure: config.onFailure }
|
|
1205
|
-
}, storage);
|
|
1206
|
-
}
|
|
1207
538
|
// src/application/events.ts
|
|
1208
|
-
import { z as
|
|
1209
|
-
var ApplicationLifecycleEventSchema =
|
|
1210
|
-
type:
|
|
539
|
+
import { z as z3 } from "zod";
|
|
540
|
+
var ApplicationLifecycleEventSchema = z3.object({
|
|
541
|
+
type: z3.literal("application-state"),
|
|
1211
542
|
applicationId: ApplicationIdSchema,
|
|
1212
|
-
epoch:
|
|
1213
|
-
revision:
|
|
543
|
+
epoch: z3.string().uuid(),
|
|
544
|
+
revision: z3.number().int().nonnegative(),
|
|
1214
545
|
lifecycle: ApplicationLifecycleSchema,
|
|
1215
546
|
health: ApplicationHealthSchema,
|
|
1216
|
-
ready:
|
|
1217
|
-
capturedAt:
|
|
1218
|
-
resources:
|
|
547
|
+
ready: z3.boolean(),
|
|
548
|
+
capturedAt: z3.string().datetime({ offset: true }),
|
|
549
|
+
resources: z3.array(ManagedResourceSnapshotSchema).readonly()
|
|
1219
550
|
}).strict().readonly();
|
|
1220
551
|
function applicationLifecycleEvent(snapshot) {
|
|
1221
552
|
return ApplicationLifecycleEventSchema.parse({
|
|
@@ -1246,13 +577,13 @@ function createApplicationEventSink(config) {
|
|
|
1246
577
|
};
|
|
1247
578
|
}
|
|
1248
579
|
// src/application/health.ts
|
|
1249
|
-
import { z as
|
|
1250
|
-
var ApplicationHealthHandlerOptionsSchema =
|
|
1251
|
-
kind:
|
|
1252
|
-
retryAfterSeconds:
|
|
580
|
+
import { z as z4 } from "zod";
|
|
581
|
+
var ApplicationHealthHandlerOptionsSchema = z4.object({
|
|
582
|
+
kind: z4.enum(["liveness", "readiness"]),
|
|
583
|
+
retryAfterSeconds: z4.number().int().nonnegative().default(5)
|
|
1253
584
|
});
|
|
1254
|
-
var ApplicationOperationalHandlersOptionsSchema =
|
|
1255
|
-
retryAfterSeconds:
|
|
585
|
+
var ApplicationOperationalHandlersOptionsSchema = z4.object({
|
|
586
|
+
retryAfterSeconds: z4.number().int().nonnegative().default(5)
|
|
1256
587
|
});
|
|
1257
588
|
function createApplicationHealthHandler(application, options) {
|
|
1258
589
|
const parsed = ApplicationHealthHandlerOptionsSchema.parse(options);
|
|
@@ -1302,7 +633,7 @@ function keyspaceResource(declaration, config) {
|
|
|
1302
633
|
id: config.id ?? `keyspace:${declaration.name}`,
|
|
1303
634
|
...config.dependsOn && { dependsOn: config.dependsOn },
|
|
1304
635
|
async start(_context) {
|
|
1305
|
-
await machine.
|
|
636
|
+
await machine.open();
|
|
1306
637
|
return { value: machine.keyspace };
|
|
1307
638
|
},
|
|
1308
639
|
stopAdmission: machine.stopAdmission,
|
|
@@ -1315,9 +646,14 @@ function keyspaceMachine(declaration, config) {
|
|
|
1315
646
|
const maxPending = config.maxPendingWrites ?? 1024;
|
|
1316
647
|
const records = new Map;
|
|
1317
648
|
let admitting = true;
|
|
649
|
+
let backend;
|
|
650
|
+
const currentBackend = () => {
|
|
651
|
+
backend ??= typeof config.backend === "function" ? config.backend() : config.backend;
|
|
652
|
+
return backend;
|
|
653
|
+
};
|
|
1318
654
|
let pending = 0;
|
|
1319
655
|
let chain = Promise.resolve();
|
|
1320
|
-
function
|
|
656
|
+
function run(work) {
|
|
1321
657
|
if (!admitting) {
|
|
1322
658
|
return Promise.reject(new Error(`[stitchkit] keyspace "${declaration.name}" is shutting down and is not accepting writes.`));
|
|
1323
659
|
}
|
|
@@ -1344,8 +680,8 @@ function keyspaceMachine(declaration, config) {
|
|
|
1344
680
|
put(value) {
|
|
1345
681
|
const parsed = declaration.schema.parse(value);
|
|
1346
682
|
const key = declaration.key(parsed);
|
|
1347
|
-
return
|
|
1348
|
-
await
|
|
683
|
+
return run(async () => {
|
|
684
|
+
await currentBackend().put(key, parsed);
|
|
1349
685
|
records.set(key, parsed);
|
|
1350
686
|
config.onChanged?.({
|
|
1351
687
|
keyspace: declaration.name,
|
|
@@ -1356,8 +692,8 @@ function keyspaceMachine(declaration, config) {
|
|
|
1356
692
|
});
|
|
1357
693
|
},
|
|
1358
694
|
delete(key) {
|
|
1359
|
-
return
|
|
1360
|
-
await
|
|
695
|
+
return run(async () => {
|
|
696
|
+
await currentBackend().delete(key);
|
|
1361
697
|
records.delete(key);
|
|
1362
698
|
config.onChanged?.({ keyspace: declaration.name, key, change: "delete" });
|
|
1363
699
|
});
|
|
@@ -1371,11 +707,19 @@ function keyspaceMachine(declaration, config) {
|
|
|
1371
707
|
return {
|
|
1372
708
|
keyspace,
|
|
1373
709
|
async load() {
|
|
1374
|
-
for (const stored of await
|
|
710
|
+
for (const stored of await currentBackend().load()) {
|
|
1375
711
|
const value = declaration.schema.parse(stored);
|
|
1376
712
|
records.set(declaration.key(value), value);
|
|
1377
713
|
}
|
|
1378
714
|
},
|
|
715
|
+
async open() {
|
|
716
|
+
backend = undefined;
|
|
717
|
+
records.clear();
|
|
718
|
+
chain = Promise.resolve();
|
|
719
|
+
pending = 0;
|
|
720
|
+
admitting = true;
|
|
721
|
+
await this.load();
|
|
722
|
+
},
|
|
1379
723
|
stopAdmission() {
|
|
1380
724
|
admitting = false;
|
|
1381
725
|
},
|
|
@@ -1384,7 +728,7 @@ function keyspaceMachine(declaration, config) {
|
|
|
1384
728
|
},
|
|
1385
729
|
async close() {
|
|
1386
730
|
reportUnwritten();
|
|
1387
|
-
await
|
|
731
|
+
await backend?.close?.();
|
|
1388
732
|
},
|
|
1389
733
|
force() {
|
|
1390
734
|
reportUnwritten();
|
|
@@ -1475,6 +819,11 @@ function managedServerResource(config) {
|
|
|
1475
819
|
...config.dependsOn && { dependsOn: config.dependsOn },
|
|
1476
820
|
...config.required !== undefined && { required: config.required },
|
|
1477
821
|
async start(context) {
|
|
822
|
+
if (startAttempted && typeof config.server !== "function") {
|
|
823
|
+
throw new Error(`[stitchkit] managed server "${config.id}" cannot be restarted: it was given a server instance, and that one has been shut down. Pass a factory — \`server: (context) => …\` — so each generation gets its own.`);
|
|
824
|
+
}
|
|
825
|
+
shutdownPromise = undefined;
|
|
826
|
+
started = undefined;
|
|
1478
827
|
startAttempted = true;
|
|
1479
828
|
started = await resolveServer(context);
|
|
1480
829
|
return { value: started };
|
|
@@ -1493,109 +842,6 @@ function managedServerResource(config) {
|
|
|
1493
842
|
}
|
|
1494
843
|
});
|
|
1495
844
|
}
|
|
1496
|
-
// src/browser/resumable.ts
|
|
1497
|
-
import { z as z7 } from "zod";
|
|
1498
|
-
var PositiveSafeIntegerSchema3 = z7.number().int().positive().safe();
|
|
1499
|
-
var BackoffPolicySchema = z7.object({
|
|
1500
|
-
minDelayMs: PositiveSafeIntegerSchema3,
|
|
1501
|
-
maxDelayMs: PositiveSafeIntegerSchema3,
|
|
1502
|
-
jitter: z7.number().min(0).max(1)
|
|
1503
|
-
}).strict().readonly().refine((policy) => policy.maxDelayMs >= policy.minDelayMs, {
|
|
1504
|
-
message: "maxDelayMs must be at least minDelayMs"
|
|
1505
|
-
});
|
|
1506
|
-
function createBackoff(policy, random = Math.random) {
|
|
1507
|
-
const { minDelayMs, maxDelayMs, jitter } = BackoffPolicySchema.parse(policy);
|
|
1508
|
-
let attempt = 0;
|
|
1509
|
-
return {
|
|
1510
|
-
next() {
|
|
1511
|
-
const exponential = Math.min(maxDelayMs, minDelayMs * 2 ** attempt);
|
|
1512
|
-
attempt += 1;
|
|
1513
|
-
const spread = exponential * jitter * random();
|
|
1514
|
-
return Math.max(1, Math.round(exponential - spread));
|
|
1515
|
-
},
|
|
1516
|
-
reset() {
|
|
1517
|
-
attempt = 0;
|
|
1518
|
-
}
|
|
1519
|
-
};
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
// src/live/watch-contract.ts
|
|
1523
|
-
import { z as z9 } from "zod";
|
|
1524
|
-
|
|
1525
|
-
// src/browser/live-state.ts
|
|
1526
|
-
import { z as z8 } from "zod";
|
|
1527
|
-
var PositiveSafeIntegerSchema4 = z8.number().int().positive().safe();
|
|
1528
|
-
var LiveStatePhaseSchema = z8.enum([
|
|
1529
|
-
"idle",
|
|
1530
|
-
"opening",
|
|
1531
|
-
"live",
|
|
1532
|
-
"resync-required",
|
|
1533
|
-
"unavailable",
|
|
1534
|
-
"closed"
|
|
1535
|
-
]);
|
|
1536
|
-
var LiveStateStopReasonSchema = z8.enum([
|
|
1537
|
-
"gap",
|
|
1538
|
-
"buffer-overflow",
|
|
1539
|
-
"source-unavailable",
|
|
1540
|
-
"source-error",
|
|
1541
|
-
"controller-error",
|
|
1542
|
-
"controller-capacity"
|
|
1543
|
-
]);
|
|
1544
|
-
var LiveStateControllerStatusSchema = z8.object({
|
|
1545
|
-
phase: LiveStatePhaseSchema,
|
|
1546
|
-
generation: z8.number().int().nonnegative(),
|
|
1547
|
-
hasValue: z8.boolean(),
|
|
1548
|
-
bufferedEvents: z8.number().int().nonnegative(),
|
|
1549
|
-
bufferedBytes: z8.number().int().nonnegative(),
|
|
1550
|
-
receivedEvents: z8.number().int().nonnegative(),
|
|
1551
|
-
appliedEvents: z8.number().int().nonnegative(),
|
|
1552
|
-
duplicateEvents: z8.number().int().nonnegative(),
|
|
1553
|
-
gapEvents: z8.number().int().nonnegative(),
|
|
1554
|
-
refusedEvents: z8.number().int().nonnegative(),
|
|
1555
|
-
reason: LiveStateStopReasonSchema.optional()
|
|
1556
|
-
}).strict().readonly();
|
|
1557
|
-
|
|
1558
|
-
// src/live/watch-contract.ts
|
|
1559
|
-
var WatchKeySchema = z9.object({
|
|
1560
|
-
service: z9.string().min(1),
|
|
1561
|
-
action: z9.string().min(1),
|
|
1562
|
-
digest: z9.string().min(1)
|
|
1563
|
-
}).strict().readonly();
|
|
1564
|
-
var WatchOpenSchema = z9.object({ key: WatchKeySchema, args: z9.unknown() }).readonly();
|
|
1565
|
-
var WatchAcceptedSchema = z9.object({
|
|
1566
|
-
accepted: z9.boolean(),
|
|
1567
|
-
reason: z9.string().optional()
|
|
1568
|
-
}).strict().readonly();
|
|
1569
|
-
var WatchValueSchema = z9.object({
|
|
1570
|
-
key: WatchKeySchema,
|
|
1571
|
-
revision: z9.number().int().nonnegative(),
|
|
1572
|
-
value: z9.unknown()
|
|
1573
|
-
}).readonly();
|
|
1574
|
-
var WatchStateSchema = z9.object({
|
|
1575
|
-
key: WatchKeySchema,
|
|
1576
|
-
phase: LiveStatePhaseSchema,
|
|
1577
|
-
reason: LiveStateStopReasonSchema.optional(),
|
|
1578
|
-
code: z9.string().optional(),
|
|
1579
|
-
message: z9.string().optional()
|
|
1580
|
-
}).readonly();
|
|
1581
|
-
var WATCH_OPEN = "stitchkit.watch.open";
|
|
1582
|
-
var WATCH_CLOSE = "stitchkit.watch.close";
|
|
1583
|
-
var WATCH_VALUE = "stitchkit.watch.value";
|
|
1584
|
-
var WATCH_STATE = "stitchkit.watch.state";
|
|
1585
|
-
var watchContract = {
|
|
1586
|
-
serverToClient: {
|
|
1587
|
-
[WATCH_VALUE]: { args: z9.tuple([WatchValueSchema]) },
|
|
1588
|
-
[WATCH_STATE]: { args: z9.tuple([WatchStateSchema]) }
|
|
1589
|
-
},
|
|
1590
|
-
clientToServer: {
|
|
1591
|
-
[WATCH_OPEN]: { args: z9.tuple([WatchOpenSchema]), ack: WatchAcceptedSchema },
|
|
1592
|
-
[WATCH_CLOSE]: { args: z9.tuple([z9.object({ key: WatchKeySchema }).readonly()]) }
|
|
1593
|
-
}
|
|
1594
|
-
};
|
|
1595
|
-
function watchKeyString(key) {
|
|
1596
|
-
return `${key.service}/${key.action}/${key.digest}`;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
845
|
// src/application/watch-hub.ts
|
|
1600
846
|
var DEFAULT_BACKOFF = { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
|
|
1601
847
|
function watchKey(operation, args) {
|
|
@@ -1614,17 +860,29 @@ function createWatchHub(config) {
|
|
|
1614
860
|
const same = config.same ?? ((previous, next) => JSON.stringify(stableValue(previous)) === JSON.stringify(stableValue(next)));
|
|
1615
861
|
let reads = 0;
|
|
1616
862
|
let closed = false;
|
|
863
|
+
function tell(key, deliver) {
|
|
864
|
+
try {
|
|
865
|
+
deliver();
|
|
866
|
+
} catch (error) {
|
|
867
|
+
config.logger?.warn?.("[stitchkit] watch subscriber threw", {
|
|
868
|
+
key: watchKeyString(key),
|
|
869
|
+
error: error instanceof Error ? error.message : String(error)
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
1617
873
|
function announceState(source, state) {
|
|
1618
874
|
source.state = state;
|
|
1619
|
-
for (const subscriber of source.subscribers)
|
|
1620
|
-
subscriber.state(state);
|
|
875
|
+
for (const subscriber of source.subscribers) {
|
|
876
|
+
tell(source.key, () => subscriber.state(state));
|
|
877
|
+
}
|
|
1621
878
|
}
|
|
1622
879
|
function publish(source, value) {
|
|
1623
880
|
source.revision += 1;
|
|
1624
881
|
source.value = value;
|
|
1625
882
|
const frame = { key: source.key, revision: source.revision, value };
|
|
1626
|
-
for (const subscriber of source.subscribers)
|
|
1627
|
-
subscriber.value(frame);
|
|
883
|
+
for (const subscriber of source.subscribers) {
|
|
884
|
+
tell(source.key, () => subscriber.value(frame));
|
|
885
|
+
}
|
|
1628
886
|
}
|
|
1629
887
|
async function pump(source) {
|
|
1630
888
|
if (source.reading || closed)
|
|
@@ -1754,10 +1012,20 @@ function createWatchHub(config) {
|
|
|
1754
1012
|
const source = acquire(operation, key, args);
|
|
1755
1013
|
source.subscribers.add(subscriber);
|
|
1756
1014
|
keys.add(id);
|
|
1757
|
-
|
|
1758
|
-
|
|
1015
|
+
try {
|
|
1016
|
+
if (source.signature !== undefined) {
|
|
1017
|
+
subscriber.value({ key, revision: source.revision, value: source.value });
|
|
1018
|
+
}
|
|
1019
|
+
subscriber.state(source.state);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
source.subscribers.delete(subscriber);
|
|
1022
|
+
keys.delete(id);
|
|
1023
|
+
release(source);
|
|
1024
|
+
return {
|
|
1025
|
+
accepted: false,
|
|
1026
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1027
|
+
};
|
|
1759
1028
|
}
|
|
1760
|
-
subscriber.state(source.state);
|
|
1761
1029
|
pump(source);
|
|
1762
1030
|
return { accepted: true };
|
|
1763
1031
|
},
|
|
@@ -1788,14 +1056,20 @@ function createWatchHub(config) {
|
|
|
1788
1056
|
size: () => sources.size,
|
|
1789
1057
|
close() {
|
|
1790
1058
|
closed = true;
|
|
1791
|
-
|
|
1059
|
+
const teardown = [...sources.values()];
|
|
1060
|
+
sources.clear();
|
|
1061
|
+
held.clear();
|
|
1062
|
+
for (const source of teardown) {
|
|
1792
1063
|
clearTimeout(source.retry);
|
|
1793
1064
|
clearTimeout(source.release);
|
|
1065
|
+
announceState(source, {
|
|
1066
|
+
key: source.key,
|
|
1067
|
+
phase: "unavailable",
|
|
1068
|
+
reason: "source-unavailable"
|
|
1069
|
+
});
|
|
1794
1070
|
for (const unsubscribe of source.unsubscribes)
|
|
1795
1071
|
unsubscribe();
|
|
1796
1072
|
}
|
|
1797
|
-
sources.clear();
|
|
1798
|
-
held.clear();
|
|
1799
1073
|
}
|
|
1800
1074
|
};
|
|
1801
1075
|
}
|
|
@@ -1819,7 +1093,6 @@ export {
|
|
|
1819
1093
|
defineKeyspace,
|
|
1820
1094
|
createWatchHub,
|
|
1821
1095
|
createManagedSchedule,
|
|
1822
|
-
createDiagnosticJournal,
|
|
1823
1096
|
createDecisionPipeline,
|
|
1824
1097
|
createCreditWindow,
|
|
1825
1098
|
createBoundedChannel,
|