@yieldstar/core 0.5.0 → 0.6.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.
Files changed (64) hide show
  1. package/dist/base/durable-value.d.ts +71 -0
  2. package/dist/base/durable-value.d.ts.map +1 -0
  3. package/dist/base/durable-value.test.d.ts +2 -0
  4. package/dist/base/durable-value.test.d.ts.map +1 -0
  5. package/dist/base/effect.d.ts +136 -0
  6. package/dist/base/effect.d.ts.map +1 -0
  7. package/dist/base/errors.d.ts +23 -0
  8. package/dist/base/errors.d.ts.map +1 -0
  9. package/dist/base/errors.test.d.ts +2 -0
  10. package/dist/base/errors.test.d.ts.map +1 -0
  11. package/dist/base/event.d.ts +6 -1
  12. package/dist/base/event.d.ts.map +1 -1
  13. package/dist/base/execution.d.ts +244 -0
  14. package/dist/base/execution.d.ts.map +1 -0
  15. package/dist/base/heap.d.ts +15 -3
  16. package/dist/base/heap.d.ts.map +1 -1
  17. package/dist/base/invoker.d.ts +47 -2
  18. package/dist/base/invoker.d.ts.map +1 -1
  19. package/dist/base/runtime.d.ts +27 -4
  20. package/dist/base/runtime.d.ts.map +1 -1
  21. package/dist/base/step.d.ts +21 -5
  22. package/dist/base/step.d.ts.map +1 -1
  23. package/dist/base/store.d.ts +135 -21
  24. package/dist/base/store.d.ts.map +1 -1
  25. package/dist/base/workflow.d.ts +15 -0
  26. package/dist/base/workflow.d.ts.map +1 -1
  27. package/dist/index.d.ts +23 -6
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1647 -172
  30. package/dist/lib/effect-state.d.ts +78 -0
  31. package/dist/lib/effect-state.d.ts.map +1 -0
  32. package/dist/lib/execution-dispatcher.d.ts +102 -0
  33. package/dist/lib/execution-dispatcher.d.ts.map +1 -0
  34. package/dist/lib/execution-state.d.ts +248 -0
  35. package/dist/lib/execution-state.d.ts.map +1 -0
  36. package/dist/lib/execution-state.test.d.ts +2 -0
  37. package/dist/lib/execution-state.test.d.ts.map +1 -0
  38. package/dist/lib/wait-for-outcome.d.ts +15 -0
  39. package/dist/lib/wait-for-outcome.d.ts.map +1 -0
  40. package/dist/lib/workflow-runner.d.ts +14 -9
  41. package/dist/lib/workflow-runner.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/base/durable-value.test.ts +285 -0
  44. package/src/base/durable-value.ts +442 -0
  45. package/src/base/effect.ts +165 -0
  46. package/src/base/errors.test.ts +210 -0
  47. package/src/base/errors.ts +72 -0
  48. package/src/base/event.ts +6 -1
  49. package/src/base/execution.ts +303 -0
  50. package/src/base/heap.ts +17 -3
  51. package/src/base/invoker.ts +60 -2
  52. package/src/base/runtime.ts +28 -7
  53. package/src/base/step.ts +29 -7
  54. package/src/base/store.ts +339 -49
  55. package/src/base/workflow.ts +15 -0
  56. package/src/index.ts +137 -3
  57. package/src/lib/effect-state.ts +254 -0
  58. package/src/lib/execution-dispatcher.ts +393 -0
  59. package/src/lib/execution-state.test.ts +249 -0
  60. package/src/lib/execution-state.ts +650 -0
  61. package/src/lib/wait-for-outcome.ts +40 -0
  62. package/src/lib/workflow-runner.ts +75 -43
  63. package/src/base/scheduler.ts +0 -8
  64. package/tsconfig.tsbuildinfo +0 -1
package/src/index.ts CHANGED
@@ -1,7 +1,133 @@
1
1
  export type { HeapClient, HeapRecord } from "./base/heap";
