@balena/pinejs 16.1.1-build-renovate-major-husky-a74e31500a0f572fc87a85ec437a55bd22bd2975-1 → 16.2.0-build-joshbwlng-tasks-cd1aff5f4bbfacaad189b013578d5cbee51c3932-1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. package/.husky/pre-commit +2 -0
  2. package/.pinejs-cache.json +1 -1
  3. package/.versionbot/CHANGELOG.yml +10 -11
  4. package/CHANGELOG.md +4 -4
  5. package/VERSION +1 -1
  6. package/out/config-loader/env.d.ts +4 -0
  7. package/out/config-loader/env.js +5 -1
  8. package/out/config-loader/env.js.map +1 -1
  9. package/out/database-layer/db.d.ts +3 -0
  10. package/out/database-layer/db.js +14 -0
  11. package/out/database-layer/db.js.map +1 -1
  12. package/out/sbvr-api/sbvr-utils.d.ts +1 -0
  13. package/out/sbvr-api/sbvr-utils.js +6 -1
  14. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  15. package/out/server-glue/module.d.ts +1 -0
  16. package/out/server-glue/module.js +2 -1
  17. package/out/server-glue/module.js.map +1 -1
  18. package/out/tasks/common.d.ts +4 -0
  19. package/out/tasks/common.js +13 -0
  20. package/out/tasks/common.js.map +1 -0
  21. package/out/tasks/index.d.ts +10 -0
  22. package/out/tasks/index.js +139 -0
  23. package/out/tasks/index.js.map +1 -0
  24. package/out/tasks/model.sbvr +60 -0
  25. package/out/tasks/types.d.ts +38 -0
  26. package/out/tasks/types.js +10 -0
  27. package/out/tasks/types.js.map +1 -0
  28. package/out/tasks/worker.d.ts +16 -0
  29. package/out/tasks/worker.js +191 -0
  30. package/out/tasks/worker.js.map +1 -0
  31. package/package.json +8 -5
  32. package/src/config-loader/env.ts +6 -1
  33. package/src/database-layer/db.ts +24 -0
  34. package/src/sbvr-api/sbvr-utils.ts +5 -1
  35. package/src/server-glue/module.ts +1 -0
  36. package/src/tasks/common.ts +14 -0
  37. package/src/tasks/index.ts +158 -0
  38. package/src/tasks/model.sbvr +60 -0
  39. package/src/tasks/types.ts +58 -0
  40. package/src/tasks/worker.ts +241 -0
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
26
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.addTaskHandler = exports.setup = exports.config = void 0;
30
+ const cronParser = __importStar(require("cron-parser"));
31
+ const env_1 = require("../config-loader/env");
32
+ const errors_1 = require("../sbvr-api/errors");
33
+ const hooks_1 = require("../sbvr-api/hooks");
34
+ const sbvr_utils_1 = require("../sbvr-api/sbvr-utils");
35
+ const common_1 = require("./common");
36
+ const worker_1 = require("./worker");
37
+ __exportStar(require("./types"), exports);
38
+ const modelText = require('./model.sbvr');
39
+ exports.config = {
40
+ models: [
41
+ {
42
+ modelName: common_1.apiRoot,
43
+ apiRoot: common_1.apiRoot,
44
+ modelText,
45
+ customServerCode: exports,
46
+ },
47
+ ],
48
+ };
49
+ async function createTrigger(tx) {
50
+ await tx.executeSql(`
51
+ CREATE OR REPLACE FUNCTION notify_task_insert()
52
+ RETURNS TRIGGER AS $$
53
+ BEGIN
54
+ PERFORM pg_notify('${common_1.channel}', NEW.id::text);
55
+ RETURN NEW;
56
+ END;
57
+ $$ LANGUAGE plpgsql;
58
+ `);
59
+ await tx.executeSql(`
60
+ CREATE OR REPLACE TRIGGER task_insert_trigger
61
+ AFTER INSERT ON task
62
+ FOR EACH ROW WHEN (NEW.status = 'pending' AND NEW."is scheduled to execute on-time" IS NULL)
63
+ EXECUTE FUNCTION notify_task_insert();
64
+ `);
65
+ }
66
+ let worker = null;
67
+ async function setup(db, tx) {
68
+ if (db.engine !== 'postgres') {
69
+ return;
70
+ }
71
+ await createTrigger(tx);
72
+ const client = new sbvr_utils_1.PinejsClient({
73
+ apiPrefix: `/${common_1.apiRoot}/`,
74
+ });
75
+ worker = new worker_1.Worker(client);
76
+ (0, hooks_1.addPureHook)('POST', common_1.apiRoot, 'task', {
77
+ POSTPARSE: async ({ req, request }) => {
78
+ request.values.is_created_by__actor =
79
+ req.user?.actor ?? req.apiKey?.actor;
80
+ if (request.values.is_created_by__actor == null) {
81
+ throw new errors_1.BadRequestError('Creating tasks with missing actor on req is not allowed');
82
+ }
83
+ request.values.status = 'pending';
84
+ request.values.attempt_count = 0;
85
+ request.values.priority ??= 1;
86
+ request.values.attempt_limit ??= 1;
87
+ if (request.values.is_scheduled_with__cron_expression != null &&
88
+ request.values.is_scheduled_to_execute_on__time == null) {
89
+ try {
90
+ request.values.is_scheduled_to_execute_on__time = cronParser
91
+ .parseExpression(request.values.is_scheduled_with__cron_expression)
92
+ .next()
93
+ .toDate()
94
+ .toISOString();
95
+ }
96
+ catch (_) {
97
+ throw new errors_1.BadRequestError(`Invalid cron expression: ${request.values.is_scheduled_with__cron_expression}`);
98
+ }
99
+ }
100
+ if (request.values.is_scheduled_to_execute_on__time != null) {
101
+ const now = new Date(new Date().getTime() + env_1.tasks.queueIntervalMS);
102
+ const startTime = new Date(request.values.is_scheduled_to_execute_on__time);
103
+ if (startTime < now) {
104
+ throw new errors_1.BadRequestError(`Task scheduled start time must be greater than ${env_1.tasks.queueIntervalMS} milliseconds in the future`);
105
+ }
106
+ }
107
+ const handlerName = request.values.is_executed_by__handler;
108
+ if (handlerName == null) {
109
+ throw new errors_1.BadRequestError(`Must specify a task handler to execute`);
110
+ }
111
+ const handler = worker?.handlers[handlerName];
112
+ if (handler == null) {
113
+ throw new errors_1.BadRequestError(`No task handler with name '${handlerName}' registered`);
114
+ }
115
+ if (handler.validate != null) {
116
+ if (!handler.validate(request.values.is_executed_with__parameter_set)) {
117
+ throw new errors_1.BadRequestError(`Invalid parameter set: ${common_1.ajv.errorsText(handler.validate.errors)}`);
118
+ }
119
+ }
120
+ },
121
+ });
122
+ worker.start();
123
+ }
124
+ exports.setup = setup;
125
+ function addTaskHandler(name, fn, schema) {
126
+ if (worker == null) {
127
+ return;
128
+ }
129
+ if (worker.handlers[name] != null) {
130
+ throw new Error(`Task handler with name '${name}' already registered`);
131
+ }
132
+ worker.handlers[name] = {
133
+ name,
134
+ fn,
135
+ validate: schema != null ? common_1.ajv.compile(schema) : undefined,
136
+ };
137
+ }
138
+ exports.addTaskHandler = addTaskHandler;
139
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tasks/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ Vocabulary: tasks
2
+
3
+ Term: id
4
+ Concept Type: Big Serial (Type)
5
+ Term: actor
6
+ Concept Type: Integer (Type)
7
+ Term: attempt count
8
+ Concept Type: Integer (Type)
9
+ Term: attempt limit
10
+ Concept Type: Integer (Type)
11
+ Term: cron expression
12
+ Concept Type: Short Text (Type)
13
+ Term: error message
14
+ Concept Type: Short Text (Type)
15
+ Term: handler
16
+ Concept Type: Short Text (Type)
17
+ Term: key
18
+ Concept Type: Short Text (Type)
19
+ Term: parameter set
20
+ Concept Type: JSON (Type)
21
+ Term: priority
22
+ Concept Type: Integer (Type)
23
+ Term: status
24
+ Concept Type: Short Text (Type)
25
+ Term: time
26
+ Concept Type: Date Time (Type)
27
+
28
+ Term: task
29
+ Fact type: task has id
30
+ Necessity: each task has exactly one id
31
+ Fact type: task has key
32
+ Necessity: each task has at most one key
33
+ Fact type: task is created by actor
34
+ Necessity: each task is created by exactly one actor
35
+ Fact type: task is executed by handler
36
+ Necessity: each task is executed by exactly one handler
37
+ Fact type: task is executed with parameter set
38
+ Necessity: each task is executed with at most one parameter set
39
+ Fact type: task has priority
40
+ Necessity: each task has exactly one priority
41
+ Necessity: each task has a priority that is greater than or equal to 0
42
+ Fact type: task is scheduled with cron expression
43
+ Necessity: each task is scheduled with at most one cron expression
44
+ Fact type: task is scheduled to execute on time
45
+ Necessity: each task is scheduled to execute on at most one time
46
+ Fact type: task has status
47
+ Necessity: each task has exactly one status
48
+ Definition: "pending" or "cancelled" or "success" or "failed"
49
+ Fact type: task started on time
50
+ Necessity: each task started on at most one time
51
+ Fact type: task ended on time
52
+ Necessity: each task ended on at most one time
53
+ Fact type: task has error message
54
+ Necessity: each task has at most one error message
55
+ Fact type: task has attempt count
56
+ Necessity: each task has exactly one attempt count
57
+ Fact type: task has attempt limit
58
+ Necessity: each task has exactly one attempt limit
59
+ Necessity: each task has an attempt limit that is greater than or equal to 1
60
+
@@ -0,0 +1,38 @@
1
+ import type { ValidateFunction } from 'ajv';
2
+ import type { AnyObject } from 'pinejs-client-core';
3
+ import type * as Db from '../database-layer/db';
4
+ import type { PinejsClient } from '../sbvr-api/sbvr-utils';
5
+ export declare const taskStatuses: readonly ["pending", "cancelled", "success", "failed"];
6
+ export type TaskStatus = (typeof taskStatuses)[number];
7
+ export interface Task {
8
+ id: number;
9
+ created_at: Date;
10
+ modified_at: Date;
11
+ is_created_by__actor: number;
12
+ is_executed_by__handler: string;
13
+ is_executed_with__parameter_set: object | null;
14
+ is_scheduled_with__cron_expression: string | null;
15
+ is_scheduled_to_execute_on__time: Date | null;
16
+ priority: number;
17
+ status: TaskStatus;
18
+ started_on__time: Date | null;
19
+ ended_on__time: Date | null;
20
+ error_message: string | null;
21
+ attempt_count: number;
22
+ attempt_limit: number;
23
+ }
24
+ export type PartialTask = Pick<Task, 'id' | 'is_created_by__actor' | 'is_executed_by__handler' | 'is_executed_with__parameter_set' | 'is_scheduled_with__cron_expression' | 'priority' | 'attempt_count' | 'attempt_limit'>;
25
+ export interface TaskArgs {
26
+ api: PinejsClient;
27
+ params: AnyObject;
28
+ tx: Db.Tx;
29
+ }
30
+ export type TaskResponse = Promise<{
31
+ status: TaskStatus;
32
+ error?: string;
33
+ }>;
34
+ export interface TaskHandler {
35
+ name: string;
36
+ fn: (options: TaskArgs) => TaskResponse;
37
+ validate?: ValidateFunction;
38
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskStatuses = void 0;
4
+ exports.taskStatuses = [
5
+ 'pending',
6
+ 'cancelled',
7
+ 'success',
8
+ 'failed',
9
+ ];
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/tasks/types.ts"],"names":[],"mappings":";;;AAKa,QAAA,YAAY,GAAG;IAC3B,SAAS;IACT,WAAW;IACX,SAAS;IACT,QAAQ;CACC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { PinejsClient } from '../sbvr-api/sbvr-utils';
2
+ import type { TaskHandler } from './types';
3
+ export declare class Worker {
4
+ handlers: Record<string, TaskHandler>;
5
+ private readonly concurrency;
6
+ private readonly interval;
7
+ private client;
8
+ private executing;
9
+ constructor(client: PinejsClient);
10
+ private canExecute;
11
+ private poll;
12
+ start(): void;
13
+ private execute;
14
+ private getNextAttemptTime;
15
+ private update;
16
+ }
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.Worker = void 0;
27
+ const env_1 = require("../config-loader/env");
28
+ const permissions = __importStar(require("../sbvr-api/permissions"));
29
+ const sbvr_utils_1 = require("../sbvr-api/sbvr-utils");
30
+ const module_1 = require("../server-glue/module");
31
+ const common_1 = require("./common");
32
+ const selectColumns = Object.entries({
33
+ id: 'id',
34
+ 'is executed by-handler': 'is_executed_by__handler',
35
+ 'is executed with-parameter set': 'is_executed_with__parameter_set',
36
+ 'is scheduled with-cron expression': 'is_scheduled_with__cron_expression',
37
+ 'attempt count': 'attempt_count',
38
+ 'attempt limit': 'attempt_limit',
39
+ priority: 'priority',
40
+ 'is created by-actor': 'is_created_by__actor',
41
+ })
42
+ .map(([key, value]) => `t."${key}" AS "${value}"`)
43
+ .join(', ');
44
+ class Worker {
45
+ constructor(client) {
46
+ this.handlers = {};
47
+ this.executing = 0;
48
+ this.client = client;
49
+ this.concurrency = env_1.tasks.queueConcurrency;
50
+ this.interval = env_1.tasks.queueIntervalMS;
51
+ }
52
+ canExecute() {
53
+ return (this.executing < this.concurrency && Object.keys(this.handlers).length > 0);
54
+ }
55
+ poll() {
56
+ let executed = false;
57
+ const names = Object.keys(this.handlers);
58
+ const binds = names.map((_, index) => `$${index + 1}`).join(', ');
59
+ module_1.sbvrUtils.db
60
+ .transaction(async (tx) => {
61
+ if (!this.canExecute()) {
62
+ return;
63
+ }
64
+ const result = await tx.executeSql(`SELECT ${selectColumns}
65
+ FROM task AS t
66
+ WHERE
67
+ t."is executed by-handler" IN (${binds}) AND
68
+ t."status" = 'pending' AND
69
+ t."attempt count" <= t."attempt limit" AND
70
+ (
71
+ t."is scheduled to execute on-time" IS NULL OR
72
+ t."is scheduled to execute on-time" <= CURRENT_TIMESTAMP + INTERVAL '${Math.ceil(this.interval / 1000)} second'
73
+ )
74
+ ORDER BY
75
+ t."is scheduled to execute on-time" ASC,
76
+ t."priority" DESC,
77
+ t."id" ASC
78
+ LIMIT ${Math.max(this.concurrency - this.executing, 0)}
79
+ FOR UPDATE SKIP LOCKED`, names);
80
+ if (result.rows.length === 0) {
81
+ return;
82
+ }
83
+ await Promise.all(result.rows.map(async (row) => {
84
+ await this.execute(tx, row);
85
+ }));
86
+ executed = true;
87
+ })
88
+ .catch((err) => {
89
+ console.error('Failed polling for tasks:', err);
90
+ })
91
+ .finally(() => {
92
+ setTimeout(() => this.poll(), executed ? 0 : this.interval);
93
+ });
94
+ }
95
+ start() {
96
+ module_1.sbvrUtils.db.on?.('notification', async (msg) => {
97
+ if (this.canExecute()) {
98
+ await module_1.sbvrUtils.db.transaction(async (tx) => {
99
+ const result = await tx.executeSql(`SELECT ${selectColumns} FROM task AS t WHERE id = $1 FOR UPDATE SKIP LOCKED`, [msg.payload]);
100
+ if (result.rows.length > 0) {
101
+ await this.execute(tx, result.rows[0]);
102
+ }
103
+ });
104
+ }
105
+ }, {
106
+ channel: common_1.channel,
107
+ });
108
+ this.poll();
109
+ }
110
+ async execute(tx, task) {
111
+ this.executing++;
112
+ try {
113
+ const handler = this.handlers[task.is_executed_by__handler];
114
+ const startedOnTime = new Date();
115
+ if (handler == null) {
116
+ await this.update(tx, task, startedOnTime, 'failed', 'Matching task handler not found');
117
+ return;
118
+ }
119
+ if (handler.validate != null &&
120
+ !handler.validate(task.is_executed_with__parameter_set)) {
121
+ await this.update(tx, task, startedOnTime, 'failed', `Invalid parameter set: ${common_1.ajv.errorsText(handler.validate.errors)}`);
122
+ return;
123
+ }
124
+ const result = await handler.fn({
125
+ api: new sbvr_utils_1.PinejsClient({
126
+ passthrough: {
127
+ tx,
128
+ },
129
+ }),
130
+ params: task.is_executed_with__parameter_set ?? {},
131
+ tx,
132
+ });
133
+ await this.update(tx, task, startedOnTime, result.status, result.error);
134
+ }
135
+ catch (err) {
136
+ console.error('Task execution failed:', err);
137
+ process.exit(1);
138
+ }
139
+ finally {
140
+ this.executing--;
141
+ }
142
+ }
143
+ getNextAttemptTime(attempt) {
144
+ const delay = Math.ceil(Math.exp(Math.min(10, attempt))) * 1000;
145
+ return new Date(Date.now() + delay);
146
+ }
147
+ async update(tx, task, startedOnTime, status, errorMessage) {
148
+ const attemptCount = task.attempt_count + 1;
149
+ const body = {
150
+ started_on__time: startedOnTime,
151
+ ended_on__time: new Date(),
152
+ status,
153
+ attempt_count: attemptCount,
154
+ ...(errorMessage != null && { error_message: errorMessage }),
155
+ };
156
+ if (status === 'failed' && attemptCount < task.attempt_limit) {
157
+ body.status = 'pending';
158
+ body.is_scheduled_to_execute_on__time =
159
+ this.getNextAttemptTime(attemptCount);
160
+ }
161
+ await this.client.patch({
162
+ resource: 'task',
163
+ passthrough: {
164
+ tx,
165
+ req: permissions.root,
166
+ },
167
+ id: task.id,
168
+ body,
169
+ });
170
+ if (['failed', 'success'].includes(body.status) &&
171
+ task.is_scheduled_with__cron_expression != null) {
172
+ await this.client.post({
173
+ resource: 'task',
174
+ passthrough: {
175
+ tx,
176
+ req: permissions.root,
177
+ },
178
+ body: {
179
+ attempt_limit: task.attempt_limit,
180
+ is_created_by__actor: task.is_created_by__actor,
181
+ is_executed_by__handler: task.is_executed_by__handler,
182
+ is_executed_with__parameter_set: task.is_executed_with__parameter_set,
183
+ is_scheduled_with__cron_expression: task.is_scheduled_with__cron_expression,
184
+ priority: task.priority,
185
+ },
186
+ });
187
+ }
188
+ }
189
+ }
190
+ exports.Worker = Worker;
191
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/tasks/worker.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balena/pinejs",
3
- "version": "16.1.1-build-renovate-major-husky-a74e31500a0f572fc87a85ec437a55bd22bd2975-1",
3
+ "version": "16.2.0-build-joshbwlng-tasks-cd1aff5f4bbfacaad189b013578d5cbee51c3932-1",
4
4
  "main": "out/server-glue/module",
5
5
  "type": "commonjs",
6
6
  "repository": "git@github.com:balena-io/pinejs.git",
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "scripts": {
14
14
  "prepublish": "require-npm4-to-publish",
15
- "prepare": "node -e \"try { (await import('husky')).default() } catch (e) { if (e.code !== 'ERR_MODULE_NOT_FOUND') throw e }\" --input-type module && npm run build",
15
+ "prepare": "node -e \"try { require('husky').install() } catch (e) {if (e.code !== 'MODULE_NOT_FOUND') throw e}\" && npm run build",
16
16
  "build": "grunt build",
17
17
  "webpack-browser": "grunt browser",
18
18
  "webpack-module": "grunt module",
@@ -20,7 +20,7 @@
20
20
  "webpack-build": "npm run webpack-browser && npm run webpack-module && npm run webpack-server",
21
21
  "lint": "balena-lint -t tsconfig.dev.json -e js -e ts src build typings Gruntfile.ts && npx tsc --project tsconfig.dev.json --noEmit",
22
22
  "test": "npm run lint && npm run build && npm run webpack-build && npm run test:compose",
23
- "test:compose": "trap 'docker compose -f docker-compose.npm-test.yml down ; echo Stopped ; exit 0' SIGINT; docker compose -f docker-compose.npm-test.yml up -d && sleep 2 && DATABASE_URL=postgres://docker:docker@localhost:5431/postgres PINEJS_WEBRESOURCE_MAXFILESIZE=1000000000 S3_ENDPOINT=http://localhost:43680 S3_ACCESS_KEY=USERNAME S3_SECRET_KEY=PASSWORD S3_STORAGE_ADAPTER_BUCKET=balena-pine-web-resources S3_REGION=us-east-1 npm run mocha",
23
+ "test:compose": "trap 'docker compose -f docker-compose.npm-test.yml down ; echo Stopped ; exit 0' SIGINT; docker compose -f docker-compose.npm-test.yml up -d && sleep 2 && DATABASE_URL=postgres://docker:docker@localhost:5431/postgres PINEJS_WEBRESOURCE_MAXFILESIZE=1000000000 S3_ENDPOINT=http://localhost:43680 S3_ACCESS_KEY=USERNAME S3_SECRET_KEY=PASSWORD S3_STORAGE_ADAPTER_BUCKET=balena-pine-web-resources S3_REGION=us-east-1 PINEJS_QUEUE_CONCURRENCY=1 npm run mocha",
24
24
  "mocha": "TS_NODE_FILES=true mocha",
25
25
  "prettify": "balena-lint -t tsconfig.dev.json -e js -e ts --fix src test build typings Gruntfile.ts"
26
26
  },
@@ -51,8 +51,10 @@
51
51
  "@types/pg": "^8.10.9",
52
52
  "@types/randomstring": "^1.1.11",
53
53
  "@types/websql": "^0.0.30",
54
+ "ajv": "^8.12.0",
54
55
  "busboy": "^1.6.0",
55
56
  "commander": "^11.1.0",
57
+ "cron-parser": "^4.9.0",
56
58
  "deep-freeze": "^0.0.1",
57
59
  "eventemitter3": "^5.0.1",
58
60
  "express-session": "^1.17.3",
@@ -88,7 +90,8 @@
88
90
  "grunt-text-replace": "^0.4.0",
89
91
  "grunt-ts": "^6.0.0-beta.22",
90
92
  "grunt-webpack": "^6.0.0",
91
- "husky": "^9.0.0",
93
+ "husky": "^8.0.3",
94
+ "json-schema-to-ts": "^3.0.1",
92
95
  "lint-staged": "^15.2.0",
93
96
  "load-grunt-tasks": "^5.1.0",
94
97
  "mocha": "^10.2.0",
@@ -144,6 +147,6 @@
144
147
  "recursive": true
145
148
  },
