@abelxiaoxing/cadence 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/run-state.ts CHANGED
@@ -47,6 +47,11 @@ export interface DeliveryBindingProjection {
47
47
  gate: DeliveryGate;
48
48
  revision: number;
49
49
  receiptHash: string;
50
+ approvalProof?: {
51
+ revision: number;
52
+ contractHash: string;
53
+ recordHash: string;
54
+ };
50
55
  }
51
56
 
52
57
  export interface RunProjection {
@@ -133,6 +138,7 @@ const LEGAL_TRANSITIONS: Readonly<Record<RunState, readonly RunState[]>> = {
133
138
  "running",
134
139
  "approval-needed",
135
140
  "recovering",
141
+ "completed",
136
142
  "discarded",
137
143
  "rejected",
138
144
  ],
@@ -263,12 +269,31 @@ function applyVerifiedEvent(
263
269
  const gate = event.payload.gate;
264
270
  const revision = event.payload.revision;
265
271
  const receiptHash = event.payload.receiptHash;
272
+ const approvalProof = event.payload.approvalProof;
266
273
  if (
267
274
  (gate !== "gate-a" && gate !== "gate-b") ||
268
275
  !Number.isSafeInteger(revision) ||
269
276
  (revision as number) < 1 ||
270
277
  typeof receiptHash !== "string" ||
271
- !SHA256.test(receiptHash)
278
+ !SHA256.test(receiptHash) ||
279
+ (approvalProof !== undefined &&
280
+ (!approvalProof ||
281
+ typeof approvalProof !== "object" ||
282
+ Array.isArray(approvalProof) ||
283
+ !Number.isSafeInteger(
284
+ (approvalProof as Record<string, unknown>).revision,
285
+ ) ||
286
+ Number((approvalProof as Record<string, unknown>).revision) < 1 ||
287
+ typeof (approvalProof as Record<string, unknown>).contractHash !==
288
+ "string" ||
289
+ !SHA256.test(
290
+ String((approvalProof as Record<string, unknown>).contractHash),
291
+ ) ||
292
+ typeof (approvalProof as Record<string, unknown>).recordHash !==
293
+ "string" ||
294
+ !SHA256.test(
295
+ String((approvalProof as Record<string, unknown>).recordHash),
296
+ )))
272
297
  ) {
273
298
  throw new Error("event-integrity-delivery-binding");
274
299
  }
@@ -276,13 +301,25 @@ function applyVerifiedEvent(
276
301
  gate: gate as DeliveryGate,
277
302
  revision: revision as number,
278
303
  receiptHash,
304
+ ...(approvalProof
305
+ ? {
306
+ approvalProof: structuredClone(
307
+ approvalProof as DeliveryBindingProjection["approvalProof"],
308
+ ),
309
+ }
310
+ : {}),
279
311
  };
280
312
  const existing = projection.deliveryBindings.find(
281
313
  (candidate) =>
282
314
  candidate.gate === binding.gate &&
283
315
  candidate.revision === binding.revision,
284
316
  );
285
- if (existing && existing.receiptHash !== binding.receiptHash) {
317
+ if (
318
+ existing &&
319
+ (existing.receiptHash !== binding.receiptHash ||
320
+ canonicalJson(existing.approvalProof) !==
321
+ canonicalJson(binding.approvalProof))
322
+ ) {
286
323
  throw new Error("event-integrity-delivery-conflict");
287
324
  }
288
325
  const deliveryBindings = existing
package/src/run-store.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  StateRootError,
18
18
  } from "./state-root.ts";
19
19
 
20
- export const RUN_STORE_SCHEMA_VERSION = 2 as const;
20
+ export const RUN_STORE_SCHEMA_VERSION = 4 as const;
21
21
 
22
22
  const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
23
23
  const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
@@ -60,6 +60,11 @@ export interface BindDeliveryInput {
60
60
  gate: DeliveryGate;
61
61
  revision: number;
62
62
  receiptHash: string;
63
+ approvalProof?: {
64
+ revision: number;
65
+ contractHash: string;
66
+ recordHash: string;
67
+ };
63
68
  operationId: string;
64
69
  lease?: OperationLease;
65
70
  }
@@ -79,7 +84,7 @@ export interface OperationLeaseStatus {
79
84
 
80
85
  const SCHEMA = `
81
86
  CREATE TABLE IF NOT EXISTS schema_meta (
82
- version INTEGER PRIMARY KEY CHECK (version = 2)
87
+ version INTEGER PRIMARY KEY CHECK (version = 4)
83
88
  ) STRICT;
84
89
 
85
90
  CREATE TABLE IF NOT EXISTS runs (
@@ -103,6 +108,9 @@ const SCHEMA = `
103
108
  gate TEXT NOT NULL,
104
109
  revision INTEGER NOT NULL,
105
110
  receipt_hash TEXT NOT NULL,
111
+ approval_revision INTEGER,
112
+ contract_hash TEXT,
113
+ record_hash TEXT,
106
114
  operation_id TEXT NOT NULL,
107
115
  PRIMARY KEY (run_id, gate, revision),
108
116
  UNIQUE (run_id, operation_id)
@@ -433,7 +441,7 @@ export class RunStore {
433
441
  }
434
442
  const key = lookupKey(input);
435
443
  return this.#transaction(() => {
436
- const existing = this.#database
444
+ let existing = this.#database
437
445
  .prepare(
438
446
  `SELECT run_id, projection_json FROM runs
439
447
  WHERE root_hash = ? AND stage = ? AND lookup_key = ?`,
@@ -441,6 +449,18 @@ export class RunStore {
441
449
  .get(this.stateRoot.consumerRootHash, input.stage, key) as
442
450
  | RunRow
443
451
  | undefined;
452
+ if (existing) {
453
+ const projection = parseProjection(existing.projection_json);
454
+ if (
455
+ input.stage === "abel-design" &&
456
+ ["completed", "discarded", "rejected"].includes(projection.state)
457
+ ) {
458
+ this.#database
459
+ .prepare("UPDATE runs SET lookup_key = ? WHERE run_id = ?")
460
+ .run(`terminal:${existing.run_id}`, existing.run_id);
461
+ existing = undefined;
462
+ }
463
+ }
444
464
  if (existing) {
445
465
  const replay = this.#operationOutcome(
446
466
  existing.run_id,
@@ -561,7 +581,12 @@ export class RunStore {
561
581
  (input.gate !== "gate-a" && input.gate !== "gate-b") ||
562
582
  !Number.isSafeInteger(input.revision) ||
563
583
  input.revision < 1 ||
564
- !SHA256.test(input.receiptHash)
584
+ !SHA256.test(input.receiptHash) ||
585
+ (input.approvalProof !== undefined &&
586
+ (!Number.isSafeInteger(input.approvalProof.revision) ||
587
+ input.approvalProof.revision < 1 ||
588
+ !SHA256.test(input.approvalProof.contractHash) ||
589
+ !SHA256.test(input.approvalProof.recordHash)))
565
590
  ) {
566
591
  throw new Error("invalid-delivery-binding");
567
592
  }
@@ -576,14 +601,27 @@ export class RunStore {
576
601
  if (replay) return replay;
577
602
  const existing = this.#database
578
603
  .prepare(
579
- `SELECT receipt_hash FROM delivery_bindings
604
+ `SELECT receipt_hash, approval_revision, contract_hash, record_hash
605
+ FROM delivery_bindings
580
606
  WHERE run_id = ? AND gate = ? AND revision = ?`,
581
607
  )
582
608
  .get(input.runId, input.gate, input.revision) as
583
- | { receipt_hash: string }
609
+ | {
610
+ receipt_hash: string;
611
+ approval_revision: number | null;
612
+ contract_hash: string | null;
613
+ record_hash: string | null;
614
+ }
584
615
  | undefined;
585
616
  if (existing) {
586
- if (existing.receipt_hash !== input.receiptHash) {
617
+ if (
618
+ existing.receipt_hash !== input.receiptHash ||
619
+ existing.approval_revision !==
620
+ (input.approvalProof?.revision ?? null) ||
621
+ existing.contract_hash !==
622
+ (input.approvalProof?.contractHash ?? null) ||
623
+ existing.record_hash !== (input.approvalProof?.recordHash ?? null)
624
+ ) {
587
625
  throw new Error("delivery-revision-conflict");
588
626
  }
589
627
  const projection = this.status(input.runId);
@@ -599,20 +637,27 @@ export class RunStore {
599
637
  this.#database
600
638
  .prepare(
601
639
  `INSERT INTO delivery_bindings(
602
- run_id, gate, revision, receipt_hash, operation_id
603
- ) VALUES (?, ?, ?, ?, ?)`,
640
+ run_id, gate, revision, receipt_hash, approval_revision,
641
+ contract_hash, record_hash, operation_id
642
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
604
643
  )
605
644
  .run(
606
645
  input.runId,
607
646
  input.gate,
608
647
  input.revision,
609
648
  input.receiptHash,
649
+ input.approvalProof?.revision ?? null,
650
+ input.approvalProof?.contractHash ?? null,
651
+ input.approvalProof?.recordHash ?? null,
610
652
  input.operationId,
611
653
  );
612
654
  const projection = this.#appendEvent(input.runId, "delivery-bound", {
613
655
  gate: input.gate,
614
656
  revision: input.revision,
615
657
  receiptHash: input.receiptHash,
658
+ ...(input.approvalProof
659
+ ? { approvalProof: structuredClone(input.approvalProof) }
660
+ : {}),
616
661
  });
617
662
  this.#recordOperation(
618
663
  input.runId,
@@ -643,6 +688,33 @@ export class RunStore {
643
688
  });
644
689
  }
645
690
 
691
+ bindDeliveriesAtomically(
692
+ inputs: readonly BindDeliveryInput[],
693
+ write: (database: DatabaseSync) => void,
694
+ ): RunProjection {
695
+ if (inputs.length === 0) throw new Error("delivery-binding-empty");
696
+ for (const input of inputs) this.#requireDeliveryBinding(input);
697
+ const runId = inputs[0]?.runId;
698
+ if (
699
+ !runId ||
700
+ inputs.some(
701
+ (input) =>
702
+ input.runId !== runId ||
703
+ (input.lease?.token ?? null) !== (inputs[0]?.lease?.token ?? null),
704
+ ) ||
705
+ typeof write !== "function"
706
+ ) {
707
+ throw new Error("delivery-binding-write-invalid");
708
+ }
709
+ return this.#transaction(() => {
710
+ let projection: RunProjection | undefined;
711
+ for (const input of inputs) projection = this.#bindDelivery(input);
712
+ write(this.#database);
713
+ if (!projection) throw new Error("delivery-binding-empty");
714
+ return projection;
715
+ });
716
+ }
717
+
646
718
  acquireLease(input: {
647
719
  runId: string;
648
720
  operationId: string;
@@ -1320,7 +1320,7 @@ export function renderActivityCall(
1320
1320
  context?: ActivityRenderContext,
1321
1321
  ): Component {
1322
1322
  const value = asRecord(args);
1323
- if (typeof value?.command === "string" && value.version === 2) {
1323
+ if (typeof value?.command === "string") {
1324
1324
  if (context?.executionStarted || context?.isError) return new Text("");
1325
1325
  const projected = projectWorkflowActivity(value, { state: "queued" }, 0);
1326
1326
  const display: ActivitySnapshot = {
@@ -166,17 +166,39 @@ function localExecutable(
166
166
  }
167
167
  }
168
168
 
169
+ function pathApi(
170
+ platform: NodeJS.Platform,
171
+ ...values: readonly string[]
172
+ ): typeof path.posix {
173
+ if (platform !== "win32") return path.posix;
174
+ if (
175
+ process.platform === "win32" ||
176
+ values.some(
177
+ (value) => /^[a-z]:[\\/]/iu.test(value) || value.startsWith("\\\\"),
178
+ )
179
+ ) {
180
+ return path.win32;
181
+ }
182
+ // Tests may model Windows executable semantics over a POSIX fixture tree.
183
+ return path.posix;
184
+ }
185
+
169
186
  function pathWithin(
170
187
  root: string,
171
188
  candidate: string,
172
189
  platform: NodeJS.Platform,
173
190
  ): boolean {
174
- const normalizedRoot = platform === "win32" ? root.toLowerCase() : root;
175
- const normalizedCandidate =
176
- platform === "win32" ? candidate.toLowerCase() : candidate;
191
+ const platformPath = pathApi(platform, root, candidate);
192
+ const normalizedRoot = platformPath.resolve(root);
193
+ const relative = platformPath.relative(
194
+ normalizedRoot,
195
+ platformPath.resolve(candidate),
196
+ );
177
197
  return (
178
- normalizedCandidate === normalizedRoot ||
179
- normalizedCandidate.startsWith(`${normalizedRoot}${path.sep}`)
198
+ relative === "" ||
199
+ (relative !== ".." &&
200
+ !relative.startsWith(`..${platformPath.sep}`) &&
201
+ !platformPath.isAbsolute(relative))
180
202
  );
181
203
  }
182
204
 
@@ -261,13 +283,24 @@ function within(root: string, candidate: string): boolean {
261
283
  return candidate === root || candidate.startsWith(`${root}${path.sep}`);
262
284
  }
263
285
 
264
- function commonDirectory(
286
+ export function commonDirectory(
265
287
  left: string,
266
288
  right: string,
267
289
  platform: NodeJS.Platform = process.platform,
268
290
  ): string {
269
- const leftParts = path.resolve(left).split(path.sep).filter(Boolean);
270
- const rightParts = path.resolve(right).split(path.sep).filter(Boolean);
291
+ const platformPath = pathApi(platform, left, right);
292
+ const leftResolved = platformPath.resolve(left);
293
+ const rightResolved = platformPath.resolve(right);
294
+ const leftRoot = platformPath.parse(leftResolved).root;
295
+ const rightRoot = platformPath.parse(rightResolved).root;
296
+ const comparable = (value: string) =>
297
+ platform === "win32" ? value.toLowerCase() : value;
298
+ if (comparable(leftRoot) !== comparable(rightRoot)) return leftRoot;
299
+
300
+ const parts = (value: string, root: string) =>
301
+ platformPath.relative(root, value).split(platformPath.sep).filter(Boolean);
302
+ const leftParts = parts(leftResolved, leftRoot);
303
+ const rightParts = parts(rightResolved, rightRoot);
271
304
  const common: string[] = [];
272
305
  for (
273
306
  let index = 0;
@@ -276,15 +309,10 @@ function commonDirectory(
276
309
  ) {
277
310
  const leftPart = leftParts[index] as string;
278
311
  const rightPart = rightParts[index] as string;
279
- if (
280
- (platform === "win32" ? leftPart.toLowerCase() : leftPart) !==
281
- (platform === "win32" ? rightPart.toLowerCase() : rightPart)
282
- ) {
283
- break;
284
- }
312
+ if (comparable(leftPart) !== comparable(rightPart)) break;
285
313
  common.push(leftPart);
286
314
  }
287
- return path.join(path.parse(path.resolve(left)).root, ...common);
315
+ return platformPath.join(leftRoot, ...common);
288
316
  }
289
317
 
290
318
  function unsafeMountSource(source: string): boolean {