2
- export type { WorkflowInvoker } from "./base/invoker";
3
- export type { EventProcessor } from "./base/runtime";
4
- export type { SchedulerClient } from "./base/scheduler";
2
+ export type { DeliveryOptions, WorkflowInvoker } from "./base/invoker";
3
+ export { WorkerExitedError } from "./base/invoker";
4
+ export type { RuntimeOptions } from "./base/runtime";
5
+ export { Runtime } from "./base/runtime";
6
+ export type {
7
+ ExecutionAcknowledgeResult,
8
+ ExecutionAdmissionErrorCode,
9
+ ExecutionAdmissionRequest,
10
+ ExecutionAdmissionResult,
11
+ ExecutionClient,
12
+ ExecutionLease,
13
+ ExecutionOutcome,
14
+ ExecutionParent,
15
+ ExecutionRecord,
16
+ ExecutionReleaseResult,
17
+ ExecutionRenewResult,
18
+ ExecutionStartRequest,
19
+ ExecutionStartResult,
20
+ ExecutionStatus,
21
+ ExecutionWakeResult,
22
+ LeaseLost,
23
+ WorkflowRunOutcome,
24
+ WorkflowStartErrorCode,
25
+ } from "./base/execution";
26
+ export {
27
+ ExecutionAdmissionError,
28
+ InfrastructureError,
29
+ LeaseLostError,
30
+ StepConflictError,
31
+ WorkflowStartError,
32
+ isLeaseLost,
33
+ isTerminalExecutionStatus,
34
+ } from "./base/execution";
35
+ export type {
36
+ EffectBeginRequest,
37
+ EffectBeginResult,
38
+ EffectClient,
39
+ EffectConflictReport,
40
+ EffectDeliveryErrorCode,
41
+ EffectRecord,
42
+ EffectSettleRequest,
43
+ EffectSettleResult,
44
+ EffectWatchRequest,
45
+ WorkflowEffectErrorCode,
46
+ } from "./base/effect";
47
+ export {
48
+ EffectClientInvariantError,
49
+ EffectDeliveryError,
50
+ WorkflowEffectError,
51
+ } from "./base/effect";
52
+ export type {
53
+ EffectConflictState,
54
+ EffectState,
55
+ PreparedEffectBegin,
56
+ PreparedEffectSettle,
57
+ } from "./lib/effect-state";
58
+ export {
59
+ createEffectState,
60
+ effectNotFound,
61
+ matchEffectBegin,
62
+ prepareEffectBegin,
63
+ prepareEffectSettle,
64
+ prepareEffectWatch,
65
+ settleEffect,
66
+ toEffectRecord,
67
+ validateEffectRequest,
68
+ } from "./lib/effect-state";
69
+ export type {
70
+ Durable,
71
+ DurableDecoding,
72
+ DurableObject,
73
+ DurableValue,
74
+ DurableValueCodec,
75
+ DurableValueEncoding,
76
+ IfDurable,
77
+ } from "./base/durable-value";
78
+ export {
79
+ DurableValueError,
80
+ decodeParsedValue,
81
+ durableValueCodec,
82
+ encodeThrownValue,
83
+ } from "./base/durable-value";
84
+ export type { ErrorClass, WorkflowValueErrorCode } from "./base/errors";
85
+ export {
86
+ WORKFLOW_RESULT_STEP_KEY,
87
+ WorkflowValueError,
88
+ registerErrorClass,
89
+ } from "./base/errors";
90
+ export type {
91
+ ExecutionState,
92
+ PreparedExecutionStart,
93
+ StartRequestState,
94
+ } from "./lib/execution-state";
95
+ export {
96
+ DEFAULT_LEASE_DURATION,
97
+ REDELIVERY_DELAY_BASE,
98
+ REDELIVERY_DELAY_MAX,
99
+ ExecutionEventEncodingError,
100
+ acknowledgeExecution,
101
+ claimExecution,
102
+ createExecutionState,
103
+ createStartedExecution,
104
+ decodeExecutionEvent,
105
+ decodeExecutionOutcome,
106
+ encodeExecutionEvent,
107
+ encodeExecutionOutcome,
108
+ executionIdConflict,
109
+ executionNotFound,
110
+ executionStatusAt,
111
+ isExecutionClaimable,
112
+ isLeaseCurrent,
113
+ matchStartRequest,
114
+ nextClaimAt,
115
+ prepareExecutionAdmission,
116
+ prepareExecutionStart,
117
+ redeliveryDelay,
118
+ releaseExecutionLease,
119
+ renewExecutionLease,
120
+ requestExecutionWake,
121
+ toExecutionRecord,
122
+ unacknowledgedDeliveryCountAt,
123
+ } from "./lib/execution-state";
124
+ export type { DispatcherOptions } from "./lib/execution-dispatcher";
125
+ export {
126
+ DEFAULT_ABORT_GRACE_PERIOD,
127
+ DEFAULT_DISPATCHER_CONCURRENCY,
128
+ ExecutionDispatcher,
129
+ } from "./lib/execution-dispatcher";
130
+ export { waitForExecutionOutcome } from "./lib/wait-for-outcome";
5
131
  export type { StandardSchemaV1 } from "@standard-schema/spec";