146
149
  "versionist": {
147
- "publishedAt": "2024-04-09T01:55:37.889Z"
150
+ "publishedAt": "2024-04-10T23:12:22.905Z"
148
151
  }
149
152
  }
@@ -49,7 +49,7 @@ export const cache = {
49
49
  apiKeyActorId: false as CacheOpts,
50
50
  };
51
51
 
52
- import { boolVar } from '@balena/env-parsing';
52
+ import { boolVar, intVar } from '@balena/env-parsing';
53
53
  import memoize from 'memoizee';
54
54
  import memoizeWeak = require('memoizee/weak');
55
55
  export const createCache = <T extends (...args: any[]) => any>(
@@ -146,3 +146,8 @@ export const migrator = {
146
146
  */
147
147
  asyncMigrationIsEnabled: boolVar('PINEJS_ASYNC_MIGRATION_ENABLED', true),
148
148
  };
149
+
150
+ export const tasks = {
151
+ queueConcurrency: intVar('PINEJS_QUEUE_CONCURRENCY', 0),
152
+ queueIntervalMS: intVar('PINEJS_QUEUE_INTERVAL_MS', 1000),
153
+ };
@@ -98,6 +98,13 @@ export interface Database extends BaseDatabase {
98
98
  ) => Promise<Result>;
99
99
  transaction: TransactionFn;
100
100
  readTransaction: TransactionFn;
101
+ on?: (
102
+ name: 'notification',
103
+ fn: (...args: any[]) => Promise<void>,
104
+ options?: {
105
+ channel?: string;
106
+ },
107
+ ) => void;
101
108
  }
