@dxos/functions 0.8.4-main.dedc0f3 → 0.8.4-main.e8ec1fe

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 (190) hide show
  1. package/dist/lib/browser/index.mjs +732 -1070
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +732 -1070
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/errors.d.ts +47 -55
  8. package/dist/types/src/errors.d.ts.map +1 -1
  9. package/dist/types/src/{examples → example}/fib.d.ts +1 -1
  10. package/dist/types/src/example/fib.d.ts.map +1 -0
  11. package/dist/types/src/example/forex-effect.d.ts +3 -0
  12. package/dist/types/src/example/forex-effect.d.ts.map +1 -0
  13. package/dist/types/src/example/index.d.ts +12 -0
  14. package/dist/types/src/example/index.d.ts.map +1 -0
  15. package/dist/types/src/{examples → example}/reply.d.ts +1 -1
  16. package/dist/types/src/example/reply.d.ts.map +1 -0
  17. package/dist/types/src/{examples → example}/sleep.d.ts +1 -1
  18. package/dist/types/src/example/sleep.d.ts.map +1 -0
  19. package/dist/types/src/index.d.ts +4 -8
  20. package/dist/types/src/index.d.ts.map +1 -1
  21. package/dist/types/src/protocol/index.d.ts +2 -0
  22. package/dist/types/src/protocol/index.d.ts.map +1 -0
  23. package/dist/types/src/protocol/protocol.d.ts +7 -0
  24. package/dist/types/src/protocol/protocol.d.ts.map +1 -0
  25. package/dist/types/src/protocol/protocol.test.d.ts +2 -0
  26. package/dist/types/src/protocol/protocol.test.d.ts.map +1 -0
  27. package/dist/types/src/sdk.d.ts +89 -0
  28. package/dist/types/src/sdk.d.ts.map +1 -0
  29. package/dist/types/src/services/credentials.d.ts +7 -3
  30. package/dist/types/src/services/credentials.d.ts.map +1 -1
  31. package/dist/types/src/services/event-logger.d.ts +20 -5
  32. package/dist/types/src/services/event-logger.d.ts.map +1 -1
  33. package/dist/types/src/services/function-invocation-service.d.ts +11 -0
  34. package/dist/types/src/services/function-invocation-service.d.ts.map +1 -0
  35. package/dist/types/src/services/index.d.ts +6 -6
  36. package/dist/types/src/services/index.d.ts.map +1 -1
  37. package/dist/types/src/services/queues.d.ts +3 -1
  38. package/dist/types/src/services/queues.d.ts.map +1 -1
  39. package/dist/types/src/services/tracing.d.ts +9 -12
  40. package/dist/types/src/services/tracing.d.ts.map +1 -1
  41. package/dist/types/src/types/Function.d.ts +58 -0
  42. package/dist/types/src/types/Function.d.ts.map +1 -0
  43. package/dist/types/src/types/Script.d.ts +28 -0
  44. package/dist/types/src/types/Script.d.ts.map +1 -0
  45. package/dist/types/src/types/Trigger.d.ts +139 -0
  46. package/dist/types/src/types/Trigger.d.ts.map +1 -0
  47. package/dist/types/src/types/TriggerEvent.d.ts +44 -0
  48. package/dist/types/src/types/TriggerEvent.d.ts.map +1 -0
  49. package/dist/types/src/types/index.d.ts +6 -0
  50. package/dist/types/src/types/index.d.ts.map +1 -0
  51. package/dist/types/src/{url.d.ts → types/url.d.ts} +1 -10
  52. package/dist/types/src/types/url.d.ts.map +1 -0
  53. package/dist/types/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +16 -70
  55. package/src/errors.ts +9 -9
  56. package/src/{examples → example}/fib.ts +5 -3
  57. package/src/example/forex-effect.ts +40 -0
  58. package/src/example/index.ts +13 -0
  59. package/src/{examples → example}/reply.ts +6 -3
  60. package/src/{examples → example}/sleep.ts +5 -3
  61. package/src/index.ts +4 -8
  62. package/src/{executor → protocol}/index.ts +1 -1
  63. package/src/protocol/protocol.test.ts +59 -0
  64. package/src/protocol/protocol.ts +145 -0
  65. package/src/sdk.ts +226 -0
  66. package/src/services/credentials.ts +12 -6
  67. package/src/services/event-logger.ts +12 -3
  68. package/src/services/function-invocation-service.ts +23 -0
  69. package/src/services/index.ts +7 -6
  70. package/src/services/queues.ts +3 -1
  71. package/src/services/tracing.ts +24 -56
  72. package/src/{schema.ts → types/Function.ts} +20 -26
  73. package/src/types/Script.ts +33 -0
  74. package/src/types/Trigger.ts +139 -0
  75. package/src/types/TriggerEvent.ts +62 -0
  76. package/src/types/index.ts +9 -0
  77. package/src/types/url.ts +31 -0
  78. package/dist/lib/browser/bundler/index.mjs +0 -265
  79. package/dist/lib/browser/bundler/index.mjs.map +0 -7
  80. package/dist/lib/browser/chunk-ANP3DFCO.mjs +0 -623
  81. package/dist/lib/browser/chunk-ANP3DFCO.mjs.map +0 -7
  82. package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
  83. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
  84. package/dist/lib/browser/edge/index.mjs +0 -83
  85. package/dist/lib/browser/edge/index.mjs.map +0 -7
  86. package/dist/lib/browser/testing/index.mjs +0 -129
  87. package/dist/lib/browser/testing/index.mjs.map +0 -7
  88. package/dist/lib/node-esm/bundler/index.mjs +0 -266
  89. package/dist/lib/node-esm/bundler/index.mjs.map +0 -7
  90. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  91. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  92. package/dist/lib/node-esm/chunk-MPKVY7ZR.mjs +0 -625
  93. package/dist/lib/node-esm/chunk-MPKVY7ZR.mjs.map +0 -7
  94. package/dist/lib/node-esm/edge/index.mjs +0 -84
  95. package/dist/lib/node-esm/edge/index.mjs.map +0 -7
  96. package/dist/lib/node-esm/testing/index.mjs +0 -130
  97. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  98. package/dist/types/src/bundler/bundler.d.ts +0 -49
  99. package/dist/types/src/bundler/bundler.d.ts.map +0 -1
  100. package/dist/types/src/bundler/bundler.test.d.ts +0 -2
  101. package/dist/types/src/bundler/bundler.test.d.ts.map +0 -1
  102. package/dist/types/src/bundler/index.d.ts +0 -2
  103. package/dist/types/src/bundler/index.d.ts.map +0 -1
  104. package/dist/types/src/edge/functions.d.ts +0 -17
  105. package/dist/types/src/edge/functions.d.ts.map +0 -1
  106. package/dist/types/src/edge/index.d.ts +0 -2
  107. package/dist/types/src/edge/index.d.ts.map +0 -1
  108. package/dist/types/src/examples/fib.d.ts.map +0 -1
  109. package/dist/types/src/examples/index.d.ts +0 -4
  110. package/dist/types/src/examples/index.d.ts.map +0 -1
  111. package/dist/types/src/examples/reply.d.ts.map +0 -1
  112. package/dist/types/src/examples/sleep.d.ts.map +0 -1
  113. package/dist/types/src/executor/executor.d.ts +0 -11
  114. package/dist/types/src/executor/executor.d.ts.map +0 -1
  115. package/dist/types/src/executor/index.d.ts +0 -2
  116. package/dist/types/src/executor/index.d.ts.map +0 -1
  117. package/dist/types/src/handler.d.ts +0 -94
  118. package/dist/types/src/handler.d.ts.map +0 -1
  119. package/dist/types/src/schema.d.ts +0 -43
  120. package/dist/types/src/schema.d.ts.map +0 -1
  121. package/dist/types/src/services/database.d.ts +0 -98
  122. package/dist/types/src/services/database.d.ts.map +0 -1
  123. package/dist/types/src/services/local-function-execution.d.ts +0 -25
  124. package/dist/types/src/services/local-function-execution.d.ts.map +0 -1
  125. package/dist/types/src/services/remote-function-execution-service.d.ts +0 -15
  126. package/dist/types/src/services/remote-function-execution-service.d.ts.map +0 -1
  127. package/dist/types/src/services/service-container.d.ts +0 -56
  128. package/dist/types/src/services/service-container.d.ts.map +0 -1
  129. package/dist/types/src/services/service-registry.d.ts +0 -29
  130. package/dist/types/src/services/service-registry.d.ts.map +0 -1
  131. package/dist/types/src/services/service-registry.test.d.ts +0 -2
  132. package/dist/types/src/services/service-registry.test.d.ts.map +0 -1
  133. package/dist/types/src/testing/index.d.ts +0 -3
  134. package/dist/types/src/testing/index.d.ts.map +0 -1
  135. package/dist/types/src/testing/layer.d.ts +0 -15
  136. package/dist/types/src/testing/layer.d.ts.map +0 -1
  137. package/dist/types/src/testing/logger.d.ts +0 -5
  138. package/dist/types/src/testing/logger.d.ts.map +0 -1
  139. package/dist/types/src/testing/persist-database.test.d.ts +0 -2
  140. package/dist/types/src/testing/persist-database.test.d.ts.map +0 -1
  141. package/dist/types/src/testing/services.d.ts +0 -59
  142. package/dist/types/src/testing/services.d.ts.map +0 -1
  143. package/dist/types/src/trace.d.ts +0 -122
  144. package/dist/types/src/trace.d.ts.map +0 -1
  145. package/dist/types/src/translations.d.ts +0 -12
  146. package/dist/types/src/translations.d.ts.map +0 -1
  147. package/dist/types/src/triggers/index.d.ts +0 -4
  148. package/dist/types/src/triggers/index.d.ts.map +0 -1
  149. package/dist/types/src/triggers/input-builder.d.ts +0 -3
  150. package/dist/types/src/triggers/input-builder.d.ts.map +0 -1
  151. package/dist/types/src/triggers/invocation-tracer.d.ts +0 -35
  152. package/dist/types/src/triggers/invocation-tracer.d.ts.map +0 -1
  153. package/dist/types/src/triggers/trigger-dispatcher.d.ts +0 -75
  154. package/dist/types/src/triggers/trigger-dispatcher.d.ts.map +0 -1
  155. package/dist/types/src/triggers/trigger-dispatcher.test.d.ts +0 -2
  156. package/dist/types/src/triggers/trigger-dispatcher.test.d.ts.map +0 -1
  157. package/dist/types/src/triggers/trigger-state-store.d.ts +0 -27
  158. package/dist/types/src/triggers/trigger-state-store.d.ts.map +0 -1
  159. package/dist/types/src/types.d.ts +0 -211
  160. package/dist/types/src/types.d.ts.map +0 -1
  161. package/dist/types/src/url.d.ts.map +0 -1
  162. package/src/bundler/bundler.test.ts +0 -58
  163. package/src/bundler/bundler.ts +0 -291
  164. package/src/bundler/index.ts +0 -5
  165. package/src/edge/functions.ts +0 -67
  166. package/src/edge/index.ts +0 -9
  167. package/src/examples/index.ts +0 -7
  168. package/src/executor/executor.ts +0 -54
  169. package/src/handler.ts +0 -201
  170. package/src/services/database.ts +0 -170
  171. package/src/services/local-function-execution.ts +0 -114
  172. package/src/services/remote-function-execution-service.ts +0 -46
  173. package/src/services/service-container.ts +0 -114
  174. package/src/services/service-registry.test.ts +0 -42
  175. package/src/services/service-registry.ts +0 -59
  176. package/src/testing/index.ts +0 -6
  177. package/src/testing/layer.ts +0 -111
  178. package/src/testing/logger.ts +0 -16
  179. package/src/testing/persist-database.test.ts +0 -87
  180. package/src/testing/services.ts +0 -115
  181. package/src/trace.ts +0 -178
  182. package/src/translations.ts +0 -20
  183. package/src/triggers/index.ts +0 -7
  184. package/src/triggers/input-builder.ts +0 -35
  185. package/src/triggers/invocation-tracer.ts +0 -99
  186. package/src/triggers/trigger-dispatcher.test.ts +0 -652
  187. package/src/triggers/trigger-dispatcher.ts +0 -512
  188. package/src/triggers/trigger-state-store.ts +0 -60
  189. package/src/types.ts +0 -200
  190. package/src/url.ts +0 -55