6
132
  export type {
7
133
  Draft,
@@ -13,6 +139,7 @@ export type {
13
139
  StoreSelector,
14
140
  StoreSnapshot,
15
141
  StoreState,
142
+ DurableStoreSchema,
16
143
  StoreStepId,
17
144
  StoreTakeResult,
18
145
  StoreUpdateFromResult,
@@ -21,15 +148,20 @@ export type {
21
148
  StoreWaiter,
22
149
  StoreMutation,
23
150
  StoreMutationCommitResult,
151
+ StoreDeletion,
152
+ StoreReadSnapshot,
24
153
  TrackedStoreUpdater,
25
154
  } from "./base/store";
26
155
  export {
27
156
  StoreClient,
28
157
  CasStoreClient,
158
+ StoreValidationError,
29
159
  assertSynchronousClaim,
30
160
  assertSynchronousUpdater,
31
161
  cloneStoreState,
162
+ decodeStoreValue,
32
163
  defineStore,
164
+ encodeStoreValue,
33
165
  diffStorePaths,
34
166
  isStoreSelectorMatch,
35
167
  storePathsIntersect,
@@ -60,6 +192,7 @@ export type {
60
192
  export {
61
193
  StepCacheCheck,
62
194
  StepDelay,
195
+ StepEffectWait,
63
196
  StepError,
64
197
  StepInvalid,
65
198
  StepKey,
@@ -70,6 +203,7 @@ export {
70
203
  WorkflowRestart,
71
204
  WorkflowResult,
72
205
  } from "./base/step";
206
+ export type { StepSchemaVerdict } from "./base/step";
73
207
 
74
208
  export { WorkflowRunner } from "./lib/workflow-runner";
75
209
 
@@ -0,0 +1,254 @@
1
+ import { durableValueCodec, isPlainObject, type DurableObject, type DurableValue } from "../base/durable-value";
2
+ import {
3
+ EffectClientInvariantError,
4
+ type EffectBeginRequest,
5
+ type EffectBeginResult,
6
+ type EffectRecord,
7
+ type EffectSettleRequest,
8
+ type EffectSettleResult,
9
+ type EffectWatchRequest,
10
+ } from "../base/effect";
11
+
12
+ /**
13
+ * An effect as runtime adapters store it. `input`, `result` and the values of
14
+ * each conflict report are canonical `durableValueCodec` encodings, so equal
15
+ * values compare as equal strings.
16
+ */
17
+ export type EffectState = {
18
+ effectId: string;
19
+ operation: string;
20
+ input: string;
21
+ status: "pending" | "settled";
22
+ result?: string;
23
+ conflicts: EffectConflictState[];
24
+ };
25
+
26
+ export type EffectConflictState = {
27
+ result: string;
28
+ conflictingResult: string;
29
+ reportedAt: number;
30
+ };
31
+
32
+ export type PreparedEffectBegin = {
33
+ effectId: string;
34
+ operation: string;
35
+ /** The canonical input. */
36
+ input: string;
37
+ executionId: string;
38
+ stepKey: string;
39
+ };
40
+
41
+ export type PreparedEffectSettle = {
42
+ effectId: string;
43
+ operation: string;
44
+ /** The canonical result. */
45
+ result: string;
46
+ };
47
+
48
+ /**
49
+ * Checks the values `step.effect` passes to `begin`, and returns a message
50
+ * naming the first invalid one. The driver rejects these requests with
51
+ * `INVALID_EFFECT_REQUEST` before calling the client.
52
+ */
53
+ export function validateEffectRequest(request: {
54
+ effectId: unknown;
55
+ operation: unknown;
56
+ input: unknown;
57
+ }): { ok: true; input: string } | { ok: false; message: string } {
58
+ if (!isNonEmptyString(request.effectId)) {
59
+ return { ok: false, message: "effectId must be a non-empty string" };
60
+ }
61
+ if (!isNonEmptyString(request.operation)) {
62
+ return { ok: false, message: "operation must be a non-empty string" };
63
+ }
64
+ if (!isPlainObject(request.input)) {
65
+ return { ok: false, message: "input must be a plain object" };
66
+ }
67
+ const encoding = durableValueCodec.encode(request.input);
68
+ if (encoding.status === "invalid") {
69
+ return { ok: false, message: `input: ${encoding.message}` };
70
+ }
71
+ return { ok: true, input: encoding.canonical };
72
+ }
73
+
74
+ /**
75
+ * Validates a `begin` request and encodes its input canonically. Adapters call
76
+ * this before touching storage; it throws `EffectClientInvariantError` for a
77
+ * request the driver should have rejected.
78
+ */
79
+ export function prepareEffectBegin(request: EffectBeginRequest): PreparedEffectBegin {
80
+ const validated = validateEffectRequest(request);
81
+ if (!validated.ok) throw invariant(`Invalid effect request: ${validated.message}`);
82
+ assertStepId(request);
83
+ return {
84
+ effectId: request.effectId,
85
+ operation: request.operation,
86
+ input: validated.input,
87
+ executionId: request.executionId,
88
+ stepKey: request.stepKey,
89
+ };
90
+ }
91
+
92
+ /** Validates a `settle` request and encodes its result canonically. */
93
+ export function prepareEffectSettle(request: EffectSettleRequest): PreparedEffectSettle {
94
+ if (!isNonEmptyString(request.effectId)) {
95
+ throw invariant("Invalid effect settlement: effectId must be a non-empty string");
96
+ }
97
+ if (!isNonEmptyString(request.operation)) {
98
+ throw invariant("Invalid effect settlement: operation must be a non-empty string");
99
+ }
100
+ const encoding = durableValueCodec.encode(request.result);
101
+ if (encoding.status === "invalid") {
102
+ throw invariant(`Invalid effect settlement: result: ${encoding.message}`);
103
+ }
104
+ return {
105
+ effectId: request.effectId,
106
+ operation: request.operation,
107
+ result: encoding.canonical,
108
+ };
109
+ }
110
+
111
+ /** Validates a `watch` request. */
112
+ export function prepareEffectWatch(request: EffectWatchRequest): EffectWatchRequest {
113
+ if (!isNonEmptyString(request.effectId)) {
114
+ throw invariant("Invalid effect watch: effectId must be a non-empty string");
115
+ }
116
+ assertStepId(request);
117
+ return {
118
+ effectId: request.effectId,
119
+ executionId: request.executionId,
120
+ stepKey: request.stepKey,
121
+ };
122
+ }
123
+
124
+ /** The pending effect a `begin` that finds no effect creates. */
125
+ export function createEffectState(begin: PreparedEffectBegin): EffectState {
126
+ return {
127
+ effectId: begin.effectId,
128
+ operation: begin.operation,
129
+ input: begin.input,
130
+ status: "pending",
131
+ conflicts: [],
132
+ };
133
+ }
134
+
135
+ /** Answers a `begin` for an effect that already exists. */
136
+ export function matchEffectBegin(
137
+ state: EffectState,
138
+ begin: PreparedEffectBegin
139
+ ): EffectBeginResult {
140
+ if (state.operation !== begin.operation || state.input !== begin.input) {
141
+ return { status: "rejected", code: "EFFECT_REQUEST_MISMATCH" };
142
+ }
143
+ if (state.status === "pending") return { status: "pending" };
144
+ return { status: "settled", result: decodeSettledResult(state) };
145
+ }
146
+
147
+ /**
148
+ * Applies a settlement. `wake` is true only when this settlement moved the
149
+ * effect from pending to settled: adapters then wake every waiter in the same
150
+ * write. `state` is the input object when nothing changed. Throws
151
+ * `EffectClientInvariantError` when the operation differs from the effect's,
152
+ * which `begin` would have rejected.
153
+ */
154
+ export function settleEffect(
155
+ state: EffectState,
156
+ settle: PreparedEffectSettle,
157
+ now: number
158
+ ): { state: EffectState; result: EffectSettleResult; wake: boolean } {
159
+ if (state.operation !== settle.operation) {
160
+ throw invariant(
161
+ `Cannot settle effect "${state.effectId}" as operation "${settle.operation}": it was begun as "${state.operation}"`
162
+ );
163
+ }
164
+ if (state.status === "pending") {
165
+ return {
166
+ state: { ...state, status: "settled", result: settle.result },
167
+ result: { status: "settled", result: decodeValue(settle.result, state.effectId) },
168
+ wake: true,
169
+ };
170
+ }
171
+ const result = decodeSettledResult(state);
172
+ if (state.result === settle.result) {
173
+ return { state, result: { status: "settled", result }, wake: false };
174
+ }
175
+ const conflictingResult = decodeValue(settle.result, state.effectId);
176
+ // One report per distinct conflicting result, so a settlement repeated after
177
+ // a lost response does not add another.
178
+ const reported = state.conflicts.some(
179
+ (conflict) => conflict.conflictingResult === settle.result
180
+ );
181
+ return {
182
+ state: reported
183
+ ? state
184
+ : {
185
+ ...state,
186
+ conflicts: [
187
+ ...state.conflicts,
188
+ { result: state.result!, conflictingResult: settle.result, reportedAt: now },
189
+ ],
190
+ },
191
+ result: { status: "conflict", result, conflictingResult },
192
+ wake: false,
193
+ };
194
+ }
195
+
196
+ export function toEffectRecord(state: EffectState): EffectRecord {
197
+ const record: EffectRecord = {
198
+ effectId: state.effectId,
199
+ operation: state.operation,
200
+ input: decodeInput(state),
201
+ status: state.status,
202
+ conflicts: state.conflicts.map((conflict) => ({
203
+ result: decodeValue(conflict.result, state.effectId),
204
+ conflictingResult: decodeValue(conflict.conflictingResult, state.effectId),
205
+ reportedAt: conflict.reportedAt,
206
+ })),
207
+ };
208
+ if (state.status === "settled") record.result = decodeSettledResult(state);
209
+ return record;
210
+ }
211
+
212
+ export function effectNotFound(effectId: string): EffectClientInvariantError {
213
+ return invariant(`Effect "${effectId}" does not exist: begin it first`);
214
+ }
215
+
216
+ function decodeSettledResult(state: EffectState): DurableValue {
217
+ if (state.result === undefined) {
218
+ throw invariant(`Settled effect "${state.effectId}" has no stored result`);
219
+ }
220
+ return decodeValue(state.result, state.effectId);
221
+ }
222
+
223
+ function decodeInput(state: EffectState): DurableObject {
224
+ const input = decodeValue(state.input, state.effectId);
225
+ if (!isPlainObject(input)) {
226
+ throw invariant(`Stored input of effect "${state.effectId}" is not an object`);
227
+ }
228
+ return input as DurableObject;
229
+ }
230
+
231
+ function decodeValue(canonical: string, effectId: string): DurableValue {
232
+ const decoded = durableValueCodec.decode(canonical);
233
+ if (decoded.status === "invalid") {
234
+ throw invariant(`Stored value of effect "${effectId}" is invalid: ${decoded.message}`);
235
+ }
236
+ return decoded.value;
237
+ }
238
+
239
+ function assertStepId(request: { executionId: unknown; stepKey: unknown }) {
240
+ if (!isNonEmptyString(request.executionId)) {
241
+ throw invariant("Invalid effect request: executionId must be a non-empty string");
242
+ }
243
+ if (!isNonEmptyString(request.stepKey)) {
244
+ throw invariant("Invalid effect request: stepKey must be a non-empty string");
245
+ }
246
+ }
247
+
248
+ function isNonEmptyString(value: unknown): value is string {
249
+ return typeof value === "string" && value !== "";
250
+ }
251
+
252
+ function invariant(message: string) {
253
+ return new EffectClientInvariantError(message);
254
+ }