@dxos/functions 0.8.4-main.a4bbb77 → 0.8.4-main.abd8ff62ef

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