@pgflow/edge-worker 0.0.9 → 0.0.10-prealpha.2
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/dist/EdgeWorker.d.ts +73 -0
- package/dist/EdgeWorker.d.ts.map +1 -0
- package/dist/EdgeWorker.js +105 -0
- package/dist/core/BatchProcessor.d.ts +13 -0
- package/dist/core/BatchProcessor.d.ts.map +1 -0
- package/dist/core/BatchProcessor.js +29 -0
- package/dist/core/ExecutionController.d.ts +15 -0
- package/dist/core/ExecutionController.d.ts.map +1 -0
- package/dist/core/ExecutionController.js +34 -0
- package/dist/core/Heartbeat.d.ts +13 -0
- package/dist/core/Heartbeat.d.ts.map +1 -0
- package/dist/core/Heartbeat.js +21 -0
- package/dist/core/Queries.d.ts +14 -0
- package/dist/core/Queries.d.ts.map +1 -0
- package/dist/core/Queries.js +31 -0
- package/dist/core/Worker.d.ts +21 -0
- package/dist/core/Worker.d.ts.map +1 -0
- package/dist/core/Worker.js +79 -0
- package/dist/core/WorkerLifecycle.d.ts +26 -0
- package/dist/core/WorkerLifecycle.d.ts.map +1 -0
- package/dist/core/WorkerLifecycle.js +69 -0
- package/dist/core/WorkerState.d.ts +37 -0
- package/dist/core/WorkerState.d.ts.map +1 -0
- package/dist/core/WorkerState.js +70 -0
- package/dist/core/types.d.ts +39 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/flow/FlowWorkerLifecycle.d.ts +26 -0
- package/dist/flow/FlowWorkerLifecycle.d.ts.map +1 -0
- package/dist/flow/FlowWorkerLifecycle.js +64 -0
- package/dist/flow/StepTaskExecutor.d.ts +28 -0
- package/dist/flow/StepTaskExecutor.d.ts.map +1 -0
- package/dist/flow/StepTaskExecutor.js +71 -0
- package/dist/flow/StepTaskPoller.d.ts +21 -0
- package/dist/flow/StepTaskPoller.d.ts.map +1 -0
- package/dist/flow/StepTaskPoller.js +34 -0
- package/dist/flow/createFlowWorker.d.ts +24 -0
- package/dist/flow/createFlowWorker.d.ts.map +1 -0
- package/dist/flow/createFlowWorker.js +56 -0
- package/dist/flow/types.d.ts +2 -0
- package/dist/flow/types.d.ts.map +1 -0
- package/dist/flow/types.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -953
- package/dist/package.json +33 -0
- package/dist/platform/DenoAdapter.d.ts +23 -0
- package/dist/platform/DenoAdapter.d.ts.map +1 -0
- package/dist/platform/DenoAdapter.js +132 -0
- package/dist/platform/createAdapter.d.ts +7 -0
- package/dist/platform/createAdapter.d.ts.map +1 -0
- package/dist/platform/createAdapter.js +17 -0
- package/dist/platform/deno-types.d.ts +13 -0
- package/dist/platform/deno-types.d.ts.map +1 -0
- package/dist/platform/deno-types.js +6 -0
- package/dist/platform/index.d.ts +5 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +4 -0
- package/dist/platform/types.d.ts +45 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/platform/types.js +1 -0
- package/dist/queue/MessageExecutor.d.ts +43 -0
- package/dist/queue/MessageExecutor.d.ts.map +1 -0
- package/dist/queue/MessageExecutor.js +95 -0
- package/dist/queue/Queue.d.ts +35 -0
- package/dist/queue/Queue.d.ts.map +1 -0
- package/dist/queue/Queue.js +87 -0
- package/dist/queue/ReadWithPollPoller.d.ts +20 -0
- package/dist/queue/ReadWithPollPoller.d.ts.map +1 -0
- package/dist/queue/ReadWithPollPoller.js +25 -0
- package/dist/queue/createQueueWorker.d.ts +74 -0
- package/dist/queue/createQueueWorker.d.ts.map +1 -0
- package/dist/queue/createQueueWorker.js +47 -0
- package/dist/queue/types.d.ts +13 -0
- package/dist/queue/types.d.ts.map +1 -0
- package/dist/queue/types.js +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +15 -4
- package/dist/CHANGELOG.md +0 -44
- package/dist/LICENSE.md +0 -660
- package/dist/README.md +0 -46
- package/dist/index.js.map +0 -7
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pgflow/edge-worker",
|
|
3
|
+
"version": "0.0.10-prealpha.2",
|
|
4
|
+
"license": "AGPL-3.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
"./package.json": "./package.json",
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@henrygd/queue": "^1.0.7",
|
|
21
|
+
"@pgflow/core": "0.0.10",
|
|
22
|
+
"@pgflow/dsl": "0.0.10",
|
|
23
|
+
"pino": "^9.6.0",
|
|
24
|
+
"postgres": "3.4.5"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@teidesu/deno-types": "1.42.4",
|
|
28
|
+
"supabase": "2.21.1"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CreateWorkerFn, Logger, PlatformAdapter, PlatformEnvironment } from './types.js';
|
|
2
|
+
import './deno-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Adapter for Deno runtime environment
|
|
5
|
+
*/
|
|
6
|
+
export declare class DenoAdapter implements PlatformAdapter {
|
|
7
|
+
private env;
|
|
8
|
+
private edgeFunctionName;
|
|
9
|
+
private worker;
|
|
10
|
+
constructor();
|
|
11
|
+
initialize(createWorkerFn: CreateWorkerFn): Promise<void>;
|
|
12
|
+
terminate(): Promise<void>;
|
|
13
|
+
getEnv(): PlatformEnvironment;
|
|
14
|
+
createLogger(module: string): Logger;
|
|
15
|
+
setWorker(worker: {
|
|
16
|
+
stop(): void;
|
|
17
|
+
}): void;
|
|
18
|
+
spawnNewEdgeFunction(functionName: string): Promise<void>;
|
|
19
|
+
private detectEnvironment;
|
|
20
|
+
private extractFunctionName;
|
|
21
|
+
setupShutdownHandler(): void;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=DenoAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DenoAdapter.d.ts","sourceRoot":"","sources":["../../src/platform/DenoAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,MAAM,EACN,eAAe,EACf,mBAAmB,EACpB,MAAM,YAAY,CAAC;AACpB,OAAO,iBAAiB,CAAC;AAEzB;;GAEG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,GAAG,CAAoC;IAC/C,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,MAAM,CAAiC;;IAazC,UAAU,CAAC,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BzD,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAOhC,MAAM,IAAI,mBAAmB;IAK7B,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IA4CpC,SAAS,CAAC,MAAM,EAAE;QAAE,IAAI,IAAI,IAAI,CAAA;KAAE,GAAG,IAAI;IAInC,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+B/D,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,mBAAmB;IAI3B,oBAAoB,IAAI,IAAI;CAY7B"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import './deno-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for Deno runtime environment
|
|
4
|
+
*/
|
|
5
|
+
export class DenoAdapter {
|
|
6
|
+
env = null;
|
|
7
|
+
edgeFunctionName = null;
|
|
8
|
+
worker = null;
|
|
9
|
+
constructor() {
|
|
10
|
+
// Guard clause to ensure we're in a Deno environment
|
|
11
|
+
// This is just for type checking during build
|
|
12
|
+
// At runtime, this class should only be instantiated in Deno
|
|
13
|
+
if (typeof Deno === 'undefined' || typeof EdgeRuntime === 'undefined') {
|
|
14
|
+
throw new Error('DenoAdapter created in non-Deno environment - this is expected during build only');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async initialize(createWorkerFn) {
|
|
18
|
+
// Get environment information
|
|
19
|
+
this.env = this.detectEnvironment();
|
|
20
|
+
// Set up HTTP listener for Deno
|
|
21
|
+
Deno.serve({}, (req) => {
|
|
22
|
+
if (!this.worker) {
|
|
23
|
+
this.edgeFunctionName = this.extractFunctionName(req);
|
|
24
|
+
// Create a logger for the adapter
|
|
25
|
+
const logger = this.createLogger('DenoAdapter');
|
|
26
|
+
logger.info(`HTTP Request: ${this.edgeFunctionName}`);
|
|
27
|
+
// Create the worker using the factory function and the logger
|
|
28
|
+
this.worker = createWorkerFn(this.createLogger.bind(this));
|
|
29
|
+
}
|
|
30
|
+
return new Response('ok', {
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
// Keep function alive for Supabase Edge Functions
|
|
35
|
+
const promiseThatNeverResolves = new Promise(() => { });
|
|
36
|
+
EdgeRuntime.waitUntil(promiseThatNeverResolves);
|
|
37
|
+
}
|
|
38
|
+
async terminate() {
|
|
39
|
+
if (this.worker) {
|
|
40
|
+
this.worker.stop();
|
|
41
|
+
this.worker = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
getEnv() {
|
|
45
|
+
if (!this.env)
|
|
46
|
+
throw new Error('Adapter not initialized');
|
|
47
|
+
return this.env;
|
|
48
|
+
}
|
|
49
|
+
createLogger(module) {
|
|
50
|
+
const workerId = this.env?.executionId || 'unknown';
|
|
51
|
+
const logLevel = this.env?.logLevel || 'info';
|
|
52
|
+
// Simple level filtering
|
|
53
|
+
const levels = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
54
|
+
const levelValue = levels[logLevel] ?? levels.info;
|
|
55
|
+
return {
|
|
56
|
+
debug: (message, ...args) => {
|
|
57
|
+
if (levelValue >= levels.debug) {
|
|
58
|
+
console.debug(`worker_id=${workerId} module=${module} ${message}`, ...args);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
info: (message, ...args) => {
|
|
62
|
+
if (levelValue >= levels.info) {
|
|
63
|
+
console.info(`worker_id=${workerId} module=${module} ${message}`, ...args);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
warn: (message, ...args) => {
|
|
67
|
+
if (levelValue >= levels.warn) {
|
|
68
|
+
console.warn(`worker_id=${workerId} module=${module} ${message}`, ...args);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
error: (message, ...args) => {
|
|
72
|
+
if (levelValue >= levels.error) {
|
|
73
|
+
console.error(`worker_id=${workerId} module=${module} ${message}`, ...args);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
setWorker(worker) {
|
|
79
|
+
this.worker = worker;
|
|
80
|
+
}
|
|
81
|
+
async spawnNewEdgeFunction(functionName) {
|
|
82
|
+
if (!functionName) {
|
|
83
|
+
throw new Error('functionName cannot be null or empty');
|
|
84
|
+
}
|
|
85
|
+
const logger = this.createLogger('spawnNewEdgeFunction');
|
|
86
|
+
logger.debug('Spawning a new Edge Function...');
|
|
87
|
+
const SUPABASE_URL = Deno.env.get('SUPABASE_URL');
|
|
88
|
+
const SUPABASE_ANON_KEY = Deno.env.get('SUPABASE_ANON_KEY');
|
|
89
|
+
const response = await fetch(`${SUPABASE_URL}/functions/v1/${functionName}`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
logger.debug('Edge Function spawned successfully!');
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`Edge function returned non-OK status: ${response.status} ${response.statusText}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
detectEnvironment() {
|
|
102
|
+
const connectionString = Deno.env.get('EDGE_WORKER_DB_URL');
|
|
103
|
+
if (!connectionString) {
|
|
104
|
+
const message = 'EDGE_WORKER_DB_URL is not set!\n' +
|
|
105
|
+
'See https://pgflow.pages.dev/edge-worker/prepare-environment/#prepare-connection-string';
|
|
106
|
+
throw new Error(message);
|
|
107
|
+
}
|
|
108
|
+
const executionId = Deno.env.get('SB_EXECUTION_ID');
|
|
109
|
+
if (!executionId) {
|
|
110
|
+
throw new Error('SB_EXECUTION_ID is not set!');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
executionId,
|
|
114
|
+
logLevel: Deno.env.get('EDGE_WORKER_LOG_LEVEL') || 'info',
|
|
115
|
+
connectionString,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
extractFunctionName(req) {
|
|
119
|
+
return new URL(req.url).pathname.replace(/^\/+|\/+$/g, '');
|
|
120
|
+
}
|
|
121
|
+
setupShutdownHandler() {
|
|
122
|
+
globalThis.onbeforeunload = async () => {
|
|
123
|
+
if (this.worker && this.edgeFunctionName) {
|
|
124
|
+
await this.spawnNewEdgeFunction(this.edgeFunctionName);
|
|
125
|
+
}
|
|
126
|
+
if (this.worker) {
|
|
127
|
+
this.worker.stop();
|
|
128
|
+
this.worker = null;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PlatformAdapter } from './types.js';
|
|
2
|
+
import './deno-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the appropriate platform adapter based on the runtime environment
|
|
5
|
+
*/
|
|
6
|
+
export declare function createAdapter(): Promise<PlatformAdapter>;
|
|
7
|
+
//# sourceMappingURL=createAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createAdapter.d.ts","sourceRoot":"","sources":["../../src/platform/createAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,iBAAiB,CAAC;AAEzB;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,eAAe,CAAC,CAS9D"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DenoAdapter } from './DenoAdapter.js';
|
|
2
|
+
import './deno-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the appropriate platform adapter based on the runtime environment
|
|
5
|
+
*/
|
|
6
|
+
export async function createAdapter() {
|
|
7
|
+
if (isDenoEnvironment()) {
|
|
8
|
+
const adapter = new DenoAdapter();
|
|
9
|
+
return adapter;
|
|
10
|
+
}
|
|
11
|
+
// For now, only support Deno
|
|
12
|
+
// Later add NodeAdapter, BrowserAdapter, etc.
|
|
13
|
+
throw new Error('Unsupported environment');
|
|
14
|
+
}
|
|
15
|
+
function isDenoEnvironment() {
|
|
16
|
+
return typeof Deno !== 'undefined';
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type definitions for Deno APIs used in our codebase.
|
|
3
|
+
* These are used for type checking during build in Node.js environment.
|
|
4
|
+
* At runtime in Deno, the actual Deno implementations will be used.
|
|
5
|
+
*/
|
|
6
|
+
declare global {
|
|
7
|
+
interface EdgeRuntimeNamespace {
|
|
8
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
9
|
+
}
|
|
10
|
+
var EdgeRuntime: EdgeRuntimeNamespace;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=deno-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deno-types.d.ts","sourceRoot":"","sources":["../../src/platform/deno-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,oBAAoB;QAC5B,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;KAC5C;IAID,IAAI,WAAW,EAAE,oBAAoB,CAAC;CACvC;AAGD,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/platform/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Worker } from '../core/Worker.js';
|
|
2
|
+
/**
|
|
3
|
+
* Basic logger interface used throughout the application
|
|
4
|
+
*/
|
|
5
|
+
export interface Logger {
|
|
6
|
+
debug(message: string, ...args: any[]): void;
|
|
7
|
+
info(message: string, ...args: any[]): void;
|
|
8
|
+
warn(message: string, ...args: any[]): void;
|
|
9
|
+
error(message: string, ...args: any[]): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Logger factory function
|
|
13
|
+
*/
|
|
14
|
+
export type CreateLoggerFn = (module: string) => Logger;
|
|
15
|
+
/**
|
|
16
|
+
* Logger factory function
|
|
17
|
+
*/
|
|
18
|
+
export type CreateWorkerFn = (createLoggerFn: CreateLoggerFn) => Worker;
|
|
19
|
+
/**
|
|
20
|
+
* Type for platform-specific environment variables
|
|
21
|
+
*/
|
|
22
|
+
export interface PlatformEnvironment {
|
|
23
|
+
executionId: string;
|
|
24
|
+
logLevel: string;
|
|
25
|
+
connectionString: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Common interface for all platform adapters
|
|
29
|
+
*/
|
|
30
|
+
export interface PlatformAdapter {
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the platform adapter with a worker factory function
|
|
33
|
+
* @param createWorkerFn Function that creates a worker instance when called with a logger
|
|
34
|
+
*/
|
|
35
|
+
initialize(createWorkerFn: CreateWorkerFn): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Clean up resources when shutting down
|
|
38
|
+
*/
|
|
39
|
+
terminate(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Get platform-specific environment variables
|
|
42
|
+
*/
|
|
43
|
+
getEnv(): PlatformEnvironment;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/platform/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;AAExD;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,cAAc,EAAE,cAAc,KAAK,MAAM,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;CAE1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,UAAU,CAAC,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1D;;OAEG;IACH,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B;;OAEG;IACH,MAAM,IAAI,mBAAmB,CAAC;CAC/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Json } from '../core/types.js';
|
|
2
|
+
import type { PgmqMessageRecord } from './types.js';
|
|
3
|
+
import type { Queue } from './Queue.js';
|
|
4
|
+
import type { Logger } from '../platform/types.js';
|
|
5
|
+
/**
|
|
6
|
+
* A class that executes a message handler.
|
|
7
|
+
*
|
|
8
|
+
* It handles the execution of the message handler and retries or archives the message
|
|
9
|
+
* based on the retry limit and delay.
|
|
10
|
+
*
|
|
11
|
+
* It also handles the abort signal and logs the error.
|
|
12
|
+
*/
|
|
13
|
+
export declare class MessageExecutor<TPayload extends Json> {
|
|
14
|
+
private readonly queue;
|
|
15
|
+
private readonly record;
|
|
16
|
+
private readonly messageHandler;
|
|
17
|
+
private readonly signal;
|
|
18
|
+
private readonly retryLimit;
|
|
19
|
+
private readonly retryDelay;
|
|
20
|
+
private logger;
|
|
21
|
+
constructor(queue: Queue<TPayload>, record: PgmqMessageRecord<TPayload>, messageHandler: (message: TPayload) => Promise<void> | void, signal: AbortSignal, retryLimit: number, retryDelay: number, logger: Logger);
|
|
22
|
+
get msgId(): number;
|
|
23
|
+
execute(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Handles the error that occurred during execution.
|
|
26
|
+
*
|
|
27
|
+
* If the error is an AbortError, it means that the worker was aborted and stopping,
|
|
28
|
+
* the message will reappear after the visibility timeout and be picked up by another worker.
|
|
29
|
+
*
|
|
30
|
+
* Otherwise, it proceeds with retry or archiving forever.
|
|
31
|
+
*/
|
|
32
|
+
private handleExecutionError;
|
|
33
|
+
/**
|
|
34
|
+
* Retries the message if it is available.
|
|
35
|
+
* Otherwise, archives the message forever and stops processing it.
|
|
36
|
+
*/
|
|
37
|
+
private retryOrArchive;
|
|
38
|
+
/**
|
|
39
|
+
* Returns true if the message can be retried.
|
|
40
|
+
*/
|
|
41
|
+
private get retryAvailable();
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=MessageExecutor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MessageExecutor.d.ts","sourceRoot":"","sources":["../../src/queue/MessageExecutor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AASnD;;;;;;;GAOG;AACH,qBAAa,eAAe,CAAC,QAAQ,SAAS,IAAI;IAI9C,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAG/B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAV7B,OAAO,CAAC,MAAM,CAAS;gBAGJ,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,EACtB,MAAM,EAAE,iBAAiB,CAAC,QAAQ,CAAC,EACnC,cAAc,EAAE,CAC/B,OAAO,EAAE,QAAQ,KACd,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EACR,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EACnC,MAAM,EAAE,MAAM;IAKhB,IAAI,KAAK,WAER;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB9B;;;;;;;OAOG;YACW,oBAAoB;IAYlC;;;OAGG;YACW,cAAc;IAY5B;;OAEG;IACH,OAAO,KAAK,cAAc,GAIzB;CACF"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
class AbortError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super('Operation aborted');
|
|
4
|
+
this.name = 'AbortError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A class that executes a message handler.
|
|
9
|
+
*
|
|
10
|
+
* It handles the execution of the message handler and retries or archives the message
|
|
11
|
+
* based on the retry limit and delay.
|
|
12
|
+
*
|
|
13
|
+
* It also handles the abort signal and logs the error.
|
|
14
|
+
*/
|
|
15
|
+
export class MessageExecutor {
|
|
16
|
+
queue;
|
|
17
|
+
record;
|
|
18
|
+
messageHandler;
|
|
19
|
+
signal;
|
|
20
|
+
retryLimit;
|
|
21
|
+
retryDelay;
|
|
22
|
+
logger;
|
|
23
|
+
constructor(queue, record, messageHandler, signal, retryLimit, retryDelay, logger) {
|
|
24
|
+
this.queue = queue;
|
|
25
|
+
this.record = record;
|
|
26
|
+
this.messageHandler = messageHandler;
|
|
27
|
+
this.signal = signal;
|
|
28
|
+
this.retryLimit = retryLimit;
|
|
29
|
+
this.retryDelay = retryDelay;
|
|
30
|
+
this.logger = logger;
|
|
31
|
+
}
|
|
32
|
+
get msgId() {
|
|
33
|
+
return this.record.msg_id;
|
|
34
|
+
}
|
|
35
|
+
async execute() {
|
|
36
|
+
try {
|
|
37
|
+
if (this.signal.aborted) {
|
|
38
|
+
throw new AbortError();
|
|
39
|
+
}
|
|
40
|
+
// Check if already aborted before starting
|
|
41
|
+
this.signal.throwIfAborted();
|
|
42
|
+
this.logger.debug(`Executing task ${this.msgId}...`);
|
|
43
|
+
await this.messageHandler(this.record.message);
|
|
44
|
+
this.logger.debug(`Task ${this.msgId} completed successfully, archiving...`);
|
|
45
|
+
await this.queue.archive(this.msgId);
|
|
46
|
+
this.logger.debug(`Archived task ${this.msgId} successfully`);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
await this.handleExecutionError(error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Handles the error that occurred during execution.
|
|
54
|
+
*
|
|
55
|
+
* If the error is an AbortError, it means that the worker was aborted and stopping,
|
|
56
|
+
* the message will reappear after the visibility timeout and be picked up by another worker.
|
|
57
|
+
*
|
|
58
|
+
* Otherwise, it proceeds with retry or archiving forever.
|
|
59
|
+
*/
|
|
60
|
+
async handleExecutionError(error) {
|
|
61
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
62
|
+
this.logger.debug(`Aborted execution for ${this.msgId}`);
|
|
63
|
+
// Do not throw - the worker was aborted and stopping,
|
|
64
|
+
// the message will reappear after the visibility timeout
|
|
65
|
+
// and be picked up by another worker
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.logger.debug(`Task ${this.msgId} failed with error: ${error}`);
|
|
69
|
+
await this.retryOrArchive();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Retries the message if it is available.
|
|
74
|
+
* Otherwise, archives the message forever and stops processing it.
|
|
75
|
+
*/
|
|
76
|
+
async retryOrArchive() {
|
|
77
|
+
if (this.retryAvailable) {
|
|
78
|
+
// adjust visibility timeout for message to appear after retryDelay
|
|
79
|
+
this.logger.debug(`Retrying ${this.msgId} in ${this.retryDelay} seconds`);
|
|
80
|
+
await this.queue.setVt(this.msgId, this.retryDelay);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// archive message forever and stop processing it
|
|
84
|
+
this.logger.debug(`Archiving ${this.msgId} forever`);
|
|
85
|
+
await this.queue.archive(this.msgId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Returns true if the message can be retried.
|
|
90
|
+
*/
|
|
91
|
+
get retryAvailable() {
|
|
92
|
+
const readCountLimit = this.retryLimit + 1; // initial read also counts
|
|
93
|
+
return this.record.read_ct < readCountLimit;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type postgres from 'postgres';
|
|
2
|
+
import type { PgmqMessageRecord } from './types.js';
|
|
3
|
+
import type { Json } from '../core/types.js';
|
|
4
|
+
import type { Logger } from '../platform/types.js';
|
|
5
|
+
export declare class Queue<TPayload extends Json> {
|
|
6
|
+
private readonly sql;
|
|
7
|
+
readonly queueName: string;
|
|
8
|
+
private logger;
|
|
9
|
+
constructor(sql: postgres.Sql, queueName: string, logger: Logger);
|
|
10
|
+
/**
|
|
11
|
+
* Creates a queue if it doesn't exist.
|
|
12
|
+
* If the queue already exists, this method does nothing.
|
|
13
|
+
*/
|
|
14
|
+
safeCreate(): Promise<postgres.RowList<postgres.Row[]>>;
|
|
15
|
+
/**
|
|
16
|
+
* Drops a queue if it exists.
|
|
17
|
+
* If the queue doesn't exist, this method does nothing.
|
|
18
|
+
*/
|
|
19
|
+
safeDrop(): Promise<postgres.RowList<postgres.Row[]>>;
|
|
20
|
+
archive(msgId: number): Promise<void>;
|
|
21
|
+
archiveBatch(msgIds: number[]): Promise<void>;
|
|
22
|
+
send(message: TPayload): Promise<void>;
|
|
23
|
+
readWithPoll(batchSize?: number, visibilityTimeout?: number, maxPollSeconds?: number, pollIntervalMs?: number): Promise<postgres.RowList<PgmqMessageRecord<TPayload>[]>>;
|
|
24
|
+
/**
|
|
25
|
+
* Sets the visibility timeout of a message to the current time plus the given offset.
|
|
26
|
+
*
|
|
27
|
+
* This is an inlined version of the pgmq.set_vt in order to fix the bug.
|
|
28
|
+
* The original uses now() instead of clock_timestamp() which is problematic in transactions.
|
|
29
|
+
* See more details here: https://github.com/tembo-io/pgmq/issues/367
|
|
30
|
+
*
|
|
31
|
+
* The only change made is now() replaced with clock_timestamp().
|
|
32
|
+
*/
|
|
33
|
+
setVt(msgId: number, vtOffsetSeconds: number): Promise<PgmqMessageRecord<TPayload>>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=Queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../src/queue/Queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AACrC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,qBAAa,KAAK,CAAC,QAAQ,SAAS,IAAI;IAIpC,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM;IAJ5B,OAAO,CAAC,MAAM,CAAS;gBAGJ,GAAG,EAAE,QAAQ,CAAC,GAAG,EACzB,SAAS,EAAE,MAAM,EAC1B,MAAM,EAAE,MAAM;IAKhB;;;OAGG;IACG,UAAU;IAUhB;;;OAGG;IACG,QAAQ;IAUR,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrC,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7C,IAAI,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtC,YAAY,CAChB,SAAS,SAAK,EACd,iBAAiB,SAAI,EACrB,cAAc,SAAI,EAClB,cAAc,SAAM;IAetB;;;;;;;;OAQG;IACG,KAAK,CACT,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;CAUxC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export class Queue {
|
|
2
|
+
sql;
|
|
3
|
+
queueName;
|
|
4
|
+
logger;
|
|
5
|
+
constructor(sql, queueName, logger) {
|
|
6
|
+
this.sql = sql;
|
|
7
|
+
this.queueName = queueName;
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Creates a queue if it doesn't exist.
|
|
12
|
+
* If the queue already exists, this method does nothing.
|
|
13
|
+
*/
|
|
14
|
+
async safeCreate() {
|
|
15
|
+
this.logger.debug(`Creating queue '${this.queueName}' if it doesn't exist`);
|
|
16
|
+
return await this.sql `
|
|
17
|
+
select * from pgmq.create(${this.queueName})
|
|
18
|
+
where not exists (
|
|
19
|
+
select 1 from pgmq.list_queues() where queue_name = ${this.queueName}
|
|
20
|
+
);
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Drops a queue if it exists.
|
|
25
|
+
* If the queue doesn't exist, this method does nothing.
|
|
26
|
+
*/
|
|
27
|
+
async safeDrop() {
|
|
28
|
+
this.logger.debug(`Dropping queue '${this.queueName}' if it exists`);
|
|
29
|
+
return await this.sql `
|
|
30
|
+
select * from pgmq.drop_queue(${this.queueName})
|
|
31
|
+
where exists (
|
|
32
|
+
select 1 from pgmq.list_queues() where queue_name = ${this.queueName}
|
|
33
|
+
);
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
async archive(msgId) {
|
|
37
|
+
this.logger.debug(`Archiving message ${msgId} from queue '${this.queueName}'`);
|
|
38
|
+
await this.sql `
|
|
39
|
+
SELECT pgmq.archive(queue_name => ${this.queueName}, msg_id => ${msgId}::bigint);
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
async archiveBatch(msgIds) {
|
|
43
|
+
this.logger.debug(`Archiving ${msgIds.length} messages from queue '${this.queueName}'`);
|
|
44
|
+
await this.sql `
|
|
45
|
+
SELECT pgmq.archive(queue_name => ${this.queueName}, msg_ids => ${msgIds}::bigint[]);
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
async send(message) {
|
|
49
|
+
this.logger.debug(`Sending message to queue '${this.queueName}'`);
|
|
50
|
+
const msgJson = JSON.stringify(message);
|
|
51
|
+
await this.sql `
|
|
52
|
+
SELECT pgmq.send(queue_name => ${this.queueName}, msg => ${msgJson}::jsonb)
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
async readWithPoll(batchSize = 20, visibilityTimeout = 2, maxPollSeconds = 5, pollIntervalMs = 200) {
|
|
56
|
+
this.logger.debug(`Reading messages from queue '${this.queueName}' with poll`);
|
|
57
|
+
return await this.sql `
|
|
58
|
+
SELECT *
|
|
59
|
+
FROM edge_worker.read_with_poll(
|
|
60
|
+
queue_name => ${this.queueName},
|
|
61
|
+
vt => ${visibilityTimeout},
|
|
62
|
+
qty => ${batchSize},
|
|
63
|
+
max_poll_seconds => ${maxPollSeconds},
|
|
64
|
+
poll_interval_ms => ${pollIntervalMs}
|
|
65
|
+
);
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Sets the visibility timeout of a message to the current time plus the given offset.
|
|
70
|
+
*
|
|
71
|
+
* This is an inlined version of the pgmq.set_vt in order to fix the bug.
|
|
72
|
+
* The original uses now() instead of clock_timestamp() which is problematic in transactions.
|
|
73
|
+
* See more details here: https://github.com/tembo-io/pgmq/issues/367
|
|
74
|
+
*
|
|
75
|
+
* The only change made is now() replaced with clock_timestamp().
|
|
76
|
+
*/
|
|
77
|
+
async setVt(msgId, vtOffsetSeconds) {
|
|
78
|
+
this.logger.debug(`Setting visibility timeout for message ${msgId} to ${vtOffsetSeconds} seconds`);
|
|
79
|
+
const records = await this.sql `
|
|
80
|
+
UPDATE ${this.sql('pgmq.q_' + this.queueName)}
|
|
81
|
+
SET vt = (clock_timestamp() + make_interval(secs => ${vtOffsetSeconds}))
|
|
82
|
+
WHERE msg_id = ${msgId}::bigint
|
|
83
|
+
RETURNING *;
|
|
84
|
+
`;
|
|
85
|
+
return records[0];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Queue } from './Queue.js';
|
|
2
|
+
import type { PgmqMessageRecord } from './types.js';
|
|
3
|
+
import type { Json } from '../core/types.js';
|
|
4
|
+
import type { Logger } from '../platform/types.js';
|
|
5
|
+
export interface PollerConfig {
|
|
6
|
+
batchSize: number;
|
|
7
|
+
maxPollSeconds: number;
|
|
8
|
+
pollIntervalMs: number;
|
|
9
|
+
visibilityTimeout: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class ReadWithPollPoller<TPayload extends Json> {
|
|
12
|
+
protected readonly queue: Queue<TPayload>;
|
|
13
|
+
protected readonly signal: AbortSignal;
|
|
14
|
+
protected readonly config: PollerConfig;
|
|
15
|
+
private logger;
|
|
16
|
+
constructor(queue: Queue<TPayload>, signal: AbortSignal, config: PollerConfig, logger: Logger);
|
|
17
|
+
poll(): Promise<PgmqMessageRecord<TPayload>[]>;
|
|
18
|
+
private isAborted;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=ReadWithPollPoller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReadWithPollPoller.d.ts","sourceRoot":"","sources":["../../src/queue/ReadWithPollPoller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,kBAAkB,CAAC,QAAQ,SAAS,IAAI;IAIjD,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC;IACzC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,WAAW;IACtC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,YAAY;IALzC,OAAO,CAAC,MAAM,CAAS;gBAGF,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,EACtB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,YAAY,EACvC,MAAM,EAAE,MAAM;IAKV,IAAI,IAAI,OAAO,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAC;IAkBpD,OAAO,CAAC,SAAS;CAGlB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class ReadWithPollPoller {
|
|
2
|
+
queue;
|
|
3
|
+
signal;
|
|
4
|
+
config;
|
|
5
|
+
logger;
|
|
6
|
+
constructor(queue, signal, config, logger) {
|
|
7
|
+
this.queue = queue;
|
|
8
|
+
this.signal = signal;
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
}
|
|
12
|
+
async poll() {
|
|
13
|
+
if (this.isAborted()) {
|
|
14
|
+
this.logger.debug('Polling aborted, returning empty array');
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
this.logger.debug(`Polling queue '${this.queue.queueName}' with batch size ${this.config.batchSize}`);
|
|
18
|
+
const messages = await this.queue.readWithPoll(this.config.batchSize, this.config.visibilityTimeout, this.config.maxPollSeconds, this.config.pollIntervalMs);
|
|
19
|
+
this.logger.debug(`Received ${messages.length} messages from queue '${this.queue.queueName}'`);
|
|
20
|
+
return messages;
|
|
21
|
+
}
|
|
22
|
+
isAborted() {
|
|
23
|
+
return this.signal.aborted;
|
|
24
|
+
}
|
|
25
|
+
}
|