@pgflow/core 0.0.5-prealpha.2 → 0.0.5
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/CHANGELOG.md +9 -0
- package/package.json +3 -2
- package/pkgs/edge-worker/dist/index.js +953 -0
- package/pkgs/edge-worker/dist/index.js.map +7 -0
- package/pkgs/edge-worker/dist/pkgs/edge-worker/LICENSE.md +660 -0
- package/pkgs/edge-worker/dist/pkgs/edge-worker/README.md +46 -0
- package/pkgs/example-flows/dist/index.js +152 -0
- package/pkgs/example-flows/dist/pkgs/example-flows/README.md +11 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Edge Worker</h1>
|
|
3
|
+
<a href="https://pgflow.dev">
|
|
4
|
+
<h3>📚 Documentation @ pgflow.dev</h3>
|
|
5
|
+
</a>
|
|
6
|
+
|
|
7
|
+
<h4>⚠️ <strong>ADVANCED PROOF of CONCEPT - NOT PRODUCTION READY</strong> ⚠️</h4>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
A task queue worker for Supabase Edge Functions that extends background tasks with useful features.
|
|
11
|
+
|
|
12
|
+
> [!NOTE]
|
|
13
|
+
> This project is licensed under [AGPL v3](./LICENSE.md) license and is part of **pgflow** stack.
|
|
14
|
+
> See [LICENSING_OVERVIEW.md](../../LICENSING_OVERVIEW.md) in root of this monorepo for more details.
|
|
15
|
+
|
|
16
|
+
## What is Edge Worker?
|
|
17
|
+
|
|
18
|
+
Edge Worker processes messages from a queue and executes user-defined functions with their payloads. It builds upon [Supabase Background Tasks](https://supabase.com/docs/guides/functions/background-tasks) to add reliability features like retries, concurrency control and monitoring.
|
|
19
|
+
|
|
20
|
+
## Key Features
|
|
21
|
+
|
|
22
|
+
- ⚡ **Reliable Processing**: Retries with configurable delays
|
|
23
|
+
- 🔄 **Concurrency Control**: Limit parallel task execution
|
|
24
|
+
- 📊 **Observability**: Built-in heartbeats and logging
|
|
25
|
+
- 📈 **Horizontal Scaling**: Deploy multiple edge functions for the same queue
|
|
26
|
+
- 🛡️ **Edge-Native**: Designed for Edge Functions' CPU/clock limits
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
[](https://mermaid.live/edit#pako:eNplkcFugzAMhl8lyrl9AQ47VLBxqdSqlZAGHEziASokyEkmTaXvvoR0o1VziGL_n_9Y9pULLZEnvFItwdSxc1op5o9xTUxU_OQmaMAgy2SL7N0pYXutTMUjGU5WlItYaLog1VFAJSv14paCXdweyw8f-2MZLnZ06LBelXxXRk_DztAM-Gp9KA-kpRP-W7bdvs3Ga4aNaAy0OC_WdzD4B4IQVsLMvvkIZMUiA4mu_8ZHYjW5MxNp4dUnKC9zUHJA-h9R_VQTG-sQyDYINlTs-IaPSCP00q_gGvCK2w5HP53EPyXQJczp5jlwVp9-lOCJJYcbTtq13V_gJgkW0x78lEeefMFgfHYC9an1GqPsraZ9XPiy99svlAqmtA)
|
|
31
|
+
|
|
32
|
+
## Edge Function Optimization
|
|
33
|
+
|
|
34
|
+
Edge Worker is specifically designed to handle Edge Function limitations:
|
|
35
|
+
|
|
36
|
+
- Stops polling near CPU/clock limits
|
|
37
|
+
- Gracefully aborts pending tasks
|
|
38
|
+
- Uses PGMQ's visibility timeout to prevent message loss
|
|
39
|
+
- Auto-spawns new instances for continuous operation
|
|
40
|
+
- Monitors worker health with database heartbeats
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
45
|
+
For detailed documentation and getting started guide, visit [pgflow.dev](https://pgflow.dev).
|
|
46
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// ../dsl/src/utils.ts
|
|
2
|
+
function validateSlug(slug) {
|
|
3
|
+
if (slug.length > 128) {
|
|
4
|
+
throw new Error(`Slug cannot be longer than 128 characters`);
|
|
5
|
+
}
|
|
6
|
+
if (/^\d/.test(slug)) {
|
|
7
|
+
throw new Error(`Slug cannot start with a number`);
|
|
8
|
+
}
|
|
9
|
+
if (/^_/.test(slug)) {
|
|
10
|
+
throw new Error(`Slug cannot start with an underscore`);
|
|
11
|
+
}
|
|
12
|
+
if (/\s/.test(slug)) {
|
|
13
|
+
throw new Error(`Slug cannot contain spaces`);
|
|
14
|
+
}
|
|
15
|
+
if (/[/:#\-?]/.test(slug)) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Slug cannot contain special characters like /, :, ?, #, -`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function validateRuntimeOptions(options, opts = { optional: false }) {
|
|
22
|
+
const { maxAttempts, baseDelay, timeout } = options;
|
|
23
|
+
if (maxAttempts !== void 0 && maxAttempts !== null) {
|
|
24
|
+
if (maxAttempts < 1) {
|
|
25
|
+
throw new Error("maxAttempts must be greater than or equal to 1");
|
|
26
|
+
}
|
|
27
|
+
} else if (!opts.optional) {
|
|
28
|
+
throw new Error("maxAttempts is required");
|
|
29
|
+
}
|
|
30
|
+
if (baseDelay !== void 0 && baseDelay !== null) {
|
|
31
|
+
if (baseDelay < 1) {
|
|
32
|
+
throw new Error("baseDelay must be greater than or equal to 1");
|
|
33
|
+
}
|
|
34
|
+
} else if (!opts.optional) {
|
|
35
|
+
throw new Error("baseDelay is required");
|
|
36
|
+
}
|
|
37
|
+
if (timeout !== void 0 && timeout !== null) {
|
|
38
|
+
if (timeout < 3) {
|
|
39
|
+
throw new Error("timeout must be greater than or equal to 3");
|
|
40
|
+
}
|
|
41
|
+
} else if (!opts.optional) {
|
|
42
|
+
throw new Error("timeout is required");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ../dsl/src/dsl.ts
|
|
47
|
+
var Flow = class _Flow {
|
|
48
|
+
/**
|
|
49
|
+
* Store step definitions with their proper types
|
|
50
|
+
*
|
|
51
|
+
* This is typed as a generic record because TypeScript cannot track the exact relationship
|
|
52
|
+
* between step slugs and their corresponding input/output types at the container level.
|
|
53
|
+
* Type safety is enforced at the method level when adding or retrieving steps.
|
|
54
|
+
*/
|
|
55
|
+
stepDefinitions;
|
|
56
|
+
stepOrder;
|
|
57
|
+
slug;
|
|
58
|
+
options;
|
|
59
|
+
constructor(config, stepDefinitions = {}, stepOrder = []) {
|
|
60
|
+
const { slug, ...options } = config;
|
|
61
|
+
validateSlug(slug);
|
|
62
|
+
validateRuntimeOptions(options, { optional: true });
|
|
63
|
+
this.slug = slug;
|
|
64
|
+
this.options = options;
|
|
65
|
+
this.stepDefinitions = stepDefinitions;
|
|
66
|
+
this.stepOrder = [...stepOrder];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get a specific step definition by slug with proper typing
|
|
70
|
+
* @throws Error if the step with the given slug doesn't exist
|
|
71
|
+
*/
|
|
72
|
+
getStepDefinition(slug) {
|
|
73
|
+
if (!(slug in this.stepDefinitions)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Step "${String(slug)}" does not exist in flow "${this.slug}"`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return this.stepDefinitions[slug];
|
|
79
|
+
}
|
|
80
|
+
// SlugType extends keyof Steps & keyof StepDependencies
|
|
81
|
+
step(opts, handler) {
|
|
82
|
+
const slug = opts.slug;
|
|
83
|
+
validateSlug(slug);
|
|
84
|
+
if (this.stepDefinitions[slug]) {
|
|
85
|
+
throw new Error(`Step "${slug}" already exists in flow "${this.slug}"`);
|
|
86
|
+
}
|
|
87
|
+
const dependencies = opts.dependsOn || [];
|
|
88
|
+
if (dependencies.length > 0) {
|
|
89
|
+
for (const dep of dependencies) {
|
|
90
|
+
if (!this.stepDefinitions[dep]) {
|
|
91
|
+
throw new Error(`Step "${slug}" depends on undefined step "${dep}"`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const options = {};
|
|
96
|
+
if (opts.maxAttempts !== void 0)
|
|
97
|
+
options.maxAttempts = opts.maxAttempts;
|
|
98
|
+
if (opts.baseDelay !== void 0)
|
|
99
|
+
options.baseDelay = opts.baseDelay;
|
|
100
|
+
if (opts.timeout !== void 0)
|
|
101
|
+
options.timeout = opts.timeout;
|
|
102
|
+
validateRuntimeOptions(options, { optional: true });
|
|
103
|
+
const newStepDefinition = {
|
|
104
|
+
slug,
|
|
105
|
+
handler,
|
|
106
|
+
dependencies,
|
|
107
|
+
options
|
|
108
|
+
};
|
|
109
|
+
const newStepDefinitions = {
|
|
110
|
+
...this.stepDefinitions,
|
|
111
|
+
[slug]: newStepDefinition
|
|
112
|
+
};
|
|
113
|
+
const newStepOrder = [...this.stepOrder, slug];
|
|
114
|
+
return new _Flow(
|
|
115
|
+
{ slug: this.slug, ...this.options },
|
|
116
|
+
newStepDefinitions,
|
|
117
|
+
newStepOrder
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ../example-flows/src/example-flow.ts
|
|
123
|
+
var ExampleFlow = new Flow({
|
|
124
|
+
slug: "example_flow",
|
|
125
|
+
maxAttempts: 3
|
|
126
|
+
}).step({ slug: "rootStep" }, async (input) => ({
|
|
127
|
+
doubledValue: input.run.value * 2
|
|
128
|
+
})).step(
|
|
129
|
+
{ slug: "normalStep", dependsOn: ["rootStep"], maxAttempts: 5 },
|
|
130
|
+
async (input) => ({
|
|
131
|
+
doubledValueArray: [input.rootStep.doubledValue]
|
|
132
|
+
})
|
|
133
|
+
).step({ slug: "thirdStep", dependsOn: ["normalStep"] }, async (input) => ({
|
|
134
|
+
// input.rootStep would be a type error since it's not in dependsOn
|
|
135
|
+
finalValue: input.normalStep.doubledValueArray.length
|
|
136
|
+
}));
|
|
137
|
+
var stepTaskRecord = {
|
|
138
|
+
flow_slug: "example_flow",
|
|
139
|
+
run_id: "123",
|
|
140
|
+
step_slug: "normalStep",
|
|
141
|
+
input: {
|
|
142
|
+
run: { value: 23 },
|
|
143
|
+
rootStep: { doubledValue: 23 }
|
|
144
|
+
// thirdStep: { finalValue: 23 }, --- this should be an error
|
|
145
|
+
// normalStep: { doubledValueArray: [1, 2, 3] }, --- this should be an error
|
|
146
|
+
},
|
|
147
|
+
msg_id: 1
|
|
148
|
+
};
|
|
149
|
+
export {
|
|
150
|
+
ExampleFlow,
|
|
151
|
+
stepTaskRecord
|
|
152
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# example-flows
|
|
2
|
+
|
|
3
|
+
This library was generated with [Nx](https://nx.dev).
|
|
4
|
+
|
|
5
|
+
## Building
|
|
6
|
+
|
|
7
|
+
Run `nx build example-flows` to build the library.
|
|
8
|
+
|
|
9
|
+
## Running unit tests
|
|
10
|
+
|
|
11
|
+
Run `nx test example-flows` to execute the unit tests via [Vitest](https://vitest.dev/).
|