@entity-access/entity-access 1.0.45 → 1.0.47

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.
Files changed (116) hide show
  1. package/.vscode/launch.json +20 -1
  2. package/dist/common/Logger.d.ts +8 -1
  3. package/dist/common/Logger.d.ts.map +1 -1
  4. package/dist/common/Logger.js +18 -1
  5. package/dist/common/Logger.js.map +1 -1
  6. package/dist/common/ObjectPool.d.ts +42 -0
  7. package/dist/common/ObjectPool.d.ts.map +1 -0
  8. package/dist/common/ObjectPool.js +86 -0
  9. package/dist/common/ObjectPool.js.map +1 -0
  10. package/dist/common/sleep.d.ts +3 -0
  11. package/dist/common/sleep.d.ts.map +1 -0
  12. package/dist/common/sleep.js +17 -0
  13. package/dist/common/sleep.js.map +1 -0
  14. package/dist/decorators/Column.js +1 -1
  15. package/dist/decorators/Column.js.map +1 -1
  16. package/dist/di/di.d.ts +2 -2
  17. package/dist/di/di.d.ts.map +1 -1
  18. package/dist/di/di.js +33 -19
  19. package/dist/di/di.js.map +1 -1
  20. package/dist/drivers/base/BaseDriver.d.ts.map +1 -1
  21. package/dist/drivers/base/BaseDriver.js.map +1 -1
  22. package/dist/drivers/postgres/PostgreSqlDriver.d.ts +2 -2
  23. package/dist/drivers/postgres/PostgreSqlDriver.d.ts.map +1 -1
  24. package/dist/drivers/postgres/PostgreSqlDriver.js +54 -25
  25. package/dist/drivers/postgres/PostgreSqlDriver.js.map +1 -1
  26. package/dist/drivers/sql-server/ExpressionToSqlServer.d.ts +2 -1
  27. package/dist/drivers/sql-server/ExpressionToSqlServer.d.ts.map +1 -1
  28. package/dist/drivers/sql-server/ExpressionToSqlServer.js +3 -0
  29. package/dist/drivers/sql-server/ExpressionToSqlServer.js.map +1 -1
  30. package/dist/eternity/ActivitySuspendedError.d.ts +3 -0
  31. package/dist/eternity/ActivitySuspendedError.d.ts.map +1 -0
  32. package/dist/eternity/ActivitySuspendedError.js +3 -0
  33. package/dist/eternity/ActivitySuspendedError.js.map +1 -0
  34. package/dist/eternity/EternityContext.d.ts +27 -0
  35. package/dist/eternity/EternityContext.d.ts.map +1 -0
  36. package/dist/eternity/EternityContext.js +230 -0
  37. package/dist/eternity/EternityContext.js.map +1 -0
  38. package/dist/eternity/EternityStorage.d.ts +40 -0
  39. package/dist/eternity/EternityStorage.d.ts.map +1 -0
  40. package/dist/eternity/EternityStorage.js +217 -0
  41. package/dist/eternity/EternityStorage.js.map +1 -0
  42. package/dist/eternity/Workflow.d.ts +24 -0
  43. package/dist/eternity/Workflow.d.ts.map +1 -0
  44. package/dist/eternity/Workflow.js +49 -0
  45. package/dist/eternity/Workflow.js.map +1 -0
  46. package/dist/eternity/WorkflowClock.d.ts +5 -0
  47. package/dist/eternity/WorkflowClock.d.ts.map +1 -0
  48. package/dist/eternity/WorkflowClock.js +18 -0
  49. package/dist/eternity/WorkflowClock.js.map +1 -0
  50. package/dist/eternity/WorkflowRegistry.d.ts +13 -0
  51. package/dist/eternity/WorkflowRegistry.d.ts.map +1 -0
  52. package/dist/eternity/WorkflowRegistry.js +24 -0
  53. package/dist/eternity/WorkflowRegistry.js.map +1 -0
  54. package/dist/migrations/Migrations.d.ts.map +1 -1
  55. package/dist/migrations/Migrations.js +1 -0
  56. package/dist/migrations/Migrations.js.map +1 -1
  57. package/dist/model/EntityContext.d.ts +1 -1
  58. package/dist/model/EntityContext.d.ts.map +1 -1
  59. package/dist/model/EntityContext.js +2 -1
  60. package/dist/model/EntityContext.js.map +1 -1
  61. package/dist/model/EntityQuery.d.ts.map +1 -1
  62. package/dist/model/EntityQuery.js +1 -3
  63. package/dist/model/EntityQuery.js.map +1 -1
  64. package/dist/query/ast/DebugStringVisitor.d.ts +3 -1
  65. package/dist/query/ast/DebugStringVisitor.d.ts.map +1 -1
  66. package/dist/query/ast/DebugStringVisitor.js +6 -0
  67. package/dist/query/ast/DebugStringVisitor.js.map +1 -1
  68. package/dist/query/ast/ExpressionToSql.d.ts +3 -1
  69. package/dist/query/ast/ExpressionToSql.d.ts.map +1 -1
  70. package/dist/query/ast/ExpressionToSql.js +19 -3
  71. package/dist/query/ast/ExpressionToSql.js.map +1 -1
  72. package/dist/query/ast/Expressions.d.ts +10 -7
  73. package/dist/query/ast/Expressions.d.ts.map +1 -1
  74. package/dist/query/ast/Expressions.js +12 -4
  75. package/dist/query/ast/Expressions.js.map +1 -1
  76. package/dist/query/ast/Visitor.d.ts +3 -1
  77. package/dist/query/ast/Visitor.d.ts.map +1 -1
  78. package/dist/query/ast/Visitor.js +10 -0
  79. package/dist/query/ast/Visitor.js.map +1 -1
  80. package/dist/query/parser/ArrowToExpression.d.ts +2 -2
  81. package/dist/query/parser/ArrowToExpression.d.ts.map +1 -1
  82. package/dist/query/parser/ArrowToExpression.js +2 -2
  83. package/dist/query/parser/ArrowToExpression.js.map +1 -1
  84. package/dist/tests/eternity/eternity-tests.d.ts +3 -0
  85. package/dist/tests/eternity/eternity-tests.d.ts.map +1 -0
  86. package/dist/tests/eternity/eternity-tests.js +91 -0
  87. package/dist/tests/eternity/eternity-tests.js.map +1 -0
  88. package/dist/tests/security/tests/place-order.js +6 -2
  89. package/dist/tests/security/tests/place-order.js.map +1 -1
  90. package/dist/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +1 -1
  92. package/src/common/Logger.ts +25 -2
  93. package/src/common/ObjectPool.ts +124 -0
  94. package/src/common/sleep.ts +16 -0
  95. package/src/decorators/Column.ts +1 -1
  96. package/src/di/di.ts +37 -20
  97. package/src/drivers/base/BaseDriver.ts +2 -1
  98. package/src/drivers/postgres/PostgreSqlDriver.ts +59 -32
  99. package/src/drivers/sql-server/ExpressionToSqlServer.ts +5 -1
  100. package/src/eternity/ActivitySuspendedError.ts +3 -0
  101. package/src/eternity/EternityContext.ts +254 -0
  102. package/src/eternity/EternityStorage.ts +180 -0
  103. package/src/eternity/Workflow.ts +55 -0
  104. package/src/eternity/WorkflowClock.ts +10 -0
  105. package/src/eternity/WorkflowRegistry.ts +34 -0
  106. package/src/migrations/Migrations.ts +2 -0
  107. package/src/model/EntityContext.ts +3 -2
  108. package/src/model/EntityQuery.ts +1 -2
  109. package/src/query/ast/DebugStringVisitor.ts +10 -1
  110. package/src/query/ast/ExpressionToSql.ts +22 -4
  111. package/src/query/ast/Expressions.ts +17 -5
  112. package/src/query/ast/Visitor.ts +11 -1
  113. package/src/query/parser/ArrowToExpression.ts +2 -2
  114. package/src/tests/eternity/eternity-tests.ts +108 -0
  115. package/src/tests/security/tests/place-order.ts +6 -2
  116. package/test.js +38 -3
