@entifix/workflow 0.12.0-0 → 0.13.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/dist/engine/make-flow-engine.d.ts.map +1 -1
- package/dist/engine/make-flow-engine.js +15 -8
- package/dist/engine/make-flow-engine.js.map +1 -1
- package/dist/postgres/sql-flow-status-store.d.ts.map +1 -1
- package/dist/postgres/sql-flow-status-store.js +6 -1
- package/dist/postgres/sql-flow-status-store.js.map +1 -1
- package/dist/status/flow-status-store.d.ts +8 -0
- package/dist/status/flow-status-store.d.ts.map +1 -1
- package/dist/status/flow-status-store.js +13 -0
- package/dist/status/flow-status-store.js.map +1 -1
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"make-flow-engine.d.ts","sourceRoot":"","sources":["../../src/engine/make-flow-engine.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,aAAa,EAGd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAW,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AACxD,OAAO,EAAmB,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAElE,OAAO,EACL,KAAK,kBAAkB,EAIxB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"make-flow-engine.d.ts","sourceRoot":"","sources":["../../src/engine/make-flow-engine.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,aAAa,EAGd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAW,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AACxD,OAAO,EAAmB,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAElE,OAAO,EACL,KAAK,kBAAkB,EAIxB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,sFAoGzB,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,kBAAkB,gVAGxD,CAAC"}
|
|
@@ -22,7 +22,12 @@ import { FlowStatusStoreTag } from '../status/flow-status-store.js';
|
|
|
22
22
|
onSome: Effect.succeed
|
|
23
23
|
}));
|
|
24
24
|
/** Completes the wait `record` is parked on, if `token` names it. */ const complete = (token, outcome)=>(record)=>Effect.gen(function*() {
|
|
25
|
-
|
|
25
|
+
// Claimed before the wait is resumed, never after: the resumed flow
|
|
26
|
+
// records its own states, and a `running` written behind them would put
|
|
27
|
+
// a finished flow back to running with nothing left to correct it. The
|
|
28
|
+
// claim is conditional, so a second signal racing this one is refused.
|
|
29
|
+
const claimed = yield* store.claimWait(record.flowId, token);
|
|
30
|
+
if (!claimed) return yield* Effect.fail(new FlowSignalRejected({
|
|
26
31
|
flowId: record.flowId,
|
|
27
32
|
token
|
|
28
33
|
}));
|
|
@@ -33,13 +38,15 @@ import { FlowStatusStoreTag } from '../status/flow-status-store.js';
|
|
|
33
38
|
executionId: record.executionId
|
|
34
39
|
}),
|
|
35
40
|
value: outcome
|
|
36
|
-
}))
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
})).pipe(// The wait was not resumed, so it is parked again and a retried signal
|
|
42
|
+
// can claim it.
|
|
43
|
+
Effect.onError(()=>store.update(record.flowId, _extends({
|
|
44
|
+
state: 'waiting',
|
|
45
|
+
step: token,
|
|
46
|
+
waitingFor: token
|
|
47
|
+
}, record.correlation === undefined ? {} : {
|
|
48
|
+
correlation: record.correlation
|
|
49
|
+
})).pipe(Effect.ignore)));
|
|
43
50
|
});
|
|
44
51
|
const engine = {
|
|
45
52
|
start: (slip, idempotencyKey)=>Effect.gen(function*() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/engine/make-flow-engine.ts"],"sourcesContent":["import {\n type FlowEngine,\n FlowEngineTag,\n FlowNotFound,\n FlowSignalRejected,\n} from '@entifix/transactions';\nimport { Context, Effect, Layer, Option } from 'effect';\nimport { DurableDeferred, WorkflowEngine } from 'effect/workflow';\n\nimport {\n type InterpreterOptions,\n SlipWorkflow,\n SlipWorkflowLayer,\n waitFor,\n} from '../interpreter/slip-workflow.js';\nimport type { FlowRecord } from '../status/flow-status-store.js';\nimport { FlowStatusStoreTag } from '../status/flow-status-store.js';\n\n/**\n * The flow engine port over Effect Workflow\n * ([ADR 0016](../../../../../docs/adr/0016-a-flow-engine-port-and-a-durable-workflow-adapter.md)).\n * A start records the flow under its key and executes the slip detached — it\n * answers the flow id, never the outcome, which arrives as `flow.*` events and\n * in the status record. A signal is accepted only by a flow parked on exactly\n * that wait.\n */\nexport const makeFlowEngine = Effect.gen(function* () {\n const context = yield* Effect.context<\n WorkflowEngine.WorkflowEngine | FlowStatusStoreTag\n >();\n const store = Context.get(context, FlowStatusStoreTag);\n const within = <A, E>(\n effect: Effect.Effect<A, E, WorkflowEngine.WorkflowEngine>,\n ) => Effect.provideContext(effect, context);\n\n const known = (flowId: string) =>\n Effect.flatMap(store.find(flowId), found =>\n Option.match(found, {\n onNone: () => Effect.fail(new FlowNotFound({ flowId })),\n onSome: Effect.succeed,\n }),\n );\n\n /** Completes the wait `record` is parked on, if `token` names it. */\n const complete = (token: string, outcome: unknown) => (record: FlowRecord) =>\n Effect.gen(function* () {\n
|
|
1
|
+
{"version":3,"sources":["../../src/engine/make-flow-engine.ts"],"sourcesContent":["import {\n type FlowEngine,\n FlowEngineTag,\n FlowNotFound,\n FlowSignalRejected,\n} from '@entifix/transactions';\nimport { Context, Effect, Layer, Option } from 'effect';\nimport { DurableDeferred, WorkflowEngine } from 'effect/workflow';\n\nimport {\n type InterpreterOptions,\n SlipWorkflow,\n SlipWorkflowLayer,\n waitFor,\n} from '../interpreter/slip-workflow.js';\nimport type { FlowRecord } from '../status/flow-status-store.js';\nimport { FlowStatusStoreTag } from '../status/flow-status-store.js';\n\n/**\n * The flow engine port over Effect Workflow\n * ([ADR 0016](../../../../../docs/adr/0016-a-flow-engine-port-and-a-durable-workflow-adapter.md)).\n * A start records the flow under its key and executes the slip detached — it\n * answers the flow id, never the outcome, which arrives as `flow.*` events and\n * in the status record. A signal is accepted only by a flow parked on exactly\n * that wait.\n */\nexport const makeFlowEngine = Effect.gen(function* () {\n const context = yield* Effect.context<\n WorkflowEngine.WorkflowEngine | FlowStatusStoreTag\n >();\n const store = Context.get(context, FlowStatusStoreTag);\n const within = <A, E>(\n effect: Effect.Effect<A, E, WorkflowEngine.WorkflowEngine>,\n ) => Effect.provideContext(effect, context);\n\n const known = (flowId: string) =>\n Effect.flatMap(store.find(flowId), found =>\n Option.match(found, {\n onNone: () => Effect.fail(new FlowNotFound({ flowId })),\n onSome: Effect.succeed,\n }),\n );\n\n /** Completes the wait `record` is parked on, if `token` names it. */\n const complete = (token: string, outcome: unknown) => (record: FlowRecord) =>\n Effect.gen(function* () {\n // Claimed before the wait is resumed, never after: the resumed flow\n // records its own states, and a `running` written behind them would put\n // a finished flow back to running with nothing left to correct it. The\n // claim is conditional, so a second signal racing this one is refused.\n const claimed = yield* store.claimWait(record.flowId, token);\n if (!claimed)\n return yield* Effect.fail(\n new FlowSignalRejected({ flowId: record.flowId, token }),\n );\n const deferred = waitFor(token);\n yield* within(\n DurableDeferred.succeed(deferred, {\n token: DurableDeferred.tokenFromExecutionId(deferred, {\n workflow: SlipWorkflow,\n executionId: record.executionId,\n }),\n value: outcome,\n }),\n ).pipe(\n // The wait was not resumed, so it is parked again and a retried signal\n // can claim it.\n Effect.onError(() =>\n store\n .update(record.flowId, {\n state: 'waiting',\n step: token,\n waitingFor: token,\n ...(record.correlation === undefined\n ? {}\n : { correlation: record.correlation }),\n })\n .pipe(Effect.ignore),\n ),\n );\n });\n\n const engine: FlowEngine = {\n start: (slip, idempotencyKey) =>\n Effect.gen(function* () {\n const payload = { key: idempotencyKey, slip };\n const executionId = yield* SlipWorkflow.executionId(payload);\n const record = yield* store.register(idempotencyKey, {\n flowId: slip.flowId,\n executionId,\n state: 'running',\n });\n if (record.executionId === executionId && record.flowId === slip.flowId)\n yield* within(SlipWorkflow.execute(payload, { discard: true }));\n return { flowId: record.flowId };\n }),\n\n status: flowId =>\n Effect.map(known(flowId), record => ({\n flowId: record.flowId,\n state: record.state,\n ...(record.step === undefined ? {} : { step: record.step }),\n })),\n\n signal: (flowId, token, outcome) =>\n Effect.flatMap(known(flowId), complete(token, outcome)),\n\n signalCorrelated: (correlation, token, outcome) =>\n Effect.gen(function* () {\n const found = yield* store.findByCorrelation(correlation);\n if (Option.isNone(found))\n return yield* Effect.fail(new FlowNotFound({ flowId: correlation }));\n return yield* complete(token, outcome)(found.value);\n }),\n\n interrupt: flowId =>\n Effect.gen(function* () {\n const record = yield* known(flowId);\n yield* within(SlipWorkflow.interrupt(record.executionId));\n yield* store.update(flowId, {\n state: 'interrupted',\n ...(record.step === undefined ? {} : { step: record.step }),\n });\n }),\n };\n return engine;\n});\n\n/**\n * The flow engine with the interpreter registered. Needs a workflow engine —\n * the in-memory one, or the cluster engine over Postgres from\n * `@entifix/workflow/postgres` — a status store, a participant client, the\n * outbox its events go through, and the breaker, bulkhead and retry budget\n * every call runs under.\n */\nexport const FlowEngineLayer = (options: InterpreterOptions) =>\n Layer.effect(FlowEngineTag, makeFlowEngine).pipe(\n Layer.provideMerge(SlipWorkflowLayer(options)),\n );\n"],"names":["FlowEngineTag","FlowNotFound","FlowSignalRejected","Context","Effect","Layer","Option","DurableDeferred","SlipWorkflow","SlipWorkflowLayer","waitFor","FlowStatusStoreTag","makeFlowEngine","gen","context","store","get","within","effect","provideContext","known","flowId","flatMap","find","found","match","onNone","fail","onSome","succeed","complete","token","outcome","record","claimed","claimWait","deferred","tokenFromExecutionId","workflow","executionId","value","pipe","onError","update","state","step","waitingFor","correlation","undefined","ignore","engine","start","slip","idempotencyKey","payload","key","register","execute","discard","status","map","signal","signalCorrelated","findByCorrelation","isNone","interrupt","FlowEngineLayer","options","provideMerge"],"mappings":";AAAA,SAEEA,aAAa,EACbC,YAAY,EACZC,kBAAkB,QACb,wBAAwB;AAC/B,SAASC,OAAO,EAAEC,MAAM,EAAEC,KAAK,EAAEC,MAAM,QAAQ,SAAS;AACxD,SAASC,eAAe,QAAwB,kBAAkB;AAElE,SAEEC,YAAY,EACZC,iBAAiB,EACjBC,OAAO,QACF,kCAAkC;AAEzC,SAASC,kBAAkB,QAAQ,iCAAiC;AAEpE;;;;;;;CAOC,GACD,OAAO,MAAMC,iBAAiBR,OAAOS,GAAG,CAAC;IACvC,MAAMC,UAAU,OAAOV,OAAOU,OAAO;IAGrC,MAAMC,QAAQZ,QAAQa,GAAG,CAACF,SAASH;IACnC,MAAMM,SAAS,CACbC,SACGd,OAAOe,cAAc,CAACD,QAAQJ;IAEnC,MAAMM,QAAQ,CAACC,SACbjB,OAAOkB,OAAO,CAACP,MAAMQ,IAAI,CAACF,SAASG,CAAAA,QACjClB,OAAOmB,KAAK,CAACD,OAAO;gBAClBE,QAAQ,IAAMtB,OAAOuB,IAAI,CAAC,IAAI1B,aAAa;wBAAEoB;oBAAO;gBACpDO,QAAQxB,OAAOyB,OAAO;YACxB;IAGJ,mEAAmE,GACnE,MAAMC,WAAW,CAACC,OAAeC,UAAqB,CAACC,SACrD7B,OAAOS,GAAG,CAAC;gBACT,oEAAoE;gBACpE,wEAAwE;gBACxE,uEAAuE;gBACvE,uEAAuE;gBACvE,MAAMqB,UAAU,OAAOnB,MAAMoB,SAAS,CAACF,OAAOZ,MAAM,EAAEU;gBACtD,IAAI,CAACG,SACH,OAAO,OAAO9B,OAAOuB,IAAI,CACvB,IAAIzB,mBAAmB;oBAAEmB,QAAQY,OAAOZ,MAAM;oBAAEU;gBAAM;gBAE1D,MAAMK,WAAW1B,QAAQqB;gBACzB,OAAOd,OACLV,gBAAgBsB,OAAO,CAACO,UAAU;oBAChCL,OAAOxB,gBAAgB8B,oBAAoB,CAACD,UAAU;wBACpDE,UAAU9B;wBACV+B,aAAaN,OAAOM,WAAW;oBACjC;oBACAC,OAAOR;gBACT,IACAS,IAAI,CACJ,uEAAuE;gBACvE,gBAAgB;gBAChBrC,OAAOsC,OAAO,CAAC,IACb3B,MACG4B,MAAM,CAACV,OAAOZ,MAAM,EAAE;wBACrBuB,OAAO;wBACPC,MAAMd;wBACNe,YAAYf;uBACRE,OAAOc,WAAW,KAAKC,YACvB,CAAC,IACD;wBAAED,aAAad,OAAOc,WAAW;oBAAC,IAEvCN,IAAI,CAACrC,OAAO6C,MAAM;YAG3B;IAEF,MAAMC,SAAqB;QACzBC,OAAO,CAACC,MAAMC,iBACZjD,OAAOS,GAAG,CAAC;gBACT,MAAMyC,UAAU;oBAAEC,KAAKF;oBAAgBD;gBAAK;gBAC5C,MAAMb,cAAc,OAAO/B,aAAa+B,WAAW,CAACe;gBACpD,MAAMrB,SAAS,OAAOlB,MAAMyC,QAAQ,CAACH,gBAAgB;oBACnDhC,QAAQ+B,KAAK/B,MAAM;oBACnBkB;oBACAK,OAAO;gBACT;gBACA,IAAIX,OAAOM,WAAW,KAAKA,eAAeN,OAAOZ,MAAM,KAAK+B,KAAK/B,MAAM,EACrE,OAAOJ,OAAOT,aAAaiD,OAAO,CAACH,SAAS;oBAAEI,SAAS;gBAAK;gBAC9D,OAAO;oBAAErC,QAAQY,OAAOZ,MAAM;gBAAC;YACjC;QAEFsC,QAAQtC,CAAAA,SACNjB,OAAOwD,GAAG,CAACxC,MAAMC,SAASY,CAAAA,SAAW;oBACnCZ,QAAQY,OAAOZ,MAAM;oBACrBuB,OAAOX,OAAOW,KAAK;mBACfX,OAAOY,IAAI,KAAKG,YAAY,CAAC,IAAI;oBAAEH,MAAMZ,OAAOY,IAAI;gBAAC;QAG7DgB,QAAQ,CAACxC,QAAQU,OAAOC,UACtB5B,OAAOkB,OAAO,CAACF,MAAMC,SAASS,SAASC,OAAOC;QAEhD8B,kBAAkB,CAACf,aAAahB,OAAOC,UACrC5B,OAAOS,GAAG,CAAC;gBACT,MAAMW,QAAQ,OAAOT,MAAMgD,iBAAiB,CAAChB;gBAC7C,IAAIzC,OAAO0D,MAAM,CAACxC,QAChB,OAAO,OAAOpB,OAAOuB,IAAI,CAAC,IAAI1B,aAAa;oBAAEoB,QAAQ0B;gBAAY;gBACnE,OAAO,OAAOjB,SAASC,OAAOC,SAASR,MAAMgB,KAAK;YACpD;QAEFyB,WAAW5C,CAAAA,SACTjB,OAAOS,GAAG,CAAC;gBACT,MAAMoB,SAAS,OAAOb,MAAMC;gBAC5B,OAAOJ,OAAOT,aAAayD,SAAS,CAAChC,OAAOM,WAAW;gBACvD,OAAOxB,MAAM4B,MAAM,CAACtB,QAAQ;oBAC1BuB,OAAO;mBACHX,OAAOY,IAAI,KAAKG,YAAY,CAAC,IAAI;oBAAEH,MAAMZ,OAAOY,IAAI;gBAAC;YAE7D;IACJ;IACA,OAAOK;AACT,GAAG;AAEH;;;;;;CAMC,GACD,OAAO,MAAMgB,kBAAkB,CAACC,UAC9B9D,MAAMa,MAAM,CAAClB,eAAeY,gBAAgB6B,IAAI,CAC9CpC,MAAM+D,YAAY,CAAC3D,kBAAkB0D,WACrC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-flow-status-store.d.ts","sourceRoot":"","sources":["../../src/postgres/sql-flow-status-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAEL,KAAK,eAAe,EACpB,kBAAkB,EACnB,MAAM,gCAAgC,CAAC;AAwBxC,kEAAkE;AAClE,eAAO,MAAM,qBAAqB,GAAI,KAAK,SAAS,CAAC,SAAS,qEAUZ,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GACjC,KAAK,SAAS,CAAC,SAAS,KACvB,
|
|
1
|
+
{"version":3,"file":"sql-flow-status-store.d.ts","sourceRoot":"","sources":["../../src/postgres/sql-flow-status-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAEL,KAAK,eAAe,EACpB,kBAAkB,EACnB,MAAM,gCAAgC,CAAC;AAwBxC,kEAAkE;AAClE,eAAO,MAAM,qBAAqB,GAAI,KAAK,SAAS,CAAC,SAAS,qEAUZ,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GACjC,KAAK,SAAS,CAAC,SAAS,KACvB,eAqDD,CAAC;AAEH,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,4FAOnC,CAAC"}
|
|
@@ -57,7 +57,12 @@ const fault = (error)=>translateSqlError(error);
|
|
|
57
57
|
waiting_for = ${(_patch_waitingFor = patch.waitingFor) != null ? _patch_waitingFor : null},
|
|
58
58
|
correlation = ${(_patch_correlation = patch.correlation) != null ? _patch_correlation : null}
|
|
59
59
|
WHERE flow_id = ${flowId}`.pipe(Effect.asVoid, Effect.mapError(fault));
|
|
60
|
-
}
|
|
60
|
+
},
|
|
61
|
+
claimWait: (flowId, token)=>sql`
|
|
62
|
+
UPDATE entifix_flow_status
|
|
63
|
+
SET state = 'running', step = ${token}, waiting_for = NULL
|
|
64
|
+
WHERE flow_id = ${flowId} AND state = 'waiting' AND waiting_for = ${token}
|
|
65
|
+
RETURNING flow_id`.pipe(Effect.map((rows)=>rows.length > 0), Effect.mapError(fault))
|
|
61
66
|
});
|
|
62
67
|
/** The SQL status store, its table ensured as the layer builds. */ export const SqlFlowStatusStoreLayer = Layer.effect(FlowStatusStoreTag, Effect.gen(function*() {
|
|
63
68
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/postgres/sql-flow-status-store.ts"],"sourcesContent":["import { translateSqlError } from '@entifix/sql';\nimport type { FlowState } from '@entifix/transactions';\nimport { Effect, Layer, Option } from 'effect';\nimport { SqlClient } from 'effect/sql';\n\nimport {\n type FlowRecord,\n type FlowStatusStore,\n FlowStatusStoreTag,\n} from '../status/flow-status-store.js';\n\n// A type alias, not an interface: a row must be assignable to the record a\n// driver hands back, and an interface has no index signature.\ntype FlowStatusRow = {\n readonly flow_id: string;\n readonly execution_id: string;\n readonly state: FlowState;\n readonly step: string | null;\n readonly waiting_for: string | null;\n readonly correlation: string | null;\n};\n\nconst recordOf = (row: FlowStatusRow): FlowRecord => ({\n flowId: row.flow_id,\n executionId: row.execution_id,\n state: row.state,\n ...(row.step === null ? {} : { step: row.step }),\n ...(row.waiting_for === null ? {} : { waitingFor: row.waiting_for }),\n ...(row.correlation === null ? {} : { correlation: row.correlation }),\n});\n\nconst fault = (error: unknown) => translateSqlError(error);\n\n/** Creates the flow status table if it is missing. Idempotent. */\nexport const ensureFlowStatusTable = (sql: SqlClient.SqlClient) =>\n sql`\n CREATE TABLE IF NOT EXISTS entifix_flow_status (\n flow_id text PRIMARY KEY,\n idempotency_key text NOT NULL UNIQUE,\n execution_id text NOT NULL,\n state text NOT NULL,\n step text,\n waiting_for text,\n correlation text\n )`.pipe(Effect.asVoid, Effect.mapError(fault));\n\n/**\n * The flow status store over SQL, beside the workflow engine's own tables. A\n * registration inserts under the key or answers the flow already there, in one\n * statement and a read.\n */\nexport const makeSqlFlowStatusStore = (\n sql: SqlClient.SqlClient,\n): FlowStatusStore => ({\n register: (key, record) =>\n Effect.gen(function* () {\n const [inserted] = yield* sql<FlowStatusRow>`\n INSERT INTO entifix_flow_status\n (flow_id, idempotency_key, execution_id, state)\n VALUES (${record.flowId}, ${key}, ${record.executionId}, ${record.state})\n ON CONFLICT DO NOTHING\n RETURNING *`;\n if (inserted !== undefined) return recordOf(inserted);\n const [existing] = yield* sql<FlowStatusRow>`\n SELECT * FROM entifix_flow_status\n WHERE idempotency_key = ${key} OR flow_id = ${record.flowId}`;\n // Gone between the two statements cannot happen: nothing deletes a flow.\n return recordOf(existing as FlowStatusRow);\n }).pipe(Effect.mapError(fault)),\n\n find: flowId =>\n sql<FlowStatusRow>`\n SELECT * FROM entifix_flow_status WHERE flow_id = ${flowId}`.pipe(\n Effect.map(([row]) =>\n row === undefined ? Option.none() : Option.some(recordOf(row)),\n ),\n Effect.mapError(fault),\n ),\n\n findByCorrelation: correlation =>\n sql<FlowStatusRow>`\n SELECT * FROM entifix_flow_status\n WHERE correlation = ${correlation}`.pipe(\n Effect.map(([row]) =>\n row === undefined ? Option.none() : Option.some(recordOf(row)),\n ),\n Effect.mapError(fault),\n ),\n\n update: (flowId, patch) =>\n sql`\n UPDATE entifix_flow_status\n SET state = ${patch.state}, step = ${patch.step ?? null},\n waiting_for = ${patch.waitingFor ?? null},\n correlation = ${patch.correlation ?? null}\n WHERE flow_id = ${flowId}`.pipe(Effect.asVoid, Effect.mapError(fault)),\n});\n\n/** The SQL status store, its table ensured as the layer builds. */\nexport const SqlFlowStatusStoreLayer = Layer.effect(\n FlowStatusStoreTag,\n Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n yield* ensureFlowStatusTable(sql);\n return makeSqlFlowStatusStore(sql);\n }),\n);\n"],"names":["translateSqlError","Effect","Layer","Option","SqlClient","FlowStatusStoreTag","recordOf","row","flowId","flow_id","executionId","execution_id","state","step","waiting_for","waitingFor","correlation","fault","error","ensureFlowStatusTable","sql","pipe","asVoid","mapError","makeSqlFlowStatusStore","register","key","record","gen","inserted","undefined","existing","find","map","none","some","findByCorrelation","update","patch","SqlFlowStatusStoreLayer","effect"],"mappings":";AAAA,SAASA,iBAAiB,QAAQ,eAAe;AAEjD,SAASC,MAAM,EAAEC,KAAK,EAAEC,MAAM,QAAQ,SAAS;AAC/C,SAASC,SAAS,QAAQ,aAAa;AAEvC,SAGEC,kBAAkB,QACb,iCAAiC;AAaxC,MAAMC,WAAW,CAACC,MAAoC;QACpDC,QAAQD,IAAIE,OAAO;QACnBC,aAAaH,IAAII,YAAY;QAC7BC,OAAOL,IAAIK,KAAK;OACZL,IAAIM,IAAI,KAAK,OAAO,CAAC,IAAI;QAAEA,MAAMN,IAAIM,IAAI;IAAC,GAC1CN,IAAIO,WAAW,KAAK,OAAO,CAAC,IAAI;QAAEC,YAAYR,IAAIO,WAAW;IAAC,GAC9DP,IAAIS,WAAW,KAAK,OAAO,CAAC,IAAI;QAAEA,aAAaT,IAAIS,WAAW;IAAC;AAGrE,MAAMC,QAAQ,CAACC,QAAmBlB,kBAAkBkB;AAEpD,gEAAgE,GAChE,OAAO,MAAMC,wBAAwB,CAACC,MACpCA,GAAG,CAAC;;;;;;;;;KASD,CAAC,CAACC,IAAI,CAACpB,OAAOqB,MAAM,EAAErB,OAAOsB,QAAQ,CAACN,QAAQ;AAEnD;;;;CAIC,GACD,OAAO,MAAMO,yBAAyB,CACpCJ,MACqB,CAAA;QACrBK,UAAU,CAACC,KAAKC,SACd1B,OAAO2B,GAAG,CAAC;gBACT,MAAM,CAACC,SAAS,GAAG,OAAOT,GAAkB,CAAC;;;gBAGnC,EAAEO,OAAOnB,MAAM,CAAC,EAAE,EAAEkB,IAAI,EAAE,EAAEC,OAAOjB,WAAW,CAAC,EAAE,EAAEiB,OAAOf,KAAK,CAAC;;mBAE7D,CAAC;gBACd,IAAIiB,aAAaC,WAAW,OAAOxB,SAASuB;gBAC5C,MAAM,CAACE,SAAS,GAAG,OAAOX,GAAkB,CAAC;;gCAEnB,EAAEM,IAAI,cAAc,EAAEC,OAAOnB,MAAM,CAAC,CAAC;gBAC/D,yEAAyE;gBACzE,OAAOF,SAASyB;YAClB,GAAGV,IAAI,CAACpB,OAAOsB,QAAQ,CAACN;QAE1Be,MAAMxB,CAAAA,SACJY,GAAkB,CAAC;wDACiC,EAAEZ,OAAO,CAAC,CAACa,IAAI,CACjEpB,OAAOgC,GAAG,CAAC,CAAC,CAAC1B,IAAI,GACfA,QAAQuB,YAAY3B,OAAO+B,IAAI,KAAK/B,OAAOgC,IAAI,CAAC7B,SAASC,QAE3DN,OAAOsB,QAAQ,CAACN;QAGpBmB,mBAAmBpB,CAAAA,cACjBI,GAAkB,CAAC;;0BAEG,EAAEJ,YAAY,CAAC,CAACK,IAAI,CACxCpB,OAAOgC,GAAG,CAAC,CAAC,CAAC1B,IAAI,GACfA,QAAQuB,YAAY3B,OAAO+B,IAAI,KAAK/B,OAAOgC,IAAI,CAAC7B,SAASC,QAE3DN,OAAOsB,QAAQ,CAACN;QAGpBoB,QAAQ,CAAC7B,QAAQ8B;gBAGwBA,aACjBA,mBACAA;mBAJtBlB,GAAG,CAAC;;kBAEU,EAAEkB,MAAM1B,KAAK,CAAC,SAAS,GAAE0B,cAAAA,MAAMzB,IAAI,YAAVyB,cAAc,KAAK;wBACtC,GAAEA,oBAAAA,MAAMvB,UAAU,YAAhBuB,oBAAoB,KAAK;wBAC3B,GAAEA,qBAAAA,MAAMtB,WAAW,YAAjBsB,qBAAqB,KAAK;sBAC9B,EAAE9B,OAAO,CAAC,CAACa,IAAI,CAACpB,OAAOqB,MAAM,EAAErB,OAAOsB,QAAQ,CAACN;;
|
|
1
|
+
{"version":3,"sources":["../../src/postgres/sql-flow-status-store.ts"],"sourcesContent":["import { translateSqlError } from '@entifix/sql';\nimport type { FlowState } from '@entifix/transactions';\nimport { Effect, Layer, Option } from 'effect';\nimport { SqlClient } from 'effect/sql';\n\nimport {\n type FlowRecord,\n type FlowStatusStore,\n FlowStatusStoreTag,\n} from '../status/flow-status-store.js';\n\n// A type alias, not an interface: a row must be assignable to the record a\n// driver hands back, and an interface has no index signature.\ntype FlowStatusRow = {\n readonly flow_id: string;\n readonly execution_id: string;\n readonly state: FlowState;\n readonly step: string | null;\n readonly waiting_for: string | null;\n readonly correlation: string | null;\n};\n\nconst recordOf = (row: FlowStatusRow): FlowRecord => ({\n flowId: row.flow_id,\n executionId: row.execution_id,\n state: row.state,\n ...(row.step === null ? {} : { step: row.step }),\n ...(row.waiting_for === null ? {} : { waitingFor: row.waiting_for }),\n ...(row.correlation === null ? {} : { correlation: row.correlation }),\n});\n\nconst fault = (error: unknown) => translateSqlError(error);\n\n/** Creates the flow status table if it is missing. Idempotent. */\nexport const ensureFlowStatusTable = (sql: SqlClient.SqlClient) =>\n sql`\n CREATE TABLE IF NOT EXISTS entifix_flow_status (\n flow_id text PRIMARY KEY,\n idempotency_key text NOT NULL UNIQUE,\n execution_id text NOT NULL,\n state text NOT NULL,\n step text,\n waiting_for text,\n correlation text\n )`.pipe(Effect.asVoid, Effect.mapError(fault));\n\n/**\n * The flow status store over SQL, beside the workflow engine's own tables. A\n * registration inserts under the key or answers the flow already there, in one\n * statement and a read.\n */\nexport const makeSqlFlowStatusStore = (\n sql: SqlClient.SqlClient,\n): FlowStatusStore => ({\n register: (key, record) =>\n Effect.gen(function* () {\n const [inserted] = yield* sql<FlowStatusRow>`\n INSERT INTO entifix_flow_status\n (flow_id, idempotency_key, execution_id, state)\n VALUES (${record.flowId}, ${key}, ${record.executionId}, ${record.state})\n ON CONFLICT DO NOTHING\n RETURNING *`;\n if (inserted !== undefined) return recordOf(inserted);\n const [existing] = yield* sql<FlowStatusRow>`\n SELECT * FROM entifix_flow_status\n WHERE idempotency_key = ${key} OR flow_id = ${record.flowId}`;\n // Gone between the two statements cannot happen: nothing deletes a flow.\n return recordOf(existing as FlowStatusRow);\n }).pipe(Effect.mapError(fault)),\n\n find: flowId =>\n sql<FlowStatusRow>`\n SELECT * FROM entifix_flow_status WHERE flow_id = ${flowId}`.pipe(\n Effect.map(([row]) =>\n row === undefined ? Option.none() : Option.some(recordOf(row)),\n ),\n Effect.mapError(fault),\n ),\n\n findByCorrelation: correlation =>\n sql<FlowStatusRow>`\n SELECT * FROM entifix_flow_status\n WHERE correlation = ${correlation}`.pipe(\n Effect.map(([row]) =>\n row === undefined ? Option.none() : Option.some(recordOf(row)),\n ),\n Effect.mapError(fault),\n ),\n\n update: (flowId, patch) =>\n sql`\n UPDATE entifix_flow_status\n SET state = ${patch.state}, step = ${patch.step ?? null},\n waiting_for = ${patch.waitingFor ?? null},\n correlation = ${patch.correlation ?? null}\n WHERE flow_id = ${flowId}`.pipe(Effect.asVoid, Effect.mapError(fault)),\n\n claimWait: (flowId, token) =>\n sql<{ readonly flow_id: string }>`\n UPDATE entifix_flow_status\n SET state = 'running', step = ${token}, waiting_for = NULL\n WHERE flow_id = ${flowId} AND state = 'waiting' AND waiting_for = ${token}\n RETURNING flow_id`.pipe(\n Effect.map(rows => rows.length > 0),\n Effect.mapError(fault),\n ),\n});\n\n/** The SQL status store, its table ensured as the layer builds. */\nexport const SqlFlowStatusStoreLayer = Layer.effect(\n FlowStatusStoreTag,\n Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n yield* ensureFlowStatusTable(sql);\n return makeSqlFlowStatusStore(sql);\n }),\n);\n"],"names":["translateSqlError","Effect","Layer","Option","SqlClient","FlowStatusStoreTag","recordOf","row","flowId","flow_id","executionId","execution_id","state","step","waiting_for","waitingFor","correlation","fault","error","ensureFlowStatusTable","sql","pipe","asVoid","mapError","makeSqlFlowStatusStore","register","key","record","gen","inserted","undefined","existing","find","map","none","some","findByCorrelation","update","patch","claimWait","token","rows","length","SqlFlowStatusStoreLayer","effect"],"mappings":"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"}
|
|
@@ -38,6 +38,14 @@ export interface FlowStatusStore {
|
|
|
38
38
|
/** The flow parked on a wait correlated with `correlation`, if one is. */
|
|
39
39
|
readonly findByCorrelation: (correlation: string) => Effect.Effect<Option.Option<FlowRecord>, StoreFailure>;
|
|
40
40
|
readonly update: (flowId: string, patch: FlowRecordPatch) => Effect.Effect<void, StoreFailure>;
|
|
41
|
+
/**
|
|
42
|
+
* Moves a flow parked on `token` to `running`, and answers whether it was
|
|
43
|
+
* parked on it. The one conditional write a signal makes, and it is made
|
|
44
|
+
* **before** the wait is resumed: written after, it could land behind the
|
|
45
|
+
* resumed flow's own writes and put a finished flow back to `running` for
|
|
46
|
+
* good. Two signals racing one wait claim it once.
|
|
47
|
+
*/
|
|
48
|
+
readonly claimWait: (flowId: string, token: string) => Effect.Effect<boolean, StoreFailure>;
|
|
41
49
|
}
|
|
42
50
|
/**
|
|
43
51
|
* Every state the interpreter journals a flow into, by state and the slice it
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-status-store.d.ts","sourceRoot":"","sources":["../../src/status/flow-status-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,CACjB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,UAAU,KACf,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,CACb,MAAM,EAAE,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,YAAY,CAAC,CAAC;IAC5D,0EAA0E;IAC1E,QAAQ,CAAC,iBAAiB,EAAE,CAC1B,WAAW,EAAE,MAAM,KAChB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,YAAY,CAAC,CAAC;IAC5D,QAAQ,CAAC,MAAM,EAAE,CACf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,eAAe,KACnB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"flow-status-store.d.ts","sourceRoot":"","sources":["../../src/status/flow-status-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,CACjB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,UAAU,KACf,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,CACb,MAAM,EAAE,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,YAAY,CAAC,CAAC;IAC5D,0EAA0E;IAC1E,QAAQ,CAAC,iBAAiB,EAAE,CAC1B,WAAW,EAAE,MAAM,KAChB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,YAAY,CAAC,CAAC;IAC5D,QAAQ,CAAC,MAAM,EAAE,CACf,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,eAAe,KACnB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IACvC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,CAClB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,KACV,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,wBAErB,CAAC;;AAEH,qBAAa,kBAAmB,SAAQ,uBAGA;CAAG;AAE3C;;;GAGG;AACH,eAAO,MAAM,2BAA2B,QAAO,eAwD9C,CAAC"}
|
|
@@ -43,6 +43,19 @@ export class FlowStatusStoreTag extends Context.Service()('@entifix/workflow/Flo
|
|
|
43
43
|
}, patch.correlation === undefined ? {} : {
|
|
44
44
|
correlation: patch.correlation
|
|
45
45
|
}));
|
|
46
|
+
}),
|
|
47
|
+
claimWait: (flowId, token)=>Effect.sync(()=>{
|
|
48
|
+
const current = records.get(flowId);
|
|
49
|
+
if ((current == null ? void 0 : current.state) !== 'waiting' || current.waitingFor !== token) return false;
|
|
50
|
+
records.set(flowId, _extends({
|
|
51
|
+
flowId: current.flowId,
|
|
52
|
+
executionId: current.executionId,
|
|
53
|
+
state: 'running',
|
|
54
|
+
step: token
|
|
55
|
+
}, current.correlation === undefined ? {} : {
|
|
56
|
+
correlation: current.correlation
|
|
57
|
+
}));
|
|
58
|
+
return true;
|
|
46
59
|
})
|
|
47
60
|
};
|
|
48
61
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/status/flow-status-store.ts"],"sourcesContent":["import type { StoreFailure } from '@entifix/core';\nimport type { FlowState } from '@entifix/transactions';\nimport { Context, Effect, Metric, Option } from 'effect';\n\n/**\n * What the engine knows about one flow beyond its journal: the state a reader\n * is answered with, the step it is at, and the wait it is parked on.\n *\n * Kept beside the journal because the journal answers \"did it finish\" and\n * nothing finer — a flow parked on a signal and one mid-call look the same to\n * it, and a signal must be refused unless the flow is waiting for exactly it.\n */\nexport interface FlowRecord {\n readonly flowId: string;\n /** The workflow execution the flow runs as. */\n readonly executionId: string;\n readonly state: FlowState;\n readonly step?: string;\n /** The step whose signal the flow waits for, while `state` is `waiting`. */\n readonly waitingFor?: string;\n /** What that wait is correlated with — the order a delivery flow follows. */\n readonly correlation?: string;\n}\n\n/** A change to a flow's record: its new state, and where it now stands. */\nexport interface FlowRecordPatch {\n readonly state: FlowState;\n readonly step?: string;\n readonly waitingFor?: string;\n readonly correlation?: string;\n}\n\nexport interface FlowStatusStore {\n /**\n * Records a flow under its idempotency key, unless the key already names\n * one — then that flow is answered and nothing is written, so a repeated\n * start finds the flow the first one started.\n */\n readonly register: (\n key: string,\n record: FlowRecord,\n ) => Effect.Effect<FlowRecord, StoreFailure>;\n readonly find: (\n flowId: string,\n ) => Effect.Effect<Option.Option<FlowRecord>, StoreFailure>;\n /** The flow parked on a wait correlated with `correlation`, if one is. */\n readonly findByCorrelation: (\n correlation: string,\n ) => Effect.Effect<Option.Option<FlowRecord>, StoreFailure>;\n readonly update: (\n flowId: string,\n patch: FlowRecordPatch,\n ) => Effect.Effect<void, StoreFailure>;\n}\n\n/**\n * Every state the interpreter journals a flow into, by state and the slice it\n * runs for. Counted inside the Activity that records it, so a replay — which\n * reads the journal rather than executing again — never counts a state twice.\n * `suspended` is the series to alert on: a flow past its commit point that\n * cannot go on, which a person has to look at.\n */\nexport const flowStates = Metric.counter('entifix_flow_states_total', {\n description: 'Flow states the interpreter journaled, by state and source.',\n});\n\nexport class FlowStatusStoreTag extends Context.Service<\n FlowStatusStoreTag,\n FlowStatusStore\n>()('@entifix/workflow/FlowStatusStore') {}\n\n/**\n * A status store in memory, for a single process that keeps no flow past its\n * life — specs, and the in-memory workflow engine.\n */\nexport const makeInMemoryFlowStatusStore = (): FlowStatusStore => {\n const byKey = new Map<string, string>();\n const records = new Map<string, FlowRecord>();\n return {\n register: (key, record) =>\n Effect.sync(() => {\n const known = byKey.get(key);\n if (known !== undefined) return records.get(known) as FlowRecord;\n byKey.set(key, record.flowId);\n records.set(record.flowId, record);\n return record;\n }),\n find: flowId =>\n Effect.sync(() => Option.fromNullishOr(records.get(flowId))),\n findByCorrelation: correlation =>\n Effect.sync(() =>\n Option.fromNullishOr(\n [...records.values()].find(\n record => record.correlation === correlation,\n ),\n ),\n ),\n update: (flowId, patch) =>\n Effect.sync(() => {\n const current = records.get(flowId);\n if (current === undefined) return;\n records.set(flowId, {\n flowId: current.flowId,\n executionId: current.executionId,\n state: patch.state,\n ...(patch.step === undefined ? {} : { step: patch.step }),\n ...(patch.waitingFor === undefined\n ? {}\n : { waitingFor: patch.waitingFor }),\n ...(patch.correlation === undefined\n ? {}\n : { correlation: patch.correlation }),\n });\n }),\n };\n};\n"],"names":["Context","Effect","Metric","Option","flowStates","counter","description","FlowStatusStoreTag","Service","makeInMemoryFlowStatusStore","byKey","Map","records","register","key","record","sync","known","get","undefined","set","flowId","find","fromNullishOr","findByCorrelation","correlation","values","update","patch","current","executionId","state","step","waitingFor"],"mappings":";AAEA,SAASA,OAAO,EAAEC,MAAM,EAAEC,MAAM,EAAEC,MAAM,QAAQ,SAAS;
|
|
1
|
+
{"version":3,"sources":["../../src/status/flow-status-store.ts"],"sourcesContent":["import type { StoreFailure } from '@entifix/core';\nimport type { FlowState } from '@entifix/transactions';\nimport { Context, Effect, Metric, Option } from 'effect';\n\n/**\n * What the engine knows about one flow beyond its journal: the state a reader\n * is answered with, the step it is at, and the wait it is parked on.\n *\n * Kept beside the journal because the journal answers \"did it finish\" and\n * nothing finer — a flow parked on a signal and one mid-call look the same to\n * it, and a signal must be refused unless the flow is waiting for exactly it.\n */\nexport interface FlowRecord {\n readonly flowId: string;\n /** The workflow execution the flow runs as. */\n readonly executionId: string;\n readonly state: FlowState;\n readonly step?: string;\n /** The step whose signal the flow waits for, while `state` is `waiting`. */\n readonly waitingFor?: string;\n /** What that wait is correlated with — the order a delivery flow follows. */\n readonly correlation?: string;\n}\n\n/** A change to a flow's record: its new state, and where it now stands. */\nexport interface FlowRecordPatch {\n readonly state: FlowState;\n readonly step?: string;\n readonly waitingFor?: string;\n readonly correlation?: string;\n}\n\nexport interface FlowStatusStore {\n /**\n * Records a flow under its idempotency key, unless the key already names\n * one — then that flow is answered and nothing is written, so a repeated\n * start finds the flow the first one started.\n */\n readonly register: (\n key: string,\n record: FlowRecord,\n ) => Effect.Effect<FlowRecord, StoreFailure>;\n readonly find: (\n flowId: string,\n ) => Effect.Effect<Option.Option<FlowRecord>, StoreFailure>;\n /** The flow parked on a wait correlated with `correlation`, if one is. */\n readonly findByCorrelation: (\n correlation: string,\n ) => Effect.Effect<Option.Option<FlowRecord>, StoreFailure>;\n readonly update: (\n flowId: string,\n patch: FlowRecordPatch,\n ) => Effect.Effect<void, StoreFailure>;\n /**\n * Moves a flow parked on `token` to `running`, and answers whether it was\n * parked on it. The one conditional write a signal makes, and it is made\n * **before** the wait is resumed: written after, it could land behind the\n * resumed flow's own writes and put a finished flow back to `running` for\n * good. Two signals racing one wait claim it once.\n */\n readonly claimWait: (\n flowId: string,\n token: string,\n ) => Effect.Effect<boolean, StoreFailure>;\n}\n\n/**\n * Every state the interpreter journals a flow into, by state and the slice it\n * runs for. Counted inside the Activity that records it, so a replay — which\n * reads the journal rather than executing again — never counts a state twice.\n * `suspended` is the series to alert on: a flow past its commit point that\n * cannot go on, which a person has to look at.\n */\nexport const flowStates = Metric.counter('entifix_flow_states_total', {\n description: 'Flow states the interpreter journaled, by state and source.',\n});\n\nexport class FlowStatusStoreTag extends Context.Service<\n FlowStatusStoreTag,\n FlowStatusStore\n>()('@entifix/workflow/FlowStatusStore') {}\n\n/**\n * A status store in memory, for a single process that keeps no flow past its\n * life — specs, and the in-memory workflow engine.\n */\nexport const makeInMemoryFlowStatusStore = (): FlowStatusStore => {\n const byKey = new Map<string, string>();\n const records = new Map<string, FlowRecord>();\n return {\n register: (key, record) =>\n Effect.sync(() => {\n const known = byKey.get(key);\n if (known !== undefined) return records.get(known) as FlowRecord;\n byKey.set(key, record.flowId);\n records.set(record.flowId, record);\n return record;\n }),\n find: flowId =>\n Effect.sync(() => Option.fromNullishOr(records.get(flowId))),\n findByCorrelation: correlation =>\n Effect.sync(() =>\n Option.fromNullishOr(\n [...records.values()].find(\n record => record.correlation === correlation,\n ),\n ),\n ),\n update: (flowId, patch) =>\n Effect.sync(() => {\n const current = records.get(flowId);\n if (current === undefined) return;\n records.set(flowId, {\n flowId: current.flowId,\n executionId: current.executionId,\n state: patch.state,\n ...(patch.step === undefined ? {} : { step: patch.step }),\n ...(patch.waitingFor === undefined\n ? {}\n : { waitingFor: patch.waitingFor }),\n ...(patch.correlation === undefined\n ? {}\n : { correlation: patch.correlation }),\n });\n }),\n claimWait: (flowId, token) =>\n Effect.sync(() => {\n const current = records.get(flowId);\n if (current?.state !== 'waiting' || current.waitingFor !== token)\n return false;\n records.set(flowId, {\n flowId: current.flowId,\n executionId: current.executionId,\n state: 'running',\n step: token,\n ...(current.correlation === undefined\n ? {}\n : { correlation: current.correlation }),\n });\n return true;\n }),\n };\n};\n"],"names":["Context","Effect","Metric","Option","flowStates","counter","description","FlowStatusStoreTag","Service","makeInMemoryFlowStatusStore","byKey","Map","records","register","key","record","sync","known","get","undefined","set","flowId","find","fromNullishOr","findByCorrelation","correlation","values","update","patch","current","executionId","state","step","waitingFor","claimWait","token"],"mappings":";AAEA,SAASA,OAAO,EAAEC,MAAM,EAAEC,MAAM,EAAEC,MAAM,QAAQ,SAAS;AAgEzD;;;;;;CAMC,GACD,OAAO,MAAMC,aAAaF,OAAOG,OAAO,CAAC,6BAA6B;IACpEC,aAAa;AACf,GAAG;AAEH,OAAO,MAAMC,2BAA2BP,QAAQQ,OAAO,GAGnD;AAAsC;AAE1C;;;CAGC,GACD,OAAO,MAAMC,8BAA8B;IACzC,MAAMC,QAAQ,IAAIC;IAClB,MAAMC,UAAU,IAAID;IACpB,OAAO;QACLE,UAAU,CAACC,KAAKC,SACdd,OAAOe,IAAI,CAAC;gBACV,MAAMC,QAAQP,MAAMQ,GAAG,CAACJ;gBACxB,IAAIG,UAAUE,WAAW,OAAOP,QAAQM,GAAG,CAACD;gBAC5CP,MAAMU,GAAG,CAACN,KAAKC,OAAOM,MAAM;gBAC5BT,QAAQQ,GAAG,CAACL,OAAOM,MAAM,EAAEN;gBAC3B,OAAOA;YACT;QACFO,MAAMD,CAAAA,SACJpB,OAAOe,IAAI,CAAC,IAAMb,OAAOoB,aAAa,CAACX,QAAQM,GAAG,CAACG;QACrDG,mBAAmBC,CAAAA,cACjBxB,OAAOe,IAAI,CAAC,IACVb,OAAOoB,aAAa,CAClB;uBAAIX,QAAQc,MAAM;iBAAG,CAACJ,IAAI,CACxBP,CAAAA,SAAUA,OAAOU,WAAW,KAAKA;QAIzCE,QAAQ,CAACN,QAAQO,QACf3B,OAAOe,IAAI,CAAC;gBACV,MAAMa,UAAUjB,QAAQM,GAAG,CAACG;gBAC5B,IAAIQ,YAAYV,WAAW;gBAC3BP,QAAQQ,GAAG,CAACC,QAAQ;oBAClBA,QAAQQ,QAAQR,MAAM;oBACtBS,aAAaD,QAAQC,WAAW;oBAChCC,OAAOH,MAAMG,KAAK;mBACdH,MAAMI,IAAI,KAAKb,YAAY,CAAC,IAAI;oBAAEa,MAAMJ,MAAMI,IAAI;gBAAC,GACnDJ,MAAMK,UAAU,KAAKd,YACrB,CAAC,IACD;oBAAEc,YAAYL,MAAMK,UAAU;gBAAC,GAC/BL,MAAMH,WAAW,KAAKN,YACtB,CAAC,IACD;oBAAEM,aAAaG,MAAMH,WAAW;gBAAC;YAEzC;QACFS,WAAW,CAACb,QAAQc,QAClBlC,OAAOe,IAAI,CAAC;gBACV,MAAMa,UAAUjB,QAAQM,GAAG,CAACG;gBAC5B,IAAIQ,CAAAA,2BAAAA,QAASE,KAAK,MAAK,aAAaF,QAAQI,UAAU,KAAKE,OACzD,OAAO;gBACTvB,QAAQQ,GAAG,CAACC,QAAQ;oBAClBA,QAAQQ,QAAQR,MAAM;oBACtBS,aAAaD,QAAQC,WAAW;oBAChCC,OAAO;oBACPC,MAAMG;mBACFN,QAAQJ,WAAW,KAAKN,YACxB,CAAC,IACD;oBAAEM,aAAaI,QAAQJ,WAAW;gBAAC;gBAEzC,OAAO;YACT;IACJ;AACF,EAAE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Durable flows: the slip interpreter on Effect Workflow, behind the flow engine port, with Postgres storage and socket runners.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -89,16 +89,16 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@swc/helpers": "0.5.23",
|
|
92
|
-
"@entifix/business": "0.
|
|
93
|
-
"@entifix/
|
|
94
|
-
"@entifix/
|
|
95
|
-
"@entifix/transactions": "0.
|
|
92
|
+
"@entifix/business": "0.13.0",
|
|
93
|
+
"@entifix/core": "0.13.0",
|
|
94
|
+
"@entifix/sql": "0.13.0",
|
|
95
|
+
"@entifix/transactions": "0.13.0"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@effect/platform-node": "4.0.6",
|
|
99
99
|
"@effect/sql-pg": "4.0.6",
|
|
100
100
|
"effect": "4.0.6",
|
|
101
|
-
"@entifix/testing-unit": "0.
|
|
101
|
+
"@entifix/testing-unit": "0.13.0"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
104
|
"@effect/platform-node": "^4.0.6",
|