@lotiai/composer 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Loti, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,461 @@
1
+ # @lotiai/composer
2
+
3
+ [![CI](https://github.com/lotillc/composer/actions/workflows/ci.yml/badge.svg)](https://github.com/lotillc/composer/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@lotiai/composer.svg)](https://www.npmjs.com/package/@lotiai/composer)
5
+
6
+ A framework for building type-safe, DAG-based workflows with optional Temporal integration.
7
+
8
+ Composer lets you define steps with explicit inputs and outputs, compose them into workflows with automatic dependency resolution and parallel execution, and run them synchronously (in-process) or asynchronously (via [Temporal](https://temporal.io/)).
9
+
10
+ > **Status:** pre-1.0. The public API may change between minor versions until 1.0.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add @lotiai/composer
16
+ # or
17
+ npm install @lotiai/composer
18
+ ```
19
+
20
+ ## Core Concepts
21
+
22
+ ### Bag
23
+
24
+ The **bag** is a typed object that flows through a workflow. Each step declares which fields it reads from the bag (`needs`) and which fields it writes (`provides`). The bag type is defined once and shared across all steps and workflows.
25
+
26
+ ```typescript
27
+ interface MyBag {
28
+ userId: string;
29
+ user: { name: string; email: string };
30
+ greeting: string;
31
+ }
32
+ ```
33
+
34
+ ### Step
35
+
36
+ A **step** is a single unit of work. Steps declare their dependencies and outputs, and the framework validates them at both compile-time and runtime.
37
+
38
+ ```typescript
39
+ import { step } from "@lotiai/composer";
40
+
41
+ // The double function call pattern lets TypeScript infer Needs/Provides from the definition.
42
+ // The first call binds Bag and Context; the second binds the step shape.
43
+ export const fetchUser = step<MyBag, MyContext>()({
44
+ name: "fetchUser",
45
+ needs: ["userId"],
46
+ provides: ["user"],
47
+ run: async (ctx, bag) => {
48
+ const user = await ctx.db.findUser(bag.userId);
49
+ return { user };
50
+ },
51
+ });
52
+
53
+ export const greet = step<MyBag, MyContext>()({
54
+ name: "greet",
55
+ needs: ["user"],
56
+ provides: ["greeting"],
57
+ run: async (_ctx, bag) => {
58
+ return { greeting: `Hello, ${bag.user.name}!` };
59
+ },
60
+ });
61
+ ```
62
+
63
+ **Type safety guarantees:**
64
+
65
+ - Steps can only access fields declared in `needs`
66
+ - Steps must return exactly the fields declared in `provides`
67
+ - Return values must be JSON-serializable (enforced at compile-time and runtime)
68
+ - Excess or missing return properties are caught at both compile-time and runtime
69
+
70
+ ### Workflow
71
+
72
+ A **workflow** is a collection of steps with automatic dependency resolution. The framework builds a DAG, plans parallel execution batches, and validates all dependencies at compile-time.
73
+
74
+ ```typescript
75
+ import { createWorkflow } from "@lotiai/composer";
76
+
77
+ // Workflow without required initial data
78
+ const simpleWorkflow = createWorkflow<MyBag>("simple")
79
+ .build([fetchUser, greet]);
80
+
81
+ // Workflow requiring initial data at runtime
82
+ const greetWorkflow = createWorkflow<MyBag>("greet-user")
83
+ .requires("userId")
84
+ .build([fetchUser, greet]);
85
+
86
+ // Workflow with pre-configured values
87
+ const configuredWorkflow = createWorkflow<MyBag>("configured")
88
+ .configure({ userId: "default-user" })
89
+ .build([fetchUser, greet]);
90
+
91
+ // Configure + require (configure provides defaults, requires demands runtime values)
92
+ const hybridWorkflow = createWorkflow<MyBag>("hybrid")
93
+ .configure({ someDefault: "value" })
94
+ .requires("userId")
95
+ .build([fetchUser, greet]);
96
+ ```
97
+
98
+ **Compile-time dependency validation:** If a step needs a field that no prior step provides and it's not in initial/configured data, you get a clear compile error:
99
+
100
+ ```
101
+ "WORKFLOW ERROR: A step needs user but it is not available. Available fields: [userId]
102
+ -> FIX: Add a step that provides user before the step that needs it"
103
+ ```
104
+
105
+ ### Workflow Composition
106
+
107
+ Workflows can be composed into other workflows using the `use()` helper. Child workflow steps are flattened into the parent's DAG, enabling maximum parallelization while maintaining logical boundaries for observability.
108
+
109
+ ```typescript
110
+ import { createWorkflow, use } from "@lotiai/composer";
111
+
112
+ const authWorkflow = createWorkflow<MyBag>("auth")
113
+ .requires("userId")
114
+ .build([validateUser, checkPermissions]);
115
+
116
+ const mainWorkflow = createWorkflow<MyBag>("main")
117
+ .requires("userId")
118
+ .build([
119
+ fetchData,
120
+ use(authWorkflow), // Steps are flattened into parent DAG
121
+ processRequest,
122
+ ]);
123
+ ```
124
+
125
+ Composed workflows:
126
+ - Flatten steps for maximum parallelization across workflow boundaries
127
+ - Auto-namespace step names (e.g. `auth.validateUser`) to prevent conflicts
128
+ - Preserve observability via SubWorkflow spans in traces
129
+ - Support arbitrary nesting depth
130
+
131
+ ### Execution Model
132
+
133
+ Workflows execute using batch-based parallelism:
134
+
135
+ 1. **Dependency analysis** -- topological sort builds a DAG
136
+ 2. **Batch formation** -- groups steps that can run simultaneously
137
+ 3. **Parallel execution** -- each batch runs with `Promise.allSettled`
138
+ 4. **Progressive merge** -- successful outputs merge into the bag even if sibling steps fail
139
+
140
+ ```
141
+ Given: stepA(needs:[]) -> stepB(needs:["x"]) -> stepC(needs:["x"]) -> stepD(needs:["y","z"])
142
+
143
+ Batch 1: [stepA] -- sequential (no deps)
144
+ Batch 2: [stepB, stepC] -- parallel (both need only "x")
145
+ Batch 3: [stepD] -- sequential (needs "y" and "z")
146
+ ```
147
+
148
+ ## Usage
149
+
150
+ ### Creating a Composer
151
+
152
+ The `createComposer` function creates a configured instance that manages context lifecycle and execution:
153
+
154
+ ```typescript
155
+ import { createComposer } from "@lotiai/composer";
156
+
157
+ // Sync-only composer (no Temporal config)
158
+ const syncComposer = createComposer({
159
+ contextProvider: {
160
+ beforeStep: async (stepName) => ({
161
+ db: await getDbConnection(),
162
+ }),
163
+ afterStep: async (ctx, error) => {
164
+ if (!error) await ctx.db.flush();
165
+ ctx.db.release();
166
+ },
167
+ },
168
+ });
169
+
170
+ // Full composer with Temporal support
171
+ const composer = createComposer({
172
+ contextProvider: myContextProvider,
173
+ temporal: {
174
+ serverAddress: "localhost:7233",
175
+ namespace: "my-namespace",
176
+ },
177
+ // Optional settings:
178
+ logger: myPinoLogger, // defaults to console
179
+ deepFreeze: true, // freeze step outputs for immutability protection
180
+ });
181
+ ```
182
+
183
+ ### Running Workflows
184
+
185
+ ```typescript
186
+ // Synchronous execution (in-process)
187
+ const { bag, error } = await composer.runSyncWorkflow(greetWorkflow, { userId: "u_123" });
188
+
189
+ if (error) {
190
+ console.error("Workflow failed:", error);
191
+ } else {
192
+ console.log(bag.greeting); // "Hello, Alice!"
193
+ }
194
+
195
+ // Asynchronous execution (via Temporal) -- requires temporal config
196
+ const { bag: asyncBag, error: asyncError } = await composer.runAsyncWorkflow(greetWorkflow, { userId: "u_123" });
197
+ ```
198
+
199
+ Workflows never throw. Errors are returned in `error`, and `bag` always contains any data produced by successful steps.
200
+
201
+ ### Checkpoints (Async Only)
202
+
203
+ Checkpoints allow async workflows to return partial results early while continuing execution in the background:
204
+
205
+ ```typescript
206
+ const workflow = createWorkflow<MyBag>("with-checkpoint")
207
+ .requires("userId")
208
+ .build([persistStep, processStep, finalizeStep])
209
+ .checkpoint("persisted", { afterStep: persistStep })
210
+ .checkpoint("processed", { afterStep: processStep, timeout: 60000 });
211
+
212
+ // Get partial result after persistStep completes
213
+ const { bag, error } = await composer.runAsyncWorkflow(workflow, data, {
214
+ awaitCheckpoint: "persisted",
215
+ });
216
+ // bag contains outputs up through the batch containing persistStep
217
+ // The workflow continues running in the background
218
+ ```
219
+
220
+ Checkpoint names are validated at compile-time -- using a name that doesn't exist on the workflow produces a type error.
221
+
222
+ ### Error Handling
223
+
224
+ Attach an error handler to workflows for domain-specific error recovery:
225
+
226
+ ```typescript
227
+ const workflow = createWorkflow<MyBag>("resilient")
228
+ .requires("userId")
229
+ .build([fetchUser, processData])
230
+ .onError(async (ctx, bag, error) => {
231
+ // Check error type
232
+ if (isExpectedError(error)) {
233
+ bag.result = getFallbackResult();
234
+ return undefined; // Error handled, error will be undefined in the result
235
+ }
236
+ return error; // Unknown error, propagate as-is
237
+ });
238
+ ```
239
+
240
+ Error handler return values:
241
+ - `undefined` -- error fully handled, `error` will be `undefined` in the result
242
+ - `Error` -- propagate or transform the error into the result's `error`
243
+ - Throwing -- `error` will be a `WorkflowErrorHandlerFailure` wrapping both the original error and the handler error
244
+
245
+ ### Context Provider
246
+
247
+ The context provider manages per-step resources (e.g. database connections, loggers). It is configured once via `createComposer` and used automatically for every step execution.
248
+
249
+ ```typescript
250
+ import { type StepContextProvider } from "@lotiai/composer";
251
+
252
+ interface MyContext {
253
+ em: SqlEntityManager;
254
+ }
255
+
256
+ const contextProvider: StepContextProvider<MyContext> = {
257
+ beforeStep: async (stepName) => ({
258
+ em: await DatabaseConnection.getInstance().getForkedEntityManager(),
259
+ }),
260
+ afterStep: async (ctx, error) => {
261
+ if (!error) await ctx.em.flush();
262
+ ctx.em.clear();
263
+ },
264
+ };
265
+ ```
266
+
267
+ ### Custom Logger
268
+
269
+ Composer accepts any logger with `info`, `warn`, `error`, and `debug` methods. Compatible with `console`, `pino`, `winston`, and most logging libraries.
270
+
271
+ ```typescript
272
+ import pino from "pino";
273
+
274
+ const composer = createComposer({
275
+ contextProvider,
276
+ logger: {
277
+ info: (msg, meta) => pino().info(meta, msg),
278
+ warn: (msg, meta) => pino().warn(meta, msg),
279
+ error: (msg, meta) => pino().error(meta, msg),
280
+ debug: (msg, meta) => pino().debug(meta, msg),
281
+ },
282
+ });
283
+ ```
284
+
285
+ ## CLI
286
+
287
+ The package includes a CLI for building and managing Temporal workflow/activity bundles. It reads configuration from a `composer.build-config.ts` file in your package root.
288
+
289
+ ```bash
290
+ npx @lotiai/composer build # Build bundles (fast unversioned flow)
291
+ npx @lotiai/composer build --git-hash=abc # Versioned build (vendor bundle + version copies + manifest)
292
+ npx @lotiai/composer validate # Validate config and definitions
293
+ npx @lotiai/composer dev # Watch mode: compile, bundle, restart workers on changes
294
+ npx @lotiai/composer dev --git-hash=abc # Watch mode with versioned bundle flow
295
+ npx @lotiai/composer dev:clean # Remove versioned bundle artifacts
296
+ npx @lotiai/composer dev:up # Start Temporal dev server (PostgreSQL + Temporal + UI)
297
+ npx @lotiai/composer dev:down # Stop Temporal dev server
298
+ npx @lotiai/composer merge --git-hash=abc # Merge versioned bundles into combined bundles for workers
299
+ npx @lotiai/composer upload --git-hash=abc --type=versions # Upload version bundles to S3
300
+ npx @lotiai/composer upload --git-hash=abc --type=merged # Upload merged bundles to S3
301
+ npx @lotiai/composer cleanup # Remove old versioned bundles (retention policy)
302
+ ```
303
+
304
+ ### Output Directory
305
+
306
+ The CLI resolves the output directory automatically from your `tsconfig.json`'s `compilerOptions.outDir`. If `outDir` is not set in the `tsconfig.json` (sibling file to `composer.build-config.ts`), it falls back to `"dist"`. This means bundle output (workflow.js, activity.js, vendor bundles, version copies) is placed alongside your compiled TypeScript output without any extra configuration.
307
+
308
+ ### Build Configuration File
309
+
310
+ Create a `composer.build-config.ts` in your package root:
311
+
312
+ ```typescript
313
+ import { defineBuildConfig } from "@lotiai/composer/build-config";
314
+
315
+ export default defineBuildConfig({
316
+ // Required: directories containing step and workflow definitions
317
+ stepsDir: "src/steps",
318
+ workflowsDir: "src/workflows",
319
+
320
+ // Optional: S3 config for bundle storage (required for upload/merge/cleanup commands)
321
+ s3: {
322
+ bucketName: "composer-bundles",
323
+ // Optional: provide a custom S3Client (e.g. for LocalStack or custom credentials)
324
+ // customClient: new S3Client({ region: "us-east-1", endpoint: "http://localhost:4566", forcePathStyle: true }),
325
+ },
326
+
327
+ // Optional: override worker profile defaults
328
+ workerProfiles: {
329
+ standard: {
330
+ cpu: 1024,
331
+ memory: 4096,
332
+ maxConcurrentActivities: 20,
333
+ },
334
+ },
335
+
336
+ // Optional: build settings
337
+ build: {
338
+ minify: true, // default: true
339
+ },
340
+
341
+ // Optional: dev command settings
342
+ dev: {
343
+ startAllWorkersJsScript: "dist/scripts/start-all-workers.js",
344
+ // watchPatterns: ["src/**/*.ts"], // default: ["src/**/*.ts"]
345
+ },
346
+ });
347
+ ```
348
+
349
+ Note: Temporal connection config (`serverAddress`, `namespace`) is passed to `createComposer()` at runtime, not in `composer.build-config.ts`. The config file is for build and tooling concerns only.
350
+
351
+ ### Monorepo Consumers and Vendor Bundles
352
+
353
+ When using versioned builds (`--git-hash`), the CLI creates a **vendor bundle** containing shared npm dependencies. In a monorepo, you typically want to exclude your own workspace packages from the vendor bundle (they contain business logic, not shared vendor deps) while still including their transitive npm dependencies.
354
+
355
+ The CLI auto-detects which packages to exclude by looking for dependencies declared with pnpm's `workspace:` protocol in your `package.json`:
356
+
357
+ ```json
358
+ {
359
+ "dependencies": {
360
+ "@myorg/shared-lib": "workspace:*",
361
+ "@myorg/db": "workspace:*",
362
+ "es-toolkit": "^1.43.0",
363
+ "uuid": "^11.1.0"
364
+ }
365
+ }
366
+ ```
367
+
368
+ In this example, `@myorg/shared-lib` and `@myorg/db` are excluded from the vendor bundle, but their transitive npm dependencies are still discovered and included. `es-toolkit` and `uuid` are included directly. Dependencies using pnpm's `catalog:` protocol are treated as normal npm dependencies and included in the vendor bundle.
369
+
370
+ ## Starting Temporal Workers
371
+
372
+ For production or local development with Temporal, start workers using the helper functions:
373
+
374
+ ```typescript
375
+ import { startAllWorkers } from "@lotiai/composer";
376
+ import { composer } from "./my-app-composer";
377
+
378
+ // Start both workflow and activity workers in one process (local dev)
379
+ await startAllWorkers(composer, {
380
+ workflow: {
381
+ taskQueues: ["workflow-tasks"],
382
+ maxConcurrentWorkflowTaskExecutions: 100,
383
+ },
384
+ activity: {
385
+ taskQueues: ["standard-tasks"],
386
+ maxConcurrentActivityTaskExecutions: 15,
387
+ },
388
+ });
389
+ ```
390
+
391
+ For production, run workflow and activity workers in separate processes for independent scaling:
392
+
393
+ ```typescript
394
+ import { startWorkflowWorker, startActivityWorker } from "@lotiai/composer";
395
+
396
+ // In workflow worker process:
397
+ await startWorkflowWorker(composer, {
398
+ taskQueues: ["workflow-tasks"],
399
+ maxConcurrentWorkflowTaskExecutions: 100,
400
+ });
401
+
402
+ // In activity worker process:
403
+ await startActivityWorker(composer, {
404
+ taskQueues: ["standard-tasks"],
405
+ maxConcurrentActivityTaskExecutions: 15,
406
+ });
407
+ ```
408
+
409
+ ## Worker Profiles
410
+
411
+ Steps can declare a `workerProfile` to control which Temporal worker pool they run on. Currently, there is a single `"standard"` profile:
412
+
413
+ | Profile | Task Queue | CPU | Memory | Concurrent Activities |
414
+ |------------|------------------|---------|--------|-----------------------|
415
+ | `standard` | standard-tasks | 0.5 vCPU| 2 GB | 15 |
416
+
417
+ ```typescript
418
+ export const heavyStep = step<MyBag, MyContext>()({
419
+ name: "heavyComputation",
420
+ needs: ["input"],
421
+ provides: ["output"],
422
+ workerProfile: "standard", // default if omitted
423
+ run: async (ctx, bag) => { /* ... */ },
424
+ });
425
+ ```
426
+
427
+ Defaults can be overridden per-deployment via `workerProfiles` in `composer.build-config.ts`.
428
+
429
+ ## Exports
430
+
431
+ ### Main entry point (`@lotiai/composer`)
432
+
433
+ | Export | Description |
434
+ |--------|-------------|
435
+ | `createComposer` | Create a configured Composer instance |
436
+ | `createWorkflow` | Create a workflow with compile-time dependency validation |
437
+ | `step` | Factory for creating type-safe steps |
438
+ | `use` | Compose a child workflow into a parent |
439
+ | `defineBuildConfig` | Type-safe build config helper (also available from `@lotiai/composer/build-config`) |
440
+ | `startAllWorkers` | Start both Temporal workers in one process |
441
+ | `startWorkflowWorker` | Start a Temporal workflow worker |
442
+ | `startActivityWorker` | Start a Temporal activity worker |
443
+ | `getAllTaskQueues` | Get all configured task queue names |
444
+ | `getTaskQueueForProfile` | Get the task queue for a worker profile |
445
+ | `WorkflowStepError` | Error class for step failures |
446
+ | `WorkflowBatchError` | Error class for batch failures |
447
+ | `WorkflowErrorHandlerFailure` | Error class for error handler failures |
448
+
449
+ ### Build config entry point (`@lotiai/composer/build-config`)
450
+
451
+ Build configuration schema, loader, and `defineBuildConfig` helper for `composer.build-config.ts` files.
452
+
453
+ ## Deep Freeze (Immutability Protection)
454
+
455
+ Enable `deepFreeze: true` in `createComposer` to freeze all step outputs before merging them into the bag. This catches mutation bugs that can cause non-deterministic behavior:
456
+
457
+ - Parallel batch mutations (two steps mutating shared references)
458
+ - Downstream mutations (a step modifying data from a previous step)
459
+ - Post-return mutations (keeping a reference and mutating it later)
460
+
461
+ Recommended for development and testing. The overhead (~0.5--10ms per output) is negligible for I/O-bound workflows.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require("../dist/cli/index.js");
@@ -0,0 +1,144 @@
1
+ # Temporal Server for local development
2
+ #
3
+ # temporalio/auto-setup was removed after 1.24.x. Local dev now uses
4
+ # temporalio/admin-tools to initialise the schema, then starts the bare
5
+ # temporalio/server image -- the same split used in production (ECS).
6
+ #
7
+ # Usage (from any directory):
8
+ # docker compose -f <path-to>/docker-compose.temporal.yml up -d
9
+
10
+ services:
11
+ postgresql:
12
+ image: postgres:14
13
+ container_name: temporal-postgresql
14
+ environment:
15
+ POSTGRES_USER: temporal
16
+ POSTGRES_PASSWORD: temporal
17
+ POSTGRES_DB: temporal
18
+ ports:
19
+ # TEMPORAL_DB_PORT controls the host-side port only; containers always
20
+ # talk to postgresql:5432 over the Docker network.
21
+ - "${TEMPORAL_DB_PORT:-5432}:5432"
22
+ volumes:
23
+ - postgres-data:/var/lib/postgresql/data
24
+ networks:
25
+ - temporal-network
26
+ healthcheck:
27
+ test: ["CMD-SHELL", "pg_isready -U temporal"]
28
+ interval: 5s
29
+ timeout: 5s
30
+ retries: 5
31
+
32
+ # One-shot init container: creates the visibility database and runs
33
+ # schema setup for both the main and visibility databases, then exits.
34
+ temporal-schema-init:
35
+ image: temporalio/admin-tools:1.30.1
36
+ container_name: temporal-schema-init
37
+ depends_on:
38
+ postgresql:
39
+ condition: service_healthy
40
+ environment:
41
+ # temporal-sql-tool reads these env vars natively
42
+ - SQL_PLUGIN=postgres12
43
+ - SQL_HOST=postgresql
44
+ - SQL_PORT=5432
45
+ - SQL_USER=temporal
46
+ - SQL_PASSWORD=temporal
47
+ entrypoint: ["sh", "-c"]
48
+ command:
49
+ - |
50
+ set -eu
51
+
52
+ # Create the visibility database (the main 'temporal' db is created by PostgreSQL's POSTGRES_DB)
53
+ temporal-sql-tool --database temporal_visibility create-database || true
54
+
55
+ # Set up or update the main temporal database schema
56
+ temporal-sql-tool --database temporal setup-schema -v 0.0
57
+ temporal-sql-tool --database temporal \
58
+ update-schema -d /etc/temporal/schema/postgresql/v12/temporal/versioned
59
+
60
+ # Set up or update the visibility database schema
61
+ temporal-sql-tool --database temporal_visibility setup-schema -v 0.0
62
+ temporal-sql-tool --database temporal_visibility \
63
+ update-schema -d /etc/temporal/schema/postgresql/v12/visibility/versioned
64
+
65
+ echo "Temporal schema initialisation complete."
66
+ networks:
67
+ - temporal-network
68
+
69
+ temporal:
70
+ image: temporalio/server:1.30.1
71
+ container_name: temporal-dev
72
+ depends_on:
73
+ postgresql:
74
+ condition: service_healthy
75
+ temporal-schema-init:
76
+ condition: service_completed_successfully
77
+ ports:
78
+ - "7233:7233" # Temporal Server (gRPC)
79
+ environment:
80
+ - DB=postgres12
81
+ - DB_PORT=5432
82
+ - POSTGRES_USER=temporal
83
+ - POSTGRES_PWD=temporal
84
+ - POSTGRES_SEEDS=postgresql
85
+ # The bare server image does not ship a dynamic config file but requires
86
+ # one to exist. Run as root so we can create it, then exec the entrypoint.
87
+ user: root
88
+ entrypoint: ["sh", "-c"]
89
+ command:
90
+ - |
91
+ mkdir -p /etc/temporal/config/dynamicconfig
92
+ echo '{}' > /etc/temporal/config/dynamicconfig/docker.yaml
93
+ exec /etc/temporal/entrypoint.sh
94
+ networks:
95
+ - temporal-network
96
+
97
+ temporal-namespace-init:
98
+ image: temporalio/admin-tools:1.30.1
99
+ container_name: temporal-namespace-init
100
+ depends_on:
101
+ - temporal
102
+ entrypoint: ["sh", "-c"]
103
+ # Uses the `temporal` CLI; `tctl` was removed from temporalio/admin-tools and would block forever in the health-wait loop.
104
+ command:
105
+ - |
106
+ set -eu
107
+
108
+ until temporal operator cluster health --address temporal:7233 >/dev/null 2>&1; do
109
+ sleep 2
110
+ done
111
+
112
+ # Keep local namespaces aligned with the canonical shared constants.
113
+ for namespace in interchange outpost josu; do
114
+ if temporal operator namespace describe -n "$namespace" --address temporal:7233 >/dev/null 2>&1; then
115
+ echo "Temporal namespace '$namespace' already exists."
116
+ else
117
+ temporal operator namespace create -n "$namespace" --retention 90d --address temporal:7233
118
+ echo "Temporal namespace '$namespace' created."
119
+ fi
120
+ done
121
+ networks:
122
+ - temporal-network
123
+
124
+ temporal-ui:
125
+ image: temporalio/ui:2.47.3
126
+ container_name: temporal-ui
127
+ depends_on:
128
+ - temporal
129
+ - temporal-namespace-init
130
+ ports:
131
+ - "8080:8080" # Temporal Web UI
132
+ environment:
133
+ - TEMPORAL_ADDRESS=temporal:7233
134
+ - TEMPORAL_CORS_ORIGINS=http://localhost:3000
135
+ networks:
136
+ - temporal-network
137
+
138
+ volumes:
139
+ postgres-data:
140
+ driver: local
141
+
142
+ networks:
143
+ temporal-network:
144
+ driver: bridge
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@lotiai/composer",
3
+ "version": "0.2.0",
4
+ "description": "A framework for building type-safe, DAG-based workflows with optional Temporal integration.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "temporal",
8
+ "workflow",
9
+ "dag",
10
+ "typescript",
11
+ "type-safe",
12
+ "orchestration"
13
+ ],
14
+ "homepage": "https://github.com/lotillc/composer#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/lotillc/composer/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/lotillc/composer.git"
21
+ },
22
+ "main": "dist/index.js",
23
+ "types": "dist/index.d.ts",
24
+ "bin": {
25
+ "composer": "bin/composer.js"
26
+ },
27
+ "files": [
28
+ "bin",
29
+ "dist",
30
+ "!dist/**/*.vitest.*",
31
+ "!dist/**/__tests__",
32
+ "docker-compose.temporal.yml"
33
+ ],
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ },
39
+ "./build-config": {
40
+ "types": "./dist/build-config/index.d.ts",
41
+ "default": "./dist/build-config/index.js"
42
+ },
43
+ "./temporal-naming": {
44
+ "types": "./dist/temporal-naming.d.ts",
45
+ "default": "./dist/temporal-naming.js"
46
+ },
47
+ "./schedule-sync": {
48
+ "types": "./dist/schedule-sync.d.ts",
49
+ "default": "./dist/schedule-sync.js"
50
+ }
51
+ },
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org"
55
+ },
56
+ "scripts": {
57
+ "compile": "tsc --build",
58
+ "clean": "rm -rf dist *.tsbuildinfo",
59
+ "test": "pnpm test:vitest",
60
+ "test:vitest": "vitest run --config vitest.config.mts --typecheck",
61
+ "test:vitest:watch": "vitest --config vitest.config.mts",
62
+ "test:watch": "pnpm test:vitest:watch",
63
+ "test:coverage": "vitest run --config vitest.config.mts --coverage",
64
+ "dev:temporal:up": "docker-compose -f docker-compose.temporal.yml up -d",
65
+ "dev:temporal:down": "docker-compose -f docker-compose.temporal.yml down",
66
+ "dev:temporal:logs": "docker-compose -f docker-compose.temporal.yml logs -f",
67
+ "cli": "node dist/cli/index.js",
68
+ "cli:dev": "node dist/cli/index.js dev"
69
+ },
70
+ "dependencies": {
71
+ "@aws-sdk/client-cloudwatch": "^3.946.0",
72
+ "@aws-sdk/client-lambda": "^3.658.1",
73
+ "@lifeomic/attempt": "^3.0.0",
74
+ "@opentelemetry/api": "^1.9.0",
75
+ "@temporalio/activity": "^1.15.0",
76
+ "@temporalio/client": "^1.15.0",
77
+ "@temporalio/common": "^1.15.0",
78
+ "@temporalio/worker": "^1.15.0",
79
+ "@temporalio/workflow": "^1.15.0",
80
+ "chokidar": "^3.5.3",
81
+ "es-toolkit": "^1.43.0",
82
+ "glob": "^13.0.0",
83
+ "jiti": "^2.6.1",
84
+ "long": "^5.0.0",
85
+ "typescript": "^5.9.2",
86
+ "uuid": "^11.1.0",
87
+ "yargs": "^17.7.2",
88
+ "zod": "^4.1.13"
89
+ },
90
+ "devDependencies": {
91
+ "@types/yargs": "^17.0.24",
92
+ "@vitest/coverage-v8": "^4.1.4",
93
+ "vitest": "^4.1.4"
94
+ },
95
+ "engines": {
96
+ "node": ">=22"
97
+ },
98
+ "packageManager": "pnpm@10.28.2"
99
+ }