flowli 0.2.1

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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +815 -0
  3. package/dist/bun-redis.d.ts +1 -0
  4. package/dist/bun-redis.js +3 -0
  5. package/dist/core/define-jobs.d.ts +8 -0
  6. package/dist/core/define-jobs.js +47 -0
  7. package/dist/core/errors.d.ts +21 -0
  8. package/dist/core/errors.js +35 -0
  9. package/dist/core/job.d.ts +9 -0
  10. package/dist/core/job.js +20 -0
  11. package/dist/core/types.d.ts +175 -0
  12. package/dist/core/types.js +1 -0
  13. package/dist/driver/duration.d.ts +2 -0
  14. package/dist/driver/duration.js +23 -0
  15. package/dist/driver/encoding.d.ts +2 -0
  16. package/dist/driver/encoding.js +9 -0
  17. package/dist/driver/keys.d.ts +15 -0
  18. package/dist/driver/keys.js +14 -0
  19. package/dist/driver/records.d.ts +25 -0
  20. package/dist/driver/records.js +61 -0
  21. package/dist/driver/scheduling.d.ts +6 -0
  22. package/dist/driver/scheduling.js +127 -0
  23. package/dist/drivers/bun-redis.d.ts +24 -0
  24. package/dist/drivers/bun-redis.js +21 -0
  25. package/dist/drivers/ioredis.d.ts +16 -0
  26. package/dist/drivers/ioredis.js +31 -0
  27. package/dist/drivers/redis.d.ts +27 -0
  28. package/dist/drivers/redis.js +23 -0
  29. package/dist/drivers/shared.d.ts +21 -0
  30. package/dist/drivers/shared.js +172 -0
  31. package/dist/hono.d.ts +1 -0
  32. package/dist/hono.js +3 -0
  33. package/dist/index.d.ts +4 -0
  34. package/dist/index.js +11 -0
  35. package/dist/integrations/hono.d.ts +10 -0
  36. package/dist/integrations/hono.js +7 -0
  37. package/dist/integrations/next.d.ts +18 -0
  38. package/dist/integrations/tanstack-start.d.ts +20 -0
  39. package/dist/ioredis.d.ts +1 -0
  40. package/dist/ioredis.js +3 -0
  41. package/dist/next.d.ts +1 -0
  42. package/dist/next.js +4 -0
  43. package/dist/redis.d.ts +1 -0
  44. package/dist/redis.js +3 -0
  45. package/dist/runner/create-runner.d.ts +3 -0
  46. package/dist/runner/create-runner.js +104 -0
  47. package/dist/runner/types.d.ts +20 -0
  48. package/dist/runner/types.js +1 -0
  49. package/dist/runner.d.ts +2 -0
  50. package/dist/runner.js +3 -0
  51. package/dist/runtime/create-job-surface.d.ts +2 -0
  52. package/dist/runtime/create-job-surface.js +24 -0
  53. package/dist/runtime/invoke-handler.d.ts +2 -0
  54. package/dist/runtime/invoke-handler.js +7 -0
  55. package/dist/runtime/normalize-jobs.d.ts +5 -0
  56. package/dist/runtime/normalize-jobs.js +14 -0
  57. package/dist/runtime/resolve-context.d.ts +2 -0
  58. package/dist/runtime/resolve-context.js +6 -0
  59. package/dist/runtime/validate.d.ts +2 -0
  60. package/dist/runtime/validate.js +15 -0
  61. package/dist/strategies/delay.d.ts +2 -0
  62. package/dist/strategies/delay.js +32 -0
  63. package/dist/strategies/enqueue.d.ts +2 -0
  64. package/dist/strategies/enqueue.js +30 -0
  65. package/dist/strategies/run.d.ts +2 -0
  66. package/dist/strategies/run.js +10 -0
  67. package/dist/strategies/schedule.d.ts +2 -0
  68. package/dist/strategies/schedule.js +33 -0
  69. package/dist/tanstack-start.d.ts +1 -0
  70. package/dist/tanstack-start.js +4 -0
  71. package/jsr.json +26 -0
  72. package/package.json +135 -0
