@aldus-runtime/release 0.1.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/LICENSE +201 -0
- package/NOTICE +21 -0
- package/dist/adapter.d.ts +140 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +104 -0
- package/dist/adapter.js.map +1 -0
- package/dist/authorization.d.ts +55 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +42 -0
- package/dist/authorization.js.map +1 -0
- package/dist/bundle.d.ts +56 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +67 -0
- package/dist/bundle.js.map +1 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +56 -0
- package/dist/errors.js.map +1 -0
- package/dist/executor.d.ts +171 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +342 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/operation.d.ts +102 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/operation.js +23 -0
- package/dist/operation.js.map +1 -0
- package/dist/ports.d.ts +76 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +80 -0
- package/dist/ports.js.map +1 -0
- package/package.json +50 -0
- package/src/adapter.ts +202 -0
- package/src/authorization.ts +74 -0
- package/src/bundle.ts +101 -0
- package/src/errors.ts +65 -0
- package/src/executor.ts +538 -0
- package/src/index.ts +75 -0
- package/src/operation.ts +112 -0
- package/src/ports.ts +114 -0
package/src/operation.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release operations (architecture contract §17).
|
|
3
|
+
*
|
|
4
|
+
* §17 requires that "pre-release hard gates and post-upload best-effort operations MUST be
|
|
5
|
+
* distinguished". The distinction is made **structurally** rather than with a severity field:
|
|
6
|
+
* a required operation and a best-effort one are different types, produced by different
|
|
7
|
+
* constructors, and held in different arrays of a {@link ReleaseBundle}.
|
|
8
|
+
*
|
|
9
|
+
* A `criticality: "required" | "best_effort"` field would have been smaller, and wrong. A field
|
|
10
|
+
* is set at a call site, often far from where the consequence lands, and setting it incorrectly
|
|
11
|
+
* turns a failed thumbnail into a failed release or — far worse — a failed media upload into a
|
|
12
|
+
* release that reports success. Requiring the caller to *place* the operation in one list or the
|
|
13
|
+
* other makes the mistake a type error rather than a typo.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Brand distinguishing the two operation categories at the type level.
|
|
18
|
+
*
|
|
19
|
+
* Declared but never exported as a value, so an operation of either kind can only come from the
|
|
20
|
+
* constructor below. A caller cannot hand-write an object literal that satisfies
|
|
21
|
+
* {@link RequiredOperation}.
|
|
22
|
+
*/
|
|
23
|
+
declare const CRITICALITY: unique symbol;
|
|
24
|
+
|
|
25
|
+
/** Fields shared by both categories. */
|
|
26
|
+
export interface ReleaseOperationBase {
|
|
27
|
+
/**
|
|
28
|
+
* Identity of this operation within its bundle.
|
|
29
|
+
*
|
|
30
|
+
* Stable across resumption: it is how a stored receipt is matched back to the operation that
|
|
31
|
+
* produced it, so renaming one orphans its receipt.
|
|
32
|
+
*/
|
|
33
|
+
operationId: string;
|
|
34
|
+
/**
|
|
35
|
+
* What kind of operation this is, e.g. a media upload, a caption attachment, or a visibility
|
|
36
|
+
* transition.
|
|
37
|
+
*
|
|
38
|
+
* An OPEN string, never a Core-defined enum. Contract §17 lists candidate operations — media
|
|
39
|
+
* upload, captions, thumbnail, title and description, privacy transition, playlist, podcast
|
|
40
|
+
* storage and RSS, notification channels — as an illustration of what an adopter might need,
|
|
41
|
+
* and §4.2 keeps adopter process out of the runtime. Do not narrow this to a union.
|
|
42
|
+
*/
|
|
43
|
+
kind: string;
|
|
44
|
+
/**
|
|
45
|
+
* Where the operation is directed.
|
|
46
|
+
*
|
|
47
|
+
* An OPEN string. Contract §1.2 explicitly rules out prescribing particular release targets,
|
|
48
|
+
* so a destination is an adopter's name for one of its own, resolved to an adapter at
|
|
49
|
+
* execution time.
|
|
50
|
+
*/
|
|
51
|
+
destination: string;
|
|
52
|
+
/**
|
|
53
|
+
* Digests of exactly what this operation releases (contract §13.4).
|
|
54
|
+
*
|
|
55
|
+
* §13.4 requires release approval to bind to the final render, captions, metadata,
|
|
56
|
+
* destination, and visibility policy. These digests are what an approval binds, and they feed
|
|
57
|
+
* the idempotency key, so changing what is released changes the operation's identity.
|
|
58
|
+
*/
|
|
59
|
+
inputHashes: readonly string[];
|
|
60
|
+
/**
|
|
61
|
+
* The authority this operation requires, if any (contract §13.4, §18.1).
|
|
62
|
+
*
|
|
63
|
+
* Names an operation string a gate grants — for example the separate upload and publication
|
|
64
|
+
* authorities §13.4 demands. Left absent only for operations that genuinely need no approval.
|
|
65
|
+
* The gate engine decides whether the authority is held; this package never re-decides it.
|
|
66
|
+
*/
|
|
67
|
+
requiresAuthority?: string;
|
|
68
|
+
/** Opaque parameters passed through to the adapter. Never inspected here. */
|
|
69
|
+
parameters?: Readonly<Record<string, unknown>>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* An operation whose failure fails the release (contract §17 "pre-release hard gates").
|
|
74
|
+
*
|
|
75
|
+
* Required operations run in declaration order, and the first failure stops the bundle: a media
|
|
76
|
+
* upload that failed must not be followed by a visibility transition making nothing public.
|
|
77
|
+
*/
|
|
78
|
+
export interface RequiredOperation extends ReleaseOperationBase {
|
|
79
|
+
readonly [CRITICALITY]: "required";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* An operation whose failure is recorded but does not fail the release (contract §17
|
|
84
|
+
* "post-upload best-effort operations").
|
|
85
|
+
*
|
|
86
|
+
* A failed thumbnail or notification leaves a `failed` receipt and an operator-visible warning;
|
|
87
|
+
* it does not undo an upload that succeeded.
|
|
88
|
+
*/
|
|
89
|
+
export interface BestEffortOperation extends ReleaseOperationBase {
|
|
90
|
+
readonly [CRITICALITY]: "best_effort";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Either category, where only the shared fields matter. */
|
|
94
|
+
export type ReleaseOperation = RequiredOperation | BestEffortOperation;
|
|
95
|
+
|
|
96
|
+
/** Declare an operation whose failure fails the release (contract §17). */
|
|
97
|
+
export function requiredOperation(operation: ReleaseOperationBase): RequiredOperation {
|
|
98
|
+
return { ...operation } as RequiredOperation;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Declare an operation whose failure is recorded but tolerated (contract §17). */
|
|
102
|
+
export function bestEffortOperation(operation: ReleaseOperationBase): BestEffortOperation {
|
|
103
|
+
return { ...operation } as BestEffortOperation;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Which category an operation belongs to.
|
|
108
|
+
*
|
|
109
|
+
* Derived from where the bundle holds it rather than read off the operation, because the arrays
|
|
110
|
+
* are the source of truth — see this module's header.
|
|
111
|
+
*/
|
|
112
|
+
export type OperationCriticality = "required" | "best_effort";
|
package/src/ports.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ports the release executor reads and writes through.
|
|
3
|
+
*
|
|
4
|
+
* Contract §7 requires core models to be independent of physical storage, so this package binds
|
|
5
|
+
* to interfaces rather than to a store. Each is kept to the operations actually used and tested:
|
|
6
|
+
* an aspirational method on a port is worse than an absent one, because a second adapter is
|
|
7
|
+
* written against it and only discovers at runtime that nothing honours it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AldusEvent, ReleaseReceipt } from "@aldus-runtime/core";
|
|
11
|
+
import type { EventStore, RunStore } from "@aldus-runtime/file-store";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Storage for release receipts (contract §7 `release.json`).
|
|
15
|
+
*
|
|
16
|
+
* There is no update in place and no delete. §17's receipts are an audit record of what was
|
|
17
|
+
* attempted against a destination: an operation retried after a failure produces a second
|
|
18
|
+
* receipt, not an edit to the first, because the fact that the first attempt failed is what
|
|
19
|
+
* explains the retry. {@link latestFor} resolves the current outcome by reading them in order.
|
|
20
|
+
*/
|
|
21
|
+
export interface ReleaseReceiptStore {
|
|
22
|
+
/** Every receipt recorded for a Run, in the order they were appended. */
|
|
23
|
+
list(runId: string): Promise<ReleaseReceipt[]>;
|
|
24
|
+
/** Append one receipt. */
|
|
25
|
+
append(runId: string, receipt: ReleaseReceipt): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Where lifecycle events go (contract §6.4).
|
|
30
|
+
*
|
|
31
|
+
* §6.4 requires **every** state mutation to emit an immutable event, so recording a receipt and
|
|
32
|
+
* emitting its event are one operation from the executor's point of view.
|
|
33
|
+
*/
|
|
34
|
+
export interface ReleaseEventSink {
|
|
35
|
+
/** Emit one event. */
|
|
36
|
+
emit(event: AldusEvent): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The most recent receipt per idempotency key.
|
|
41
|
+
*
|
|
42
|
+
* Later receipts win, which is what makes a retry after a failure resolve to the retry's outcome
|
|
43
|
+
* rather than the original failure.
|
|
44
|
+
*/
|
|
45
|
+
export function latestByKey(receipts: readonly ReleaseReceipt[]): Map<string, ReleaseReceipt> {
|
|
46
|
+
const latest = new Map<string, ReleaseReceipt>();
|
|
47
|
+
for (const receipt of receipts) latest.set(receipt.idempotencyKey, receipt);
|
|
48
|
+
return latest;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An in-memory {@link ReleaseReceiptStore}, for tests and for dry runs. */
|
|
52
|
+
export class MemoryReleaseReceiptStore implements ReleaseReceiptStore {
|
|
53
|
+
readonly #byRun = new Map<string, ReleaseReceipt[]>();
|
|
54
|
+
|
|
55
|
+
list(runId: string): Promise<ReleaseReceipt[]> {
|
|
56
|
+
return Promise.resolve([...(this.#byRun.get(runId) ?? [])]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
append(runId: string, receipt: ReleaseReceipt): Promise<void> {
|
|
60
|
+
const existing = this.#byRun.get(runId);
|
|
61
|
+
if (existing === undefined) this.#byRun.set(runId, [receipt]);
|
|
62
|
+
else existing.push(receipt);
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Discard every receipt for a Run, as a lost or never-written `release.json` would.
|
|
68
|
+
*
|
|
69
|
+
* Exists for the reconciliation tests: losing a receipt whose operation succeeded remotely is
|
|
70
|
+
* the exact condition §17's reconciliation requirement addresses, and it has to be reproducible
|
|
71
|
+
* to be tested.
|
|
72
|
+
*/
|
|
73
|
+
forget(runId: string): void {
|
|
74
|
+
this.#byRun.delete(runId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** An in-memory {@link ReleaseEventSink} that retains what it was given, for tests. */
|
|
79
|
+
export class MemoryReleaseEventSink implements ReleaseEventSink {
|
|
80
|
+
readonly events: AldusEvent[] = [];
|
|
81
|
+
|
|
82
|
+
emit(event: AldusEvent): Promise<void> {
|
|
83
|
+
this.events.push(event);
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A {@link ReleaseReceiptStore} backed by `@aldus-runtime/file-store`'s per-Run `release.json` (§7).
|
|
90
|
+
*
|
|
91
|
+
* `RunStore.addRecord` takes the Run lock itself, so this must not be called from inside code
|
|
92
|
+
* that already holds it — file locks are not re-entrant and the acquisition is refused outright
|
|
93
|
+
* (ADR-0005). The executor therefore holds no Run lock while writing receipts.
|
|
94
|
+
*/
|
|
95
|
+
export function runStoreReceipts(runs: RunStore): ReleaseReceiptStore {
|
|
96
|
+
return {
|
|
97
|
+
list: (runId) => runs.listRecords(runId, "release"),
|
|
98
|
+
append: (runId, receipt) => runs.addRecord(runId, "release", receipt),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A {@link ReleaseEventSink} backed by `@aldus-runtime/file-store`'s event log (§6.4).
|
|
104
|
+
*
|
|
105
|
+
* Carries the same caution as {@link runStoreReceipts}: `EventStore.append` takes the Run lock to
|
|
106
|
+
* assign a sequence (ADR-0005).
|
|
107
|
+
*/
|
|
108
|
+
export function eventStoreSink(events: EventStore): ReleaseEventSink {
|
|
109
|
+
return {
|
|
110
|
+
emit: async (event) => {
|
|
111
|
+
await events.append(event.runId, event);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|