@synkro/core 0.1.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/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # @synkro/core
2
+
3
+ Lightweight workflow and state machine orchestrator powered by Redis. Define event-driven workflows via configuration or code.
4
+
5
+ ## Features
6
+
7
+ - **Standalone Events** — Simple pub/sub event handlers with Redis
8
+ - **Sequential Workflows** — Multi-step workflows that execute in order, with state tracked in Redis
9
+ - **Simple API** — Single `Synkro` class with minimal configuration
10
+ - **TypeScript** — Full type support out of the box
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @synkro/core
16
+ ```
17
+
18
+ > Requires a running Redis instance.
19
+
20
+ ## Quick Start
21
+
22
+ ```ts
23
+ import { Synkro } from "@synkro/core";
24
+
25
+ const synkro = await Synkro.start({
26
+ redisUrl: "redis://localhost:6379",
27
+ events: [
28
+ {
29
+ type: "UserSignedUp",
30
+ handler: async (ctx) => {
31
+ console.log("New user:", ctx.payload);
32
+ },
33
+ },
34
+ ],
35
+ });
36
+
37
+ await synkro.publish("UserSignedUp", { email: "user@example.com" });
38
+ ```
39
+
40
+ ## Workflows
41
+
42
+ Define multi-step sequential workflows. Each step runs after the previous one completes, with state persisted in Redis.
43
+
44
+ ```ts
45
+ const synkro = await Synkro.start({
46
+ redisUrl: "redis://localhost:6379",
47
+ workflows: [
48
+ {
49
+ name: "ProcessOrder",
50
+ steps: [
51
+ {
52
+ type: "ValidateStock",
53
+ handler: async (ctx) => {
54
+ console.log("Checking stock for order:", ctx.requestId);
55
+ },
56
+ },
57
+ {
58
+ type: "ProcessPayment",
59
+ handler: async (ctx) => {
60
+ console.log("Processing payment...");
61
+ },
62
+ },
63
+ {
64
+ type: "SendConfirmation",
65
+ handler: async (ctx) => {
66
+ console.log("Order confirmed!");
67
+ },
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ });
73
+
74
+ // Triggers all 3 steps in sequence
75
+ await synkro.publish("ProcessOrder", { orderId: "abc-123", amount: 49.99 });
76
+ ```
77
+
78
+ ## API
79
+
80
+ ### `Synkro.start(options): Promise<Synkro>`
81
+
82
+ Creates and returns a running instance.
83
+
84
+ ```ts
85
+ type SynkroOptions = {
86
+ redisUrl: string;
87
+ events?: SynkroEvent[];
88
+ workflows?: SynkroWorkflow[];
89
+ };
90
+ ```
91
+
92
+ ### `synkro.on(eventType, handler): void`
93
+
94
+ Registers an event handler at runtime.
95
+
96
+ ```ts
97
+ synkro.on("StockUpdate", async (ctx) => {
98
+ console.log(ctx.requestId, ctx.payload);
99
+ });
100
+ ```
101
+
102
+ ### `synkro.publish(event, payload?, requestId?): Promise<string>`
103
+
104
+ Publishes an event or starts a workflow. Returns a `requestId` for correlation. A UUID is generated by default, but you can provide your own ID.
105
+
106
+ ```ts
107
+ // Auto-generated UUID
108
+ const id = await synkro.publish("UserSignedUp", { email: "user@example.com" });
109
+
110
+ // Custom request ID
111
+ const id = await synkro.publish("UserSignedUp", { email: "user@example.com" }, "my-custom-id");
112
+ ```
113
+
114
+ ### `synkro.stop(): Promise<void>`
115
+
116
+ Disconnects all Redis clients.
117
+
118
+ ## Types
119
+
120
+ ```ts
121
+ type SynkroEvent = {
122
+ type: string;
123
+ handler: HandlerFunction;
124
+ };
125
+
126
+ type SynkroWorkflow = {
127
+ name: string;
128
+ steps: SynkroWorkflowStep[];
129
+ };
130
+
131
+ type SynkroWorkflowStep = {
132
+ type: string;
133
+ handler: HandlerFunction;
134
+ };
135
+
136
+ type HandlerCtx = {
137
+ requestId: string;
138
+ payload: unknown;
139
+ };
140
+
141
+ type HandlerFunction = (ctx: HandlerCtx) => void | Promise<void>;
142
+ ```
143
+
144
+ ## License
145
+
146
+ ISC
@@ -0,0 +1,10 @@
1
+ import type { RedisManager } from "./redis.js";
2
+ import type { HandlerFunction } from "./types.js";
3
+ export declare class HandlerRegistry {
4
+ private redis;
5
+ private handlers;
6
+ constructor(redis: RedisManager);
7
+ register(eventType: string, handlerFn: HandlerFunction): void;
8
+ private handleMessage;
9
+ }
10
+ //# sourceMappingURL=handler-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-registry.d.ts","sourceRoot":"","sources":["../package/handler-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9D,qBAAa,eAAe;IAGd,OAAO,CAAC,KAAK;IAFzB,OAAO,CAAC,QAAQ,CAAsC;gBAElC,KAAK,EAAE,YAAY;IAEvC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,GAAG,IAAI;YAQ/C,aAAa;CAkB5B"}
@@ -0,0 +1,23 @@
1
+ export class HandlerRegistry {
2
+ redis;
3
+ handlers = new Map();
4
+ constructor(redis) {
5
+ this.redis = redis;
6
+ }
7
+ register(eventType, handlerFn) {
8
+ this.handlers.set(eventType, handlerFn);
9
+ this.redis.subscribeToChannel(eventType, (message) => {
10
+ this.handleMessage(eventType, message);
11
+ });
12
+ }
13
+ async handleMessage(eventType, message) {
14
+ const handler = this.handlers.get(eventType);
15
+ if (!handler) {
16
+ return;
17
+ }
18
+ const event = JSON.parse(message);
19
+ await handler({ requestId: event.requestId, payload: event.payload });
20
+ this.redis.publishMessage(`event:${eventType}:completed`, JSON.stringify({ requestId: event.requestId, payload: event.payload }));
21
+ }
22
+ }
23
+ //# sourceMappingURL=handler-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-registry.js","sourceRoot":"","sources":["../package/handler-registry.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,eAAe;IAGN;IAFZ,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;IAEtD,YAAoB,KAAmB;QAAnB,UAAK,GAAL,KAAK,CAAc;IAAG,CAAC;IAE3C,QAAQ,CAAC,SAAiB,EAAE,SAA0B;QACpD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAExC,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,SAAS,EAAE,CAAC,OAAe,EAAE,EAAE;YAC3D,IAAI,CAAC,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,aAAa,CACzB,SAAiB,EACjB,OAAe;QAEf,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAe,CAAC;QAEhD,MAAM,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAEtE,IAAI,CAAC,KAAK,CAAC,cAAc,CACvB,SAAS,SAAS,YAAY,EAC9B,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CACvE,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,13 @@
1
+ import type { HandlerFunction, SynkroOptions } from "./types.js";
2
+ export declare class Synkro {
3
+ private redis;
4
+ private handlerRegistry;
5
+ private workflowRegistry;
6
+ private constructor();
7
+ static start(options: SynkroOptions): Promise<Synkro>;
8
+ on(eventType: string, handler: HandlerFunction): void;
9
+ publish(event: string, payload?: unknown, requestId?: string): Promise<string>;
10
+ stop(): Promise<void>;
11
+ }
12
+ export type { HandlerCtx, HandlerFunction, SynkroEvent, SynkroOptions, SynkroWorkflow, SynkroWorkflowStep, } from "./types.js";
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../package/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEjE,qBAAa,MAAM;IACjB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,gBAAgB,CAAmB;IAE3C,OAAO;WAMM,KAAK,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAkB3D,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAI/C,OAAO,CACX,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,OAAO,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC;IAeZ,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAG5B;AAED,YAAY,EACV,UAAU,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { HandlerRegistry } from "./handler-registry.js";
3
+ import { setDebug } from "./logger.js";
4
+ import { RedisManager } from "./redis.js";
5
+ import { WorkflowRegistry } from "./workflow-registry.js";
6
+ export class Synkro {
7
+ redis;
8
+ handlerRegistry;
9
+ workflowRegistry;
10
+ constructor(redis) {
11
+ this.redis = redis;
12
+ this.handlerRegistry = new HandlerRegistry(redis);
13
+ this.workflowRegistry = new WorkflowRegistry(redis, this.handlerRegistry);
14
+ }
15
+ static async start(options) {
16
+ setDebug(options.debug ?? false);
17
+ const redis = new RedisManager(options.redisUrl);
18
+ const instance = new Synkro(redis);
19
+ if (options.events) {
20
+ for (const event of options.events) {
21
+ instance.on(event.type, event.handler);
22
+ }
23
+ }
24
+ if (options.workflows) {
25
+ instance.workflowRegistry.registerWorkflows(options.workflows);
26
+ }
27
+ return instance;
28
+ }
29
+ on(eventType, handler) {
30
+ this.handlerRegistry.register(eventType, handler);
31
+ }
32
+ async publish(event, payload, requestId) {
33
+ requestId = requestId ?? randomUUID();
34
+ if (this.workflowRegistry.hasWorkflow(event)) {
35
+ await this.workflowRegistry.startWorkflow(event, requestId, payload);
36
+ return requestId;
37
+ }
38
+ this.redis.publishMessage(event, JSON.stringify({ requestId, payload }));
39
+ return requestId;
40
+ }
41
+ async stop() {
42
+ await this.redis.disconnect();
43
+ }
44
+ }
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../package/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAI1D,MAAM,OAAO,MAAM;IACT,KAAK,CAAe;IACpB,eAAe,CAAkB;IACjC,gBAAgB,CAAmB;IAE3C,YAAoB,KAAmB;QACrC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,eAAe,GAAG,IAAI,eAAe,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,CAAC,gBAAgB,GAAG,IAAI,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAsB;QACvC,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;QAEnC,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;gBACnC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YACzC,CAAC;QACH,CAAC;QAED,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACtB,QAAQ,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACjE,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,EAAE,CAAC,SAAiB,EAAE,OAAwB;QAC5C,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,OAAO,CACX,KAAa,EACb,OAAiB,EACjB,SAAkB;QAElB,SAAS,GAAG,SAAS,IAAI,UAAU,EAAE,CAAC;QAEtC,IAAI,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7C,MAAM,IAAI,CAAC,gBAAgB,CAAC,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;YACrE,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,cAAc,CACvB,KAAK,EACL,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CACvC,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;IAChC,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ export declare function setDebug(enabled: boolean): void;
2
+ export declare const logger: {
3
+ debug(...args: unknown[]): void;
4
+ warn(...args: unknown[]): void;
5
+ error(...args: unknown[]): void;
6
+ };
7
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../package/logger.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE/C;AAED,eAAO,MAAM,MAAM;mBACF,OAAO,EAAE,GAAG,IAAI;kBAKjB,OAAO,EAAE,GAAG,IAAI;mBAKf,OAAO,EAAE,GAAG,IAAI;CAGhC,CAAC"}
package/dist/logger.js ADDED
@@ -0,0 +1,20 @@
1
+ let debugEnabled = false;
2
+ export function setDebug(enabled) {
3
+ debugEnabled = enabled;
4
+ }
5
+ export const logger = {
6
+ debug(...args) {
7
+ if (debugEnabled) {
8
+ console.log(...args);
9
+ }
10
+ },
11
+ warn(...args) {
12
+ if (debugEnabled) {
13
+ console.warn(...args);
14
+ }
15
+ },
16
+ error(...args) {
17
+ console.error(...args);
18
+ },
19
+ };
20
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../package/logger.ts"],"names":[],"mappings":"AAAA,IAAI,YAAY,GAAG,KAAK,CAAC;AAEzB,MAAM,UAAU,QAAQ,CAAC,OAAgB;IACvC,YAAY,GAAG,OAAO,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,CAAC,GAAG,IAAe;QACtB,IAAI,YAAY,EAAE,CAAC;YACjB,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IACD,IAAI,CAAC,GAAG,IAAe;QACrB,IAAI,YAAY,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IACD,KAAK,CAAC,GAAG,IAAe;QACtB,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IACzB,CAAC;CACF,CAAC"}
@@ -0,0 +1,13 @@
1
+ export declare class RedisManager {
2
+ private publisher;
3
+ private subscriber;
4
+ private cacheClient;
5
+ constructor(redisUrl: string);
6
+ publishMessage(channel: string, message: string): void;
7
+ subscribeToChannel(channel: string, callback: (message: string) => void): void;
8
+ getCache(key: string): Promise<string | null>;
9
+ setCache(key: string, value: string, ttlSeconds?: number): Promise<void>;
10
+ deleteCache(key: string): Promise<void>;
11
+ disconnect(): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=redis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../package/redis.ts"],"names":[],"mappings":"AAIA,qBAAa,YAAY;IACvB,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,WAAW,CAAQ;gBAEf,QAAQ,EAAE,MAAM;IAM5B,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAItD,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAClC,IAAI;IAmBD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAI7C,QAAQ,CACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IAQV,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAKlC"}
package/dist/redis.js ADDED
@@ -0,0 +1,50 @@
1
+ import { Redis } from "ioredis";
2
+ import { logger } from "./logger.js";
3
+ export class RedisManager {
4
+ publisher;
5
+ subscriber;
6
+ cacheClient;
7
+ constructor(redisUrl) {
8
+ this.publisher = new Redis(redisUrl);
9
+ this.subscriber = new Redis(redisUrl);
10
+ this.cacheClient = new Redis(redisUrl);
11
+ }
12
+ publishMessage(channel, message) {
13
+ this.publisher.publish(channel, message);
14
+ }
15
+ subscribeToChannel(channel, callback) {
16
+ this.subscriber
17
+ .subscribe(channel)
18
+ .then((count) => {
19
+ logger.debug(`Subscribed to ${count} channel(s). Listening on "${channel}".`);
20
+ })
21
+ .catch((err) => {
22
+ logger.error(`Failed to subscribe to channel ${channel}:`, err);
23
+ });
24
+ this.subscriber.on("message", (chan, message) => {
25
+ if (chan === channel) {
26
+ callback(message);
27
+ }
28
+ });
29
+ }
30
+ async getCache(key) {
31
+ return await this.cacheClient.get(key);
32
+ }
33
+ async setCache(key, value, ttlSeconds) {
34
+ if (ttlSeconds) {
35
+ await this.cacheClient.set(key, value, "EX", ttlSeconds);
36
+ }
37
+ else {
38
+ await this.cacheClient.set(key, value);
39
+ }
40
+ }
41
+ async deleteCache(key) {
42
+ await this.cacheClient.del(key);
43
+ }
44
+ async disconnect() {
45
+ await this.publisher.quit();
46
+ await this.subscriber.quit();
47
+ await this.cacheClient.quit();
48
+ }
49
+ }
50
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.js","sourceRoot":"","sources":["../package/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,OAAO,YAAY;IACf,SAAS,CAAQ;IACjB,UAAU,CAAQ;IAClB,WAAW,CAAQ;IAE3B,YAAY,QAAgB;QAC1B,IAAI,CAAC,SAAS,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,UAAU,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,CAAC,WAAW,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,cAAc,CAAC,OAAe,EAAE,OAAe;QAC7C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,kBAAkB,CAChB,OAAe,EACf,QAAmC;QAEnC,IAAI,CAAC,UAAU;aACZ,SAAS,CAAC,OAAO,CAAC;aAClB,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;YACd,MAAM,CAAC,KAAK,CACV,iBAAiB,KAAK,8BAA8B,OAAO,IAAI,CAChE,CAAC;QACJ,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACtB,MAAM,CAAC,KAAK,CAAC,kCAAkC,OAAO,GAAG,EAAE,GAAG,CAAC,CAAC;QAClE,CAAC,CAAC,CAAC;QAEL,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAY,EAAE,OAAe,EAAE,EAAE;YAC9D,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;gBACrB,QAAQ,CAAC,OAAO,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAW;QACxB,OAAO,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,QAAQ,CACZ,GAAW,EACX,KAAa,EACb,UAAmB;QAEnB,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;QAC3D,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,GAAW;QAC3B,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QAC7B,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;IAChC,CAAC;CACF"}
@@ -0,0 +1,24 @@
1
+ export type SynkroEvent = {
2
+ type: string;
3
+ handler: HandlerFunction;
4
+ };
5
+ export type SynkroWorkflowStep = {
6
+ type: string;
7
+ handler: HandlerFunction;
8
+ };
9
+ export type SynkroWorkflow = {
10
+ name: string;
11
+ steps: SynkroWorkflowStep[];
12
+ };
13
+ export type SynkroOptions = {
14
+ redisUrl: string;
15
+ debug?: boolean;
16
+ events?: SynkroEvent[];
17
+ workflows?: SynkroWorkflow[];
18
+ };
19
+ export type HandlerCtx = {
20
+ requestId: string;
21
+ payload: unknown;
22
+ };
23
+ export type HandlerFunction = (ctx: HandlerCtx) => void | Promise<void>;
24
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../package/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,eAAe,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,eAAe,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../package/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,20 @@
1
+ import type { HandlerRegistry } from "./handler-registry.js";
2
+ import type { RedisManager } from "./redis.js";
3
+ import type { SynkroWorkflow } from "./types.js";
4
+ export declare class WorkflowRegistry {
5
+ private redis;
6
+ private handlerRegistry;
7
+ private workflows;
8
+ private eventToWorkflows;
9
+ constructor(redis: RedisManager, handlerRegistry: HandlerRegistry);
10
+ registerWorkflows(workflows: SynkroWorkflow[]): void;
11
+ hasWorkflow(name: string): boolean;
12
+ startWorkflow(workflowName: string, requestId: string, payload: unknown): Promise<void>;
13
+ private subscribeToWorkflowEvents;
14
+ private handleStepCompletion;
15
+ private stepChannel;
16
+ private stateKey;
17
+ private saveState;
18
+ private getState;
19
+ }
20
+ //# sourceMappingURL=workflow-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-registry.d.ts","sourceRoot":"","sources":["../package/workflow-registry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAQjD,qBAAa,gBAAgB;IAQzB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,eAAe;IARzB,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,gBAAgB,CAGpB;gBAGM,KAAK,EAAE,YAAY,EACnB,eAAe,EAAE,eAAe;IAG1C,iBAAiB,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI;IAwBpD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI5B,aAAa,CACjB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,IAAI,CAAC;IA2BhB,OAAO,CAAC,yBAAyB;YAYnB,oBAAoB;IAiDlC,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,QAAQ;YAIF,SAAS;YAWT,QAAQ;CAKvB"}
@@ -0,0 +1,98 @@
1
+ import { logger } from "./logger.js";
2
+ export class WorkflowRegistry {
3
+ redis;
4
+ handlerRegistry;
5
+ workflows = new Map();
6
+ eventToWorkflows = new Map();
7
+ constructor(redis, handlerRegistry) {
8
+ this.redis = redis;
9
+ this.handlerRegistry = handlerRegistry;
10
+ }
11
+ registerWorkflows(workflows) {
12
+ for (const workflow of workflows) {
13
+ this.workflows.set(workflow.name, workflow);
14
+ for (let i = 0; i < workflow.steps.length; i++) {
15
+ const step = workflow.steps[i];
16
+ const key = step.type;
17
+ if (!this.eventToWorkflows.has(key)) {
18
+ this.eventToWorkflows.set(key, []);
19
+ }
20
+ this.eventToWorkflows.get(key).push({ workflow, stepIndex: i });
21
+ const channel = this.stepChannel(workflow.name, step.type);
22
+ this.handlerRegistry.register(channel, step.handler);
23
+ }
24
+ this.subscribeToWorkflowEvents(workflow);
25
+ logger.debug(`[WorkflowRegistry] - Workflow "${workflow.name}" registered with ${workflow.steps.length} steps`);
26
+ }
27
+ }
28
+ hasWorkflow(name) {
29
+ return this.workflows.has(name);
30
+ }
31
+ async startWorkflow(workflowName, requestId, payload) {
32
+ const workflow = this.workflows.get(workflowName);
33
+ if (!workflow) {
34
+ throw new Error(`[WorkflowRegistry] - Workflow "${workflowName}" not found`);
35
+ }
36
+ const state = {
37
+ workflowName,
38
+ currentStep: 0,
39
+ status: "running",
40
+ };
41
+ await this.saveState(requestId, state);
42
+ const firstStep = workflow.steps[0];
43
+ const channel = this.stepChannel(workflowName, firstStep.type);
44
+ logger.debug(`[WorkflowRegistry] - Starting workflow "${workflowName}" (requestId: ${requestId}), publishing "${firstStep.type}"`);
45
+ this.redis.publishMessage(channel, JSON.stringify({ requestId, payload }));
46
+ }
47
+ subscribeToWorkflowEvents(workflow) {
48
+ for (let i = 0; i < workflow.steps.length; i++) {
49
+ const step = workflow.steps[i];
50
+ const channel = this.stepChannel(workflow.name, step.type);
51
+ const completionChannel = `event:${channel}:completed`;
52
+ this.redis.subscribeToChannel(completionChannel, (message) => {
53
+ this.handleStepCompletion(workflow, i, message);
54
+ });
55
+ }
56
+ }
57
+ async handleStepCompletion(workflow, stepIndex, message) {
58
+ const { requestId, payload } = JSON.parse(message);
59
+ const state = await this.getState(requestId);
60
+ if (!state || state.workflowName !== workflow.name) {
61
+ return;
62
+ }
63
+ if (state.currentStep !== stepIndex) {
64
+ logger.warn(`[WorkflowRegistry] - Step mismatch for "${workflow.name}" (requestId: ${requestId}): expected step ${state.currentStep}, got ${stepIndex}`);
65
+ return;
66
+ }
67
+ const nextStepIndex = stepIndex + 1;
68
+ if (nextStepIndex >= workflow.steps.length) {
69
+ state.status = "completed";
70
+ state.currentStep = stepIndex;
71
+ await this.saveState(requestId, state);
72
+ logger.debug(`[WorkflowRegistry] - Workflow "${workflow.name}" completed (requestId: ${requestId})`);
73
+ return;
74
+ }
75
+ state.currentStep = nextStepIndex;
76
+ await this.saveState(requestId, state);
77
+ const nextStep = workflow.steps[nextStepIndex];
78
+ const nextChannel = this.stepChannel(workflow.name, nextStep.type);
79
+ logger.debug(`[WorkflowRegistry] - Workflow "${workflow.name}" advancing to step ${nextStepIndex}: "${nextStep.type}" (requestId: ${requestId})`);
80
+ this.redis.publishMessage(nextChannel, JSON.stringify({ requestId, payload }));
81
+ }
82
+ stepChannel(workflowName, stepType) {
83
+ return `workflow:${workflowName}:${stepType}`;
84
+ }
85
+ stateKey(requestId) {
86
+ return `workflow:state:${requestId}`;
87
+ }
88
+ async saveState(requestId, state) {
89
+ await this.redis.setCache(this.stateKey(requestId), JSON.stringify(state), 86400);
90
+ }
91
+ async getState(requestId) {
92
+ const raw = await this.redis.getCache(this.stateKey(requestId));
93
+ if (!raw)
94
+ return null;
95
+ return JSON.parse(raw);
96
+ }
97
+ }
98
+ //# sourceMappingURL=workflow-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-registry.js","sourceRoot":"","sources":["../package/workflow-registry.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@synkro/core",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight workflow and state machine orchestrator powered by Redis. Define event-driven workflows via configuration or code.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "type-check": "tsc --noEmit",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "workflow",
27
+ "state-machine",
28
+ "orchestrator",
29
+ "event-driven",
30
+ "redis"
31
+ ],
32
+ "author": "buemura",
33
+ "license": "ISC",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/buemura/synkro.git"
37
+ },
38
+ "homepage": "https://github.com/buemura/synkro#readme",
39
+ "type": "module",
40
+ "dependencies": {
41
+ "ioredis": "^5.10.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^25.3.3",
45
+ "tsx": "^4.21.0",
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^4.0.18"
48
+ }
49
+ }