task-manager-logic 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "task-manager-logic",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
- "description": "任务管理实现:状态机、组聚合、任务树存储",
5
+ "description": "Runtime-first TaskSpace transition processors and backend-neutral in-memory owner",
6
6
  "type": "module",
7
7
  "main": "./src/index.ts",
8
8
  "exports": {
9
9
  ".": "./src/index.ts"
10
10
  },
11
11
  "dependencies": {
12
- "task-manager-contract": "0.1.1"
12
+ "task-manager-contract": "0.1.3"
13
13
  },
14
14
  "files": [
15
15
  "src"
package/src/errors.ts ADDED
@@ -0,0 +1,32 @@
1
+ export type TaskManagerErrorCode =
2
+ | 'artifact-conflict'
3
+ | 'artifact-not-found'
4
+ | 'canonical-data-invalid'
5
+ | 'claim-expired'
6
+ | 'claim-not-expired'
7
+ | 'claim-token-mismatch'
8
+ | 'command-receipt-mismatch'
9
+ | 'cross-space-identity'
10
+ | 'cyclic-task-forest'
11
+ | 'duplicate-task-identity'
12
+ | 'invalid-commit'
13
+ | 'invalid-task-definition'
14
+ | 'invalid-task-transition'
15
+ | 'lease-out-of-range'
16
+ | 'revision-conflict'
17
+ | 'task-not-found'
18
+ | 'task-not-ready'
19
+ | 'task-space-already-exists'
20
+ | 'task-space-not-found'
21
+ | 'terminal-task-immutable';
22
+
23
+ export class TaskManagerError extends Error {
24
+ readonly name = 'TaskManagerError';
25
+
26
+ constructor(
27
+ readonly code: TaskManagerErrorCode,
28
+ message: string,
29
+ ) {
30
+ super(`[${code}] ${message}`);
31
+ }
32
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export * from './errors.js';
1
2
  export * from './statusMachine.js';
2
- export * from './tree.js';
3
- export * from './stores.js';
3
+ export * from './validation.js';
4
+ export * from './memory-owner.js';
5
+ export * from './processors.js';
@@ -0,0 +1,191 @@
1
+ import type {
2
+ TaskArtifactBody,
3
+ TaskArtifactDigest,
4
+ TaskCommandId,
5
+ TaskCommandReceipt,
6
+ TaskHistoryEvent,
7
+ TaskSpaceCommit,
8
+ TaskSpaceCommitResult,
9
+ TaskSpaceId,
10
+ TaskSpaceOwnerPort,
11
+ TaskSpaceSnapshot,
12
+ } from 'task-manager-contract';
13
+ import { canonicalOwnDataDigest } from 'task-manager-contract';
14
+ import { TaskManagerError } from './errors.js';
15
+ import { assertArtifactBody, assertArtifactRef, deepImmutableClone } from './validation.js';
16
+
17
+ interface MemoryTaskSpaceState {
18
+ readonly snapshot: TaskSpaceSnapshot;
19
+ readonly history: readonly TaskHistoryEvent[];
20
+ readonly receiptsByCommand: ReadonlyMap<TaskCommandId, TaskCommandReceipt>;
21
+ readonly artifacts: ReadonlyMap<TaskArtifactDigest, TaskArtifactBody>;
22
+ }
23
+
24
+ /** Deterministic effect adapter for tests and embedded single-process use. */
25
+ export class InMemoryTaskSpaceOwner implements TaskSpaceOwnerPort {
26
+ readonly #spaces = new Map<TaskSpaceId, MemoryTaskSpaceState>();
27
+
28
+ async readSnapshot(taskSpaceId: TaskSpaceId): Promise<TaskSpaceSnapshot | undefined> {
29
+ const snapshot = this.#spaces.get(taskSpaceId)?.snapshot;
30
+ return snapshot === undefined ? undefined : deepImmutableClone(snapshot);
31
+ }
32
+
33
+ async readReceiptByCommand(
34
+ taskSpaceId: TaskSpaceId,
35
+ commandId: TaskCommandId,
36
+ ): Promise<TaskCommandReceipt | undefined> {
37
+ const receipt = this.#spaces.get(taskSpaceId)?.receiptsByCommand.get(commandId);
38
+ return receipt === undefined ? undefined : deepImmutableClone(receipt);
39
+ }
40
+
41
+ async readHistory(taskSpaceId: TaskSpaceId): Promise<readonly TaskHistoryEvent[]> {
42
+ return deepImmutableClone(this.#spaces.get(taskSpaceId)?.history ?? []);
43
+ }
44
+
45
+ async readArtifact(
46
+ taskSpaceId: TaskSpaceId,
47
+ digest: TaskArtifactDigest,
48
+ ): Promise<TaskArtifactBody | undefined> {
49
+ const artifact = this.#spaces.get(taskSpaceId)?.artifacts.get(digest);
50
+ return artifact === undefined ? undefined : deepImmutableClone(artifact);
51
+ }
52
+
53
+ async compareAndSwap(commit: TaskSpaceCommit): Promise<TaskSpaceCommitResult> {
54
+ commit = deepImmutableClone(commit);
55
+ const current = this.#spaces.get(commit.taskSpaceId);
56
+ const replay = current?.receiptsByCommand.get(commit.commandId);
57
+ if (current && replay) {
58
+ if (replay.commandDigest !== commit.commandDigest) {
59
+ throw new TaskManagerError('command-receipt-mismatch', `command ${commit.commandId} digest does not match prior admission`);
60
+ }
61
+ return deepImmutableClone({
62
+ kind: 'command-replayed',
63
+ taskSpaceId: commit.taskSpaceId,
64
+ revision: current.snapshot.revision,
65
+ snapshot: current.snapshot,
66
+ receipt: replay,
67
+ });
68
+ }
69
+
70
+ const actualRevision = current?.snapshot.revision ?? null;
71
+ if (actualRevision !== commit.expectedRevision) {
72
+ return deepImmutableClone({
73
+ kind: 'revision-conflict',
74
+ taskSpaceId: commit.taskSpaceId,
75
+ expectedRevision: commit.expectedRevision,
76
+ actualRevision,
77
+ });
78
+ }
79
+
80
+ const receipt = validateCommit(commit, current);
81
+ const receiptsByCommand = new Map(current?.receiptsByCommand ?? []);
82
+ for (const candidate of commit.receipts) receiptsByCommand.set(candidate.commandId, deepImmutableClone(candidate));
83
+ const artifacts = new Map(current?.artifacts ?? []);
84
+ for (const artifact of commit.artifacts) {
85
+ const prior = artifacts.get(artifact.digest);
86
+ if (prior && !sameArtifact(prior, artifact)) {
87
+ throw new TaskManagerError('artifact-conflict', `digest ${artifact.digest} has conflicting content`);
88
+ }
89
+ artifacts.set(artifact.digest, deepImmutableClone(artifact));
90
+ }
91
+ const next: MemoryTaskSpaceState = {
92
+ snapshot: deepImmutableClone(commit.snapshot),
93
+ history: deepImmutableClone([...(current?.history ?? []), ...commit.events]),
94
+ receiptsByCommand,
95
+ artifacts,
96
+ };
97
+ this.#spaces.set(commit.taskSpaceId, next);
98
+ return deepImmutableClone({
99
+ kind: 'commit-accepted',
100
+ taskSpaceId: commit.taskSpaceId,
101
+ revision: next.snapshot.revision,
102
+ snapshot: next.snapshot,
103
+ receipt,
104
+ });
105
+ }
106
+ }
107
+
108
+ function validateCommit(
109
+ commit: TaskSpaceCommit,
110
+ current: MemoryTaskSpaceState | undefined,
111
+ ): TaskCommandReceipt {
112
+ const expectedNextRevision = commit.expectedRevision === null ? 0 : commit.expectedRevision + 1;
113
+ if (commit.kind !== 'task-space-commit'
114
+ || commit.command.commandId !== commit.commandId
115
+ || commit.command.taskSpaceId !== commit.taskSpaceId
116
+ || canonicalOwnDataDigest(commit.command) !== commit.commandDigest
117
+ || commit.snapshot.taskSpaceId !== commit.taskSpaceId
118
+ || commit.snapshot.definition.taskSpaceId !== commit.taskSpaceId
119
+ || commit.snapshot.revision !== expectedNextRevision) {
120
+ invalid('commit snapshot identity or revision is inconsistent');
121
+ }
122
+ if (commit.receipts.length !== 1) invalid('one command admission must contain exactly one receipt');
123
+ const receipt = commit.receipts[0];
124
+ if (!receipt
125
+ || receipt.commandId !== commit.commandId
126
+ || receipt.commandDigest !== commit.commandDigest
127
+ || receipt.taskSpaceId !== commit.taskSpaceId) {
128
+ invalid('commit receipt does not match command identity');
129
+ }
130
+ if (receipt.toRevision !== expectedNextRevision) invalid('receipt toRevision does not match snapshot');
131
+ if (receipt.fromRevision !== (commit.expectedRevision ?? 0)) invalid('receipt fromRevision does not match CAS');
132
+
133
+ const priorHistoryIds = current?.snapshot.historyEventIds ?? [];
134
+ const expectedHistoryIds = [...priorHistoryIds, ...commit.events.map(({ eventId }) => eventId)];
135
+ if (!sameStrings(commit.snapshot.historyEventIds, expectedHistoryIds)) invalid('snapshot history is not append-only');
136
+ const priorReceiptIds = current?.snapshot.issuedReceiptIds ?? [];
137
+ const expectedReceiptIds = [...priorReceiptIds, ...commit.receipts.map(({ receiptId }) => receiptId)];
138
+ if (!sameStrings(commit.snapshot.issuedReceiptIds, expectedReceiptIds)) invalid('snapshot receipt history is not append-only');
139
+
140
+ const seenEvents = new Set(priorHistoryIds);
141
+ for (let index = 0; index < commit.events.length; index += 1) {
142
+ const event = commit.events[index]!;
143
+ if (event.taskSpaceId !== commit.taskSpaceId
144
+ || event.commandId !== commit.commandId
145
+ || event.sequence !== priorHistoryIds.length + index
146
+ || event.fromRevision !== (commit.expectedRevision ?? 0)
147
+ || event.toRevision !== expectedNextRevision
148
+ || seenEvents.has(event.eventId)) {
149
+ invalid(`event ${event.eventId} violates append-only commit identity`);
150
+ }
151
+ seenEvents.add(event.eventId);
152
+ }
153
+ const availableArtifacts = new Map(current?.artifacts ?? []);
154
+ for (const body of commit.artifacts) {
155
+ assertArtifactBody(body, `artifact body ${body.digest}`);
156
+ const prior = availableArtifacts.get(body.digest);
157
+ if (prior && canonicalOwnDataDigest(prior) !== canonicalOwnDataDigest(body)) {
158
+ throw new TaskManagerError('artifact-conflict', `artifact ${body.digest} has conflicting content`);
159
+ }
160
+ availableArtifacts.set(body.digest, body);
161
+ }
162
+ const refs = [
163
+ ...commit.snapshot.definition.tasks.flatMap(({ inputArtifacts }) => inputArtifacts),
164
+ ...commit.snapshot.tasks.flatMap(({ outputArtifacts }) => outputArtifacts),
165
+ ];
166
+ for (const ref of refs) {
167
+ assertArtifactRef(ref, `artifact ref ${ref.digest}`);
168
+ const body = availableArtifacts.get(ref.digest);
169
+ if (!body) throw new TaskManagerError('artifact-not-found', `artifact ${ref.digest} is not present in owner CAS`);
170
+ if (body.mediaType !== ref.mediaType || body.sizeBytes !== ref.sizeBytes) {
171
+ throw new TaskManagerError('artifact-conflict', `artifact ${ref.digest} metadata disagrees with owner body`);
172
+ }
173
+ }
174
+ return receipt;
175
+ }
176
+
177
+ function sameStrings(left: readonly string[], right: readonly string[]): boolean {
178
+ return left.length === right.length && left.every((value, index) => value === right[index]);
179
+ }
180
+
181
+ function sameArtifact(left: TaskArtifactBody, right: TaskArtifactBody): boolean {
182
+ return left.digest === right.digest
183
+ && left.mediaType === right.mediaType
184
+ && left.sizeBytes === right.sizeBytes
185
+ && left.encoding === right.encoding
186
+ && left.data === right.data;
187
+ }
188
+
189
+ function invalid(message: string): never {
190
+ throw new TaskManagerError('invalid-commit', message);
191
+ }