@flowmarkdev/node 0.0.0-stage → 0.1.0-alpha.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -2
  3. package/dist/adapters/next.d.ts +17 -0
  4. package/dist/adapters/next.js +38 -0
  5. package/dist/adapters/proxy.d.ts +25 -0
  6. package/dist/adapters/proxy.js +94 -0
  7. package/dist/browser/index.d.ts +52 -0
  8. package/dist/browser/index.js +115 -0
  9. package/dist/client/create-client.d.ts +21 -0
  10. package/dist/client/create-client.js +136 -0
  11. package/dist/client/runtime.d.ts +5 -0
  12. package/dist/client/runtime.js +72 -0
  13. package/dist/config/api-origin.d.ts +2 -0
  14. package/dist/config/api-origin.js +12 -0
  15. package/dist/config/define-config.d.ts +11 -0
  16. package/dist/config/define-config.js +12 -0
  17. package/dist/config/settings.d.ts +11 -0
  18. package/dist/config/settings.js +31 -0
  19. package/dist/errors/index.d.ts +49 -0
  20. package/dist/errors/index.js +21 -0
  21. package/dist/features/delivery/adapters/http-transport.d.ts +8 -0
  22. package/dist/features/delivery/adapters/http-transport.js +31 -0
  23. package/dist/features/delivery/adapters/http.d.ts +4 -0
  24. package/dist/features/delivery/adapters/http.js +11 -0
  25. package/dist/features/delivery/adapters/memory-transport.d.ts +10 -0
  26. package/dist/features/delivery/adapters/memory-transport.js +12 -0
  27. package/dist/features/delivery/delivery-queue.d.ts +15 -0
  28. package/dist/features/delivery/delivery-queue.js +56 -0
  29. package/dist/features/delivery/ports/workflow-transport.d.ts +9 -0
  30. package/dist/features/delivery/ports/workflow-transport.js +3 -0
  31. package/dist/features/workflows/workflow-engine.d.ts +27 -0
  32. package/dist/features/workflows/workflow-engine.js +232 -0
  33. package/dist/index.d.ts +7 -0
  34. package/dist/index.js +5 -0
  35. package/dist/types/index.d.ts +167 -0
  36. package/dist/types/index.js +1 -0
  37. package/dist/utils/failure-from-unknown.d.ts +3 -0
  38. package/dist/utils/failure-from-unknown.js +36 -0
  39. package/dist/utils/operation-args.d.ts +5 -0
  40. package/dist/utils/operation-args.js +3 -0
  41. package/dist/utils/operation-callback.d.ts +2 -0
  42. package/dist/utils/operation-callback.js +18 -0
  43. package/package.json +75 -3
