@rex0220/ksql-flownet 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +81 -0
  3. package/README.md +117 -0
  4. package/dist/bundle/bundle-builder.d.ts +49 -0
  5. package/dist/bundle/bundle-builder.js +208 -0
  6. package/dist/bundle/index.d.ts +4 -0
  7. package/dist/bundle/index.js +4 -0
  8. package/dist/bundle/upload.d.ts +28 -0
  9. package/dist/bundle/upload.js +72 -0
  10. package/dist/bundle/zip-store.d.ts +17 -0
  11. package/dist/bundle/zip-store.js +175 -0
  12. package/dist/cli/archive-run-command.d.ts +12 -0
  13. package/dist/cli/archive-run-command.js +131 -0
  14. package/dist/cli/cancel-run-command.d.ts +9 -0
  15. package/dist/cli/cancel-run-command.js +77 -0
  16. package/dist/cli/force-unlock-network-command.d.ts +8 -0
  17. package/dist/cli/force-unlock-network-command.js +129 -0
  18. package/dist/cli/index.d.ts +3 -0
  19. package/dist/cli/index.js +132 -0
  20. package/dist/cli/plan-command.d.ts +2 -0
  21. package/dist/cli/plan-command.js +100 -0
  22. package/dist/cli/poll-requests-command.d.ts +3 -0
  23. package/dist/cli/poll-requests-command.js +74 -0
  24. package/dist/cli/record-job-unlock-command.d.ts +11 -0
  25. package/dist/cli/record-job-unlock-command.js +78 -0
  26. package/dist/cli/resolve-node-command.d.ts +13 -0
  27. package/dist/cli/resolve-node-command.js +174 -0
  28. package/dist/cli/run-network-command.d.ts +14 -0
  29. package/dist/cli/run-network-command.js +418 -0
  30. package/dist/cli/status-command.d.ts +5 -0
  31. package/dist/cli/status-command.js +137 -0
  32. package/dist/cli/validate-command.d.ts +6 -0
  33. package/dist/cli/validate-command.js +17 -0
  34. package/dist/dag/descendants.d.ts +9 -0
  35. package/dist/dag/descendants.js +67 -0
  36. package/dist/dag/topological-sort.d.ts +7 -0
  37. package/dist/dag/topological-sort.js +32 -0
  38. package/dist/domain/business-key.d.ts +15 -0
  39. package/dist/domain/business-key.js +249 -0
  40. package/dist/domain/canonical-lock-key.d.ts +17 -0
  41. package/dist/domain/canonical-lock-key.js +60 -0
  42. package/dist/domain/canonical-record-key.d.ts +18 -0
  43. package/dist/domain/canonical-record-key.js +70 -0
  44. package/dist/domain/load-network.d.ts +4 -0
  45. package/dist/domain/load-network.js +47 -0
  46. package/dist/domain/network-definition.d.ts +47 -0
  47. package/dist/domain/network-definition.js +7 -0
  48. package/dist/domain/persistence-model.d.ts +198 -0
  49. package/dist/domain/persistence-model.js +2 -0
  50. package/dist/domain/run-aggregate.d.ts +3 -0
  51. package/dist/domain/run-aggregate.js +15 -0
  52. package/dist/domain/validate-network-path.d.ts +11 -0
  53. package/dist/domain/validate-network-path.js +33 -0
  54. package/dist/domain/validate-network.d.ts +3 -0
  55. package/dist/domain/validate-network.js +180 -0
  56. package/dist/executor/attempt-executor.d.ts +43 -0
  57. package/dist/executor/attempt-executor.js +236 -0
  58. package/dist/executor/job-log-reader.d.ts +37 -0
  59. package/dist/executor/job-log-reader.js +91 -0
  60. package/dist/executor/ksql-flow-cli.d.ts +86 -0
  61. package/dist/executor/ksql-flow-cli.js +161 -0
  62. package/dist/executor/preflight.d.ts +41 -0
  63. package/dist/executor/preflight.js +150 -0
  64. package/dist/executor/result-classifier.d.ts +69 -0
  65. package/dist/executor/result-classifier.js +237 -0
  66. package/dist/executor/run-subprocess.d.ts +64 -0
  67. package/dist/executor/run-subprocess.js +175 -0
  68. package/dist/io/input-baseline.d.ts +19 -0
  69. package/dist/io/input-baseline.js +214 -0
  70. package/dist/io/io-config.d.ts +6 -0
  71. package/dist/io/io-config.js +33 -0
  72. package/dist/io/io-path.d.ts +37 -0
  73. package/dist/io/io-path.js +279 -0
  74. package/dist/orchestration/archive-run.d.ts +53 -0
  75. package/dist/orchestration/archive-run.js +323 -0
  76. package/dist/orchestration/cancel-request.d.ts +18 -0
  77. package/dist/orchestration/cancel-request.js +67 -0
  78. package/dist/orchestration/ensure-run.d.ts +83 -0
  79. package/dist/orchestration/ensure-run.js +632 -0
  80. package/dist/orchestration/job-lock-audit.d.ts +24 -0
  81. package/dist/orchestration/job-lock-audit.js +123 -0
  82. package/dist/orchestration/orphan-attempt-adjudication.d.ts +39 -0
  83. package/dist/orchestration/orphan-attempt-adjudication.js +167 -0
  84. package/dist/orchestration/reconciliation.d.ts +41 -0
  85. package/dist/orchestration/reconciliation.js +526 -0
  86. package/dist/orchestration/resolve-node.d.ts +29 -0
  87. package/dist/orchestration/resolve-node.js +105 -0
  88. package/dist/orchestration/run-activity.d.ts +13 -0
  89. package/dist/orchestration/run-activity.js +14 -0
  90. package/dist/orchestration/sequential-scheduler.d.ts +79 -0
  91. package/dist/orchestration/sequential-scheduler.js +706 -0
  92. package/dist/orchestration/status.d.ts +91 -0
  93. package/dist/orchestration/status.js +171 -0
  94. package/dist/persistence/in-memory-repository.d.ts +41 -0
  95. package/dist/persistence/in-memory-repository.js +333 -0
  96. package/dist/persistence/kintone/client.d.ts +36 -0
  97. package/dist/persistence/kintone/client.js +93 -0
  98. package/dist/persistence/kintone/design-notes.d.ts +38 -0
  99. package/dist/persistence/kintone/design-notes.js +38 -0
  100. package/dist/persistence/kintone/repository.d.ts +44 -0
  101. package/dist/persistence/kintone/repository.js +836 -0
  102. package/dist/persistence/kintone/schema.d.ts +116 -0
  103. package/dist/persistence/kintone/schema.js +115 -0
  104. package/dist/persistence/network-lock-reader.d.ts +24 -0
  105. package/dist/persistence/network-lock-reader.js +43 -0
  106. package/dist/persistence/network-lock-recovery.d.ts +66 -0
  107. package/dist/persistence/network-lock-recovery.js +392 -0
  108. package/dist/persistence/network-lock.d.ts +97 -0
  109. package/dist/persistence/network-lock.js +408 -0
  110. package/dist/persistence/repository.d.ts +101 -0
  111. package/dist/persistence/repository.js +11 -0
  112. package/dist/persistence/state-transition.d.ts +4 -0
  113. package/dist/persistence/state-transition.js +25 -0
  114. package/dist/requests/flownet-child-client.d.ts +78 -0
  115. package/dist/requests/flownet-child-client.js +243 -0
  116. package/dist/requests/kintone-request-store.d.ts +44 -0
  117. package/dist/requests/kintone-request-store.js +202 -0
  118. package/dist/requests/poll-requests-config.d.ts +26 -0
  119. package/dist/requests/poll-requests-config.js +105 -0
  120. package/dist/requests/request-model.d.ts +57 -0
  121. package/dist/requests/request-model.js +305 -0
  122. package/dist/requests/request-poller.d.ts +33 -0
  123. package/dist/requests/request-poller.js +353 -0
  124. package/dist/requests/request-result.d.ts +18 -0
  125. package/dist/requests/request-result.js +215 -0
  126. package/dist/requests/start-request.d.ts +26 -0
  127. package/dist/requests/start-request.js +83 -0
  128. package/package.json +63 -0
  129. package/schemas/.gitkeep +1 -0
  130. package/schemas/network-definition.schema.json +121 -0