package/README.md ADDED
@@ -0,0 +1,815 @@
1
+ # Flowli
2
+
3
+ **Typed jobs for modern TypeScript backends.**
4
+
5
+ Flowli is a jobs runtime with a code-first API, first-class execution strategies, runtime-scoped context injection, and pluggable Redis drivers.
6
+
7
+ Define jobs once. Run them anywhere.
8
+
9
+ ## Navigate
10
+
11
+ - [Why Flowli](#why-flowli)
12
+ - [What It Feels Like](#what-it-feels-like)
13
+ - [The Core Idea](#the-core-idea)
14
+ - [Primary Authoring Path](#primary-authoring-path)
15
+ - [`run()` Works Without Infrastructure](#run-works-without-infrastructure)
16
+ - [Rich Example](#rich-example)
17
+ - [Context vs Meta](#context-vs-meta)
18
+ - [Async Execution](#async-execution)
19
+ - [Runner](#runner)
20
+ - [Async Semantics](#async-semantics)
21
+ - [Reusable Predeclared Jobs](#reusable-predeclared-jobs)
22
+ - [Hono](#hono)
23
+ - [Next.js](#nextjs)
24
+ - [TanStack Start](#tanstack-start)
25
+ - [Install](#install)
26
+ - [Production](#production)
27
+ - [What Flowli Optimizes For](#what-flowli-optimizes-for)
28
+ - [Exports](#exports)
29
+ - [Status](#status)
30
+
31
+ ## Why Flowli
32
+
33
+ Most job systems make one of these tradeoffs:
34
+
35
+ - great queues, weak TypeScript ergonomics
36
+ - strong typing, but framework lock-in
37
+ - easy background work, but awkward direct execution in app code and tests
38
+
39
+ Flowli is built around a different model:
40
+
41
+ - author jobs like application code, not infrastructure config
42
+ - keep `context` centralized in the runtime
43
+ - use the same job surface in route handlers, scripts, tests, and workers
44
+ - choose execution strategy per call: `run`, `enqueue`, `delay`, `schedule`
45
+ - swap Redis clients without rewriting job definitions
46
+
47
+ It is a typed runtime for background and deferred execution with a code-first, framework-agnostic design.
48
+
49
+ ```mermaid
50
+ flowchart LR
51
+ App["App Code<br/>Routes, Services, Scripts, Tests"] --> Runtime["defineJobs()<br/>Flowli Runtime"]
52
+ Jobs["Job Definitions<br/>input, meta, handler"] --> Runtime
53
+ Context["Runtime Context<br/>db, logger, mailer, config"] --> Runtime
54
+
55
+ Runtime --> Run["run()<br/>in-process"]
56
+ Runtime --> Persist["enqueue() / delay() / schedule()"]
57
+
58
+ Persist --> Driver["Redis Driver<br/>ioredis, redis, Bun Redis"]
59
+ Driver --> Redis["Redis-compatible backend"]
60
+ Runner["createRunner()"] --> Driver
61
+ Runner --> Jobs
62
+
63
+ Frameworks["Hono / Next.js / TanStack Start"] --> Runtime
64
+ ```
65
+
66
+ ## What It Feels Like
67
+
68
+ ```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
+ 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
+ import { defineJobs } from "flowli";
147
+ 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
+
157
+ export const flowli = defineJobs({
158
+ driver: ioredisDriver({
159
+ client: redis,
160
+ prefix: "app",
161
+ }),
162
+ context: async () => ({
163
+ db,
164
+ schema,
165
+ logger,
166
+ mailer,
167
+ }),
168
+ jobs,
169
+ });
170
+ ```
171
+
172
+ Then use it where the work happens:
173
+
174
+ ```ts
175
+ await flowli.createAuditLog.run(
176
+ {
177
+ entityType: "invoice",
178
+ entityId: "inv_123",
179
+ action: "invoice.created",
180
+ message: "Invoice created",
181
+ },
182
+ {
183
+ meta: {
184
+ requestId: "req_123",
185
+ actorId: "user_123",
186
+ },
187
+ },
188
+ );
189
+
190
+ await flowli.sendNotificationEmail.enqueue({
191
+ email: "sam@example.com",
192
+ subject: "Flowli event received",
193
+ message: "A new invoice was created.",
194
+ });
195
+
196
+ await flowli.sendNotificationEmail.delay("10m", {
197
+ email: "sam@example.com",
198
+ subject: "Delayed follow-up",
199
+ message: "This is your delayed notification.",
200
+ });
201
+
202
+ await flowli.sendNotificationEmail.schedule({
203
+ cron: "0 8 * * *",
204
+ input: {
205
+ email: "sam@example.com",
206
+ subject: "Daily digest",
207
+ message: "Here is your daily digest.",
208
+ },
209
+ });
210
+ ```
211
+
212
+ ## The Core Idea
213
+
214
+ Flowli is built around four primitives:
215
+
216
+ 1. `job()`
217
+ Define one unit of work with typed `input`, optional typed `meta`, and a handler.
218
+ 2. `defineJobs()`
219
+ Bind jobs to a runtime `context`, optional driver, and shared defaults.
220
+ 3. execution strategies
221
+ Choose `run`, `enqueue`, `delay`, or `schedule` per invocation.
222
+ 4. optional async runtime
223
+ Attach `createRunner({ flowli })` only when you want persisted async processing.
224
+
225
+ ```mermaid
226
+ flowchart TD
227
+ Define["job()"] --> Bind["defineJobs()"]
228
+ Bind --> Choose{"Choose a strategy"}
229
+ Choose --> Run["run()<br/>validate -> resolve context -> invoke handler"]
230
+ Choose --> Enqueue["enqueue()<br/>persist now"]
231
+ Choose --> Delay["delay()<br/>persist with scheduledFor"]
232
+ Choose --> Schedule["schedule()<br/>persist recurring definition"]
233
+ ```
234
+
235
+ ## Primary Authoring Path
236
+
237
+ The canonical Flowli path is runtime-first:
238
+
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
+ ```ts
281
+ // src/flowli/index.ts
282
+ 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
+
290
+ export const flowli = defineJobs({
291
+ context: {
292
+ logger,
293
+ db,
294
+ },
295
+ jobs,
296
+ });
297
+ ```
298
+
299
+ This keeps the mental model clean:
300
+
301
+ - runtime `context` is defined in one place
302
+ - jobs are authored against that runtime
303
+ - handlers receive typed `ctx`
304
+ - app wiring stays separate from business logic
305
+
306
+ ## `run()` Works Without Infrastructure
307
+
308
+ `run()` is intentionally independent of drivers, queues, leases, and runner state.
309
+
310
+ That makes Flowli useful in:
311
+
312
+ - route handlers
313
+ - CLI scripts
314
+ - local development
315
+ - unit tests
316
+ - synchronous side-effect flows
317
+
318
+ ```ts
319
+ await flowli.createAuditLog.run(
320
+ {
321
+ entityId: "record_1",
322
+ action: "record.created",
323
+ },
324
+ {
325
+ meta: {
326
+ requestId: "req_1",
327
+ },
328
+ },
329
+ );
330
+ ```
331
+
332
+ ## Rich Example
333
+
334
+ A realistic service can use both direct execution and background work from the same runtime:
335
+
336
+ ```ts
337
+ // src/services/create-record.ts
338
+ import { flowli } from "../flowli";
339
+
340
+ export async function createRecord(input: {
341
+ title: string;
342
+ description?: string;
343
+ }) {
344
+ const [record] = await db
345
+ .insert(schema.records)
346
+ .values({
347
+ title: input.title,
348
+ description: input.description ?? null,
349
+ })
350
+ .returning();
351
+
352
+ await flowli.createAuditLog.run(
353
+ {
354
+ entityType: "record",
355
+ entityId: String(record.id),
356
+ action: "record.created",
357
+ message: `Record "${record.title}" was created`,
358
+ },
359
+ {
360
+ meta: {
361
+ requestId: "req_123",
362
+ actorId: "user_123",
363
+ },
364
+ },
365
+ );
366
+
367
+ await flowli.sendNotificationEmail.enqueue({
368
+ email: "owner@example.com",
369
+ subject: "Record created",
370
+ message: `A new record named "${record.title}" is ready.`,
371
+ });
372
+
373
+ return record;
374
+ }
375
+ ```
376
+
377
+ One job surface. Multiple execution modes. Same typing story.
378
+
379
+ ## Context vs Meta
380
+
381
+ This separation is intentional and important.
382
+
383
+ Use `context` for runtime-scoped dependencies:
384
+
385
+ - `db`
386
+ - `schema`
387
+ - `logger`
388
+ - `mailer`
389
+ - `storage`
390
+ - `config`
391
+
392
+ Use `meta` for invocation-scoped values:
393
+
394
+ - `requestId`
395
+ - `actorId`
396
+ - `locale`
397
+ - `tenantId`
398
+ - `traceId`
399
+
400
+ In short:
401
+
402
+ - `context` is infrastructure and shared services
403
+ - `meta` is request or invocation data
404
+
405
+ ## Async Execution
406
+
407
+ To persist jobs, add a driver:
408
+
409
+ ```ts
410
+ // src/flowli/jobs/send-email.ts
411
+ 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
+ import { defineJobs } from "flowli";
439
+ 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
+
448
+ export const flowli = defineJobs({
449
+ driver: ioredisDriver({
450
+ client: redis,
451
+ prefix: "app",
452
+ }),
453
+ context: async () => ({
454
+ db,
455
+ logger,
456
+ mailer,
457
+ }),
458
+ jobs,
459
+ });
460
+ ```
461
+
462
+ Flowli supports:
463
+
464
+ - `flowli/ioredis`
465
+ - `flowli/redis`
466
+ - `flowli/bun-redis`
467
+
468
+ The job definitions stay the same. Only the driver changes.
469
+
470
+ ## Runner
471
+
472
+ The runner is explicit and secondary by design.
473
+
474
+ Flowli is not a worker-first framework. The story stays:
475
+
476
+ - define jobs
477
+ - configure Flowli
478
+ - optionally attach a runner
479
+
480
+ ```ts
481
+ import { createRunner } from "flowli/runner";
482
+
483
+ const runner = createRunner({
484
+ flowli,
485
+ concurrency: 5,
486
+ pollIntervalMs: 1_000,
487
+ leaseMs: 30_000,
488
+ });
489
+
490
+ await runner.runOnce();
491
+ await runner.start();
492
+ await runner.stop();
493
+ ```
494
+
495
+ `createRunner()` consumes an existing runtime. It does not recreate jobs or rebuild context.
496
+
497
+ ## Async Semantics
498
+
499
+ Persisted execution in Flowli is:
500
+
501
+ - at-least-once
502
+ - lease-based
503
+ - retry-capable
504
+ - idempotency-sensitive
505
+
506
+ Handlers that run asynchronously should be safe to run more than once.
507
+
508
+ ```mermaid
509
+ sequenceDiagram
510
+ participant App as App Code
511
+ participant Flowli as Flowli Runtime
512
+ participant Driver as Redis Driver
513
+ participant Runner as Runner
514
+ participant Handler as Job Handler
515
+
516
+ App->>Flowli: enqueue() / delay() / schedule()
517
+ Flowli->>Driver: persist job or schedule
518
+ Runner->>Driver: reserve due work with lease
519
+ Driver-->>Runner: acquired job
520
+ Runner->>Flowli: resolve context + validate payload
521
+ Flowli->>Handler: invoke handler
522
+ alt success
523
+ Runner->>Driver: mark completed
524
+ else failure
525
+ Runner->>Driver: mark failed or retry
526
+ end
527
+ ```
528
+
529
+ ## Reusable Predeclared Jobs
530
+
531
+ If you want shareable job modules outside the runtime declaration, Flowli supports that too.
532
+
533
+ This is the advanced path:
534
+
535
+ ```ts
536
+ import * as v from "valibot";
537
+ import { defineJobs, job } from "flowli";
538
+
539
+ type AppContext = {
540
+ logger: {
541
+ info(payload: unknown): void;
542
+ };
543
+ };
544
+
545
+ const auditLogSchema = v.object({
546
+ entityId: v.string(),
547
+ });
548
+
549
+ export const createAuditLog = job.withContext<AppContext>()(
550
+ "create_audit_log",
551
+ {
552
+ input: auditLogSchema,
553
+ handler: async ({ input, ctx }) => {
554
+ ctx.logger.info(input.entityId);
555
+ },
556
+ },
557
+ );
558
+
559
+ export const flowli = defineJobs.withContext<AppContext>()({
560
+ jobs: { createAuditLog },
561
+ context: {
562
+ logger,
563
+ },
564
+ });
565
+ ```
566
+
567
+ When you use this path, Flowli checks at compile time that the runtime `context` satisfies the predeclared job requirements.
568
+
569
+ ## Hono
570
+
571
+ Attach an existing runtime to Hono without creating a second abstraction:
572
+
573
+ ```ts
574
+ import { honoJobs } from "flowli/hono";
575
+
576
+ app.use("*", honoJobs(flowli));
577
+ ```
578
+
579
+ ## Next.js
580
+
581
+ Use the same configured runtime in route handlers and server actions without rebuilding anything:
582
+
583
+ ```ts
584
+ // app/api/audit/[entityId]/route.ts
585
+ import { nextAction, nextRoute } from "flowli/next";
586
+ import { flowli } from "@/src/flowli";
587
+
588
+ export const POST = nextRoute(
589
+ flowli,
590
+ async ({ request, flowli, params }) => {
591
+ const body = await request.json();
592
+
593
+ await flowli.createAuditLog.run(
594
+ {
595
+ entityType: body.entityType ?? "record",
596
+ entityId: params?.entityId ?? body.entityId,
597
+ action: body.action ?? "record.updated",
598
+ message: "Audit event received from route handler",
599
+ },
600
+ {
601
+ meta: {
602
+ requestId: request.headers.get("x-request-id") ?? "unknown",
603
+ },
604
+ },
605
+ );
606
+
607
+ return Response.json({ ok: true });
608
+ },
609
+ );
610
+ ```
611
+
612
+ ```ts
613
+ // app/actions/send-notification.ts
614
+ export const sendNotificationAction = nextAction(
615
+ flowli,
616
+ async ({ flowli }, formData: FormData) => {
617
+ await flowli.sendNotificationEmail.enqueue({
618
+ email: String(formData.get("email")),
619
+ subject: String(formData.get("subject")),
620
+ message: String(formData.get("message")),
621
+ });
622
+ },
623
+ );
624
+ ```
625
+
626
+ `flowli/next` stays lightweight:
627
+
628
+ - no second runtime
629
+ - no hidden global registry
630
+ - no direct dependency on Next internals inside your jobs
631
+ - works with an already configured `flowli` instance
632
+
633
+ ## TanStack Start
634
+
635
+ Use the same configured runtime in TanStack Start server routes and server functions:
636
+
637
+ ```ts
638
+ // src/routes/api/audit.$entityId.ts
639
+ import { createFileRoute } from "@tanstack/react-router";
640
+ import { tanstackStartRoute } from "flowli/tanstack-start";
641
+ import { flowli } from "@/src/flowli";
642
+
643
+ export const Route = createFileRoute("/api/audit/$entityId")({
644
+ server: {
645
+ handlers: {
646
+ POST: tanstackStartRoute(
647
+ flowli,
648
+ async ({ request, params, flowli }) => {
649
+ const body = await request.json();
650
+
651
+ await flowli.createAuditLog.run({
652
+ entityType: body.entityType ?? "record",
653
+ entityId: params.entityId,
654
+ action: body.action ?? "record.updated",
655
+ message: "Audit event received from TanStack Start",
656
+ });
657
+
658
+ return Response.json({ ok: true });
659
+ },
660
+ ),
661
+ },
662
+ },
663
+ });
664
+ ```
665
+
666
+ ```ts
667
+ // src/lib/notifications.functions.ts
668
+ import { createServerFn } from "@tanstack/react-start";
669
+ import { tanstackStartServerFn } from "flowli/tanstack-start";
670
+ import { flowli } from "@/src/flowli";
671
+
672
+ export const sendNotification = createServerFn({ method: "POST" }).handler(
673
+ tanstackStartServerFn(
674
+ flowli,
675
+ async ({ flowli, data }: { data: { email: string; subject: string } }) => {
676
+ await flowli.sendNotificationEmail.enqueue({
677
+ email: data.email,
678
+ subject: data.subject,
679
+ message: "Triggered from a TanStack Start server function.",
680
+ });
681
+ },
682
+ ),
683
+ );
684
+ ```
685
+
686
+ `flowli/tanstack-start` stays lightweight:
687
+
688
+ - no second runtime
689
+ - no hidden registry
690
+ - no framework state inside your job definitions
691
+ - works with existing TanStack Start server route and server function patterns
692
+
693
+ ## Install
694
+
695
+ ```bash
696
+ npm install flowli
697
+ ```
698
+
699
+ ```bash
700
+ pnpm add flowli
701
+ ```
702
+
703
+ ```bash
704
+ bun add flowli
705
+ ```
706
+
707
+ Optional schema peers:
708
+
709
+ ```bash
710
+ bun add valibot zod
711
+ ```
712
+
713
+ Optional framework peers:
714
+
715
+ ```bash
716
+ bun add next
717
+ ```
718
+
719
+ ```bash
720
+ bun add @tanstack/react-start
721
+ ```
722
+
723
+ Optional Redis client peers:
724
+
725
+ ```bash
726
+ bun add ioredis
727
+ ```
728
+
729
+ ```bash
730
+ bun add redis
731
+ ```
732
+
733
+ Real Redis integration testing:
734
+
735
+ ```bash
736
+ bun run docker:up
737
+ ```
738
+
739
+ ```bash
740
+ bun run test:redis:docker
741
+ ```
742
+
743
+ ```bash
744
+ bun run docker:down
745
+ ```
746
+
747
+ ## Production
748
+
749
+ Flowli is close to production use, but its async model is explicit and you should deploy it like queue infrastructure, not just a helper library.
750
+
751
+ Recommended production baseline:
752
+
753
+ - run a dedicated `createRunner({ flowli })` process
754
+ - use Redis/Valkey/Dragonfly with persistence configured appropriately for your durability needs
755
+ - keep handlers idempotent because persisted execution is at-least-once
756
+ - tune `leaseMs`, `concurrency`, and `maxJobsPerTick` to match handler duration and load
757
+ - monitor failed jobs and handler error rates
758
+ - validate delayed and scheduled workloads against real infrastructure before rollout
759
+
760
+ Operational notes:
761
+
762
+ - `run()` is in-process and does not depend on Redis
763
+ - `enqueue()`, `delay()`, and `schedule()` depend on a configured driver
764
+ - async work is lease-based and can be retried after failures or lease recovery
765
+ - if a runner crashes after reserving work, expired leases are recovered and jobs are re-queued
766
+ - schedule execution is UTC-based in v1
767
+
768
+ Suggested rollout plan:
769
+
770
+ 1. Start with `run()` in app code and tests.
771
+ 2. Enable `enqueue()` with one runner process.
772
+ 3. Verify handler idempotency and retry behavior.
773
+ 4. Add delayed and scheduled workloads after observing real job throughput and failure patterns.
774
+
775
+ For local real-backend validation, Flowli ships a Redis setup in [docker-compose.yml](/Users/alialnaghmoush/Documents/GitHub/flowli/docker-compose.yml). The default local test URL is `redis://127.0.0.1:6379/0`, which matches `bun run test:redis:docker`.
776
+
777
+ ## What Flowli Optimizes For
778
+
779
+ - small API surface
780
+ - explicit runtime wiring
781
+ - strong autocomplete
782
+ - clean context injection
783
+ - type-safe invocation surfaces
784
+ - tree-shakable subpath exports
785
+ - framework-agnostic core design
786
+
787
+ ## Exports
788
+
789
+ - `flowli`
790
+ - `flowli/ioredis`
791
+ - `flowli/redis`
792
+ - `flowli/bun-redis`
793
+ - `flowli/next`
794
+ - `flowli/tanstack-start`
795
+ - `flowli/hono`
796
+ - `flowli/runner`
797
+
798
+ ## Status
799
+
800
+ Flowli v1 currently includes:
801
+
802
+ - typed `job()` definitions
803
+ - runtime-first `defineJobs()`
804
+ - `run`, `enqueue`, `delay`, and `schedule`
805
+ - pluggable Redis drivers
806
+ - Next.js helpers
807
+ - TanStack Start helpers
808
+ - explicit runner support
809
+ - Hono middleware
810
+ - lease recovery for expired active jobs
811
+ - npm and JSR publish configuration
812
+
813
+ If you want the shortest description:
814
+
815
+ **Flowli is a typed jobs runtime for TypeScript with first-class execution strategies and pluggable Redis drivers.**