package/src/di/di.ts CHANGED
@@ -14,39 +14,43 @@ const registrationsSymbol = Symbol("registrations");
14
14
 
15
15
  const serviceProvider = Symbol("serviceProvider");
16
16
 
17
- const parentServiceProvider = Symbol("parentServiceProvider");
17
+ const globalServiceProvider = Symbol("globalInstance");
18
18
 
19
19
  export class ServiceProvider implements IDisposable {
20
20
 
21
- public static get global() {
22
- return this.globalInstance ??= new ServiceProvider();
21
+ public static from(owner: any) {
22
+ return (owner[serviceProvider]) as ServiceProvider;
23
23
  }
24
24
 
25
-
26
25
  public static resolve<T>(serviceOwner: any, type: IClassOf<T>): T {
27
- const sp = (serviceOwner[serviceProvider] ?? this.global) as ServiceProvider;
26
+ const sp = serviceOwner[serviceProvider] as ServiceProvider;
28
27
  return sp.resolve(type);
29
28
  }
30
29
 
31
30
  static create<T>(serviceOwner, type: IClassOf<T>): T {
32
- const sp = (serviceOwner[serviceProvider] ?? this.global) as ServiceProvider;
31
+ const sp = serviceOwner[serviceProvider] as ServiceProvider;
33
32
  return sp.createFromType(type);
34
33
  }
35
34
 
36
- private static globalInstance: ServiceProvider;
35
+ static createScope<T>(serviceOwner): ServiceProvider {
36
+ const sp = serviceOwner[globalServiceProvider] as ServiceProvider;
37
+ return sp.createScope();
38
+ }
37
39
 
38
40
  private map: Map<any,any> = new Map();
39
41
  private disposables: IDisposable[];
40
42
 
41
43
  constructor(parent?: ServiceProvider) {
42
44
  this[serviceProvider] = this;
43
- this[parentServiceProvider] = parent;
45
+ this[globalServiceProvider] = parent?.[globalServiceProvider] ?? this;
46
+ this.map.set(ServiceProvider, this);
44
47
  }
45
48
 
46
49
  add<T1, T extends T1>(type: IAbstractClassOf<T1> | IClassOf<T1>, instance: T) {
47
50
  this.getRegistration(type, true);
48
51
  this.map.set(type, instance);
49
52
  instance[serviceProvider] = this;
53
+ instance[globalServiceProvider] = this[globalServiceProvider];
50
54
  this.resolveProperties(instance);
51
55
  return instance;
52
56
  }
@@ -66,7 +70,7 @@ export class ServiceProvider implements IDisposable {
66
70
  const sd = this.getRegistration(type);
67
71
  switch(sd.kind) {
68
72
  case "Scoped":
69
- if (!this[parentServiceProvider]) {
73
+ if (this[globalServiceProvider] === this) {
70
74
  throw new Error(`Unable to create scoped service ${type?.name ?? type} in global scope.`);
71
75
  }
72
76
  instance = this.map.get(type);
@@ -74,20 +78,19 @@ export class ServiceProvider implements IDisposable {
74
78
  instance = this.createFromDescriptor(sd);
75
79
  this.map.set(type, instance);
76
80
  instance[serviceProvider] = this;
81
+ instance[globalServiceProvider] = this[globalServiceProvider];
77
82
  if (instance[Symbol.disposable] || instance[Symbol.asyncDisposable]) {
78
83
  (this.disposables ??= []).push(instance);
79
84
  }
80
85
  }
81
86
  return instance;
82
87
  case "Singleton":
83
- let sp = this;
84
- while (sp[parentServiceProvider]) {
85
- sp = sp[parentServiceProvider];
86
- }
88
+ const sp = this[globalServiceProvider];
87
89
  instance = sp.map.get(type);
88
90
  if (!instance) {
89
91
  instance = sp.createFromDescriptor(sd);
90
- instance[serviceProvider] = sp;
92
+ instance[serviceProvider] = this;
93
+ instance[globalServiceProvider] = sp;
91
94
  sp.map.set(type, instance);
92
95
  if (instance[Symbol.disposable] || instance[Symbol.asyncDisposable]) {
93
96
  (sp.disposables ??= []).push(instance);
@@ -96,7 +99,8 @@ export class ServiceProvider implements IDisposable {
96
99
  return instance;
97
100
  case "Transient":
98
101
  instance = sp.createFromDescriptor(sd);
99
- instance[serviceProvider] = sp;
102
+ instance[serviceProvider] = this;
103
+ instance[globalServiceProvider] = sp;
100
104
  return instance;
101
105
  }
102
106
  }
@@ -120,7 +124,7 @@ export class ServiceProvider implements IDisposable {
120
124
  if (!sd) {
121
125
 
122
126
  if (add) {
123
- const registration: IServiceDescriptor = { key: type, kind: this[parentServiceProvider] ? "Scoped" : "Singleton" };
127
+ const registration: IServiceDescriptor = { key: type, kind: this[globalServiceProvider] !== this ? "Scoped" : "Singleton" };
124
128
  registrations.set(type, registration);
125
129
  return registration;
126
130
  }
@@ -155,7 +159,7 @@ export class ServiceProvider implements IDisposable {
155
159
  for (const key in keys) {
156
160
  if (Object.prototype.hasOwnProperty.call(keys, key)) {
157
161
  const element = keys[key];
158
- instance[key] = this.resolve(element);
162
+ instance[key] ??= this.resolve(element);
159
163
  }
160
164
  }
161
165
  }
@@ -168,6 +172,7 @@ export class ServiceProvider implements IDisposable {
168
172
  : [];
169
173
  const instance = new type(... injectServices);
170
174
  instance[serviceProvider] = this;
175
+ instance[globalServiceProvider] = this[globalServiceProvider];
171
176
  // initialize properties...
172
177
  this.resolveProperties(instance, type);
173
178
  return instance;
@@ -194,9 +199,21 @@ export const ServiceCollection = {
194
199
  export default function Inject(target, key, index?: number) {
195
200
 
196
201
  if (index !== void 0) {
197
- const plist = (Reflect as any).getMetadata("design:paramtypes", target, key);
198
- const serviceTypes = target[injectServiceTypesSymbol] ??= [];
199
- serviceTypes[index] = plist[index];
202
+
203
+ if (key) {
204
+
205
+ // this is parameter inside a method...
206
+ const plist = (Reflect as any).getMetadata("design:paramtypes", target, key);
207
+ const pTypes = (target[injectServiceKeysSymbol] ??= {})[key] = [];
208
+ pTypes[index] = plist[index];
209
+
210
+ } else {
211
+
212
+ const plist = (Reflect as any).getMetadata("design:paramtypes", target, key);
213
+ const serviceTypes = target[injectServiceTypesSymbol] ??= [];
214
+ serviceTypes[index] = plist[index];
215
+ }
216
+
200
217
  return;
201
218
  }
202
219
 
@@ -1,8 +1,9 @@
1
+ import EntityAccessError from "../../common/EntityAccessError.js";
1
2
  import QueryCompiler from "../../compiler/QueryCompiler.js";
2
3
  import EntityType from "../../entity-query/EntityType.js";
3
4
  import Migrations from "../../migrations/Migrations.js";
4
5
  import ChangeEntry from "../../model/changes/ChangeEntry.js";
5
- import { BinaryExpression, Constant, DeleteStatement, Expression, InsertStatement, QuotedLiteral, ReturnUpdated, TableLiteral, UpdateStatement, ValuesStatement } from "../../query/ast/Expressions.js";
6
+ import { BinaryExpression, Constant, DeleteStatement, ExistsExpression, Expression, InsertStatement, NotExits, QuotedLiteral, ReturnUpdated, SelectStatement, TableLiteral, UnionAllStatement, UpdateStatement, ValuesStatement } from "../../query/ast/Expressions.js";
6
7
 
7
8
  export const disposableSymbol: unique symbol = (Symbol as any).dispose ??= Symbol("disposable");
8
9
 
@@ -1,16 +1,13 @@
1
1
  /* eslint-disable no-console */
2
+ import EntityAccessError from "../../common/EntityAccessError.js";
3
+ import ObjectPool, { IPooledObject } from "../../common/ObjectPool.js";
4
+ import TimedCache from "../../common/cache/TimedCache.js";
2
5
  import QueryCompiler from "../../compiler/QueryCompiler.js";
3
- import { IColumn } from "../../decorators/IColumn.js";
4
- import EntityType from "../../entity-query/EntityType.js";
5
6
  import Migrations from "../../migrations/Migrations.js";
6
7
  import PostgresAutomaticMigrations from "../../migrations/postgres/PostgresAutomaticMigrations.js";
7
- import { Query } from "../../query/Query.js";
8
8
  import { BaseDriver, IDbConnectionString, IDbReader, IQuery, IRecord, toQuery } from "../base/BaseDriver.js";
9
- import pkg from "pg";
9
+ import pg from "pg";
10
10
  import Cursor from "pg-cursor";
11
-
12
- const { Client } = pkg;
13
-
14
11
  export interface IPgSqlConnectionString extends IDbConnectionString {
15
12
 
16
13
  user?: string, // default process.env.PGUSER || process.env.USER
@@ -30,7 +27,7 @@ export interface IPgSqlConnectionString extends IDbConnectionString {
30
27
 
31
28
  class DbReader implements IDbReader {
32
29
 
33
- constructor(private cursor, private client) {
30
+ constructor(private cursor: Cursor, private client: IPooledObject<pg.Client>) {
34
31
 
35
32
  }
36
33
 
@@ -47,27 +44,29 @@ class DbReader implements IDbReader {
47
44
  async dispose() {
48
45
  try {
49
46
  await this.cursor.close();
50
- } catch {
51
- // intentionally left blank
47
+ } catch (error) {
48
+ console.error(error);
52
49
  }
53
50
 
54
51
  try {
55
52
  if (this.client) {
56
- await this.client.end();
53
+ await this.client[Symbol.asyncDisposable]();
57
54
  }
58
- } catch {
59
- // intentionally left blank
55
+ } catch (error) {
56
+ console.error(error);
60
57
  }
61
58
  }
62
59
  }
63
60
 
61
+ const poolCache = new TimedCache<string, ObjectPool<pg.Client>>();
62
+
64
63
  export default class PostgreSqlDriver extends BaseDriver {
65
64
 
66
65
  public get compiler() {
67
66
  return this.myCompiler;
68
67
  }
69
68
 
70
- private transaction: pkg.Client;
69
+ private transaction: IPooledObject<pg.Client>;
71
70
  private myCompiler = new QueryCompiler();
72
71
 
73
72
  constructor(private readonly config: IPgSqlConnectionString) {
@@ -75,6 +74,9 @@ export default class PostgreSqlDriver extends BaseDriver {
75
74
  }
76
75
 
77
76
  public async runInTransaction<T>(fx?: () => Promise<T>): Promise<T> {
77
+ if (this.transaction) {
78
+ throw new EntityAccessError(`Nested Transactions not supported`);
79
+ }
78
80
  const connection = await this.getConnection();
79
81
  let result: T;
80
82
  try {
@@ -88,7 +90,7 @@ export default class PostgreSqlDriver extends BaseDriver {
88
90
  throw error;
89
91
  } finally {
90
92
  this.transaction = void 0;
91
- await connection.end();
93
+ await connection[Symbol.asyncDisposable]();
92
94
  }
93
95
  }
94
96
 
@@ -112,28 +114,35 @@ export default class PostgreSqlDriver extends BaseDriver {
112
114
  return result;
113
115
  } finally {
114
116
  if (!this.transaction) {
115
- await connection.end();
117
+ await connection[Symbol.asyncDisposable]();
116
118
  }
117
119
  }
118
120
  }
119
121
 
120
122
  public ensureDatabase() {
121
123
  const create = async () => {
122
- const defaultDb = "postgres";
123
- const db = this.config.database;
124
- this.config.database = defaultDb;
125
- const connection = await this.getConnection();
126
- // @ts-expect-error readonly
127
- this.config = { ... this.config };
128
- this.config.database = db;
129
124
  try {
130
- const r = await connection.query("SELECT FROM pg_database WHERE datname = $1", [ db ]);
131
- if(r.rowCount === 1) {
132
- return;
125
+ // const defaultDb = "postgres";
126
+ const db = this.config.database;
127
+ // this.config.database = defaultDb;
128
+ // // const connection = await this.getConnection();
129
+ // // @ts-expect-error readonly
130
+ // this.config = { ... this.config };
131
+ // this.config.database = db;
132
+ const connection = new pg.Client({ ... this.config, database: "postgres" });
133
+ await connection.connect();
134
+ try {
135
+ const r = await connection.query("SELECT FROM pg_database WHERE datname = $1", [ db ]);
136
+ if(r.rowCount === 1) {
137
+ return;
138
+ }
139
+ await connection.query("CREATE DATABASE " + JSON.stringify(db));
140
+ } finally {
141
+ await connection.end();
133
142
  }
134
- await connection.query("CREATE DATABASE " + JSON.stringify(db));
135
- } finally {
136
- await connection.end();
143
+ } catch (error) {
144
+ console.error(error);
145
+ throw error;
137
146
  }
138
147
  };
139
148
  const value = create();
@@ -153,8 +162,26 @@ export default class PostgreSqlDriver extends BaseDriver {
153
162
  if (this.transaction) {
154
163
  return this.transaction;
155
164
  }
156
- const client = new Client(this.config);
157
- await client.connect();
165
+
166
+ const key = `${this.config.host}:${this.config.port}//${this.config.database}?${this.config.user}&${this.config.password}`;
167
+
168
+ const pooledClient = poolCache.getOrCreate(key, 1, () => new ObjectPool({
169
+ asyncFactory: async () => {
170
+ const c = new pg.Client(this.config);
171
+ await c.connect();
172
+ return c;
173
+ },
174
+ destroy(item) {
175
+ return item.end();
176
+ },
177
+ subscribeForRemoval(po, clear) {
178
+ po.on("end", clear);
179
+ },
180
+ }));
181
+ const client = await pooledClient.acquire();
182
+
183
+ // const client = new Client(this.config);
184
+ // await client.connect();
158
185
  const row = await client.query("SELECT pg_backend_pid() as id");
159
186
  const clientId = (row.rows as any).id;
160
187
  // there is no support to kill the query running inside
@@ -165,7 +192,7 @@ export default class PostgreSqlDriver extends BaseDriver {
165
192
  }
166
193
 
167
194
  private async kill(id) {
168
- const client = new Client(this.config);
195
+ const client = new pg.Client(this.config);
169
196
  try {
170
197
  await client.connect();
171
198
  await client.query("SELECT pg_cancel_backend($1)", [id]);
@@ -1,5 +1,5 @@
1
1
  import ExpressionToSql from "../../query/ast/ExpressionToSql.js";
2
- import { Identifier, InsertStatement, OrderByExpression, ReturnUpdated, SelectStatement, ValuesStatement } from "../../query/ast/Expressions.js";
2
+ import { BooleanLiteral, Identifier, InsertStatement, OrderByExpression, ReturnUpdated, SelectStatement, ValuesStatement } from "../../query/ast/Expressions.js";
3
3
  import { ITextQuery, prepare } from "../../query/ast/IStringTransformer.js";
4
4
 
5
5
  export default class ExpressionToSqlServer extends ExpressionToSql {
@@ -100,4 +100,8 @@ export default class ExpressionToSqlServer extends ExpressionToSql {
100
100
  return prepare `(VALUES ${rows}) ${fields}`;
101
101
 
102
102
  }
103
+
104
+ visitBooleanLiteral({ value }: BooleanLiteral): ITextQuery {
105
+ return value ? [ " 1 "] : [ " 0 "];
106
+ }
103
107
  }
@@ -0,0 +1,3 @@
1
+
2
+
3
+ export class ActivitySuspendedError extends Error { }
@@ -0,0 +1,254 @@
1
+ /* eslint-disable no-console */
2
+ import { randomUUID } from "crypto";
3
+ import EntityAccessError from "../common/EntityAccessError.js";
4
+ import { IClassOf } from "../decorators/IClassOf.js";
5
+ import Inject, { RegisterSingleton, ServiceProvider, injectServiceKeysSymbol } from "../di/di.js";
6
+ import DateTime from "../types/DateTime.js";
7
+ import EternityStorage, { WorkflowStorage } from "./EternityStorage.js";
8
+ import type Workflow from "./Workflow.js";
9
+ import { ActivitySuspendedError } from "./ActivitySuspendedError.js";
10
+ import { IWorkflowSchema, WorkflowRegistry } from "./WorkflowRegistry.js";
11
+ import crypto from "crypto";
12
+ import TimeSpan from "../types/TimeSpan.js";
13
+ import WorkflowClock from "./WorkflowClock.js";
14
+
15
+ async function hash(text) {
16
+ const sha256 = crypto.createHash("sha256");
17
+ return sha256.update(text).digest("hex");
18
+ }
19
+
20
+ function bindStep(context: EternityContext, store: WorkflowStorage, name: string, old: (... a: any[]) => any, unique = false) {
21
+ return async function runStep(this: Workflow, ... a: any[]) {
22
+ const input = JSON.stringify(a);
23
+ const ts = unique ? "0" : Math.floor(this.currentTime.msSinceEpoch);
24
+ const params = input.length < 150 ? input : await hash(input);
25
+ const id = `${this.id}(${params},${ts})`;
26
+
27
+ const clock = context.storage.clock;
28
+
29
+ const existing = await context.storage.get(id);
30
+ if (existing) {
31
+ if (existing.state === "failed" && existing.error) {
32
+ throw new Error(existing.error);
33
+ }
34
+ if (existing.state === "done") {
35
+ (this as any).currentTime = existing.updated;
36
+ return JSON.parse(existing.output);
37
+ }
38
+ }
39
+
40
+ store.lastID = id;
41
+
42
+ const step: Partial<WorkflowStorage> = {
43
+ id,
44
+ parentID: this.id,
45
+ eta: this.eta,
46
+ queued: this.eta,
47
+ updated: this.eta,
48
+ isWorkflow: false,
49
+ name,
50
+ input
51
+ };
52
+
53
+ // execute...
54
+ const start = clock.utcNow;
55
+ let lastError: Error;
56
+ let lastResult: any;
57
+
58
+ if (name === "delay" || name === "waitForExternalEvent") {
59
+
60
+ // first parameter is the ts
61
+ const maxTS = a[0] as TimeSpan;
62
+ const eta = this.currentTime.add(maxTS);
63
+ step.eta = eta;
64
+
65
+ if (eta <= start) {
66
+ // time is up...
67
+ lastResult = "";
68
+ step.state = "done";
69
+ }
70
+
71
+ } else {
72
+
73
+ try {
74
+
75
+ const types = Object.getPrototypeOf(this)?.[injectServiceKeysSymbol]?.[name] as any[];
76
+ for (let index = a.length; index < types.length; index++) {
77
+ const element = ServiceProvider.resolve(this, types[index]);
78
+ a.push(element);
79
+ }
80
+ lastResult = (await old.apply(this, a)) ?? 0;
81
+ step.output = JSON.stringify(lastResult);
82
+ step.state = "done";
83
+ step.eta = clock.utcNow;
84
+ (this as any).currentTime = step.eta;
85
+ } catch (error) {
86
+ if (error instanceof ActivitySuspendedError) {
87
+ return;
88
+ }
89
+ lastError = error;
90
+ step.error = error.stack ?? error.toString();
91
+ step.state = "failed";
92
+ step.eta = clock.utcNow;
93
+ (this as any).currentTime = step.eta;
94
+ }
95
+ step.queued = start;
96
+ step.updated = step.updated;
97
+ }
98
+ await this.context.storage.save(step);
99
+ if (lastError) {
100
+ throw lastError;
101
+ }
102
+ if (step.state !== "done") {
103
+ throw new ActivitySuspendedError();
104
+ }
105
+ return lastResult;
106
+ };
107
+ }
108
+
109
+ export interface IWorkflowResult<T> {
110
+ output: T;
111
+ state: "done" | "failed" | "queued";
112
+ error: string;
113
+ }
114
+
115
+ @RegisterSingleton
116
+ export default class EternityContext {
117
+
118
+ private waiter: AbortController;
119
+
120
+ private registry: Map<string, IWorkflowSchema> = new Map();
121
+
122
+ constructor(
123
+ @Inject
124
+ public storage: EternityStorage
125
+ ) {
126
+
127
+ }
128
+
129
+ public register(type: IClassOf<Workflow>) {
130
+ this.registry.set(type.name, WorkflowRegistry.register(type, void 0));
131
+ }
132
+
133
+ public async start(signal?: AbortSignal) {
134
+ while(!signal?.aborted) {
135
+ await this.processQueueOnce(signal);
136
+ }
137
+ }
138
+
139
+ public async get<T = any>(c: IClassOf<Workflow<any, T>> | string, id?: string): Promise<IWorkflowResult<T>> {
140
+ id ??= (c as string);
141
+ const s = await this.storage.get(id);
142
+ if (s) {
143
+ return {
144
+ state: s.state,
145
+ output: s.output ? JSON.parse(s.output): null,
146
+ error: s.error
147
+ };
148
+ }
149
+ return null;
150
+ }
151
+
152
+ public async queue<T>(
153
+ type: IClassOf<Workflow<T>>,
154
+ input: Partial<T>,
155
+ { id, throwIfExists, eta }: { id?: string, throwIfExists?: boolean, eta?: DateTime } = {}) {
156
+ const clock = this.storage.clock;
157
+ if (id) {
158
+ const r = await this.storage.get(id);
159
+ if (r) {
160
+ if (throwIfExists) {
161
+ throw new EntityAccessError(`Workflow with ID ${id} already exists`);
162
+ }
163
+ return id;
164
+ }
165
+ } else {
166
+ id = randomUUID();
167
+ while(await this.storage.get(id) !== null) {
168
+ console.log(`Generating UUID again ${id}`);
169
+ id = randomUUID();
170
+ }
171
+ }
172
+
173
+ // this will ensure even empty workflow !!
174
+ const schema = WorkflowRegistry.register(type, void 0);
175
+
176
+ const now = clock.utcNow;
177
+ eta ??= now;
178
+ await this.storage.save({
179
+ id,
180
+ name: schema.name,
181
+ input: JSON.stringify(input),
182
+ isWorkflow: true,
183
+ queued: now,
184
+ updated: now,
185
+ eta
186
+ });
187
+
188
+ if(eta < clock.utcNow) {
189
+ this.waiter.abort();
190
+ }
191
+
192
+ return id;
193
+ }
194
+
195
+ public async processQueueOnce(signal?: AbortSignal) {
196
+ const pending = await this.storage.dequeue(signal);
197
+ // run...
198
+ for (const iterator of pending) {
199
+ try {
200
+ await this.run(iterator);
201
+ } catch (error) {
202
+ console.error(error);
203
+ }
204
+ }
205
+ }
206
+ private async run(workflow: WorkflowStorage) {
207
+
208
+ const clock = this.storage.clock;
209
+
210
+ if (workflow.state === "failed" || workflow.state === "done") {
211
+ if (workflow.eta <= clock.utcNow) {
212
+ // time to delete...
213
+ await this.storage.delete(workflow.id);
214
+ }
215
+ return;
216
+ }
217
+
218
+ const scope = ServiceProvider.from(this).createScope();
219
+
220
+ try {
221
+
222
+ const schema = WorkflowRegistry.getByName(workflow.name);
223
+ const { input, eta, id, updated } = workflow;
224
+ const instance = new (schema.type)({ input, eta, id, currentTime: DateTime.from(updated) });
225
+ for (const iterator of schema.activities) {
226
+ instance[iterator] = bindStep(this, workflow, iterator, instance[iterator]);
227
+ }
228
+ for (const iterator of schema.uniqueActivities) {
229
+ instance[iterator] = bindStep(this, workflow, iterator, instance[iterator], true);
230
+ }
231
+ scope.add( schema.type, instance);
232
+ try {
233
+ const result = await instance.run();
234
+ workflow.output = JSON.stringify(result ?? 0);
235
+ workflow.state = "done";
236
+ workflow.eta = clock.utcNow.add(instance.preserveTime);
237
+ } catch (error) {
238
+ if (error instanceof ActivitySuspendedError) {
239
+ // this will update last id...
240
+ await this.storage.save(workflow);
241
+ return;
242
+ }
243
+ workflow.error = JSON.stringify(error.stack ?? error);
244
+ workflow.state = "failed";
245
+ workflow.eta = clock.utcNow.add(instance.failedPreserveTime);
246
+ }
247
+
248
+ await this.storage.save(workflow);
249
+
250
+ } finally {
251
+ scope.dispose();
252
+ }
253
+ }
254
+ }