anbaric-state-machine 1.20.1 → 1.21.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-state-machine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "This is the state machine library that one can use to add state management to an application. It will, by default, be locally runnable but can deployed to the Anbaric Cloud via the CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "chris@anbaric.ai",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"typescript": "^7.0.2"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"anbaric-impl-cloud": "^1.
|
|
19
|
-
"anbaric-tsapi": "^1.
|
|
18
|
+
"anbaric-impl-cloud": "^1.21.0",
|
|
19
|
+
"anbaric-tsapi": "^1.21.0"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"src"
|
package/src/StateMachine.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Action,
|
|
3
3
|
Actor,
|
|
4
|
+
AppAware,
|
|
4
5
|
Auditor,
|
|
5
6
|
Await,
|
|
6
7
|
Consumer,
|
|
8
|
+
currentAppId,
|
|
7
9
|
Job,
|
|
8
10
|
JobPersistence,
|
|
9
11
|
PropertyDefinition,
|
|
@@ -19,7 +21,7 @@ import {ConsumerFactory} from "./scheduling/ConsumerFactory";
|
|
|
19
21
|
import {AuditorFactory} from "./auditing/AuditorFactory";
|
|
20
22
|
import {Code} from "./actors/Code";
|
|
21
23
|
|
|
22
|
-
class StateMachine {
|
|
24
|
+
class StateMachine implements AppAware {
|
|
23
25
|
|
|
24
26
|
readonly workflowId: string;
|
|
25
27
|
readonly states: Map<string, State>;
|
|
@@ -30,44 +32,51 @@ class StateMachine {
|
|
|
30
32
|
private queue: Queue;
|
|
31
33
|
private consumer: Consumer;
|
|
32
34
|
private machineActor: Code;
|
|
33
|
-
private sameStateDelayMs: number;
|
|
34
35
|
private auditor: Auditor;
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
private readonly NO_TRANSITION_REQUEUE_DELAY: number = 5 * 60_000;
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
constructor(workflowId : string, states : Array<State>, startState? : string, dataSchema : Array<PropertyDefinition> = [], persistence : JobPersistence = JobPersistenceFactory.instance(), queue : Queue = QueueFactory.instance(), auditor : Auditor = AuditorFactory.instance()) {
|
|
40
|
+
|
|
41
|
+
// The workflow's identity is the composite (appId, workflowId): the app
|
|
42
|
+
// it is deployed in (from the environment, via getAppId()) and the
|
|
43
|
+
// machine's own id, kept as separate values rather than a concatenated
|
|
44
|
+
// string.
|
|
45
|
+
this.workflowId = workflowId;
|
|
40
46
|
this.states = new Map(states.map(state => [state.id, state]));
|
|
41
47
|
this.startState = startState ?? states[0].id;
|
|
42
48
|
this.dataSchema = new Map(dataSchema.map(property => [property.id, property]));
|
|
43
49
|
this.persistence = persistence;
|
|
44
50
|
this.queue = queue;
|
|
45
51
|
this.machineActor = new Code(this.workflowId, "state-machine");
|
|
46
|
-
this.sameStateDelayMs = sameStateDelayMs;
|
|
47
52
|
this.auditor = auditor;
|
|
48
53
|
|
|
49
54
|
this.consumer = ConsumerFactory.instance(queue)
|
|
50
|
-
this.consumer.subscribe(this.workflowId, jobId => this.progressJob(jobId));
|
|
55
|
+
this.consumer.subscribe(this.getAppId(), this.workflowId, jobId => this.progressJob(jobId));
|
|
51
56
|
|
|
52
|
-
void this.auditor.audit("state-machine", this.workflowId, SystemActor.actor, ["INITIALIZE"],
|
|
57
|
+
void this.auditor.audit(this.getAppId(), "state-machine", this.workflowId, SystemActor.actor, ["INITIALIZE"],
|
|
53
58
|
"State machine initialised", this.describe()).catch(() => {});
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
getAppId() : string {
|
|
62
|
+
return currentAppId();
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
private describe() : WorkflowDefinition {
|
|
57
|
-
return WorkflowDefinition.describe(this.workflowId, this.startState,
|
|
66
|
+
return WorkflowDefinition.describe(this.getAppId(), this.workflowId, this.startState,
|
|
58
67
|
[...this.states.values()], [...this.dataSchema.values()]);
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
async startJob(properties?: Map<string, any>, actor : Actor = this.machineActor): Promise<Job> {
|
|
62
71
|
|
|
63
72
|
const job = new Job(crypto.randomUUID(), properties, this.startState, this.workflowId,
|
|
64
|
-
actor?.id ?? this.workflowId);
|
|
73
|
+
this.getAppId(), actor?.id ?? this.workflowId);
|
|
65
74
|
|
|
66
75
|
if (! this.validateProperties(properties ?? new Map(), true)) throw new Error("Invalid properties");
|
|
67
76
|
if (! this.authorizeActor(actor, job)) throw new Error("Unauthorized");
|
|
68
77
|
|
|
69
78
|
await this.persistence.create(actor, job);
|
|
70
|
-
await this.queue.enqueue(job.id, this.workflowId);
|
|
79
|
+
await this.queue.enqueue(job.id, this.getAppId(), this.workflowId);
|
|
71
80
|
|
|
72
81
|
return job;
|
|
73
82
|
}
|
|
@@ -161,7 +170,7 @@ class StateMachine {
|
|
|
161
170
|
job.waitingFor = undefined;
|
|
162
171
|
await this.persistence.save(involvedActors[0] ?? this.machineActor, `Job ${job.id} progressed automatically`,
|
|
163
172
|
job, propertiesChanged ? job.properties : undefined, newState);
|
|
164
|
-
if (! this.states.get(newState)?.isTerminal) await this.queue.enqueue(job.id, this.workflowId);
|
|
173
|
+
if (! this.states.get(newState)?.isTerminal) await this.queue.enqueue(job.id, this.getAppId(), this.workflowId);
|
|
165
174
|
return;
|
|
166
175
|
}
|
|
167
176
|
|
|
@@ -171,7 +180,7 @@ class StateMachine {
|
|
|
171
180
|
|
|
172
181
|
await this.persistence.save(involvedActors[0] ?? this.machineActor, `Job ${job.id} progressed automatically`,
|
|
173
182
|
job, job.properties);
|
|
174
|
-
await this.queue.schedule(job.id, this.workflowId, new Date(Date.now() + this.
|
|
183
|
+
await this.queue.schedule(job.id, this.getAppId(), this.workflowId, new Date(Date.now() + this.NO_TRANSITION_REQUEUE_DELAY));
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
private async updateJobInternal(actor: Actor, message : string, job: Job, newProperties?: Map<string, any>, newState? : string) {
|
|
@@ -179,7 +188,7 @@ class StateMachine {
|
|
|
179
188
|
if (newProperties && !this.validateProperties(newProperties, false)) throw new Error("Invalid properties");
|
|
180
189
|
|
|
181
190
|
await this.persistence.save(actor, "Properties Updated", job, newProperties, newState)
|
|
182
|
-
await this.queue.enqueue(job.id, this.workflowId);
|
|
191
|
+
await this.queue.enqueue(job.id, this.getAppId(), this.workflowId);
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
private validateProperties(properties : Map<string, any>, isNew : boolean) : boolean {
|
|
@@ -2,9 +2,10 @@ import {Actor, Auditor} from "anbaric-tsapi";
|
|
|
2
2
|
|
|
3
3
|
class ConsoleAuditor implements Auditor {
|
|
4
4
|
|
|
5
|
-
async audit(resourceType : string, resourceId : string, actor : Actor,
|
|
6
|
-
description : string, details : any) : Promise<void> {
|
|
7
|
-
|
|
5
|
+
async audit(appId : string | undefined, resourceType : string, resourceId : string, actor : Actor,
|
|
6
|
+
interaction : Array<string>, description : string, details : any) : Promise<void> {
|
|
7
|
+
const scope = appId ? `${appId}/` : "";
|
|
8
|
+
console.log(`[${scope}${resourceType} ${resourceId}] ${actor.id} ${interaction.join(",")} ${description} ${JSON.stringify(details ?? null)}`);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
}
|
|
Binary file
|
|
@@ -5,12 +5,12 @@ class InMemoryQueue implements Dequeue {
|
|
|
5
5
|
private ready : Array<QueueMessage> = [];
|
|
6
6
|
private scheduled : Array<{ message : QueueMessage, due : Date }> = [];
|
|
7
7
|
|
|
8
|
-
async enqueue(jobId : string, workflowId : string) : Promise<void> {
|
|
9
|
-
this.ready.push({ jobId, workflowId });
|
|
8
|
+
async enqueue(jobId : string, appId : string | undefined, workflowId : string) : Promise<void> {
|
|
9
|
+
this.ready.push({ jobId, appId, workflowId });
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async schedule(jobId : string, workflowId : string, due : Date) : Promise<void> {
|
|
13
|
-
this.scheduled.push({ message: { jobId, workflowId }, due });
|
|
12
|
+
async schedule(jobId : string, appId : string | undefined, workflowId : string, due : Date) : Promise<void> {
|
|
13
|
+
this.scheduled.push({ message: { jobId, appId, workflowId }, due });
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
async dequeueSome() : Promise<Array<QueueMessage>> {
|
|
@@ -10,8 +10,14 @@ class PullConsumer implements Consumer {
|
|
|
10
10
|
|
|
11
11
|
constructor(private queue : Dequeue, private pollIntervalMs : number = 1000) {}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Subscribers are keyed by the (appId, workflowId) composite, encoded as a
|
|
14
|
+
// tuple so an app and a machine id can never collide.
|
|
15
|
+
private key(appId : string | undefined, workflowId : string) : string {
|
|
16
|
+
return JSON.stringify([appId || null, workflowId]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
subscribe(appId : string | undefined, workflowId : string, processJob : ProcessJob) : void {
|
|
20
|
+
this.subscribers.set(this.key(appId, workflowId), processJob);
|
|
15
21
|
if (!this.ticker) {
|
|
16
22
|
this.ticker = setInterval(() => void this.drain(), this.pollIntervalMs);
|
|
17
23
|
this.ticker.unref();
|
|
@@ -37,15 +43,15 @@ class PullConsumer implements Consumer {
|
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
private async forward(message : QueueMessage) : Promise<void> {
|
|
40
|
-
const processJob = this.subscribers.get(message.workflowId);
|
|
46
|
+
const processJob = this.subscribers.get(this.key(message.appId, message.workflowId));
|
|
41
47
|
if (!processJob) {
|
|
42
|
-
await this.queue.enqueue(message.jobId, message.workflowId);
|
|
48
|
+
await this.queue.enqueue(message.jobId, message.appId, message.workflowId);
|
|
43
49
|
return;
|
|
44
50
|
}
|
|
45
51
|
try {
|
|
46
52
|
await processJob(message.jobId);
|
|
47
53
|
} catch {
|
|
48
|
-
await this.queue.enqueue(message.jobId, message.workflowId);
|
|
54
|
+
await this.queue.enqueue(message.jobId, message.appId, message.workflowId);
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|