@@ -1,512 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import { Cause, Context, Cron, Duration, Effect, Either, Exit, Fiber, Layer, Option, Record, Schedule } from 'effect';
6
-
7
- import { DXN, Filter, Obj, Query } from '@dxos/echo';
8
- import { causeToError } from '@dxos/effect';
9
- import { invariant } from '@dxos/invariant';
10
- import { log } from '@dxos/log';
11
- import { KEY_QUEUE_POSITION } from '@dxos/protocols';
12
-
13
- import { deserializeFunction } from '../handler';
14
- import { FunctionType } from '../schema';
15
- import { ComputeEventLogger, DatabaseService, QueueService, type Services, TracingService } from '../services';
16
- import { LocalFunctionExecutionService } from '../services/local-function-execution';
17
- import {
18
- type EventType,
19
- FunctionTrigger,
20
- type QueueTriggerOutput,
21
- type SubscriptionTriggerOutput,
22
- type TimerTrigger,
23
- type TimerTriggerOutput,
24
- type TriggerKind,
25
- } from '../types';
26
-
27
- import { createInvocationPayload } from './input-builder';
28
- import { InvocationTracer } from './invocation-tracer';
29
- import { type TriggerState, TriggerStateStore } from './trigger-state-store';
30
-
31
- export type TimeControl = 'natural' | 'manual';
32
-
33
- export interface TriggerDispatcherOptions {
34
- /**
35
- * Time control mode.
36
- * - 'natural': Use real time.
37
- * - 'manual': Use internal clock for testing.
38
- */
39
- timeControl: TimeControl;
40
-
41
- /**
42
- * Starting time for manual time control mode.
43
- * @default current time
44
- */
45
- startingTime?: Date;
46
-
47
- /**
48
- * Poll interval for cron triggers in 'natural' time control mode.
49
- * @default 1 second
50
- */
51
- livePollInterval?: Duration.Duration;
52
- }
53
-
54
- export interface InvokeTriggerOptions {
55
- trigger: FunctionTrigger;
56
- event: EventType;
57
- }
58
- export interface TriggerExecutionResult {
59
- triggerId: string;
60
- result: Exit.Exit<unknown>;
61
- }
62
-
63
- /**
64
- * Cront trigger runtime state.
65
- */
66
- interface ScheduledTrigger {
67
- trigger: FunctionTrigger;
68
- cron: Cron.Cron;
69
- nextExecution: Date;
70
- }
71
-
72
- // TODO(dmaretskyi): Refactor service management.
73
- type TriggerDispatcherServices =
74
- | Exclude<Services, ComputeEventLogger | TracingService>
75
- | LocalFunctionExecutionService
76
- // TODO(dmaretskyi): Move those into layer deps.
77
- | TriggerStateStore
78
- | InvocationTracer;
79
-
80
- export class TriggerDispatcher extends Context.Tag('@dxos/functions/TriggerDispatcher')<
81
- TriggerDispatcher,
82
- {
83
- readonly timeControl: TimeControl;
84
-
85
- get running(): boolean;
86
-
87
- /**
88
- * Start the trigger dispatcher.
89
- * Will automatically invoke triggers.
90
- */
91
- start(): Effect.Effect<void, never, TriggerDispatcherServices>;
92
-
93
- /**
94
- * Stop the trigger dispatcher.
95
- */
96
- stop(): Effect.Effect<void>;
97
-
98
- /**
99
- * Refresh triggers.
100
- */
101
- refreshTriggers(): Effect.Effect<void, never, DatabaseService>;
102
-
103
- /**
104
- * Manually invoke a specific trigger.
105
- */
106
- invokeTrigger(
107
- options: InvokeTriggerOptions,
108
- ): Effect.Effect<TriggerExecutionResult, never, TriggerDispatcherServices>;
109
-
110
- /**
111
- * Invoke all scheduled triggers who are due.
112
- */
113
- invokeScheduledTriggers(opts?: {
114
- kinds?: TriggerKind[];
115
- }): Effect.Effect<TriggerExecutionResult[], never, TriggerDispatcherServices>;
116
-
117
- /**
118
- * Advance the internal clock (manual time control only).
119
- * Note: Does not invoke triggers.
120
- */
121
- advanceTime(duration: Duration.Duration): Effect.Effect<void>;
122
-
123
- /**
124
- * Get current time based on time control mode.
125
- */
126
- getCurrentTime(): Date;
127
- }
128
- >() {
129
- static layer = (options: Omit<TriggerDispatcherOptions, 'database'>) =>
130
- Layer.effect(
131
- TriggerDispatcher,
132
- Effect.gen(function* () {
133
- return new TriggerDispatcherImpl(options);
134
- }),
135
- );
136
- }
137
-
138
- class TriggerDispatcherImpl implements Context.Tag.Service<TriggerDispatcher> {
139
- readonly livePollInterval: Duration.Duration;
140
- readonly timeControl: TimeControl;
141
-
142
- private _running = false;
143
- private _internalTime: Date;
144
- private _timerFiber: Fiber.Fiber<void, void> | undefined;
145
- private _scheduledTriggers = new Map<string, ScheduledTrigger>();
146
-
147
- constructor(options: TriggerDispatcherOptions) {
148
- this.timeControl = options.timeControl;
149
- this.livePollInterval = options.livePollInterval ?? Duration.seconds(1);
150
- this._internalTime = options.startingTime ?? new Date();
151
- }
152
-
153
- get running(): boolean {
154
- return this._running;
155
- }
156
-
157
- start = (): Effect.Effect<void, never, TriggerDispatcherServices> =>
158
- Effect.gen(this, function* () {
159
- if (this._running) {
160
- return;
161
- }
162
-
163
- this._running = true;
164
-
165
- // Start natural time processing if enabled
166
- if (this.timeControl === 'natural') {
167
- this._timerFiber = yield* this._startNaturalTimeProcessing().pipe(
168
- Effect.tapErrorCause((cause) => {
169
- const error = causeToError(cause);
170
- log.error('trigger dispatcher error', { error });
171
- this._running = false;
172
- return Effect.void;
173
- }),
174
- Effect.forkDaemon,
175
- );
176
- } else {
177
- return yield* Effect.dieMessage('TriggerDispatcher started in manual time control mode');
178
- }
179
-
180
- log.info('TriggerDispatcher started', { timeControl: this.timeControl });
181
- });
182
-
183
- stop = (): Effect.Effect<void> =>
184
- Effect.gen(this, function* () {
185
- if (!this._running) {
186
- return;
187
- }
188
-
189
- this._running = false;
190
-
191
- // Stop timer processing
192
- if (this._timerFiber) {
193
- yield* Fiber.interrupt(this._timerFiber);
194
- this._timerFiber = undefined;
195
- }
196
-
197
- // Clear scheduled triggers
198
- this._scheduledTriggers.clear();
199
-
200
- log.info('TriggerDispatcher stopped');
201
- });
202
-
203
- invokeTrigger = (
204
- options: InvokeTriggerOptions,
205
- ): Effect.Effect<TriggerExecutionResult, never, TriggerDispatcherServices> =>
206
- Effect.gen(this, function* () {
207
- const { trigger, event } = options;
208
- log.info('running trigger', { triggerId: trigger.id, spec: trigger.spec, event });
209
-
210
- const tracer = yield* InvocationTracer;
211
- const trace = yield* tracer.traceInvocationStart({
212
- target: trigger.function?.dxn,
213
- payload: {
214
- trigger: {
215
- id: trigger.id,
216
- // TODO(dmaretskyi): Is `spec` always there>
217
- kind: trigger.spec!.kind,
218
- },
219
- data: event,
220
- },
221
- });
222
-
223
- // Sandboxed section.
224
- const result = yield* Effect.gen(this, function* () {
225
- if (!trigger.enabled) {
226
- return yield* Effect.dieMessage('Attempting to invoke disabled trigger');
227
- }
228
-
229
- if (!trigger.function) {
230
- return yield* Effect.dieMessage('Trigger has no function reference');
231
- }
232
-
233
- // Resolve the function
234
- const serialiedFunction = yield* DatabaseService.load(trigger.function!).pipe(Effect.orDie);
235
- invariant(Obj.instanceOf(FunctionType, serialiedFunction));
236
- const functionDef = deserializeFunction(serialiedFunction);
237
-
238
- // Prepare input data
239
- const inputData = this._prepareInputData(trigger, event);
240
-
241
- // Invoke the function
242
- return yield* LocalFunctionExecutionService.invokeFunction(functionDef, inputData).pipe(
243
- Effect.provide(
244
- ComputeEventLogger.layerFromTracing.pipe(
245
- Layer.provideMerge(TracingService.layerQueue(trace.invocationTraceQueue)),
246
- ),
247
- ),
248
- );
249
- }).pipe(Effect.exit);
250
-
251
- const triggerExecutionResult: TriggerExecutionResult = {
252
- triggerId: trigger.id,
253
- result,
254
- };
255
- if (Exit.isSuccess(result)) {
256
- log.info('trigger execution success', {
257
- triggerId: trigger.id,
258
- });
259
- } else {
260
- log.error('trigger execution failure', {
261
- error: causeToError(result.cause),
262
- });
263
- }
264
- yield* tracer.traceInvocationEnd({
265
- trace,
266
- // TODO(dmaretskyi): Might miss errors.
267
- exception: Exit.isFailure(result) ? Cause.prettyErrors(result.cause)[0] : undefined,
268
- });
269
- return triggerExecutionResult;
270
- });
271
-
272
- invokeScheduledTriggers = ({ kinds = ['timer', 'queue', 'subscription'] } = {}): Effect.Effect<
273
- TriggerExecutionResult[],
274
- never,
275
- TriggerDispatcherServices
276
- > =>
277
- Effect.gen(this, function* () {
278
- const invocations: TriggerExecutionResult[] = [];
279
- for (const kind of kinds) {
280
- switch (kind) {
281
- case 'timer':
282
- {
283
- yield* this.refreshTriggers();
284
- const now = this.getCurrentTime();
285
- const triggersToInvoke: FunctionTrigger[] = [];
286
-
287
- for (const [triggerId, scheduledTrigger] of this._scheduledTriggers.entries()) {
288
- if (scheduledTrigger.nextExecution <= now) {
289
- triggersToInvoke.push(scheduledTrigger.trigger);
290
-
291
- // Update next execution time using Effect's Cron
292
- scheduledTrigger.nextExecution = Cron.next(scheduledTrigger.cron, now);
293
- }
294
- }
295
-
296
- // Invoke all due triggers
297
- invocations.push(
298
- ...(yield* Effect.forEach(
299
- triggersToInvoke,
300
- (trigger) =>
301
- this.invokeTrigger({
302
- trigger,
303
- event: { tick: now.getTime() } satisfies TimerTriggerOutput,
304
- }),
305
- { concurrency: 1 },
306
- )),
307
- );
308
- }
309
- break;
310
- case 'queue': {
311
- const triggers = yield* this._fetchTriggers();
312
- for (const trigger of triggers) {
313
- const spec = trigger.spec;
314
- if (spec?.kind !== 'queue') {
315
- continue;
316
- }
317
- const cursor = Obj.getKeys(trigger, KEY_QUEUE_CURSOR).at(0)?.id;
318
- const queue = yield* QueueService.getQueue(DXN.parse(spec.queue));
319
-
320
- // TODO(dmaretskyi): Include cursor & limit in the query.
321
- const objects = yield* Effect.promise(() => queue.queryObjects());
322
- for (const object of objects) {
323
- const objectPos = Obj.getKeys(object, KEY_QUEUE_POSITION).at(0)?.id;
324
- // TODO(dmaretskyi): Extract methods for managing queue position.
325
- if (!objectPos || (cursor && parseInt(cursor) >= parseInt(objectPos))) {
326
- continue;
327
- }
328
-
329
- invocations.push(
330
- yield* this.invokeTrigger({
331
- trigger,
332
- event: {
333
- queue: spec.queue,
334
- item: object,
335
- cursor: objectPos,
336
- } satisfies QueueTriggerOutput,
337
- }),
338
- );
339
-
340
- // Update trigger cursor.
341
- Obj.deleteKeys(trigger, KEY_QUEUE_CURSOR);
342
- Obj.getMeta(trigger).keys.push({ source: KEY_QUEUE_CURSOR, id: objectPos });
343
- yield* DatabaseService.flush();
344
-
345
- // We only invoke one trigger for each queue at a time.
346
- break;
347
- }
348
- }
349
- break;
350
- }
351
- case 'subscription': {
352
- const triggers = yield* this._fetchTriggers();
353
- for (const trigger of triggers) {
354
- const spec = Obj.getSnapshot(trigger).spec;
355
- if (spec?.kind !== 'subscription') {
356
- continue;
357
- }
358
-
359
- const { objects } = yield* DatabaseService.runQuery(Query.fromAst(spec.query));
360
-
361
- const state: TriggerState = yield* TriggerStateStore.getState(trigger.id).pipe(
362
- Effect.catchTag('TRIGGER_STATE_NOT_FOUND', () =>
363
- Effect.succeed({
364
- version: '1',
365
- triggerId: trigger.id,
366
- state: {
367
- _tag: 'subscription',
368
- processedVersions: {} as Record<string, string>,
369
- },
370
- } satisfies TriggerState),
371
- ),
372
- );
373
- invariant(state.state?._tag === 'subscription');
374
-
375
- let updated = false;
376
- for (const object of objects) {
377
- const existingVersion = Record.get(state.state.processedVersions, object.id).pipe(
378
- Option.map(Obj.decodeVersion),
379
- );
380
- const currentVersion = Obj.version(object);
381
- const run =
382
- Option.isNone(existingVersion) ||
383
- Obj.compareVersions(currentVersion, existingVersion.value) === 'different';
384
-
385
- if (!run) {
386
- continue;
387
- }
388
-
389
- invocations.push(
390
- yield* this.invokeTrigger({
391
- trigger,
392
- event: {
393
- // TODO(dmaretskyi): Change type not supported.
394
- type: 'unknown',
395
- changedObjectId: object.id,
396
- } satisfies SubscriptionTriggerOutput,
397
- }),
398
- );
399
- (state.state.processedVersions as any)[object.id] = Obj.encodeVersion(currentVersion);
400
- updated = true;
401
- }
402
-
403
- if (updated) {
404
- yield* TriggerStateStore.saveState(state);
405
- }
406
- }
407
- break;
408
- }
409
- default: {
410
- return yield* Effect.dieMessage(`Unknown trigger kind: ${kind}`);
411
- }
412
- }
413
- }
414
- return invocations;
415
- });
416
-
417
- advanceTime = (duration: Duration.Duration): Effect.Effect<void> =>
418
- Effect.gen(this, function* () {
419
- if (this.timeControl !== 'manual') {
420
- return yield* Effect.dieMessage('advanceTime can only be used in manual time control mode');
421
- }
422
-
423
- const millis = Duration.toMillis(duration);
424
- this._internalTime = new Date(this._internalTime.getTime() + millis);
425
-
426
- log('Advanced internal time', {
427
- newTime: this._internalTime,
428
- advancedBy: Duration.format(duration),
429
- });
430
- }).pipe(Effect.orDie);
431
-
432
- getCurrentTime = (): Date => {
433
- if (this.timeControl === 'natural') {
434
- return new Date();
435
- } else {
436
- return new Date(this._internalTime);
437
- }
438
- };
439
-
440
- refreshTriggers = (): Effect.Effect<void, never, DatabaseService> =>
441
- Effect.gen(this, function* () {
442
- const triggers = yield* this._fetchTriggers();
443
- const currentTriggerIds = new Set(triggers.map((t) => t.id));
444
-
445
- // Remove triggers that are no longer present
446
- for (const triggerId of this._scheduledTriggers.keys()) {
447
- if (!currentTriggerIds.has(triggerId)) {
448
- this._scheduledTriggers.delete(triggerId);
449
- }
450
- }
451
-
452
- // Add or update triggers
453
- for (const trigger of triggers) {
454
- if (trigger.spec?.kind === 'timer' && trigger.enabled) {
455
- const timerSpec = trigger.spec as TimerTrigger;
456
-
457
- // Parse cron expression using Effect's Cron module
458
- const cronEither = Cron.parse(timerSpec.cron);
459
-
460
- if (Either.isRight(cronEither)) {
461
- const cron = cronEither.right;
462
- const existing = this._scheduledTriggers.get(trigger.id);
463
- const now = this.getCurrentTime();
464
- const nextExecution = existing?.nextExecution ?? Cron.next(cron, now);
465
-
466
- log('Updated scheduled trigger', {
467
- triggerId: trigger.id,
468
- cron: timerSpec.cron,
469
- nextExecution,
470
- now,
471
- });
472
- this._scheduledTriggers.set(trigger.id, {
473
- trigger,
474
- cron,
475
- nextExecution,
476
- });
477
- } else {
478
- log.error('Invalid cron expression', {
479
- triggerId: trigger.id,
480
- cron: timerSpec.cron,
481
- error: cronEither.left.message,
482
- });
483
- }
484
- }
485
- }
486
-
487
- log('Updated scheduled triggers', { count: this._scheduledTriggers.size });
488
- }).pipe(Effect.withSpan('TriggerDispatcher.refreshTriggers'));
489
-
490
- private _fetchTriggers = () =>
491
- Effect.gen(this, function* () {
492
- const { objects } = yield* DatabaseService.runQuery(Filter.type(FunctionTrigger));
493
- return objects;
494
- }).pipe(Effect.withSpan('TriggerDispatcher.fetchTriggers'));
495
-
496
- private _startNaturalTimeProcessing = (): Effect.Effect<void, never, TriggerDispatcherServices> =>
497
- Effect.gen(this, function* () {
498
- yield* this.invokeScheduledTriggers();
499
- }).pipe(Effect.repeat(Schedule.fixed(this.livePollInterval)), Effect.asVoid);
500
-
501
- private _prepareInputData = (trigger: FunctionTrigger, event: EventType): any => {
502
- return createInvocationPayload(trigger, event);
503
- };
504
- }
505
-
506
- // Re-exports
507
- export { FunctionTrigger, type TimerTrigger } from '../types';
508
-
509
- /**
510
- * Key for the current queue cursor for queue triggers.
511
- */
512
- const KEY_QUEUE_CURSOR = 'dxos.org/key/local-trigger-dispatcher/queue-cursor';
@@ -1,60 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import { KeyValueStore } from '@effect/platform';
6
- import { Context } from 'effect';
7
- import { Schema } from 'effect';
8
- import { Effect, Layer } from 'effect';
9
- import { Option } from 'effect';
10
-
11
- import { ObjectId } from '@dxos/keys';
12
-
13
- import { TriggerStateNotFoundError } from '../errors';
14
-
15
- export const TriggerState = Schema.Struct({
16
- version: Schema.Literal('1'),
17
- triggerId: Schema.String,
18
- state: Schema.optional(
19
- Schema.Union(
20
- Schema.TaggedStruct('subscription', {
21
- processedVersions: Schema.Record({ key: ObjectId, value: Schema.String }),
22
- }),
23
- ),
24
- ),
25
- });
26
- export interface TriggerState extends Schema.Schema.Type<typeof TriggerState> {}
27
-
28
- export class TriggerStateStore extends Context.Tag('@dxos/functions/TriggerStateStore')<
29
- TriggerStateStore,
30
- {
31
- getState(triggerId: ObjectId): Effect.Effect<TriggerState, TriggerStateNotFoundError>;
32
- saveState(state: TriggerState): Effect.Effect<void>;
33
- }
34
- >() {
35
- static getState = Effect.serviceFunctionEffect(TriggerStateStore, (_) => _.getState);
36
- static saveState = Effect.serviceFunctionEffect(TriggerStateStore, (_) => _.saveState);
37
-
38
- static layerKv = Layer.effect(
39
- TriggerStateStore,
40
- Effect.gen(function* () {
41
- const kv = yield* KeyValueStore.KeyValueStore;
42
- const schemaStore = kv.forSchema(Schema.parseJson(TriggerState));
43
- const store: Context.Tag.Service<TriggerStateStore> = {
44
- getState: Effect.fn('TriggerStateStore.getState')(function* (triggerId: ObjectId) {
45
- const valueOption = yield* schemaStore.get(triggerId).pipe(Effect.orDie);
46
- if (Option.isNone(valueOption)) {
47
- return yield* Effect.fail(new TriggerStateNotFoundError('Trigger state not found'));
48
- }
49
- return valueOption.value;
50
- }),
51
- saveState: Effect.fn('TriggerStateStore.saveState')(function* (state: TriggerState) {
52
- yield* schemaStore.set(state.triggerId, state).pipe(Effect.orDie);
53
- }),
54
- };
55
- return store;
56
- }),
57
- );
58
-
59
- static layerMemory = TriggerStateStore.layerKv.pipe(Layer.provide(KeyValueStore.layerMemory));
60
- }