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

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 +593 -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/functions-ai-http-client.test.d.ts +2 -0
  10. package/dist/types/src/protocol/functions-ai-http-client.test.d.ts.map +1 -0
  11. package/dist/types/src/protocol/index.d.ts +2 -0
  12. package/dist/types/src/protocol/index.d.ts.map +1 -0
  13. package/dist/types/src/protocol/protocol.d.ts +19 -0
  14. package/dist/types/src/protocol/protocol.d.ts.map +1 -0
  15. package/dist/types/src/protocol/protocol.test.d.ts +2 -0
  16. package/dist/types/src/protocol/protocol.test.d.ts.map +1 -0
  17. package/dist/types/src/sdk.d.ts +10 -0
  18. package/dist/types/src/sdk.d.ts.map +1 -0
  19. package/dist/types/src/services/credentials.d.ts +22 -39
  20. package/dist/types/src/services/credentials.d.ts.map +1 -1
  21. package/dist/types/src/services/function-invocation-service.d.ts +15 -0
  22. package/dist/types/src/services/function-invocation-service.d.ts.map +1 -0
  23. package/dist/types/src/services/index.d.ts +3 -7
  24. package/dist/types/src/services/index.d.ts.map +1 -1
  25. package/dist/types/src/services/queues.d.ts +1 -44
  26. package/dist/types/src/services/queues.d.ts.map +1 -1
  27. package/dist/types/src/services/tracing.d.ts +2 -54
  28. package/dist/types/src/services/tracing.d.ts.map +1 -1
  29. package/dist/types/src/types/index.d.ts +2 -0
  30. package/dist/types/src/types/index.d.ts.map +1 -0
  31. package/dist/types/src/types/url.d.ts +13 -0
  32. package/dist/types/src/types/url.d.ts.map +1 -0
  33. package/dist/types/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +25 -72
  35. package/src/index.ts +3 -9
  36. package/src/protocol/functions-ai-http-client.test.ts +105 -0
  37. package/src/protocol/functions-ai-http-client.ts +141 -0
  38. package/src/{executor → protocol}/index.ts +1 -1
  39. package/src/protocol/protocol.test.ts +58 -0
  40. package/src/protocol/protocol.ts +428 -0
  41. package/src/sdk.ts +31 -0
  42. package/src/services/credentials.ts +90 -111
  43. package/src/services/function-invocation-service.ts +33 -0
  44. package/src/services/index.ts +3 -7
  45. package/src/services/queues.ts +1 -78
  46. package/src/services/tracing.ts +1 -133
  47. package/src/types/index.ts +5 -0
  48. package/src/types/url.ts +32 -0
  49. package/dist/lib/browser/bundler/index.mjs +0 -265
  50. package/dist/lib/browser/bundler/index.mjs.map +0 -7
  51. package/dist/lib/browser/chunk-ANP3DFCO.mjs +0 -623
  52. package/dist/lib/browser/chunk-ANP3DFCO.mjs.map +0 -7
  53. package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
  54. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
  55. package/dist/lib/browser/edge/index.mjs +0 -83
  56. package/dist/lib/browser/edge/index.mjs.map +0 -7
  57. package/dist/lib/browser/index.mjs +0 -1265
  58. package/dist/lib/browser/index.mjs.map +0 -7
  59. package/dist/lib/browser/meta.json +0 -1
  60. package/dist/lib/browser/testing/index.mjs +0 -129
  61. package/dist/lib/browser/testing/index.mjs.map +0 -7
  62. package/dist/lib/node-esm/bundler/index.mjs +0 -266
  63. package/dist/lib/node-esm/bundler/index.mjs.map +0 -7
  64. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  65. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  66. package/dist/lib/node-esm/chunk-MPKVY7ZR.mjs +0 -625
  67. package/dist/lib/node-esm/chunk-MPKVY7ZR.mjs.map +0 -7
  68. package/dist/lib/node-esm/edge/index.mjs +0 -84
  69. package/dist/lib/node-esm/edge/index.mjs.map +0 -7
  70. package/dist/lib/node-esm/index.mjs +0 -1266
  71. package/dist/lib/node-esm/index.mjs.map +0 -7
  72. package/dist/lib/node-esm/meta.json +0 -1
  73. package/dist/lib/node-esm/testing/index.mjs +0 -130
  74. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  75. package/dist/types/src/bundler/bundler.d.ts +0 -49
  76. package/dist/types/src/bundler/bundler.d.ts.map +0 -1
  77. package/dist/types/src/bundler/bundler.test.d.ts +0 -2
  78. package/dist/types/src/bundler/bundler.test.d.ts.map +0 -1
  79. package/dist/types/src/bundler/index.d.ts +0 -2
  80. package/dist/types/src/bundler/index.d.ts.map +0 -1
  81. package/dist/types/src/edge/functions.d.ts +0 -17
  82. package/dist/types/src/edge/functions.d.ts.map +0 -1
  83. package/dist/types/src/edge/index.d.ts +0 -2
  84. package/dist/types/src/edge/index.d.ts.map +0 -1
  85. package/dist/types/src/errors.d.ts +0 -137
  86. package/dist/types/src/errors.d.ts.map +0 -1
  87. package/dist/types/src/examples/fib.d.ts +0 -7
  88. package/dist/types/src/examples/fib.d.ts.map +0 -1
  89. package/dist/types/src/examples/index.d.ts +0 -4
  90. package/dist/types/src/examples/index.d.ts.map +0 -1
  91. package/dist/types/src/examples/reply.d.ts +0 -3
  92. package/dist/types/src/examples/reply.d.ts.map +0 -1
  93. package/dist/types/src/examples/sleep.d.ts +0 -5
  94. package/dist/types/src/examples/sleep.d.ts.map +0 -1
  95. package/dist/types/src/executor/executor.d.ts +0 -11
  96. package/dist/types/src/executor/executor.d.ts.map +0 -1
  97. package/dist/types/src/executor/index.d.ts +0 -2
  98. package/dist/types/src/executor/index.d.ts.map +0 -1
  99. package/dist/types/src/handler.d.ts +0 -94
  100. package/dist/types/src/handler.d.ts.map +0 -1
  101. package/dist/types/src/schema.d.ts +0 -43
  102. package/dist/types/src/schema.d.ts.map +0 -1
  103. package/dist/types/src/services/database.d.ts +0 -98
  104. package/dist/types/src/services/database.d.ts.map +0 -1
  105. package/dist/types/src/services/event-logger.d.ts +0 -72
  106. package/dist/types/src/services/event-logger.d.ts.map +0 -1
  107. package/dist/types/src/services/local-function-execution.d.ts +0 -25
  108. package/dist/types/src/services/local-function-execution.d.ts.map +0 -1
  109. package/dist/types/src/services/remote-function-execution-service.d.ts +0 -15
  110. package/dist/types/src/services/remote-function-execution-service.d.ts.map +0 -1
  111. package/dist/types/src/services/service-container.d.ts +0 -56
  112. package/dist/types/src/services/service-container.d.ts.map +0 -1
  113. package/dist/types/src/services/service-registry.d.ts +0 -29
  114. package/dist/types/src/services/service-registry.d.ts.map +0 -1
  115. package/dist/types/src/services/service-registry.test.d.ts +0 -2
  116. package/dist/types/src/services/service-registry.test.d.ts.map +0 -1
  117. package/dist/types/src/testing/index.d.ts +0 -3
  118. package/dist/types/src/testing/index.d.ts.map +0 -1
  119. package/dist/types/src/testing/layer.d.ts +0 -15
  120. package/dist/types/src/testing/layer.d.ts.map +0 -1
  121. package/dist/types/src/testing/logger.d.ts +0 -5
  122. package/dist/types/src/testing/logger.d.ts.map +0 -1
  123. package/dist/types/src/testing/persist-database.test.d.ts +0 -2
  124. package/dist/types/src/testing/persist-database.test.d.ts.map +0 -1
  125. package/dist/types/src/testing/services.d.ts +0 -59
  126. package/dist/types/src/testing/services.d.ts.map +0 -1
  127. package/dist/types/src/trace.d.ts +0 -122
  128. package/dist/types/src/trace.d.ts.map +0 -1
  129. package/dist/types/src/translations.d.ts +0 -12
  130. package/dist/types/src/translations.d.ts.map +0 -1
  131. package/dist/types/src/triggers/index.d.ts +0 -4
  132. package/dist/types/src/triggers/index.d.ts.map +0 -1
  133. package/dist/types/src/triggers/input-builder.d.ts +0 -3
  134. package/dist/types/src/triggers/input-builder.d.ts.map +0 -1
  135. package/dist/types/src/triggers/invocation-tracer.d.ts +0 -35
  136. package/dist/types/src/triggers/invocation-tracer.d.ts.map +0 -1
  137. package/dist/types/src/triggers/trigger-dispatcher.d.ts +0 -75
  138. package/dist/types/src/triggers/trigger-dispatcher.d.ts.map +0 -1
  139. package/dist/types/src/triggers/trigger-dispatcher.test.d.ts +0 -2
  140. package/dist/types/src/triggers/trigger-dispatcher.test.d.ts.map +0 -1
  141. package/dist/types/src/triggers/trigger-state-store.d.ts +0 -27
  142. package/dist/types/src/triggers/trigger-state-store.d.ts.map +0 -1
  143. package/dist/types/src/types.d.ts +0 -211
  144. package/dist/types/src/types.d.ts.map +0 -1
  145. package/dist/types/src/url.d.ts +0 -21
  146. package/dist/types/src/url.d.ts.map +0 -1
  147. package/src/bundler/bundler.test.ts +0 -58
  148. package/src/bundler/bundler.ts +0 -291
  149. package/src/bundler/index.ts +0 -5
  150. package/src/edge/functions.ts +0 -67
  151. package/src/edge/index.ts +0 -9
  152. package/src/errors.ts +0 -21
  153. package/src/examples/fib.ts +0 -30
  154. package/src/examples/index.ts +0 -7
  155. package/src/examples/reply.ts +0 -18
  156. package/src/examples/sleep.ts +0 -22
  157. package/src/executor/executor.ts +0 -54
  158. package/src/handler.ts +0 -201
  159. package/src/schema.ts +0 -68
  160. package/src/services/database.ts +0 -170
  161. package/src/services/event-logger.ts +0 -118
  162. package/src/services/local-function-execution.ts +0 -114
  163. package/src/services/remote-function-execution-service.ts +0 -46
  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 -111
  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 -652
  178. package/src/triggers/trigger-dispatcher.ts +0 -512
  179. package/src/triggers/trigger-state-store.ts +0 -60
  180. package/src/types.ts +0 -200
  181. 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
- }