flowli 0.2.1 → 0.2.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/README.md +128 -168
- package/jsr.json +11 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Typed jobs for modern TypeScript backends.**
|
|
4
4
|
|
|
5
|
+
[npm](https://www.npmjs.com/package/flowli) · [JSR](https://jsr.io/@alialnaghmoush/flowli) · [GitHub](https://github.com/alialnaghmoush/flowli)
|
|
6
|
+
|
|
5
7
|
Flowli is a jobs runtime with a code-first API, first-class execution strategies, runtime-scoped context injection, and pluggable Redis drivers.
|
|
6
8
|
|
|
7
9
|
Define jobs once. Run them anywhere.
|
|
@@ -9,6 +11,7 @@ Define jobs once. Run them anywhere.
|
|
|
9
11
|
## Navigate
|
|
10
12
|
|
|
11
13
|
- [Why Flowli](#why-flowli)
|
|
14
|
+
- [Compare](#compare)
|
|
12
15
|
- [What It Feels Like](#what-it-feels-like)
|
|
13
16
|
- [The Core Idea](#the-core-idea)
|
|
14
17
|
- [Primary Authoring Path](#primary-authoring-path)
|
|
@@ -46,6 +49,30 @@ Flowli is built around a different model:
|
|
|
46
49
|
|
|
47
50
|
It is a typed runtime for background and deferred execution with a code-first, framework-agnostic design.
|
|
48
51
|
|
|
52
|
+
## Compare
|
|
53
|
+
|
|
54
|
+
Choose Flowli when you want:
|
|
55
|
+
|
|
56
|
+
- application-first jobs authored in code, not in a dashboard
|
|
57
|
+
- a single runtime that supports both direct execution and persisted async work
|
|
58
|
+
- typed `context` injection without framework lock-in
|
|
59
|
+
- pluggable Redis clients behind a small API surface
|
|
60
|
+
|
|
61
|
+
Flowli vs BullMQ:
|
|
62
|
+
|
|
63
|
+
- Flowli centers the typed job-definition experience; BullMQ centers queue primitives and worker infrastructure
|
|
64
|
+
- Flowli makes `run()` a first-class in-process path; BullMQ is primarily queue-first
|
|
65
|
+
|
|
66
|
+
Flowli vs Trigger.dev:
|
|
67
|
+
|
|
68
|
+
- Flowli stays library-first and infrastructure-light
|
|
69
|
+
- Trigger.dev is stronger when you want a hosted platform, dashboard, and workflow operations out of the box
|
|
70
|
+
|
|
71
|
+
Flowli vs Inngest:
|
|
72
|
+
|
|
73
|
+
- Flowli is better suited when you want application-local jobs and direct runtime wiring
|
|
74
|
+
- Inngest is stronger when you want event-first workflows across services
|
|
75
|
+
|
|
49
76
|
```mermaid
|
|
50
77
|
flowchart LR
|
|
51
78
|
App["App Code<br/>Routes, Services, Scripts, Tests"] --> Runtime["defineJobs()<br/>Flowli Runtime"]
|
|
@@ -66,93 +93,9 @@ flowchart LR
|
|
|
66
93
|
## What It Feels Like
|
|
67
94
|
|
|
68
95
|
```ts
|
|
69
|
-
// src/flowli/jobs/create-audit-log.ts
|
|
70
|
-
import * as v from "valibot";
|
|
71
|
-
import { job } from "flowli";
|
|
72
|
-
import type { AppContext } from "..";
|
|
73
|
-
|
|
74
|
-
export const auditLogSchema = v.object({
|
|
75
|
-
entityType: v.string(),
|
|
76
|
-
entityId: v.string(),
|
|
77
|
-
action: v.string(),
|
|
78
|
-
message: v.string(),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
export const auditLogMeta = v.object({
|
|
82
|
-
requestId: v.string(),
|
|
83
|
-
actorId: v.optional(v.string()),
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
export const createAuditLog = job.withContext<AppContext>()(
|
|
87
|
-
"create_audit_log",
|
|
88
|
-
{
|
|
89
|
-
input: auditLogSchema,
|
|
90
|
-
meta: auditLogMeta,
|
|
91
|
-
handler: async ({ input, ctx, meta }) => {
|
|
92
|
-
await ctx.db.insert(ctx.schema.auditLogs).values({
|
|
93
|
-
entityType: input.entityType,
|
|
94
|
-
entityId: input.entityId,
|
|
95
|
-
action: input.action,
|
|
96
|
-
message: input.message,
|
|
97
|
-
requestId: meta?.requestId,
|
|
98
|
-
actorId: meta?.actorId ?? null,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
ctx.logger.info({
|
|
102
|
-
job: "create_audit_log",
|
|
103
|
-
requestId: meta?.requestId,
|
|
104
|
-
entityId: input.entityId,
|
|
105
|
-
});
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
);
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
// src/flowli/jobs/send-notification-email.ts
|
|
113
96
|
import * as v from "valibot";
|
|
114
|
-
import { job } from "flowli";
|
|
115
|
-
import type { AppContext } from "..";
|
|
116
|
-
|
|
117
|
-
export const notificationEmailSchema = v.object({
|
|
118
|
-
email: v.string(),
|
|
119
|
-
subject: v.string(),
|
|
120
|
-
message: v.string(),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
export const sendNotificationEmail = job.withContext<AppContext>()(
|
|
124
|
-
"send_notification_email",
|
|
125
|
-
{
|
|
126
|
-
input: notificationEmailSchema,
|
|
127
|
-
handler: async ({ input, ctx }) => {
|
|
128
|
-
await ctx.mailer.send({
|
|
129
|
-
to: input.email,
|
|
130
|
-
subject: input.subject,
|
|
131
|
-
text: input.message,
|
|
132
|
-
});
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
);
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
// src/flowli/jobs/index.ts
|
|
140
|
-
export * from "./create-audit-log";
|
|
141
|
-
export * from "./send-notification-email";
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
// src/flowli/index.ts
|
|
146
97
|
import { defineJobs } from "flowli";
|
|
147
98
|
import { ioredisDriver } from "flowli/ioredis";
|
|
148
|
-
import * as jobs from "./jobs";
|
|
149
|
-
|
|
150
|
-
export type AppContext = {
|
|
151
|
-
db: typeof db;
|
|
152
|
-
schema: typeof schema;
|
|
153
|
-
logger: typeof logger;
|
|
154
|
-
mailer: typeof mailer;
|
|
155
|
-
};
|
|
156
99
|
|
|
157
100
|
export const flowli = defineJobs({
|
|
158
101
|
driver: ioredisDriver({
|
|
@@ -165,7 +108,59 @@ export const flowli = defineJobs({
|
|
|
165
108
|
logger,
|
|
166
109
|
mailer,
|
|
167
110
|
}),
|
|
168
|
-
jobs
|
|
111
|
+
jobs: ({ job }) => {
|
|
112
|
+
const auditLogSchema = v.object({
|
|
113
|
+
entityType: v.string(),
|
|
114
|
+
entityId: v.string(),
|
|
115
|
+
action: v.string(),
|
|
116
|
+
message: v.string(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const auditLogMetaSchema = v.object({
|
|
120
|
+
requestId: v.string(),
|
|
121
|
+
actorId: v.optional(v.string()),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const notificationEmailSchema = v.object({
|
|
125
|
+
email: v.string(),
|
|
126
|
+
subject: v.string(),
|
|
127
|
+
message: v.string(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
createAuditLog: job("create_audit_log", {
|
|
132
|
+
input: auditLogSchema,
|
|
133
|
+
meta: auditLogMetaSchema,
|
|
134
|
+
handler: async ({ input, ctx, meta }) => {
|
|
135
|
+
await ctx.db.insert(ctx.schema.auditLogs).values({
|
|
136
|
+
entityType: input.entityType,
|
|
137
|
+
entityId: input.entityId,
|
|
138
|
+
action: input.action,
|
|
139
|
+
message: input.message,
|
|
140
|
+
requestId: meta?.requestId,
|
|
141
|
+
actorId: meta?.actorId ?? null,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
ctx.logger.info({
|
|
145
|
+
job: "create_audit_log",
|
|
146
|
+
requestId: meta?.requestId,
|
|
147
|
+
entityId: input.entityId,
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
sendNotificationEmail: job("send_notification_email", {
|
|
153
|
+
input: notificationEmailSchema,
|
|
154
|
+
handler: async ({ input, ctx }) => {
|
|
155
|
+
await ctx.mailer.send({
|
|
156
|
+
to: input.email,
|
|
157
|
+
subject: input.subject,
|
|
158
|
+
text: input.message,
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
},
|
|
169
164
|
});
|
|
170
165
|
```
|
|
171
166
|
|
|
@@ -236,63 +231,45 @@ flowchart TD
|
|
|
236
231
|
|
|
237
232
|
The canonical Flowli path is runtime-first:
|
|
238
233
|
|
|
239
|
-
```ts
|
|
240
|
-
// src/flowli/jobs/create-audit-log.ts
|
|
241
|
-
import * as v from "valibot";
|
|
242
|
-
import { job } from "flowli";
|
|
243
|
-
import type { AppContext } from "..";
|
|
244
|
-
|
|
245
|
-
export const auditLogSchema = v.object({
|
|
246
|
-
entityId: v.string(),
|
|
247
|
-
action: v.string(),
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
export const auditLogMeta = v.object({
|
|
251
|
-
requestId: v.string(),
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
export const createAuditLog = job.withContext<AppContext>()(
|
|
255
|
-
"create_audit_log",
|
|
256
|
-
{
|
|
257
|
-
input: auditLogSchema,
|
|
258
|
-
meta: auditLogMeta,
|
|
259
|
-
handler: async ({ input, ctx, meta }) => {
|
|
260
|
-
await ctx.db.insert("audit_logs").values({
|
|
261
|
-
entityId: input.entityId,
|
|
262
|
-
action: input.action,
|
|
263
|
-
requestId: meta?.requestId,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
ctx.logger.info({
|
|
267
|
-
entityId: input.entityId,
|
|
268
|
-
action: input.action,
|
|
269
|
-
});
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
);
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
// src/flowli/jobs/index.ts
|
|
277
|
-
export * from "./create-audit-log";
|
|
278
|
-
```
|
|
279
|
-
|
|
280
234
|
```ts
|
|
281
235
|
// src/flowli/index.ts
|
|
236
|
+
import * as v from "valibot";
|
|
282
237
|
import { defineJobs } from "flowli";
|
|
283
|
-
import * as jobs from "./jobs";
|
|
284
|
-
|
|
285
|
-
export type AppContext = {
|
|
286
|
-
logger: typeof logger;
|
|
287
|
-
db: typeof db;
|
|
288
|
-
};
|
|
289
238
|
|
|
290
239
|
export const flowli = defineJobs({
|
|
291
240
|
context: {
|
|
292
241
|
logger,
|
|
293
242
|
db,
|
|
294
243
|
},
|
|
295
|
-
jobs
|
|
244
|
+
jobs: ({ job }) => {
|
|
245
|
+
const auditLogSchema = v.object({
|
|
246
|
+
entityId: v.string(),
|
|
247
|
+
action: v.string(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const auditLogMetaSchema = v.object({
|
|
251
|
+
requestId: v.string(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
createAuditLog: job("create_audit_log", {
|
|
256
|
+
input: auditLogSchema,
|
|
257
|
+
meta: auditLogMetaSchema,
|
|
258
|
+
handler: async ({ input, ctx, meta }) => {
|
|
259
|
+
await ctx.db.insert("audit_logs").values({
|
|
260
|
+
entityId: input.entityId,
|
|
261
|
+
action: input.action,
|
|
262
|
+
requestId: meta?.requestId,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
ctx.logger.info({
|
|
266
|
+
entityId: input.entityId,
|
|
267
|
+
action: input.action,
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
},
|
|
296
273
|
});
|
|
297
274
|
```
|
|
298
275
|
|
|
@@ -407,43 +384,9 @@ In short:
|
|
|
407
384
|
To persist jobs, add a driver:
|
|
408
385
|
|
|
409
386
|
```ts
|
|
410
|
-
// src/flowli/jobs/send-email.ts
|
|
411
387
|
import * as v from "valibot";
|
|
412
|
-
import { job } from "flowli";
|
|
413
|
-
import type { AppContext } from "..";
|
|
414
|
-
|
|
415
|
-
export const emailInputSchema = v.object({
|
|
416
|
-
email: v.string(),
|
|
417
|
-
subject: v.string(),
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
export const sendEmail = job.withContext<AppContext>()("send_email", {
|
|
421
|
-
input: emailInputSchema,
|
|
422
|
-
handler: async ({ input, ctx }) => {
|
|
423
|
-
await ctx.mailer.send({
|
|
424
|
-
to: input.email,
|
|
425
|
-
subject: input.subject,
|
|
426
|
-
});
|
|
427
|
-
},
|
|
428
|
-
});
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
```ts
|
|
432
|
-
// src/flowli/jobs/index.ts
|
|
433
|
-
export * from "./send-email";
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
```ts
|
|
437
|
-
// src/flowli/index.ts
|
|
438
388
|
import { defineJobs } from "flowli";
|
|
439
389
|
import { ioredisDriver } from "flowli/ioredis";
|
|
440
|
-
import * as jobs from "./jobs";
|
|
441
|
-
|
|
442
|
-
export type AppContext = {
|
|
443
|
-
db: typeof db;
|
|
444
|
-
logger: typeof logger;
|
|
445
|
-
mailer: typeof mailer;
|
|
446
|
-
};
|
|
447
390
|
|
|
448
391
|
export const flowli = defineJobs({
|
|
449
392
|
driver: ioredisDriver({
|
|
@@ -455,7 +398,24 @@ export const flowli = defineJobs({
|
|
|
455
398
|
logger,
|
|
456
399
|
mailer,
|
|
457
400
|
}),
|
|
458
|
-
jobs
|
|
401
|
+
jobs: ({ job }) => {
|
|
402
|
+
const emailInputSchema = v.object({
|
|
403
|
+
email: v.string(),
|
|
404
|
+
subject: v.string(),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
sendEmail: job("send_email", {
|
|
409
|
+
input: emailInputSchema,
|
|
410
|
+
handler: async ({ input, ctx }) => {
|
|
411
|
+
await ctx.mailer.send({
|
|
412
|
+
to: input.email,
|
|
413
|
+
subject: input.subject,
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
}),
|
|
417
|
+
};
|
|
418
|
+
},
|
|
459
419
|
});
|
|
460
420
|
```
|
|
461
421
|
|
package/jsr.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
|
3
3
|
"name": "@alialnaghmoush/flowli",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
7
7
|
"./ioredis": "./src/ioredis.ts",
|
|
@@ -18,9 +18,19 @@
|
|
|
18
18
|
"package.json"
|
|
19
19
|
],
|
|
20
20
|
"exclude": [
|
|
21
|
+
".cursor/",
|
|
22
|
+
".git/",
|
|
23
|
+
".gitignore",
|
|
24
|
+
".npm-cache/",
|
|
25
|
+
"biome.json",
|
|
26
|
+
"bun.lock",
|
|
27
|
+
"docker-compose.yml",
|
|
21
28
|
"dist/",
|
|
22
29
|
"node_modules/",
|
|
23
30
|
"test/",
|
|
31
|
+
"tsconfig.build.json",
|
|
32
|
+
"tsconfig.json",
|
|
33
|
+
"tsconfig.type-tests.json",
|
|
24
34
|
"type-tests/"
|
|
25
35
|
]
|
|
26
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Flowli is a jobs runtime with a code-first API, first-class execution strategies, runtime-scoped context injection, and pluggable Redis drivers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jobs",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"type": "git",
|
|
21
21
|
"url": "git+https://github.com/alialnaghmoush/flowli.git"
|
|
22
22
|
},
|
|
23
|
+
"license": "MIT",
|
|
23
24
|
"main": "./dist/index.js",
|
|
24
25
|
"types": "./dist/index.d.ts",
|
|
25
26
|
"type": "module",
|
|
@@ -68,9 +69,9 @@
|
|
|
68
69
|
},
|
|
69
70
|
"peerDependencies": {
|
|
70
71
|
"@tanstack/react-start": "^1.0.0-rc || ^1",
|
|
71
|
-
"ioredis": "",
|
|
72
|
+
"ioredis": "^5",
|
|
72
73
|
"next": "^15 || ^16",
|
|
73
|
-
"redis": "",
|
|
74
|
+
"redis": "^5",
|
|
74
75
|
"typescript": "^5",
|
|
75
76
|
"valibot": "^1.3.0",
|
|
76
77
|
"zod": "^4.3.6"
|