@yieldstar/core 0.5.0 → 0.6.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 (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 +255 -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 +1653 -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 +112 -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 +315 -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 +138 -3
  57. package/src/lib/effect-state.ts +254 -0
  58. package/src/lib/execution-dispatcher.ts +408 -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,134 @@
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
+ ExecutionClaimFilter,
12
+ ExecutionClient,
13
+ ExecutionLease,
14
+ ExecutionOutcome,
15
+ ExecutionParent,
16
+ ExecutionRecord,
17
+ ExecutionReleaseResult,
18
+ ExecutionRenewResult,
19
+ ExecutionStartRequest,
20
+ ExecutionStartResult,
21
+ ExecutionStatus,
22
+ ExecutionWakeResult,
23
+ LeaseLost,
24
+ WorkflowRunOutcome,
25
+ WorkflowStartErrorCode,
26
+ } from "./base/execution";
27
+ export {
28
+ ExecutionAdmissionError,
29
+ InfrastructureError,
30
+ LeaseLostError,
31
+ StepConflictError,
32
+ WorkflowStartError,
33
+ isLeaseLost,
34
+ isTerminalExecutionStatus,
35
+ } from "./base/execution";
36
+ export type {
37
+ EffectBeginRequest,
38
+ EffectBeginResult,
39
+ EffectClient,
40
+ EffectConflictReport,
41
+ EffectDeliveryErrorCode,
42
+ EffectRecord,
43
+ EffectSettleRequest,
44
+ EffectSettleResult,
45
+ EffectWatchRequest,
46
+ WorkflowEffectErrorCode,
47
+ } from "./base/effect";
48
+ export {
49
+ EffectClientInvariantError,
50
+ EffectDeliveryError,
51
+ WorkflowEffectError,
52
+ } from "./base/effect";
53
+ export type {
54
+ EffectConflictState,
55
+ EffectState,
56
+ PreparedEffectBegin,
57
+ PreparedEffectSettle,
58
+ } from "./lib/effect-state";
59
+ export {
60
+ createEffectState,
61
+ effectNotFound,
62
+ matchEffectBegin,
63
+ prepareEffectBegin,
64
+ prepareEffectSettle,
65
+ prepareEffectWatch,
66
+ settleEffect,
67
+ toEffectRecord,
68
+ validateEffectRequest,
69
+ } from "./lib/effect-state";
70
+ export type {
71
+ Durable,
72
+ DurableDecoding,
73
+ DurableObject,
74
+ DurableValue,
75
+ DurableValueCodec,
76
+ DurableValueEncoding,
77
+ IfDurable,
78
+ } from "./base/durable-value";
79
+ export {
80
+ DurableValueError,
81
+ decodeParsedValue,
82
+ durableValueCodec,
83
+ encodeThrownValue,
84
+ } from "./base/durable-value";
85
+ export type { ErrorClass, WorkflowValueErrorCode } from "./base/errors";
86
+ export {
87
+ WORKFLOW_RESULT_STEP_KEY,
88
+ WorkflowValueError,
89
+ registerErrorClass,
90
+ } from "./base/errors";
91
+ export type {
92
+ ExecutionState,
93
+ PreparedExecutionStart,
94
+ StartRequestState,
95
+ } from "./lib/execution-state";
96
+ export {
97
+ DEFAULT_LEASE_DURATION,
98
+ REDELIVERY_DELAY_BASE,
99
+ REDELIVERY_DELAY_MAX,
100
+ ExecutionEventEncodingError,
101
+ acknowledgeExecution,
102
+ claimExecution,
103
+ createExecutionState,
104
+ createStartedExecution,
105
+ decodeExecutionEvent,
106
+ decodeExecutionOutcome,
107
+ encodeExecutionEvent,
108
+ encodeExecutionOutcome,
109
+ executionIdConflict,
110
+ executionNotFound,
111
+ executionStatusAt,
112
+ isExecutionClaimable,
113
+ isLeaseCurrent,
114
+ matchStartRequest,
115
+ nextClaimAt,
116
+ prepareExecutionAdmission,
117
+ prepareExecutionStart,
118
+ redeliveryDelay,
119
+ releaseExecutionLease,
120
+ renewExecutionLease,
121
+ requestExecutionWake,
122
+ toExecutionRecord,
123
+ unacknowledgedDeliveryCountAt,
124
+ } from "./lib/execution-state";
125
+ export type { DispatcherOptions } from "./lib/execution-dispatcher";
126
+ export {
127
+ DEFAULT_ABORT_GRACE_PERIOD,
128
+ DEFAULT_DISPATCHER_CONCURRENCY,
129
+ ExecutionDispatcher,
130
+ } from "./lib/execution-dispatcher";
131
+ export { waitForExecutionOutcome } from "./lib/wait-for-outcome";
5
132
  export type { StandardSchemaV1 } from "@standard-schema/spec";
6
133
  export type {
7
134
  Draft,
@@ -13,6 +140,7 @@ export type {
13
140
  StoreSelector,
14
141
  StoreSnapshot,
15
142
  StoreState,
143
+ DurableStoreSchema,
16
144
  StoreStepId,
17
145
  StoreTakeResult,
18
146
  StoreUpdateFromResult,
@@ -21,15 +149,20 @@ export type {
21
149
  StoreWaiter,
22
150
  StoreMutation,
23
151
  StoreMutationCommitResult,
152
+ StoreDeletion,
153
+ StoreReadSnapshot,
24
154
  TrackedStoreUpdater,
25
155
  } from "./base/store";
26
156
  export {
27
157
  StoreClient,
28
158
  CasStoreClient,
159
+ StoreValidationError,
29
160
  assertSynchronousClaim,
30
161
  assertSynchronousUpdater,
31
162
  cloneStoreState,
163
+ decodeStoreValue,
32
164
  defineStore,
165
+ encodeStoreValue,
33
166
  diffStorePaths,
34
167
  isStoreSelectorMatch,
35
168
  storePathsIntersect,
@@ -60,6 +193,7 @@ export type {
60
193
  export {
61
194
  StepCacheCheck,
62
195
  StepDelay,
196
+ StepEffectWait,
63
197
  StepError,
64
198
  StepInvalid,
65
199
  StepKey,
@@ -70,6 +204,7 @@ export {
70
204
  WorkflowRestart,
71
205
  WorkflowResult,
72
206
  } from "./base/step";
207
+ export type { StepSchemaVerdict } from "./base/step";
73
208
 
74
209
  export { WorkflowRunner } from "./lib/workflow-runner";
75
210
 
@@ -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
+ }