@@ -0,0 +1,279 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, mkdir, open, realpath } from "node:fs/promises";
4
+ import { dirname, isAbsolute, relative, resolve, sep, win32 } from "node:path";
5
+ export class InputPathError extends Error {
6
+ code;
7
+ constructor(code, message, options) {
8
+ super(message, options);
9
+ this.code = code;
10
+ this.name = "InputPathError";
11
+ }
12
+ }
13
+ export class OutputPathError extends Error {
14
+ code = "OUTPUT_PATH_REJECTED";
15
+ constructor(message, options) {
16
+ super(message, options);
17
+ this.name = "OutputPathError";
18
+ }
19
+ }
20
+ export async function resolveNodeOutputs(input) {
21
+ const substitutedPatterns = Object.keys(input.patterns)
22
+ .sort(compareText)
23
+ .map((name) => {
24
+ const pattern = substituteOutputPattern(input.patterns[name], {
25
+ business_key: input.businessKey,
26
+ profile: input.profile,
27
+ run_id: input.runId,
28
+ node_id: input.nodeId,
29
+ });
30
+ assertRelativeOutputPattern(pattern);
31
+ return { name, pattern };
32
+ });
33
+ const ioRoot = resolve(input.ioRoot);
34
+ await requireOutputDirectory(ioRoot, "IO root");
35
+ const canonicalRoot = await outputRealpath(ioRoot, "IO root");
36
+ const outputRoot = resolve(ioRoot, "out");
37
+ assertOutputContained(ioRoot, outputRoot);
38
+ await ensureOutputDirectories(ioRoot, outputRoot);
39
+ const outputs = [];
40
+ for (const { name, pattern } of substitutedPatterns) {
41
+ const candidate = resolve(outputRoot, pattern);
42
+ assertOutputContained(outputRoot, candidate);
43
+ const parent = dirname(candidate);
44
+ await ensureOutputDirectories(ioRoot, parent);
45
+ const canonicalParent = await outputRealpath(parent, "output parent");
46
+ assertOutputContained(canonicalRoot, canonicalParent, true);
47
+ await inspectOutputTarget(candidate, canonicalRoot);
48
+ outputs.push({ name, path: candidate });
49
+ }
50
+ return outputs;
51
+ }
52
+ export async function resolveNodeInputs(input) {
53
+ const inputRoot = resolve(input.ioRoot, "in");
54
+ const canonicalRoot = await requiredRealpath(inputRoot);
55
+ await requireDirectory(inputRoot, "input root");
56
+ const baselines = [];
57
+ for (const name of Object.keys(input.patterns).sort(compareText)) {
58
+ const pattern = input.patterns[name];
59
+ const substituted = substitutePattern(pattern, {
60
+ business_key: input.businessKey,
61
+ profile: input.profile,
62
+ });
63
+ assertRelativePattern(substituted);
64
+ const candidate = resolve(inputRoot, substituted);
65
+ assertContained(inputRoot, candidate);
66
+ await inspectComponents(inputRoot, candidate);
67
+ const canonicalCandidate = await requiredRealpath(candidate);
68
+ assertContained(canonicalRoot, canonicalCandidate);
69
+ let handle;
70
+ try {
71
+ handle = await open(candidate, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
72
+ }
73
+ catch (error) {
74
+ throw fromFsError(error, `cannot open input '${name}'`);
75
+ }
76
+ try {
77
+ const stat = await handle.stat();
78
+ if (!stat.isFile()) {
79
+ throw new InputPathError("INPUT_PATH_REJECTED", `input '${name}' is not a regular file`);
80
+ }
81
+ const hash = createHash("sha256");
82
+ let bytes = 0;
83
+ for await (const chunk of handle.createReadStream({ autoClose: false })) {
84
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
85
+ bytes += buffer.byteLength;
86
+ hash.update(buffer);
87
+ }
88
+ baselines.push({
89
+ name,
90
+ path: candidate,
91
+ sha256: hash.digest("hex"),
92
+ bytes,
93
+ });
94
+ }
95
+ finally {
96
+ await handle.close();
97
+ }
98
+ }
99
+ return baselines;
100
+ }
101
+ export function percentEncodePathSegment(value) {
102
+ return [...Buffer.from(value, "utf8")]
103
+ .map((byte) => (byte >= 0x30 && byte <= 0x39) ||
104
+ (byte >= 0x41 && byte <= 0x5a) ||
105
+ (byte >= 0x61 && byte <= 0x7a) ||
106
+ byte === 0x2d ||
107
+ byte === 0x5f ||
108
+ byte === 0x7e
109
+ ? String.fromCharCode(byte)
110
+ : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`)
111
+ .join("");
112
+ }
113
+ function substitutePattern(pattern, values) {
114
+ return pattern.replace(/\{(business_key|profile)\}/gu, (_match, key) => percentEncodePathSegment(values[key]));
115
+ }
116
+ function substituteOutputPattern(pattern, values) {
117
+ return pattern.replace(/\{(business_key|profile|run_id|node_id)\}/gu, (_match, key) => percentEncodePathSegment(values[key]));
118
+ }
119
+ function assertRelativeOutputPattern(pattern) {
120
+ if (pattern.length === 0 ||
121
+ pattern.includes("\0") ||
122
+ isAbsolute(pattern) ||
123
+ win32.isAbsolute(pattern) ||
124
+ /^[A-Za-z]:/u.test(pattern) ||
125
+ pattern.split(/[\\/]/u).some((part) => part === "." || part === "..")) {
126
+ throw new OutputPathError("output pattern does not resolve to an allowed relative path");
127
+ }
128
+ }
129
+ async function ensureOutputDirectories(root, targetDirectory) {
130
+ assertOutputContained(root, targetDirectory, true);
131
+ const rel = relative(root, targetDirectory);
132
+ const parts = rel === "" ? [] : rel.split(sep);
133
+ let current = root;
134
+ await requireOutputDirectory(current, "IO root");
135
+ for (const part of parts) {
136
+ current = resolve(current, part);
137
+ try {
138
+ await mkdir(current);
139
+ }
140
+ catch (error) {
141
+ if (fsErrorCode(error) !== "EEXIST") {
142
+ throw new OutputPathError("output directory could not be created", {
143
+ cause: error,
144
+ });
145
+ }
146
+ }
147
+ await requireOutputDirectory(current, "output path component");
148
+ }
149
+ }
150
+ async function inspectOutputTarget(candidate, canonicalRoot) {
151
+ let stat;
152
+ try {
153
+ stat = await lstat(candidate);
154
+ }
155
+ catch (error) {
156
+ if (fsErrorCode(error) === "ENOENT")
157
+ return;
158
+ throw new OutputPathError("output target could not be inspected", {
159
+ cause: error,
160
+ });
161
+ }
162
+ if (stat.isSymbolicLink() || !stat.isFile()) {
163
+ throw new OutputPathError("output target is not a regular file or is a symbolic link or junction");
164
+ }
165
+ const canonicalCandidate = await outputRealpath(candidate, "output target");
166
+ assertOutputContained(canonicalRoot, canonicalCandidate);
167
+ }
168
+ async function requireOutputDirectory(path, label) {
169
+ let stat;
170
+ try {
171
+ stat = await lstat(path);
172
+ }
173
+ catch (error) {
174
+ throw new OutputPathError(`${label} is unavailable`, { cause: error });
175
+ }
176
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
177
+ throw new OutputPathError(`${label} is not a regular directory or is a symbolic link or junction`);
178
+ }
179
+ }
180
+ async function outputRealpath(path, label) {
181
+ try {
182
+ return await realpath(path);
183
+ }
184
+ catch (error) {
185
+ throw new OutputPathError(`${label} could not be resolved`, {
186
+ cause: error,
187
+ });
188
+ }
189
+ }
190
+ function assertOutputContained(root, candidate, allowRoot = false) {
191
+ const rel = relative(root, candidate);
192
+ if ((!allowRoot && rel === "") ||
193
+ rel === ".." ||
194
+ rel.startsWith(`..${sep}`) ||
195
+ isAbsolute(rel)) {
196
+ throw new OutputPathError("output path is outside the configured output root");
197
+ }
198
+ }
199
+ function fsErrorCode(error) {
200
+ return typeof error === "object" && error !== null && "code" in error
201
+ ? String(error.code)
202
+ : "";
203
+ }
204
+ function assertRelativePattern(pattern) {
205
+ if (pattern.length === 0 ||
206
+ pattern.includes("\0") ||
207
+ isAbsolute(pattern) ||
208
+ win32.isAbsolute(pattern) ||
209
+ /^[A-Za-z]:/u.test(pattern) ||
210
+ pattern.split(/[\\/]/u).some((part) => part === "." || part === "..")) {
211
+ throw new InputPathError("INPUT_PATH_REJECTED", "input pattern does not resolve to an allowed relative path");
212
+ }
213
+ }
214
+ function assertContained(root, candidate) {
215
+ const rel = relative(root, candidate);
216
+ if (rel === "" ||
217
+ rel === ".." ||
218
+ rel.startsWith(`..${sep}`) ||
219
+ isAbsolute(rel)) {
220
+ throw new InputPathError("INPUT_PATH_REJECTED", "input path is outside the configured input root");
221
+ }
222
+ }
223
+ async function inspectComponents(root, candidate) {
224
+ const rel = relative(root, candidate);
225
+ const parts = rel.split(sep);
226
+ let current = root;
227
+ await requireDirectory(current, "input root");
228
+ for (let index = 0; index < parts.length; index += 1) {
229
+ current = resolve(current, parts[index]);
230
+ let stat;
231
+ try {
232
+ stat = await lstat(current);
233
+ }
234
+ catch (error) {
235
+ throw fromFsError(error, "input path component is unavailable");
236
+ }
237
+ if (stat.isSymbolicLink()) {
238
+ throw new InputPathError("INPUT_PATH_REJECTED", "input path contains a symbolic link or junction");
239
+ }
240
+ const final = index === parts.length - 1;
241
+ if ((!final && !stat.isDirectory()) || (final && !stat.isFile())) {
242
+ throw new InputPathError("INPUT_PATH_REJECTED", final
243
+ ? "input path is not a regular file"
244
+ : "input path contains a non-directory component");
245
+ }
246
+ }
247
+ }
248
+ async function requireDirectory(path, label) {
249
+ let stat;
250
+ try {
251
+ stat = await lstat(path);
252
+ }
253
+ catch (error) {
254
+ throw fromFsError(error, `${label} is unavailable`);
255
+ }
256
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
257
+ throw new InputPathError("INPUT_PATH_REJECTED", `${label} is not a regular directory`);
258
+ }
259
+ }
260
+ async function requiredRealpath(path) {
261
+ try {
262
+ return await realpath(path);
263
+ }
264
+ catch (error) {
265
+ throw fromFsError(error, "input path does not exist");
266
+ }
267
+ }
268
+ function fromFsError(error, message) {
269
+ const code = typeof error === "object" && error !== null && "code" in error
270
+ ? String(error.code)
271
+ : "";
272
+ return new InputPathError(code === "ENOENT" || code === "ENOTDIR"
273
+ ? "INPUT_FILE_MISSING"
274
+ : "INPUT_PATH_REJECTED", message, { cause: error });
275
+ }
276
+ function compareText(left, right) {
277
+ return left < right ? -1 : left > right ? 1 : 0;
278
+ }
279
+ //# sourceMappingURL=io-path.js.map
@@ -0,0 +1,53 @@
1
+ import { type PersistenceRepository } from "../persistence/repository.js";
2
+ import type { NetworkLockReference } from "../persistence/network-lock.js";
3
+ export type ArchiveRejectedCode = "LOCK_CONFLICT" | "LOCK_UNAVAILABLE" | "LEASE_INTERRUPTED" | "RUN_READ_FAILED" | "RUN_STATUS_NOT_CLOSABLE" | "RUN_UNKNOWN_NOT_CLOSABLE" | "RUN_NOT_TERMINAL" | "RUN_ON_HOLD" | "RUN_LIVE" | "ARCHIVE_WRITE_FAILED";
4
+ export type ArchivePendingCode = "ARCHIVE_AUDIT_FAILED" | "AUDIT_CONFLICT" | "LEASE_INTERRUPTED_AFTER_ARCHIVE";
5
+ type Base = {
6
+ readonly run_id: string;
7
+ readonly event_id: string;
8
+ readonly lock_released: boolean;
9
+ };
10
+ export type ArchiveRunOutcome = (Base & {
11
+ readonly outcome: "ARCHIVED";
12
+ readonly run_revision: number;
13
+ readonly audit: "RECORDED";
14
+ }) | (Base & {
15
+ readonly outcome: "ARCHIVED";
16
+ readonly run_revision: number;
17
+ readonly audit: "PENDING";
18
+ readonly code: ArchivePendingCode;
19
+ }) | (Base & {
20
+ readonly outcome: "ALREADY_ARCHIVED";
21
+ readonly run_revision: number;
22
+ }) | (Base & {
23
+ readonly outcome: "UNCONFIRMED";
24
+ readonly run_revision: null;
25
+ readonly code: "ARCHIVE_UNCONFIRMED";
26
+ }) | (Base & {
27
+ readonly outcome: "REJECTED";
28
+ readonly run_revision: number | null;
29
+ readonly code: ArchiveRejectedCode;
30
+ });
31
+ export interface ArchiveLockManager {
32
+ acquire(): Promise<NetworkLockReference>;
33
+ release(reference: NetworkLockReference): Promise<unknown>;
34
+ }
35
+ export interface ArchiveLeaseMonitor {
36
+ start(): void;
37
+ stop(): void;
38
+ tick(): Promise<boolean>;
39
+ }
40
+ export interface ArchiveRunInput {
41
+ repository: PersistenceRepository;
42
+ lockManagerFactory: (owner: string) => ArchiveLockManager;
43
+ leaseMonitorFactory: (manager: ArchiveLockManager, reference: NetworkLockReference) => ArchiveLeaseMonitor;
44
+ runId: string;
45
+ requestedBy: string;
46
+ reason: string;
47
+ servicePrincipal: string;
48
+ now?: () => Date;
49
+ uuid?: () => string;
50
+ }
51
+ export declare function archiveRun(input: ArchiveRunInput): Promise<ArchiveRunOutcome>;
52
+ export {};
53
+ //# sourceMappingURL=archive-run.d.ts.map
@@ -0,0 +1,323 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { RepositoryError, } from "../persistence/repository.js";
3
+ const hold = (value) => value?.value.state === "REQUESTED" || value?.value.state === "ACCEPTED";
4
+ const sameAudit = (left, right) => left.event_id === right.event_id &&
5
+ left.event_type === "RUN_ARCHIVED" &&
6
+ left.run_id === right.run_id &&
7
+ left.previous_status === right.previous_status &&
8
+ left.run_revision_before === right.run_revision_before;
9
+ export async function archiveRun(input) {
10
+ const eventId = `archive_${(input.uuid ?? randomUUID)()}`;
11
+ const base = { run_id: input.runId, event_id: eventId };
12
+ const manager = input.lockManagerFactory(eventId);
13
+ let reference;
14
+ try {
15
+ reference = await manager.acquire();
16
+ }
17
+ catch (error) {
18
+ const code = typeof error === "object" &&
19
+ error !== null &&
20
+ "code" in error &&
21
+ error.code === "LOCK_CONFLICT"
22
+ ? "LOCK_CONFLICT"
23
+ : "LOCK_UNAVAILABLE";
24
+ return {
25
+ ...base,
26
+ outcome: "REJECTED",
27
+ run_revision: null,
28
+ code,
29
+ lock_released: true,
30
+ };
31
+ }
32
+ const monitor = input.leaseMonitorFactory(manager, reference);
33
+ let result;
34
+ let cleaned = false;
35
+ try {
36
+ monitor.start();
37
+ let run;
38
+ try {
39
+ run = await input.repository.getRun(input.runId);
40
+ }
41
+ catch {
42
+ result = {
43
+ ...base,
44
+ outcome: "REJECTED",
45
+ run_revision: null,
46
+ code: "RUN_READ_FAILED",
47
+ };
48
+ return await finish();
49
+ }
50
+ if (run.value.lifecycle_status === "ARCHIVED") {
51
+ result = {
52
+ ...base,
53
+ outcome: "ALREADY_ARCHIVED",
54
+ run_revision: run.revision,
55
+ };
56
+ return await finish();
57
+ }
58
+ const code = run.value.status === "SUCCESS"
59
+ ? "RUN_STATUS_NOT_CLOSABLE"
60
+ : run.value.status === "UNKNOWN"
61
+ ? "RUN_UNKNOWN_NOT_CLOSABLE"
62
+ : run.value.status === "CREATED" || run.value.status === "RUNNING"
63
+ ? "RUN_NOT_TERMINAL"
64
+ : null;
65
+ if (code !== null) {
66
+ result = {
67
+ ...base,
68
+ outcome: "REJECTED",
69
+ run_revision: run.revision,
70
+ code,
71
+ };
72
+ return await finish();
73
+ }
74
+ try {
75
+ if (hold(await input.repository.getCancelRequest(input.runId))) {
76
+ result = {
77
+ ...base,
78
+ outcome: "REJECTED",
79
+ run_revision: run.revision,
80
+ code: "RUN_ON_HOLD",
81
+ };
82
+ return await finish();
83
+ }
84
+ }
85
+ catch {
86
+ result = {
87
+ ...base,
88
+ outcome: "REJECTED",
89
+ run_revision: run.revision,
90
+ code: "RUN_READ_FAILED",
91
+ };
92
+ return await finish();
93
+ }
94
+ // RUN_LIVE(G-02: lock owner が当該 Run の Invocation に属し lease 生存)は
95
+ // ポーラーの一次審査で判定する。ここでは Network ロックを保持しているため
96
+ // 生存中の Invocation は存在し得ない。Invocation レコードの RUNNING 残骸
97
+ // (クラッシュ後の未 finalize)は reconciliation の対象であり、CLOSE を阻まない。
98
+ if (!(await monitor.tick())) {
99
+ result = {
100
+ ...base,
101
+ outcome: "REJECTED",
102
+ run_revision: run.revision,
103
+ code: "LEASE_INTERRUPTED",
104
+ };
105
+ return await finish();
106
+ }
107
+ let archived = null;
108
+ let already = false;
109
+ try {
110
+ archived = await input.repository.archiveRun(input.runId, run.revision, (input.now ?? (() => new Date()))().toISOString());
111
+ }
112
+ catch (first) {
113
+ const ambiguous = first instanceof RepositoryError && first.code === "AMBIGUOUS_WRITE";
114
+ const conflict = first instanceof RepositoryError && first.code === "REVISION_CONFLICT";
115
+ let reread;
116
+ try {
117
+ reread = await input.repository.getRun(input.runId);
118
+ }
119
+ catch {
120
+ result = {
121
+ ...base,
122
+ outcome: "UNCONFIRMED",
123
+ run_revision: null,
124
+ code: "ARCHIVE_UNCONFIRMED",
125
+ };
126
+ return await finish();
127
+ }
128
+ if (reread.value.lifecycle_status === "ARCHIVED") {
129
+ archived = reread;
130
+ already = conflict;
131
+ }
132
+ else if (conflict) {
133
+ result = {
134
+ ...base,
135
+ outcome: "UNCONFIRMED",
136
+ run_revision: null,
137
+ code: "ARCHIVE_UNCONFIRMED",
138
+ };
139
+ return await finish();
140
+ }
141
+ else if (!ambiguous) {
142
+ result = {
143
+ ...base,
144
+ outcome: "REJECTED",
145
+ run_revision: reread.revision,
146
+ code: "ARCHIVE_WRITE_FAILED",
147
+ };
148
+ return await finish();
149
+ }
150
+ else {
151
+ if (!(await monitor.tick())) {
152
+ result = {
153
+ ...base,
154
+ outcome: "REJECTED",
155
+ run_revision: reread.revision,
156
+ code: "LEASE_INTERRUPTED",
157
+ };
158
+ return await finish();
159
+ }
160
+ try {
161
+ archived = await input.repository.archiveRun(input.runId, reread.revision, (input.now ?? (() => new Date()))().toISOString());
162
+ }
163
+ catch {
164
+ try {
165
+ const final = await input.repository.getRun(input.runId);
166
+ if (final.value.lifecycle_status === "ARCHIVED")
167
+ archived = final;
168
+ else {
169
+ result = {
170
+ ...base,
171
+ outcome: "REJECTED",
172
+ run_revision: final.revision,
173
+ code: "ARCHIVE_WRITE_FAILED",
174
+ };
175
+ return await finish();
176
+ }
177
+ }
178
+ catch {
179
+ result = {
180
+ ...base,
181
+ outcome: "UNCONFIRMED",
182
+ run_revision: null,
183
+ code: "ARCHIVE_UNCONFIRMED",
184
+ };
185
+ return await finish();
186
+ }
187
+ }
188
+ }
189
+ }
190
+ if (already) {
191
+ result = {
192
+ ...base,
193
+ outcome: "ALREADY_ARCHIVED",
194
+ run_revision: archived.revision,
195
+ };
196
+ return await finish();
197
+ }
198
+ if (!(await monitor.tick())) {
199
+ result = {
200
+ ...base,
201
+ outcome: "ARCHIVED",
202
+ run_revision: archived.revision,
203
+ audit: "PENDING",
204
+ code: "LEASE_INTERRUPTED_AFTER_ARCHIVE",
205
+ };
206
+ return await finish();
207
+ }
208
+ const audit = {
209
+ event_id: eventId,
210
+ event_type: "RUN_ARCHIVED",
211
+ run_id: input.runId,
212
+ result_code: "RUN_ARCHIVED",
213
+ requested_by: input.requestedBy,
214
+ reason: input.reason,
215
+ archived_at: archived.value.updated_at,
216
+ previous_status: run.value.status,
217
+ run_revision_before: run.revision,
218
+ service_principal: input.servicePrincipal,
219
+ };
220
+ try {
221
+ await input.repository.appendOperationAudit(audit);
222
+ result = {
223
+ ...base,
224
+ outcome: "ARCHIVED",
225
+ run_revision: archived.revision,
226
+ audit: "RECORDED",
227
+ };
228
+ }
229
+ catch (error) {
230
+ if (error instanceof RepositoryError && error.code === "AUDIT_CONFLICT")
231
+ result = {
232
+ ...base,
233
+ outcome: "ARCHIVED",
234
+ run_revision: archived.revision,
235
+ audit: "PENDING",
236
+ code: "AUDIT_CONFLICT",
237
+ };
238
+ else if (error instanceof RepositoryError &&
239
+ error.code === "AMBIGUOUS_WRITE") {
240
+ try {
241
+ const found = await input.repository.getOperationAuditByEventId(eventId);
242
+ if (found !== null && sameAudit(found.value, audit))
243
+ result = {
244
+ ...base,
245
+ outcome: "ARCHIVED",
246
+ run_revision: archived.revision,
247
+ audit: "RECORDED",
248
+ };
249
+ else if (found !== null)
250
+ result = {
251
+ ...base,
252
+ outcome: "ARCHIVED",
253
+ run_revision: archived.revision,
254
+ audit: "PENDING",
255
+ code: "AUDIT_CONFLICT",
256
+ };
257
+ else {
258
+ try {
259
+ await input.repository.appendOperationAudit(audit);
260
+ result = {
261
+ ...base,
262
+ outcome: "ARCHIVED",
263
+ run_revision: archived.revision,
264
+ audit: "RECORDED",
265
+ };
266
+ }
267
+ catch {
268
+ result = {
269
+ ...base,
270
+ outcome: "ARCHIVED",
271
+ run_revision: archived.revision,
272
+ audit: "PENDING",
273
+ code: "ARCHIVE_AUDIT_FAILED",
274
+ };
275
+ }
276
+ }
277
+ }
278
+ catch {
279
+ result = {
280
+ ...base,
281
+ outcome: "ARCHIVED",
282
+ run_revision: archived.revision,
283
+ audit: "PENDING",
284
+ code: "ARCHIVE_AUDIT_FAILED",
285
+ };
286
+ }
287
+ }
288
+ else
289
+ result = {
290
+ ...base,
291
+ outcome: "ARCHIVED",
292
+ run_revision: archived.revision,
293
+ audit: "PENDING",
294
+ code: "ARCHIVE_AUDIT_FAILED",
295
+ };
296
+ }
297
+ return await finish();
298
+ }
299
+ finally {
300
+ if (!cleaned) {
301
+ monitor.stop();
302
+ try {
303
+ await manager.release(reference);
304
+ }
305
+ catch {
306
+ // Preserve the unexpected primary exception after attempting cleanup.
307
+ }
308
+ }
309
+ }
310
+ async function finish() {
311
+ monitor.stop();
312
+ let released = true;
313
+ try {
314
+ await manager.release(reference);
315
+ }
316
+ catch {
317
+ released = false;
318
+ }
319
+ cleaned = true;
320
+ return { ...result, lock_released: released };
321
+ }
322
+ }
323
+ //# sourceMappingURL=archive-run.js.map
@@ -0,0 +1,18 @@
1
+ import type { CancelRequest } from "../domain/persistence-model.js";
2
+ import type { PersistenceRepository, Versioned } from "../persistence/repository.js";
3
+ export type CancelRequestErrorCode = "RUN_ALREADY_TERMINAL";
4
+ export declare class CancelRequestError extends Error {
5
+ readonly code: CancelRequestErrorCode;
6
+ constructor(code: CancelRequestErrorCode, message: string);
7
+ }
8
+ export interface ChangeCancelRequestInput {
9
+ readonly repository: PersistenceRepository;
10
+ readonly runId: string;
11
+ readonly requestedBy: string;
12
+ readonly reason: string;
13
+ readonly release: boolean;
14
+ readonly now?: () => Date;
15
+ }
16
+ export declare function changeCancelRequest(input: ChangeCancelRequestInput): Promise<Versioned<CancelRequest> | null>;
17
+ export declare function isCancelHold(request: Versioned<CancelRequest> | null): request is Versioned<CancelRequest>;
18
+ //# sourceMappingURL=cancel-request.d.ts.map