@entity-access/entity-access 1.0.433 → 1.0.435

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.
@@ -0,0 +1,12 @@
1
+ import type { WorkflowDbContext, WorkflowItem } from "./WorkflowDbContext.js";
2
+ export default class WorkflowTask implements Disposable {
3
+ readonly item: WorkflowItem;
4
+ readonly context: WorkflowDbContext;
5
+ timer: NodeJS.Timer;
6
+ readonly signal: any;
7
+ private ac;
8
+ constructor(item: WorkflowItem, context: WorkflowDbContext);
9
+ [Symbol.dispose](): void;
10
+ renewLock: () => void;
11
+ }
12
+ //# sourceMappingURL=WorkflowTask.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkflowTask.d.ts","sourceRoot":"","sources":["../../src/workflows/WorkflowTask.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE9E,MAAM,CAAC,OAAO,OAAO,YAAa,YAAW,UAAU;aAQ/B,IAAI,EAAE,YAAY;aAClB,OAAO,EAAE,iBAAiB;IAP9C,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;IAEpB,SAAgB,MAAM,MAAC;IACvB,OAAO,CAAC,EAAE,CAAkB;gBAGR,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,iBAAiB;IAO9C,CAAC,MAAM,CAAC,OAAO,CAAC;IAOhB,SAAS,aAUP;CACL"}
@@ -0,0 +1,33 @@
1
+ /* eslint-disable no-console */
2
+ import DateTime from "../types/DateTime.js";
3
+ export default class WorkflowTask {
4
+ item;
5
+ context;
6
+ timer;
7
+ signal;
8
+ ac;
9
+ constructor(item, context) {
10
+ this.item = item;
11
+ this.context = context;
12
+ this.timer = setInterval(this.renewLock, 1000);
13
+ this.ac = new AbortController();
14
+ this.signal = this.ac.signal;
15
+ }
16
+ [Symbol.dispose]() {
17
+ clearInterval(this.timer);
18
+ const { id, lockToken } = this.item;
19
+ this.context.workflows.statements.update({ lockedTTL: null, lockToken: null }, { id, lockToken })
20
+ .catch(console.error);
21
+ }
22
+ renewLock = () => {
23
+ const { id, lockToken, lockedTTL } = this.item;
24
+ const now = DateTime.now;
25
+ if (DateTime.from(lockedTTL).msSinceEpoch < now.msSinceEpoch) {
26
+ this.ac.abort(new Error("Timed out"));
27
+ return;
28
+ }
29
+ this.context.workflows.statements.update({ lockedTTL: now.addSeconds(15) }, { id, lockToken })
30
+ .catch(console.error);
31
+ };
32
+ }
33
+ //# sourceMappingURL=WorkflowTask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkflowTask.js","sourceRoot":"","sources":["../../src/workflows/WorkflowTask.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,QAAQ,MAAM,sBAAsB,CAAC;AAG5C,MAAM,CAAC,OAAO,OAAO,YAAY;IAQT;IACA;IAPpB,KAAK,CAAe;IAEJ,MAAM,CAAC;IACf,EAAE,CAAkB;IAE5B,YACoB,IAAkB,EAClB,OAA0B;QAD1B,SAAI,GAAJ,IAAI,CAAc;QAClB,YAAO,GAAP,OAAO,CAAmB;QAE1C,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC/C,IAAI,CAAC,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC;IACjC,CAAC;IAED,CAAC,MAAM,CAAC,OAAO,CAAC;QACZ,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC;QACpC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAC,CAAC;aAC1F,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,SAAS,GAAG,GAAG,EAAE;QACb,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC;QAC/C,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC;QACzB,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,YAAY,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC;YAC3D,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;YACtC,OAAO;QACX,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,EAAC,EAAE,EAAE,SAAS,EAAC,CAAC;aACvF,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC,CAAC;CACL"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entity-access/entity-access",
3
- "version": "1.0.433",
3
+ "version": "1.0.435",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -779,7 +779,7 @@ export default class ExpressionToSql extends Visitor<ITextQuery> {
779
779
  } else {
780
780
  first = false;
781
781
  }
782
- all.push(this.visit(iterator));
782
+ all.push("(", this.visit(iterator), ")");
783
783
  }
784
784
  all.push(")");
785
785
  return all;
@@ -4,7 +4,6 @@ import EntityAccessError from "../common/EntityAccessError.js";
4
4
  import { IClassOf } from "../decorators/IClassOf.js";