102
109
 
103
110
  interface EngineParams {
@@ -689,6 +696,23 @@ if (maybePg != null) {
689
696
  return {
690
697
  engine: Engines.postgres,
691
698
  executeSql: atomicExecuteSql,
699
+ on: async (name, fn, options) => {
700
+ if (name === 'notification' && options?.channel === undefined) {
701
+ throw new Error('Missing channel option for notification listener');
702
+ }
703
+
704
+ const client = await pool.connect();
705
+ client.on(name, (msg) => {
706
+ fn(msg).catch((error) => {
707
+ console.error('Error handling message:', error);
708
+ });
709
+ });
710
+
711
+ if (name === 'notification' && options?.channel !== undefined) {
712
+ await client.query(`LISTEN "${options.channel}";`);
713
+ // client.release();
714
+ }
715
+ },
692
716
  transaction: createTransaction(async (stackTraceErr) => {
693
717
  const client = await pool.connect();
694
718
  const tx = new PostgresTx(client, false, stackTraceErr);
@@ -42,6 +42,7 @@ import { generateODataMetadata } from '../odata-metadata/odata-metadata-generato
42
42
 
43
43
  // eslint-disable-next-line @typescript-eslint/no-var-requires
44
44
  const devModel = require('./dev.sbvr');
45
+ import * as tasks from '../tasks';
45
46
  import * as permissions from './permissions';
46
47
  import {
47
48
  BadRequestError,
@@ -77,6 +78,7 @@ export {
77
78
  addPureHook,
78
79
  addSideEffectHook,
79
80
  } from './hooks';
81
+ export { addTaskHandler } from '../tasks';
80
82
 
81
83
  import memoizeWeak = require('memoizee/weak');
82
84
  import * as controlFlow from './control-flow';
@@ -773,7 +775,7 @@ export const postExecuteModels = async (tx: Db.Tx): Promise<void> => {
773
775
  // Hence, skipped migrations from earlier models are not set as executed as the `migration` table is missing
774
776
  // Here the skipped migrations that haven't been set properly are covered
775
777
  // This is mostly an edge case when running on an empty database schema and migrations model hasn't been executed, yet.
776
- // One specifc case are tests to run tests against migrated and unmigrated database states
778
+ // One specific case are tests to run tests against migrated and unmigrated database states
777
779
 
778
780
  for (const modelKey of Object.keys(models)) {
779
781
  const pendingToSetExecutedMigrations =
@@ -1983,6 +1985,7 @@ export const executeStandardModels = async (tx: Db.Tx): Promise<void> => {
1983
1985
  },
1984
1986
  });
1985
1987
  await executeModels(tx, permissions.config.models);
1988
+ await executeModels(tx, tasks.config.models);
1986
1989
  console.info('Successfully executed standard models.');
1987
1990
  } catch (err: any) {
1988
1991
  console.error('Failed to execute standard models.', err);
@@ -1999,6 +2002,7 @@ export const setup = async (
1999
2002
  await db.transaction(async (tx) => {
2000
2003
  await executeStandardModels(tx);
2001
2004
  await permissions.setup();
2005
+ await tasks.setup($db, tx);
2002
2006
  });
2003
2007
  } catch (err: any) {
2004
2008
  console.error('Could not execute standard models', err);
@@ -19,6 +19,7 @@ export * as errors from '../sbvr-api/errors';
19
19
  export * as env from '../config-loader/env';
20
20
  export * as types from '../sbvr-api/common-types';
21
21
  export * as hooks from '../sbvr-api/hooks';
22
+ export * as tasks from '../tasks';
22
23
  export * as webResourceHandler from '../webresource-handler';
23
24
  export type { configLoader as ConfigLoader };
24
25
  export type { migratorUtils as Migrator };
@@ -0,0 +1,14 @@
1
+ import Ajv from 'ajv';
2
+
3
+ // Root path for the tasks API
4
+ export const apiRoot = 'tasks';
5
+
6
+ // Channel name for task insert notifications
7
+ export const channel = 'task_insert';
8
+
9
+ // Setting inlineRefs=false as without it we run into a
10
+ // "Maximum call stack size exceeded" error apprarently caused
11
+ // by String.prototype._uncountable_words being set in sbvr-parser?
12
+ export const ajv = new Ajv({
13
+ inlineRefs: false,
14
+ });