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.20.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.20.1",
19
- "anbaric-tsapi": "^1.20.1"
18
+ "anbaric-impl-cloud": "^1.21.0",
19
+ "anbaric-tsapi": "^1.21.0"
20
20
  },
21
21
  "files": [
22
22
  "src"
@@ -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
- constructor(workflowId : string, states : Array<State>, startState? : string, dataSchema : Array<PropertyDefinition> = [], persistence : JobPersistence = JobPersistenceFactory.instance(), queue : Queue = QueueFactory.instance(), sameStateDelayMs : number = 5 * 60_000, auditor : Auditor = AuditorFactory.instance()) {
37
+ private readonly NO_TRANSITION_REQUEUE_DELAY: number = 5 * 60_000;
37
38
 
38
- const appId = process.env.ANBARIC_APP_ID;
39
- this.workflowId = appId ? `${appId}/${workflowId}` : workflowId;
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.sameStateDelayMs));
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, interaction : Array<string>,
6
- description : string, details : any) : Promise<void> {
7
- console.log(`[${resourceType} ${resourceId}] ${actor.id} ${interaction.join(",")} ${description} ${JSON.stringify(details ?? null)}`);
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
  }
@@ -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
- subscribe(workflowId : string, processJob : ProcessJob) : void {
14
- this.subscribers.set(workflowId, processJob);
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