5
5
  import Inject, { RegisterSingleton, ServiceProvider, injectServiceKeysSymbol } from "../di/di.js";
6
6
  import DateTime from "../types/DateTime.js";
7
- import WorkflowStorage, { WorkflowItem } from "./WorkflowStorage.js";
8
7
  import type Workflow from "./Workflow.js";
9
8
  import { ActivitySuspendedError } from "./ActivitySuspendedError.js";
10
9
  import { IWorkflowSchema, WorkflowRegistry } from "./WorkflowRegistry.js";
@@ -12,6 +11,8 @@ import crypto from "crypto";
12
11
  import TimeSpan from "../types/TimeSpan.js";
13
12
  import sleep from "../common/sleep.js";
14
13
  import Waiter from "./Waiter.js";
14
+ import { WorkflowItem } from "./WorkflowDbContext.js";
15
+ import WorkflowStorage from "./WorkflowStorage.js";
15
16
 
16
17
  export const runChildSymbol = Symbol("runChild");
17
18
 
@@ -310,7 +311,8 @@ export default class WorkflowContext {
310
311
  // run...
311
312
  for (const iterator of pending) {
312
313
  try {
313
- await this.run(iterator);
314
+ using l = iterator;
315
+ await this.run(iterator.item, iterator.signal);
314
316
  } catch (error) {
315
317
  console.error(error);
316
318
  }
@@ -366,7 +368,7 @@ export default class WorkflowContext {
366
368
  }
367
369
  }
368
370
 
369
- private async run(workflow: WorkflowItem) {
371
+ private async run(workflow: WorkflowItem, signal: AbortSignal) {
370
372
 
371
373
  const clock = this.storage.clock;
372
374
 
@@ -0,0 +1,123 @@
1
+ import Column from "../decorators/Column.js";
2
+ import Index from "../decorators/Index.js";
3
+ import { RelateTo } from "../decorators/Relate.js";
4
+ import Table from "../decorators/Table.js";
5
+ import { RegisterScoped } from "../di/di.js";
6
+ import EntityContext from "../model/EntityContext.js";
7
+ import DateTime from "../types/DateTime.js";
8
+
9
+ @Table("Workflows")
10
+ @Index({
11
+ name: "IX_Workflows_Group",
12
+ columns: [{ name: (x) => x.groupName, descending: false }],
13
+ filter: (x) => x.groupName !== null
14
+ })
15
+ @Index({
16
+ name: "IX_Workflows_taskGroup_ETA",
17
+ columns: [
18
+ { name: (x) => x.eta, descending: false },
19
+ { name: (x) => x.taskGroup, descending: false }
20
+ ],
21
+ filter: (x) => x.isWorkflow === true
22
+ })
23
+ @Index({
24
+ name: "IX_Workflows_Throttle_Group",
25
+ columns: [
26
+ { name: (x) => x.throttleGroup, descending: false },
27
+ { name: (x) => x.queued, descending: false }
28
+ ],
29
+ filter: (x) => x.isWorkflow === true && x.throttleGroup !== null
30
+ })
31
+ @Index({
32
+ name: "IX_Workflows_Parent_ID",
33
+ columns: [
34
+ { name: (x) => x.parentID, descending: true }
35
+ ],
36
+ filter: (x) => x.parentID !== null
37
+ })
38
+ export class WorkflowItem {
39
+
40
+ @Column({ dataType: "Char", length: 400, key: true })
41
+ public id: string;
42
+
43
+ @Column({ dataType: "Boolean" })
44
+ public isWorkflow: boolean;
45
+
46
+ @Column({ dataType: "Char", nullable: true })
47
+ public name: string;
48
+
49
+ @Column({ dataType: "Char", length: 200, nullable: true })
50
+ public groupName: string;
51
+
52
+ @Column({ dataType: "Char"})
53
+ public input: string;
54
+
55
+ @Column({ dataType: "Char", nullable: true})
56
+ public output: string;
57
+
58
+ @Column({ })
59
+ public eta: DateTime;
60
+
61
+ @Column({ })
62
+ public queued: DateTime;
63
+
64
+ @Column({ })
65
+ public updated: DateTime;
66
+
67
+ @Column({
68
+ dataType: "Char", length: 50,
69
+ default: () => `default`
70
+ })
71
+ public taskGroup: string;
72
+
73
+ @Column({
74
+ dataType: "Char", length: 200, nullable: true
75
+ })
76
+ public throttleGroup: string;
77
+
78
+ @Column({ dataType: "Int", default: () => 0})
79
+ public priority: number;
80
+
81
+ @Column({ nullable: true })
82
+ public lockedTTL: DateTime;
83
+
84
+ @Column({ nullable: true })
85
+ public lockToken: string;
86
+
87
+ @Column({ dataType: "AsciiChar", length: 10})
88
+ public state: "queued" | "failed" | "done";
89
+
90
+ @Column({ dataType: "Char", nullable: true})
91
+ public error: string;
92
+
93
+ @Column({ dataType: "Char", nullable: true})
94
+ public extra: string;
95
+
96
+ @Column({ dataType: "Char", length: 400 , nullable: true})
97
+ @RelateTo(WorkflowItem, {
98
+ property: (x) => x.parent,
99
+ inverseProperty: (x) => x.children,
100
+ foreignKeyConstraint: {
101
+ name: "FC_Workflows_Parent_ID",
102
+ cascade: "delete"
103
+ }
104
+ })
105
+ public parentID: string;
106
+
107
+ @Column({ dataType: "Char", length: 400 , nullable: true})
108
+ public lastID: string;
109
+
110
+ parent: WorkflowItem;
111
+ children: WorkflowItem[];
112
+ }
113
+
114
+ @RegisterScoped
115
+ export class WorkflowDbContext extends EntityContext {
116
+
117
+ public workflows = this.model.register(WorkflowItem);
118
+
119
+ verifyFilters: boolean = false;
120
+
121
+ raiseEvents: boolean = false;
122
+
123
+ }
@@ -1,134 +1,18 @@
1
1
  /* eslint-disable no-console */
2
2
  import { randomUUID } from "crypto";
3
- import Column from "../decorators/Column.js";
4
- import Index from "../decorators/Index.js";
5
- import Table from "../decorators/Table.js";
6
- import Inject, { RegisterScoped, RegisterSingleton, ServiceProvider } from "../di/di.js";
3
+ import Inject, { RegisterSingleton } from "../di/di.js";
7
4
  import { BaseDriver } from "../drivers/base/BaseDriver.js";
8
- import EntityContext from "../model/EntityContext.js";
9
- import { BinaryExpression, CallExpression, Expression, NullExpression, NumberLiteral, UpdateStatement } from "../query/ast/Expressions.js";
5
+ import { CallExpression, Expression, NullExpression, NumberLiteral, UpdateStatement } from "../query/ast/Expressions.js";
10
6
  import DateTime from "../types/DateTime.js";
11
7
  import WorkflowClock from "./WorkflowClock.js";
12
8
  import RawQuery from "../compiler/RawQuery.js";
13
- import { RelateTo } from "../decorators/Relate.js";
9
+ import { WorkflowDbContext, WorkflowItem } from "./WorkflowDbContext.js";
10
+ import WorkflowTask from "./WorkflowTask.js";
11
+ import Sql from "../sql/Sql.js";
14
12
 
15
13
  const loadedFromDb = Symbol("loadedFromDB");
16
14
 
17
- @Table("Workflows")
18
- @Index({
19
- name: "IX_Workflows_Group",
20
- columns: [{ name: (x) => x.groupName, descending: false }],
21
- filter: (x) => x.groupName !== null
22
- })
23
- @Index({
24
- name: "IX_Workflows_taskGroup_ETA",
25
- columns: [
26
- { name: (x) => x.eta, descending: false },
27
- { name: (x) => x.taskGroup, descending: false }
28
- ],
29
- filter: (x) => x.isWorkflow === true
30
- })
31
- @Index({
32
- name: "IX_Workflows_Throttle_Group",
33
- columns: [
34
- { name: (x) => x.throttleGroup, descending: false },
35
- { name: (x) => x.queued, descending: false }
36
- ],
37
- filter: (x) => x.isWorkflow === true && x.throttleGroup !== null
38
- })
39
- @Index({
40
- name: "IX_Workflows_Parent_ID",
41
- columns: [
42
- { name: (x) => x.parentID, descending: true }
43
- ],
44
- filter: (x) => x.parentID !== null
45
- })
46
- export class WorkflowItem {
47
15
 
48
- @Column({ dataType: "Char", length: 400, key: true })
49
- public id: string;
50
-
51
- @Column({ dataType: "Boolean" })
52
- public isWorkflow: boolean;
53
-
54
- @Column({ dataType: "Char", nullable: true })
55
- public name: string;
56
-
57
- @Column({ dataType: "Char", length: 200, nullable: true })
58
- public groupName: string;
59
-
60
- @Column({ dataType: "Char"})
61
- public input: string;
62
-
63
- @Column({ dataType: "Char", nullable: true})
64
- public output: string;
65
-
66
- @Column({ })
67
- public eta: DateTime;
68
-
69
- @Column({ })
70
- public queued: DateTime;
71
-
72
- @Column({ })
73
- public updated: DateTime;
74
-
75
- @Column({
76
- dataType: "Char", length: 50,
77
- default: () => `default`
78
- })
79
- public taskGroup: string;
80
-
81
- @Column({
82
- dataType: "Char", length: 200, nullable: true
83
- })
84
- public throttleGroup: string;
85
-
86
- @Column({ dataType: "Int", default: () => 0})
87
- public priority: number;
88
-
89
- @Column({ nullable: true })
90
- public lockedTTL: DateTime;
91
-
92
- @Column({ nullable: true })
93
- public lockToken: string;
94
-
95
- @Column({ dataType: "AsciiChar", length: 10})
96
- public state: "queued" | "failed" | "done";
97
-
98
- @Column({ dataType: "Char", nullable: true})
99
- public error: string;
100
-
101
- @Column({ dataType: "Char", nullable: true})
102
- public extra: string;
103
-
104
- @Column({ dataType: "Char", length: 400 , nullable: true})
105
- @RelateTo(WorkflowItem, {
106
- property: (x) => x.parent,
107
- inverseProperty: (x) => x.children,
108
- foreignKeyConstraint: {
109
- name: "FC_Workflows_Parent_ID",
110
- cascade: "delete"
111
- }
112
- })
113
- public parentID: string;
114
-
115
- @Column({ dataType: "Char", length: 400 , nullable: true})
116
- public lastID: string;
117
-
118
- parent: WorkflowItem;
119
- children: WorkflowItem[];
120
- }
121
-
122
- @RegisterScoped
123
- class WorkflowContext extends EntityContext {
124
-
125
- public workflows = this.model.register(WorkflowItem);
126
-
127
- verifyFilters: boolean = false;
128
-
129
- raiseEvents: boolean = false;
130
-
131
- }
132
16
 
133
17
  @RegisterSingleton
134
18
  export default class WorkflowStorage {
@@ -146,7 +30,7 @@ export default class WorkflowStorage {
146
30
 
147
31
  async getNextEta(throttle: { group: string, maxPerSecond: number }) {
148
32
 
149
- const db = new WorkflowContext(this.driver);
33
+ const db = new WorkflowDbContext(this.driver);
150
34
  const last = await db.workflows.where(throttle, (p) => (x) => x.throttleGroup === p.group
151
35
  && x.isWorkflow === true)
152
36
  .orderByDescending(void 0, (p) => (x) => x.queued)
@@ -163,7 +47,7 @@ export default class WorkflowStorage {
163
47
  }
164
48
 
165
49
  async getWorkflow(id: string) {
166
- const db = new WorkflowContext(this.driver);
50
+ const db = new WorkflowDbContext(this.driver);
167
51
  const r = await db.workflows.statements.select({}, { id, isWorkflow: true });
168
52
  if (r) {
169
53
  return {
@@ -188,7 +72,7 @@ export default class WorkflowStorage {
188
72
 
189
73
 
190
74
  async getAny(id: string) {
191
- const db = new WorkflowContext(this.driver);
75
+ const db = new WorkflowDbContext(this.driver);
192
76
  const r = await db.workflows.statements.select({}, { id });
193
77
  if (r) {
194
78
  return {
@@ -210,7 +94,7 @@ export default class WorkflowStorage {
210
94
  }
211
95
 
212
96
  async extra(id, text?) {
213
- const db = new WorkflowContext(this.driver);
97
+ const db = new WorkflowDbContext(this.driver);
214
98
  if (text) {
215
99
  // save..
216
100
  await db.workflows.statements.update({ extra: text }, { id });
@@ -229,7 +113,7 @@ export default class WorkflowStorage {
229
113
  * @returns true if all items are deleted
230
114
  */
231
115
  async delete(id) {
232
- const db = new WorkflowContext(this.driver);
116
+ const db = new WorkflowDbContext(this.driver);
233
117
  await db.workflows.where({ id}, (p) => (x) => x.parentID === p.id)
234
118
  .limit(100)
235
119
  .delete({ id }, (p) => (x) => x.parentID === p.id);
@@ -242,7 +126,7 @@ export default class WorkflowStorage {
242
126
  }
243
127
 
244
128
  async save(state: Partial<WorkflowItem>) {
245
- const db = new WorkflowContext(this.driver);
129
+ const db = new WorkflowDbContext(this.driver);
246
130
  const connection = db.connection;
247
131
  await using tx = await connection.createTransaction();
248
132
  state.state ||= "queued";
@@ -260,7 +144,7 @@ export default class WorkflowStorage {
260
144
  }
261
145
 
262
146
  async dequeue(taskGroup: string, signal?: AbortSignal) {
263
- const db = new WorkflowContext(this.driver);
147
+ const db = new WorkflowDbContext(this.driver);
264
148
  const now = this.clock.utcNow;
265
149
 
266
150
  if(!this.lockQuery) {
@@ -281,7 +165,7 @@ export default class WorkflowStorage {
281
165
  Expression.assign(
282
166
  Expression.identifier(lockTTLField),
283
167
  CallExpression.create({
284
- callee: Expression.identifier("Sql.date.addMinutes"),
168
+ callee: Expression.identifier("Sql.date.addSeconds"),
285
169
  arguments: [CallExpression.create({
286
170
  callee: Expression.identifier("Sql.date.now")
287
171
  }),
@@ -312,6 +196,8 @@ export default class WorkflowStorage {
312
196
 
313
197
  const q = this.lockQuery;
314
198
 
199
+ const uuid = randomUUID();
200
+
315
201
  const items = await db.workflows
316
202
  .where({now, taskGroup}, (p) => (x) => x.eta <= p.now
317
203
  && ( x.lockedTTL === null
@@ -323,26 +209,15 @@ export default class WorkflowStorage {
323
209
  .thenBy({}, (p) => (x) => x.priority)
324
210
  .limit(20)
325
211
  .withSignal(signal)
326
- .toArray();
327
- const list: WorkflowItem[] = [];
328
- const uuid = randomUUID();
329
- for (const iterator of items) {
330
- // try to acquire lock...
331
- iterator.lockToken = uuid;
332
- try {
333
- const r = await q.invoke(db.connection, iterator);
334
- if (r.updated > 0) {
335
- list.push(iterator);
336
- }
337
- } catch (error) {
338
- console.error(error);
339
- }
340
- }
341
- return list;
212
+ .updateSelect({ uuid}, (p) => (x) => ({
213
+ lockedTTL: Sql.date.addSeconds(Sql.date.now(), 15),
214
+ lockToken: p.uuid
215
+ }));
216
+ return items.map((x) => new WorkflowTask(x, db));
342
217
  }
343
218
 
344
219
  async seed(version?) {
345
- const db = new WorkflowContext(this.driver);
220
+ const db = new WorkflowDbContext(this.driver);
346
221
  await db.connection.ensureDatabase();
347
222
  await db.connection.automaticMigrations().migrate(db, { version, name: "workflows" });
348
223
  }
@@ -0,0 +1,39 @@
1
+ /* eslint-disable no-console */
2
+ import DateTime from "../types/DateTime.js";
3
+ import type { WorkflowDbContext, WorkflowItem } from "./WorkflowDbContext.js";
4
+
5
+ export default class WorkflowTask implements Disposable {
6
+
7
+ timer: NodeJS.Timer;
8
+
9
+ public readonly signal;
10
+ private ac: AbortController;
11
+
12
+ constructor(
13
+ public readonly item: WorkflowItem,
14
+ public readonly context: WorkflowDbContext,
15
+ ) {
16
+ this.timer = setInterval(this.renewLock, 1000);
17
+ this.ac = new AbortController();
18
+ this.signal = this.ac.signal;
19
+ }
20
+
21
+ [Symbol.dispose]() {
22
+ clearInterval(this.timer);
23
+ const { id, lockToken } = this.item;
24
+ this.context.workflows.statements.update({ lockedTTL: null, lockToken: null}, { id, lockToken})
25
+ .catch(console.error);
26
+ }
27
+
28
+ renewLock = () => {
29
+ const { id, lockToken, lockedTTL } = this.item;
30
+ const now = DateTime.now;
31
+ if (DateTime.from(lockedTTL).msSinceEpoch < now.msSinceEpoch) {
32
+ this.ac.abort(new Error("Timed out"));
33
+ return;
34
+ }
35
+
36
+ this.context.workflows.statements.update({ lockedTTL: now.addSeconds(15) }, {id, lockToken})
37
+ .catch(console.error);
38
+ };
39
+ }