@@ -0,0 +1,136 @@
1
+ import { FetchHttpClient } from "@effect/platform";
2
+ import { Layer } from "effect";
3
+ import { HttpTransportLive } from "../features/delivery/adapters/http.js";
4
+ import { FlowmarkSettings, resolveSettings } from "../config/settings.js";
5
+ import { FlowmarkConfigError } from "../errors/index.js";
6
+ import { makeClient } from "./runtime.js";
7
+ import { WorkflowTransport } from "../features/delivery/ports/workflow-transport.js";
8
+ import { withOperationCallback } from "../utils/operation-callback.js";
9
+ import { splitOperationArgs } from "../utils/operation-args.js";
10
+ /**
11
+ * Node server client. Import this from `@flowmarkdev/node` in workers and scripts.
12
+ * Next.js Route Handlers must import createFlowmark from `@flowmarkdev/node/next`.
13
+ * Browser code must import it from `@flowmarkdev/node/browser`.
14
+ * Reads FLOWMARK_API_KEY and FLOWMARK_API_URL; never put those in NEXT_PUBLIC_*.
15
+ * Call shutdown() before process exit. Create one shared client.
16
+ * @throws FlowmarkConfigError If credentials or API origin are invalid.
17
+ */
18
+ export function createFlowmark(config = {}) {
19
+ const settings = resolveSettings(config);
20
+ const transport = HttpTransportLive.pipe(Layer.provide(Layer.succeed(FlowmarkSettings, settings)), Layer.provide(FetchHttpClient.layer));
21
+ return makeClient(transport, config.onInstrumentationError, {
22
+ environment: config.environment,
23
+ release: config.release,
24
+ });
25
+ }
26
+ /**
27
+ * Advanced entry point for a custom Effect transport, primarily tests and custom integrations.
28
+ * Use createFlowmark().workflow() for ordinary application code.
29
+ * Delivery resources are drained and released after terminal completion. If
30
+ * operations are open, done() waits for their handles to close first.
31
+ * @param name Stable workflow name.
32
+ * @param options Explicit execution ID, transport, and optional diagnostics/metadata.
33
+ * @throws FlowmarkConfigError If no transport is supplied.
34
+ */
35
+ export function workflow(name, options) {
36
+ if (!options.transport) {
37
+ throw new FlowmarkConfigError({
38
+ cause: "Use createFlowmark().workflow() for automatic delivery. The low-level workflow() entry point requires a custom transport.",
39
+ });
40
+ }
41
+ const client = makeClient(Layer.succeed(WorkflowTransport, options.transport), options.onInstrumentationError);
42
+ const flow = client.workflow(name, options);
43
+ let finished = false;
44
+ let completionRequested = false;
45
+ const activeOperations = new Set();
46
+ const finish = (command) => {
47
+ if (finished) {
48
+ return;
49
+ }
50
+ finished = true;
51
+ command();
52
+ void client.shutdown().catch(() => {
53
+ console.warn("[Flowmark] Custom transport workflow completed with delivery failures");
54
+ });
55
+ };
56
+ function operation(name, callbackOrOptions, options) {
57
+ const args = splitOperationArgs(callbackOrOptions, options);
58
+ if (finished || completionRequested) {
59
+ const ignored = {
60
+ operationId: globalThis.crypto.randomUUID(),
61
+ done: () => undefined,
62
+ fail: () => undefined,
63
+ };
64
+ return args.callback === undefined
65
+ ? ignored
66
+ : withOperationCallback(ignored, args.callback);
67
+ }
68
+ const handle = flow.operation(name, args.options);
69
+ activeOperations.add(handle.operationId);
70
+ let closed = false;
71
+ const operationHandle = {
72
+ operationId: handle.operationId,
73
+ done: () => {
74
+ if (finished || closed) {
75
+ return;
76
+ }
77
+ closed = true;
78
+ activeOperations.delete(handle.operationId);
79
+ handle.done();
80
+ if (completionRequested && activeOperations.size === 0) {
81
+ finish(() => undefined);
82
+ }
83
+ },
84
+ fail: (failure) => {
85
+ if (finished || closed) {
86
+ return;
87
+ }
88
+ closed = true;
89
+ activeOperations.delete(handle.operationId);
90
+ handle.fail(failure);
91
+ if (completionRequested && activeOperations.size === 0) {
92
+ finish(() => undefined);
93
+ }
94
+ },
95
+ };
96
+ return args.callback === undefined
97
+ ? operationHandle
98
+ : withOperationCallback(operationHandle, args.callback);
99
+ }
100
+ return {
101
+ runId: flow.runId,
102
+ step(name, stepOptions) {
103
+ if (!finished && !completionRequested) {
104
+ flow.step(name, stepOptions);
105
+ }
106
+ },
107
+ done: (options) => {
108
+ if (finished || completionRequested) {
109
+ return;
110
+ }
111
+ if (activeOperations.size > 0) {
112
+ completionRequested = true;
113
+ flow.done(options);
114
+ return;
115
+ }
116
+ finish(() => flow.done(options));
117
+ },
118
+ fail: (options) => finish(() => flow.fail(options)),
119
+ error: (error) => {
120
+ if (!finished && !completionRequested) {
121
+ flow.error(error);
122
+ }
123
+ },
124
+ setProperties: (properties) => {
125
+ if (!finished && !completionRequested) {
126
+ flow.setProperties(properties);
127
+ }
128
+ },
129
+ operation,
130
+ progress: (progress) => {
131
+ if (!finished && !completionRequested) {
132
+ flow.progress(progress);
133
+ }
134
+ },
135
+ };
136
+ }
@@ -0,0 +1,5 @@
1
+ import { Layer } from "effect";
2
+ import { WorkflowTransport } from "../features/delivery/ports/workflow-transport.js";
3
+ import type { FlowmarkClient, InstrumentationErrorHandler, WorkflowRunOptions } from "../types/index.js";
4
+ /** Shared execution boundary; this module has no Node.js or framework imports. */
5
+ export declare function makeClient(transport: Layer.Layer<WorkflowTransport>, onError?: InstrumentationErrorHandler, defaults?: Pick<WorkflowRunOptions, "environment" | "release">): FlowmarkClient;
@@ -0,0 +1,72 @@
1
+ import { Effect, Layer, ManagedRuntime } from "effect";
2
+ import { FlowmarkConfigError } from "../errors/index.js";
3
+ import { DeliveryQueue } from "../features/delivery/delivery-queue.js";
4
+ import { WorkflowEngine } from "../features/workflows/workflow-engine.js";
5
+ import { withOperationCallback } from "../utils/operation-callback.js";
6
+ import { splitOperationArgs } from "../utils/operation-args.js";
7
+ /** Shared execution boundary; this module has no Node.js or framework imports. */
8
+ export function makeClient(transport, onError, defaults = {}) {
9
+ const delivery = DeliveryQueue.layer(onError).pipe(Layer.provide(transport));
10
+ const runtime = ManagedRuntime.make(WorkflowEngine.layer.pipe(Layer.provideMerge(delivery)));
11
+ const engine = runtime.runSync(WorkflowEngine);
12
+ const queue = runtime.runSync(DeliveryQueue);
13
+ let shutdownResult;
14
+ const runMarker = (command) => {
15
+ if (shutdownResult) {
16
+ console.warn("[Flowmark] Ignored workflow marker after client shutdown");
17
+ return;
18
+ }
19
+ runtime.runSync(command);
20
+ };
21
+ return {
22
+ workflow(name, options = {}) {
23
+ if (shutdownResult) {
24
+ throw new FlowmarkConfigError({ cause: "Client has been shut down" });
25
+ }
26
+ const handle = runtime.runSync(engine.start(name, {
27
+ ...defaults,
28
+ ...options,
29
+ environment: options.environment ?? defaults.environment,
30
+ release: options.release ?? defaults.release,
31
+ }));
32
+ function operation(name, callbackOrOptions, options) {
33
+ const args = splitOperationArgs(callbackOrOptions, options);
34
+ const effectHandle = shutdownResult
35
+ ? {
36
+ operationId: globalThis.crypto.randomUUID(),
37
+ done: Effect.void,
38
+ fail: () => Effect.void,
39
+ }
40
+ : runtime.runSync(handle.operation(name, args.options));
41
+ const operationHandle = {
42
+ operationId: effectHandle.operationId,
43
+ done: () => runMarker(effectHandle.done),
44
+ fail: (failure) => runMarker(effectHandle.fail(failure)),
45
+ };
46
+ if (args.callback === undefined) {
47
+ return operationHandle;
48
+ }
49
+ return withOperationCallback(operationHandle, args.callback);
50
+ }
51
+ return {
52
+ runId: handle.runId,
53
+ step: (name, stepOptions) => runMarker(handle.step(name, stepOptions)),
54
+ done: (doneOptions) => runMarker(handle.done(doneOptions)),
55
+ fail: (options) => runMarker(handle.fail(options)),
56
+ error: (error) => runMarker(handle.error(error)),
57
+ setProperties: (properties) => runMarker(handle.setProperties(properties)),
58
+ operation,
59
+ progress: (progress) => runMarker(handle.progress(progress)),
60
+ };
61
+ },
62
+ flush() {
63
+ return shutdownResult ?? runtime.runPromise(queue.flush);
64
+ },
65
+ shutdown() {
66
+ shutdownResult ??= runtime
67
+ .runPromise(queue.shutdown)
68
+ .finally(() => runtime.dispose());
69
+ return shutdownResult;
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,2 @@
1
+ import { Schema } from "effect";
2
+ export declare const ApiOriginSchema: Schema.filter<typeof Schema.String>;
@@ -0,0 +1,12 @@
1
+ import { Schema } from "effect";
2
+ export const ApiOriginSchema = Schema.String.pipe(Schema.filter((value) => {
3
+ try {
4
+ const url = new URL(value);
5
+ return (url.protocol === "https:" ||
6
+ (url.protocol === "http:" &&
7
+ ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)));
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }));
@@ -0,0 +1,11 @@
1
+ import type { FlowmarkConfig } from "../types/index.js";
2
+ /**
3
+ * Defines server configuration with autocomplete and preserved literal types.
4
+ * Returns the same object without reading environment variables, validating credentials,
5
+ * creating a client, or starting delivery. Validation happens at the client boundary.
6
+ * Export from flowmark.config.ts and pass to createFlowmark(); the file is not auto-loaded.
7
+ * Keep this configuration server-only. Use @flowmarkdev/node/browser for public browser keys.
8
+ * @example
9
+ * export default defineConfig({ apiUrl: "http://localhost:3003" })
10
+ */
11
+ export declare function defineConfig<const T extends FlowmarkConfig>(config: T & Record<Exclude<keyof T, keyof FlowmarkConfig>, never>): T;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Defines server configuration with autocomplete and preserved literal types.
3
+ * Returns the same object without reading environment variables, validating credentials,
4
+ * creating a client, or starting delivery. Validation happens at the client boundary.
5
+ * Export from flowmark.config.ts and pass to createFlowmark(); the file is not auto-loaded.
6
+ * Keep this configuration server-only. Use @flowmarkdev/node/browser for public browser keys.
7
+ * @example
8
+ * export default defineConfig({ apiUrl: "http://localhost:3003" })
9
+ */
10
+ export function defineConfig(config) {
11
+ return config;
12
+ }
@@ -0,0 +1,11 @@
1
+ import { Context, Redacted } from "effect";
2
+ import type { FlowmarkConfig } from "../types/index.js";
3
+ export interface FlowmarkSettingsService {
4
+ readonly apiKey: Redacted.Redacted<string>;
5
+ readonly apiUrl: string;
6
+ }
7
+ declare const FlowmarkSettings_base: Context.TagClass<FlowmarkSettings, "@flowmarkdev/node/Settings", FlowmarkSettingsService>;
8
+ export declare class FlowmarkSettings extends FlowmarkSettings_base {
9
+ }
10
+ export declare function resolveSettings(config: FlowmarkConfig): FlowmarkSettingsService;
11
+ export {};
@@ -0,0 +1,31 @@
1
+ import { Context, Either, Redacted, Schema } from "effect";
2
+ import { FlowmarkConfigError } from "../errors/index.js";
3
+ import { ApiOriginSchema } from "./api-origin.js";
4
+ const SettingsSchema = Schema.Struct({
5
+ apiKey: Schema.RedactedFromSelf(Schema.String.pipe(Schema.minLength(1))),
6
+ apiUrl: ApiOriginSchema,
7
+ });
8
+ export class FlowmarkSettings extends Context.Tag("@flowmarkdev/node/Settings")() {
9
+ }
10
+ export function resolveSettings(config) {
11
+ const apiKey = config.apiKey ?? process.env.FLOWMARK_API_KEY;
12
+ if (!apiKey) {
13
+ throw new FlowmarkConfigError({
14
+ cause: "A server write key is required. Set FLOWMARK_API_KEY in the server environment or pass apiKey to createFlowmark(). For the local repository demo, run pnpm dev:setup first. Never put a server key in browser configuration.",
15
+ });
16
+ }
17
+ const decoded = Schema.decodeUnknownEither(SettingsSchema)({
18
+ apiKey: apiKey === undefined ? undefined : Redacted.make(apiKey),
19
+ apiUrl: config.apiUrl ??
20
+ process.env.FLOWMARK_API_URL ??
21
+ (process.env.NODE_ENV !== "production"
22
+ ? "http://localhost:3003"
23
+ : undefined),
24
+ });
25
+ return Either.match(decoded, {
26
+ onLeft: (cause) => {
27
+ throw new FlowmarkConfigError({ cause });
28
+ },
29
+ onRight: (settings) => settings,
30
+ });
31
+ }
@@ -0,0 +1,49 @@
1
+ import { type Cause } from "effect";
2
+ import type { WorkflowEvent, WorkflowEventValidationError } from "@flowmarkdev/core";
3
+ declare const FlowmarkConfigError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
4
+ readonly _tag: "FlowmarkConfigError";
5
+ } & Readonly<A>;
6
+ /** Invalid credentials/origin, unsupported execution context, or use of a closed client. */
7
+ export declare class FlowmarkConfigError extends FlowmarkConfigError_base<{
8
+ readonly cause: unknown;
9
+ }> {
10
+ get message(): string;
11
+ }
12
+ declare const FlowmarkHttpError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
13
+ readonly _tag: "FlowmarkHttpError";
14
+ } & Readonly<A>;
15
+ /** HTTP delivery failure; status is undefined for encoding, network, or timeout failures. Preserves the original cause. */
16
+ export declare class FlowmarkHttpError extends FlowmarkHttpError_base<{
17
+ readonly status: number | undefined;
18
+ readonly cause: unknown;
19
+ }> {
20
+ }
21
+ declare const WorkflowTransportError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
22
+ readonly _tag: "WorkflowTransportError";
23
+ } & Readonly<A>;
24
+ /** Associates a failed event with its Effect Cause, including original transport failures/defects. */
25
+ export declare class WorkflowTransportError extends WorkflowTransportError_base<{
26
+ readonly event: WorkflowEvent;
27
+ readonly cause: Cause.Cause<unknown>;
28
+ }> {
29
+ }
30
+ declare const FlowmarkQueueError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
31
+ readonly _tag: "FlowmarkQueueError";
32
+ } & Readonly<A>;
33
+ /** An event could not be queued because the 1,000-event limit was reached or delivery was closed. */
34
+ export declare class FlowmarkQueueError extends FlowmarkQueueError_base<{
35
+ readonly event: WorkflowEvent;
36
+ readonly reason: "capacity" | "closed";
37
+ }> {
38
+ }
39
+ /** Failures reported to onInstrumentationError or collected by flush()/shutdown(). */
40
+ export type DeliveryFailure = WorkflowTransportError | WorkflowEventValidationError | FlowmarkQueueError;
41
+ declare const FlowmarkDeliveryError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
42
+ readonly _tag: "FlowmarkDeliveryError";
43
+ } & Readonly<A>;
44
+ /** Rejection from flush()/shutdown(). The failures array preserves each reported delivery/validation error. */
45
+ export declare class FlowmarkDeliveryError extends FlowmarkDeliveryError_base<{
46
+ readonly failures: ReadonlyArray<DeliveryFailure>;
47
+ }> {
48
+ }
49
+ export {};
@@ -0,0 +1,21 @@
1
+ import { Data } from "effect";
2
+ /** Invalid credentials/origin, unsupported execution context, or use of a closed client. */
3
+ export class FlowmarkConfigError extends Data.TaggedError("FlowmarkConfigError") {
4
+ get message() {
5
+ return typeof this.cause === "string"
6
+ ? this.cause
7
+ : "Invalid Flowmark configuration. Check the API URL and server credentials; see the original cause for validation details.";
8
+ }
9
+ }
10
+ /** HTTP delivery failure; status is undefined for encoding, network, or timeout failures. Preserves the original cause. */
11
+ export class FlowmarkHttpError extends Data.TaggedError("FlowmarkHttpError") {
12
+ }
13
+ /** Associates a failed event with its Effect Cause, including original transport failures/defects. */
14
+ export class WorkflowTransportError extends Data.TaggedError("WorkflowTransportError") {
15
+ }
16
+ /** An event could not be queued because the 1,000-event limit was reached or delivery was closed. */
17
+ export class FlowmarkQueueError extends Data.TaggedError("FlowmarkQueueError") {
18
+ }
19
+ /** Rejection from flush()/shutdown(). The failures array preserves each reported delivery/validation error. */
20
+ export class FlowmarkDeliveryError extends Data.TaggedError("FlowmarkDeliveryError") {
21
+ }
@@ -0,0 +1,8 @@
1
+ import { HttpClient } from "@effect/platform";
2
+ import { Effect, type Redacted } from "effect";
3
+ import { WorkflowTransport } from "../ports/workflow-transport.js";
4
+ export interface HttpTransportOptions {
5
+ readonly endpoint: string;
6
+ readonly apiKey?: Redacted.Redacted<string>;
7
+ }
8
+ export declare const makeHttpTransport: (settings: HttpTransportOptions) => Effect.Effect<WorkflowTransport, never, HttpClient.HttpClient>;
@@ -0,0 +1,31 @@
1
+ import { HttpClient, HttpClientRequest } from "@effect/platform";
2
+ import { Effect, Match, Schedule } from "effect";
3
+ import { FlowmarkHttpError } from "../../../errors/index.js";
4
+ import { WorkflowTransport } from "../ports/workflow-transport.js";
5
+ export const makeHttpTransport = Effect.fn("Flowmark.HttpTransport.make")(function* (settings) {
6
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk, HttpClient.withScope);
7
+ const emit = Effect.fn("Flowmark.HttpTransport.emit")(function* (event) {
8
+ const eventId = yield* Effect.sync(() => globalThis.crypto.randomUUID());
9
+ // Build once so all retries carry the same idempotency key and payload.
10
+ const baseRequest = HttpClientRequest.post(settings.endpoint);
11
+ const request = yield* (settings.apiKey
12
+ ? baseRequest.pipe(HttpClientRequest.bearerToken(settings.apiKey))
13
+ : baseRequest).pipe(HttpClientRequest.bodyJson({ eventId, event }), Effect.mapError((cause) => new FlowmarkHttpError({ status: undefined, cause })));
14
+ yield* Effect.scoped(client.execute(request)).pipe(Effect.mapError((cause) => new FlowmarkHttpError({
15
+ status: Match.value(cause).pipe(Match.tag("ResponseError", ({ response }) => response.status), Match.tag("RequestError", () => undefined), Match.exhaustive),
16
+ cause,
17
+ })), Effect.timeoutFail({
18
+ duration: "3 seconds",
19
+ onTimeout: () => new FlowmarkHttpError({
20
+ status: undefined,
21
+ cause: "Ingestion timed out",
22
+ }),
23
+ }), Effect.retry({
24
+ schedule: Schedule.exponential("100 millis").pipe(Schedule.intersect(Schedule.recurs(2))),
25
+ while: (error) => error.status === undefined ||
26
+ error.status === 429 ||
27
+ error.status >= 500,
28
+ }));
29
+ });
30
+ return WorkflowTransport.of({ emit });
31
+ });
@@ -0,0 +1,4 @@
1
+ import { Layer } from "effect";
2
+ import { FlowmarkSettings } from "../../../config/settings.js";
3
+ import { WorkflowTransport } from "../ports/workflow-transport.js";
4
+ export declare const HttpTransportLive: Layer.Layer<WorkflowTransport, never, FlowmarkSettings | import("@effect/platform/HttpClient").HttpClient>;
@@ -0,0 +1,11 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { FlowmarkSettings } from "../../../config/settings.js";
3
+ import { WorkflowTransport } from "../ports/workflow-transport.js";
4
+ import { makeHttpTransport } from "./http-transport.js";
5
+ export const HttpTransportLive = Layer.effect(WorkflowTransport, Effect.gen(function* () {
6
+ const settings = yield* FlowmarkSettings;
7
+ return yield* makeHttpTransport({
8
+ endpoint: new URL("/v1/events", settings.apiUrl).toString(),
9
+ apiKey: settings.apiKey,
10
+ });
11
+ }));
@@ -0,0 +1,10 @@
1
+ import { Effect } from "effect";
2
+ import type { WorkflowEvent } from "@flowmarkdev/core";
3
+ import type { WorkflowTransport } from "../ports/workflow-transport.js";
4
+ /** In-memory transport for tests/custom integrations. Nothing is sent to the Flowmark API. */
5
+ export declare class MemoryTransport implements WorkflowTransport {
6
+ /** Successfully emitted events in delivery order. Not persisted across process restarts. */
7
+ readonly events: WorkflowEvent[];
8
+ /** Returns a lazy Effect that appends an event when executed. */
9
+ emit(event: WorkflowEvent): Effect.Effect<void>;
10
+ }
@@ -0,0 +1,12 @@
1
+ import { Effect } from "effect";
2
+ /** In-memory transport for tests/custom integrations. Nothing is sent to the Flowmark API. */
3
+ export class MemoryTransport {
4
+ /** Successfully emitted events in delivery order. Not persisted across process restarts. */
5
+ events = [];
6
+ /** Returns a lazy Effect that appends an event when executed. */
7
+ emit(event) {
8
+ return Effect.sync(() => {
9
+ this.events.push(event);
10
+ });
11
+ }
12
+ }
@@ -0,0 +1,15 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { type WorkflowEvent } from "@flowmarkdev/core";
3
+ import { FlowmarkDeliveryError } from "../../errors/index.js";
4
+ import type { InstrumentationErrorHandler } from "../../types/index.js";
5
+ import { WorkflowTransport } from "./ports/workflow-transport.js";
6
+ export interface DeliveryQueueService {
7
+ readonly enqueue: (event: WorkflowEvent, ordering: Effect.Semaphore) => Effect.Effect<void>;
8
+ readonly flush: Effect.Effect<void, FlowmarkDeliveryError>;
9
+ readonly shutdown: Effect.Effect<void, FlowmarkDeliveryError>;
10
+ }
11
+ declare const DeliveryQueue_base: Context.TagClass<DeliveryQueue, "@flowmarkdev/node/DeliveryQueue", DeliveryQueueService>;
12
+ export declare class DeliveryQueue extends DeliveryQueue_base {
13
+ static layer(onError?: InstrumentationErrorHandler): Layer.Layer<DeliveryQueue, never, WorkflowTransport>;
14
+ }
15
+ export {};
@@ -0,0 +1,56 @@
1
+ import { Context, Effect, FiberSet, Layer, Ref } from "effect";
2
+ import { decodeWorkflowEvent } from "@flowmarkdev/core";
3
+ import { FlowmarkDeliveryError, FlowmarkQueueError, WorkflowTransportError, } from "../../errors/index.js";
4
+ import { WorkflowTransport } from "./ports/workflow-transport.js";
5
+ export class DeliveryQueue extends Context.Tag("@flowmarkdev/node/DeliveryQueue")() {
6
+ static layer(onError) {
7
+ return Layer.scoped(DeliveryQueue, Effect.gen(function* () {
8
+ const transport = yield* WorkflowTransport;
9
+ const pending = yield* FiberSet.make();
10
+ const failures = yield* Ref.make([]);
11
+ const closed = yield* Ref.make(false);
12
+ const queueAccess = yield* Effect.makeSemaphore(1);
13
+ const report = Effect.fn("Flowmark.DeliveryQueue.report")(function* (error, event) {
14
+ yield* Ref.update(failures, (errors) => [...errors, error]);
15
+ if (onError) {
16
+ yield* Effect.sync(() => onError(error, event)).pipe(Effect.catchAllCause(() => Effect.logWarning("[Flowmark] Instrumentation error callback failed")));
17
+ }
18
+ else {
19
+ yield* Effect.logWarning(`[Flowmark] Could not deliver ${event.type} for ${event.workflow}`, {
20
+ error: error._tag,
21
+ });
22
+ }
23
+ });
24
+ const deliver = Effect.fn("Flowmark.DeliveryQueue.deliver")(function* (event) {
25
+ const decoded = yield* decodeWorkflowEvent(event);
26
+ yield* transport
27
+ .emit(decoded)
28
+ .pipe(Effect.catchAllCause((cause) => Effect.fail(new WorkflowTransportError({ event: decoded, cause }))));
29
+ });
30
+ const enqueue = Effect.fn("Flowmark.DeliveryQueue.enqueue")(function* (event, ordering) {
31
+ const isClosed = yield* Ref.get(closed);
32
+ if (isClosed || (yield* FiberSet.size(pending)) >= 1000) {
33
+ return yield* report(new FlowmarkQueueError({
34
+ event,
35
+ reason: isClosed ? "closed" : "capacity",
36
+ }), event);
37
+ }
38
+ yield* FiberSet.run(pending, ordering.withPermits(1)(deliver(event).pipe(Effect.catchAll((error) => report(error, event)), Effect.catchAllCause((cause) => report(new WorkflowTransportError({ event, cause }), event)))));
39
+ }, queueAccess.withPermits(1), Effect.asVoid);
40
+ const flush = Effect.gen(function* () {
41
+ yield* FiberSet.awaitEmpty(pending);
42
+ const errors = yield* Ref.getAndSet(failures, []);
43
+ if (errors.length > 0) {
44
+ return yield* Effect.fail(new FlowmarkDeliveryError({ failures: errors }));
45
+ }
46
+ });
47
+ return DeliveryQueue.of({
48
+ enqueue,
49
+ flush,
50
+ shutdown: queueAccess
51
+ .withPermits(1)(Ref.set(closed, true))
52
+ .pipe(Effect.andThen(flush)),
53
+ });
54
+ }));
55
+ }
56
+ }
@@ -0,0 +1,9 @@
1
+ import { Context, type Effect } from "effect";
2
+ import type { WorkflowEvent } from "@flowmarkdev/core";
3
+ /** Port for delivering validated workflow events to an external destination. */
4
+ export interface WorkflowTransport {
5
+ /** Returns a lazy delivery Effect. Expected failures use the Effect error channel. */
6
+ readonly emit: (event: WorkflowEvent) => Effect.Effect<void, Error>;
7
+ }
8
+ /** Effect dependency tag for replacing delivery implementations. */
9
+ export declare const WorkflowTransport: Context.Tag<WorkflowTransport, WorkflowTransport>;
@@ -0,0 +1,3 @@
1
+ import { Context } from "effect";
2
+ /** Effect dependency tag for replacing delivery implementations. */
3
+ export const WorkflowTransport = Context.GenericTag("@flowmarkdev/node/WorkflowTransport");
@@ -0,0 +1,27 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import type { WorkflowError, WorkflowName, WorkflowOperationFailure, WorkflowProperties, WorkflowProgress, WorkflowStepName } from "@flowmarkdev/core";
3
+ import type { WorkflowDoneOptions, WorkflowFailOptions, WorkflowOperationOptions, WorkflowRunOptions, WorkflowStepOptions } from "../../types/index.js";
4
+ import { DeliveryQueue } from "../delivery/delivery-queue.js";
5
+ export interface WorkflowOperationHandle {
6
+ readonly operationId: string;
7
+ readonly done: Effect.Effect<void>;
8
+ readonly fail: (failure: WorkflowOperationFailure) => Effect.Effect<void>;
9
+ }
10
+ export interface WorkflowHandle {
11
+ readonly runId: string;
12
+ readonly step: (name: WorkflowStepName, options?: WorkflowStepOptions) => Effect.Effect<void>;
13
+ readonly done: (options?: WorkflowDoneOptions) => Effect.Effect<void>;
14
+ readonly fail: (options: WorkflowFailOptions) => Effect.Effect<void>;
15
+ readonly error: (error: WorkflowError) => Effect.Effect<void>;
16
+ readonly setProperties: (properties: WorkflowProperties) => Effect.Effect<void>;
17
+ readonly operation: (name: WorkflowStepName, options?: WorkflowOperationOptions) => Effect.Effect<WorkflowOperationHandle>;
18
+ readonly progress: (progress: WorkflowProgress) => Effect.Effect<void>;
19
+ }
20
+ export interface WorkflowEngineService {
21
+ readonly start: (name: WorkflowName, options: WorkflowRunOptions) => Effect.Effect<WorkflowHandle>;
22
+ }
23
+ declare const WorkflowEngine_base: Context.TagClass<WorkflowEngine, "@flowmarkdev/node/WorkflowEngine", WorkflowEngineService>;
24
+ export declare class WorkflowEngine extends WorkflowEngine_base {
25
+ static readonly layer: Layer.Layer<WorkflowEngine, never, DeliveryQueue>;
26
+ }
27
+ export {};