@dxos/functions 0.8.3 → 0.8.4-main.1da679c

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 (161) hide show
  1. package/dist/lib/browser/bundler/index.mjs +73 -44
  2. package/dist/lib/browser/bundler/index.mjs.map +3 -3
  3. package/dist/lib/browser/chunk-D2XO7XXY.mjs +611 -0
  4. package/dist/lib/browser/chunk-D2XO7XXY.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  6. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  7. package/dist/lib/browser/edge/index.mjs +24 -10
  8. package/dist/lib/browser/edge/index.mjs.map +3 -3
  9. package/dist/lib/browser/index.mjs +981 -137
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/testing/index.mjs +110 -9
  13. package/dist/lib/browser/testing/index.mjs.map +4 -4
  14. package/dist/lib/node-esm/bundler/index.mjs +72 -44
  15. package/dist/lib/node-esm/bundler/index.mjs.map +3 -3
  16. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  17. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  18. package/dist/lib/node-esm/chunk-Z4CJ62WS.mjs +613 -0
  19. package/dist/lib/node-esm/chunk-Z4CJ62WS.mjs.map +7 -0
  20. package/dist/lib/node-esm/edge/index.mjs +23 -10
  21. package/dist/lib/node-esm/edge/index.mjs.map +3 -3
  22. package/dist/lib/node-esm/index.mjs +981 -137
  23. package/dist/lib/node-esm/index.mjs.map +4 -4
  24. package/dist/lib/node-esm/meta.json +1 -1
  25. package/dist/lib/node-esm/testing/index.mjs +110 -9
  26. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  27. package/dist/types/src/bundler/bundler.d.ts +12 -14
  28. package/dist/types/src/bundler/bundler.d.ts.map +1 -1
  29. package/dist/types/src/edge/functions.d.ts +4 -3
  30. package/dist/types/src/edge/functions.d.ts.map +1 -1
  31. package/dist/types/src/errors.d.ts +137 -0
  32. package/dist/types/src/errors.d.ts.map +1 -0
  33. package/dist/types/src/examples/fib.d.ts +7 -0
  34. package/dist/types/src/examples/fib.d.ts.map +1 -0
  35. package/dist/types/src/examples/index.d.ts +4 -0
  36. package/dist/types/src/examples/index.d.ts.map +1 -0
  37. package/dist/types/src/examples/reply.d.ts +3 -0
  38. package/dist/types/src/examples/reply.d.ts.map +1 -0
  39. package/dist/types/src/examples/sleep.d.ts +5 -0
  40. package/dist/types/src/examples/sleep.d.ts.map +1 -0
  41. package/dist/types/src/executor/executor.d.ts +4 -1
  42. package/dist/types/src/executor/executor.d.ts.map +1 -1
  43. package/dist/types/src/handler.d.ts +46 -16
  44. package/dist/types/src/handler.d.ts.map +1 -1
  45. package/dist/types/src/index.d.ts +3 -0
  46. package/dist/types/src/index.d.ts.map +1 -1
  47. package/dist/types/src/schema.d.ts +8 -3
  48. package/dist/types/src/schema.d.ts.map +1 -1
  49. package/dist/types/src/services/credentials.d.ts +18 -4
  50. package/dist/types/src/services/credentials.d.ts.map +1 -1
  51. package/dist/types/src/services/database.d.ts +55 -3
  52. package/dist/types/src/services/database.d.ts.map +1 -1
  53. package/dist/types/src/services/event-logger.d.ts +65 -30
  54. package/dist/types/src/services/event-logger.d.ts.map +1 -1
  55. package/dist/types/src/services/index.d.ts +2 -2
  56. package/dist/types/src/services/index.d.ts.map +1 -1
  57. package/dist/types/src/services/local-function-execution.d.ts +25 -0
  58. package/dist/types/src/services/local-function-execution.d.ts.map +1 -0
  59. package/dist/types/src/services/queues.d.ts +33 -6
  60. package/dist/types/src/services/queues.d.ts.map +1 -1
  61. package/dist/types/src/services/remote-function-execution-service.d.ts +15 -0
  62. package/dist/types/src/services/remote-function-execution-service.d.ts.map +1 -0
  63. package/dist/types/src/services/service-container.d.ts +29 -17
  64. package/dist/types/src/services/service-container.d.ts.map +1 -1
  65. package/dist/types/src/services/service-registry.d.ts +29 -0
  66. package/dist/types/src/services/service-registry.d.ts.map +1 -0
  67. package/dist/types/src/services/service-registry.test.d.ts +2 -0
  68. package/dist/types/src/services/service-registry.test.d.ts.map +1 -0
  69. package/dist/types/src/services/tracing.d.ts +46 -4
  70. package/dist/types/src/services/tracing.d.ts.map +1 -1
  71. package/dist/types/src/testing/index.d.ts +1 -0
  72. package/dist/types/src/testing/index.d.ts.map +1 -1
  73. package/dist/types/src/testing/layer.d.ts +15 -0
  74. package/dist/types/src/testing/layer.d.ts.map +1 -0
  75. package/dist/types/src/testing/logger.d.ts +3 -3
  76. package/dist/types/src/testing/logger.d.ts.map +1 -1
  77. package/dist/types/src/testing/persist-database.test.d.ts +2 -0
  78. package/dist/types/src/testing/persist-database.test.d.ts.map +1 -0
  79. package/dist/types/src/testing/services.d.ts +55 -9
  80. package/dist/types/src/testing/services.d.ts.map +1 -1
  81. package/dist/types/src/trace.d.ts +20 -22
  82. package/dist/types/src/trace.d.ts.map +1 -1
  83. package/dist/types/src/translations.d.ts +9 -9
  84. package/dist/types/src/translations.d.ts.map +1 -1
  85. package/dist/types/src/triggers/index.d.ts +4 -0
  86. package/dist/types/src/triggers/index.d.ts.map +1 -0
  87. package/dist/types/src/triggers/input-builder.d.ts +3 -0
  88. package/dist/types/src/triggers/input-builder.d.ts.map +1 -0
  89. package/dist/types/src/triggers/invocation-tracer.d.ts +35 -0
  90. package/dist/types/src/triggers/invocation-tracer.d.ts.map +1 -0
  91. package/dist/types/src/triggers/trigger-dispatcher.d.ts +75 -0
  92. package/dist/types/src/triggers/trigger-dispatcher.d.ts.map +1 -0
  93. package/dist/types/src/triggers/trigger-dispatcher.test.d.ts +2 -0
  94. package/dist/types/src/triggers/trigger-dispatcher.test.d.ts.map +1 -0
  95. package/dist/types/src/triggers/trigger-state-store.d.ts +27 -0
  96. package/dist/types/src/triggers/trigger-state-store.d.ts.map +1 -0
  97. package/dist/types/src/types.d.ts +55 -245
  98. package/dist/types/src/types.d.ts.map +1 -1
  99. package/dist/types/src/url.d.ts +10 -6
  100. package/dist/types/src/url.d.ts.map +1 -1
  101. package/dist/types/tsconfig.tsbuildinfo +1 -1
  102. package/package.json +35 -25
  103. package/src/bundler/bundler.test.ts +9 -10
  104. package/src/bundler/bundler.ts +56 -35
  105. package/src/edge/functions.ts +9 -6
  106. package/src/errors.ts +21 -0
  107. package/src/examples/fib.ts +30 -0
  108. package/src/examples/index.ts +7 -0
  109. package/src/examples/reply.ts +18 -0
  110. package/src/examples/sleep.ts +22 -0
  111. package/src/executor/executor.ts +22 -15
  112. package/src/handler.ts +117 -27
  113. package/src/index.ts +3 -2
  114. package/src/schema.ts +11 -0
  115. package/src/services/credentials.ts +87 -5
  116. package/src/services/database.ts +146 -3
  117. package/src/services/event-logger.ts +68 -37
  118. package/src/services/index.ts +2 -2
  119. package/src/services/local-function-execution.ts +127 -0
  120. package/src/services/queues.ts +56 -11
  121. package/src/services/remote-function-execution-service.ts +46 -0
  122. package/src/services/service-container.ts +47 -42
  123. package/src/services/service-registry.test.ts +42 -0
  124. package/src/services/service-registry.ts +59 -0
  125. package/src/services/tracing.ts +118 -5
  126. package/src/testing/index.ts +1 -0
  127. package/src/testing/layer.ts +111 -0
  128. package/src/testing/logger.ts +4 -4
  129. package/src/testing/persist-database.test.ts +87 -0
  130. package/src/testing/services.ts +97 -14
  131. package/src/trace.ts +17 -19
  132. package/src/translations.ts +4 -4
  133. package/src/triggers/index.ts +7 -0
  134. package/src/triggers/input-builder.ts +35 -0
  135. package/src/triggers/invocation-tracer.ts +99 -0
  136. package/src/triggers/trigger-dispatcher.test.ts +652 -0
  137. package/src/triggers/trigger-dispatcher.ts +516 -0
  138. package/src/triggers/trigger-state-store.ts +60 -0
  139. package/src/types.ts +39 -36
  140. package/src/url.ts +13 -10
  141. package/dist/lib/browser/chunk-WEFZUEL2.mjs +0 -300
  142. package/dist/lib/browser/chunk-WEFZUEL2.mjs.map +0 -7
  143. package/dist/lib/node/bundler/index.cjs +0 -260
  144. package/dist/lib/node/bundler/index.cjs.map +0 -7
  145. package/dist/lib/node/chunk-IJAE7FZK.cjs +0 -320
  146. package/dist/lib/node/chunk-IJAE7FZK.cjs.map +0 -7
  147. package/dist/lib/node/edge/index.cjs +0 -94
  148. package/dist/lib/node/edge/index.cjs.map +0 -7
  149. package/dist/lib/node/index.cjs +0 -522
  150. package/dist/lib/node/index.cjs.map +0 -7
  151. package/dist/lib/node/meta.json +0 -1
  152. package/dist/lib/node/testing/index.cjs +0 -43
  153. package/dist/lib/node/testing/index.cjs.map +0 -7
  154. package/dist/lib/node-esm/chunk-LIYPMWNQ.mjs +0 -302
  155. package/dist/lib/node-esm/chunk-LIYPMWNQ.mjs.map +0 -7
  156. package/dist/types/src/services/ai.d.ts +0 -12
  157. package/dist/types/src/services/ai.d.ts.map +0 -1
  158. package/dist/types/src/services/function-call-service.d.ts +0 -16
  159. package/dist/types/src/services/function-call-service.d.ts.map +0 -1
  160. package/src/services/ai.ts +0 -32
  161. package/src/services/function-call-service.ts +0 -64
@@ -0,0 +1,652 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { FetchHttpClient } from '@effect/platform';
6
+ import { describe, it } from '@effect/vitest';
7
+ import { Duration, Effect, Exit, Layer, pipe } from 'effect';
8
+
9
+ import { AiService } from '@dxos/ai';
10
+ import { Filter, Obj, Query, Ref } from '@dxos/echo';
11
+ import { invariant } from '@dxos/invariant';
12
+ import { DataType } from '@dxos/schema';
13
+
14
+ import { default as reply } from '../examples/reply';
15
+ import { serializeFunction } from '../handler';
16
+ import { FunctionType } from '../schema';
17
+ import {
18
+ ComputeEventLogger,
19
+ CredentialsService,
20
+ DatabaseService,
21
+ QueueService,
22
+ RemoteFunctionExecutionService,
23
+ TracingService,
24
+ } from '../services';
25
+ import { FunctionImplementationResolver, LocalFunctionExecutionService } from '../services/local-function-execution';
26
+ import { TestDatabaseLayer } from '../testing';
27
+ import { FunctionTrigger } from '../types';
28
+
29
+ import { InvocationTracer } from './invocation-tracer';
30
+ import { TriggerDispatcher } from './trigger-dispatcher';
31
+ import { TriggerStateStore } from './trigger-state-store';
32
+
33
+ const TestLayer = pipe(
34
+ Layer.mergeAll(ComputeEventLogger.layerFromTracing, InvocationTracer.layerTest, TriggerStateStore.layerMemory),
35
+ Layer.provideMerge(
36
+ Layer.mergeAll(
37
+ AiService.notAvailable,
38
+ TestDatabaseLayer({
39
+ types: [FunctionType, FunctionTrigger, DataType.Person, DataType.Task],
40
+ }),
41
+ CredentialsService.layerConfig([]),
42
+ LocalFunctionExecutionService.layerLive,
43
+ RemoteFunctionExecutionService.mockLayer,
44
+ TracingService.layerLogInfo(),
45
+ FetchHttpClient.layer,
46
+ ),
47
+ ),
48
+ Layer.provideMerge(FunctionImplementationResolver.layerTest({ functions: [reply] })),
49
+ );
50
+
51
+ const TestTriggerDispatcherLayer = Layer.provideMerge(
52
+ TriggerDispatcher.layer({ timeControl: 'manual', startingTime: new Date('2025-09-05T15:01:00.000Z') }),
53
+ TestLayer,
54
+ );
55
+
56
+ describe('TriggerDispatcher', () => {
57
+ describe('Time Control', () => {
58
+ it.effect(
59
+ 'should get current time based on time control',
60
+ Effect.fnUntraced(function* ({ expect }) {
61
+ const dispatcher = yield* TriggerDispatcher;
62
+
63
+ const initialTime = dispatcher.getCurrentTime();
64
+
65
+ // Advance time by 1 hour
66
+ yield* dispatcher.advanceTime(Duration.hours(1));
67
+
68
+ const newTime = dispatcher.getCurrentTime();
69
+ const timeDiff = newTime.getTime() - initialTime.getTime();
70
+
71
+ expect(timeDiff).toBe(Duration.toMillis(Duration.hours(1)));
72
+ }, Effect.provide(TestTriggerDispatcherLayer)),
73
+ );
74
+ });
75
+
76
+ describe('Manual Invocation', () => {
77
+ it.effect(
78
+ 'should manually invoke trigger',
79
+ Effect.fnUntraced(function* ({ expect }) {
80
+ const functionObj = serializeFunction(reply);
81
+ yield* DatabaseService.add(functionObj);
82
+ const trigger = Obj.make(FunctionTrigger, {
83
+ function: Ref.make(functionObj),
84
+ enabled: true,
85
+ spec: {
86
+ kind: 'timer',
87
+ cron: '*/5 * * * *',
88
+ },
89
+ });
90
+ yield* DatabaseService.add(trigger);
91
+ const dispatcher = yield* TriggerDispatcher;
92
+ const { result } = yield* dispatcher.invokeTrigger({
93
+ trigger,
94
+ event: { tick: 0 },
95
+ });
96
+
97
+ expect(result).toEqual(Exit.succeed({ tick: 0 }));
98
+ }, Effect.provide(TestTriggerDispatcherLayer)),
99
+ );
100
+ });
101
+
102
+ describe('Timer Triggers', () => {
103
+ it.effect(
104
+ 'should invoke scheduled timer triggers',
105
+ Effect.fnUntraced(function* ({ expect }) {
106
+ const functionObj = serializeFunction(reply);
107
+ yield* DatabaseService.add(functionObj);
108
+ const trigger = Obj.make(FunctionTrigger, {
109
+ function: Ref.make(functionObj),
110
+ enabled: true,
111
+ spec: {
112
+ kind: 'timer',
113
+ cron: '* * * * *', // Every minute - should trigger immediately
114
+ },
115
+ });
116
+ yield* DatabaseService.add(trigger);
117
+
118
+ const dispatcher = yield* TriggerDispatcher;
119
+ yield* dispatcher.refreshTriggers();
120
+
121
+ // Manually invoke the trigger
122
+ yield* dispatcher.advanceTime(Duration.minutes(1));
123
+ const results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['timer'] });
124
+
125
+ // Should have executed successfully
126
+ expect(results.length).toBe(1);
127
+ expect(results[0].triggerId).toBe(trigger.id);
128
+ expect(Exit.isSuccess(results[0].result)).toBe(true);
129
+ }, Effect.provide(TestTriggerDispatcherLayer)),
130
+ );
131
+
132
+ it.effect(
133
+ 'should handle disabled triggers',
134
+ Effect.fnUntraced(function* ({ expect }) {
135
+ const functionObj = serializeFunction(reply);
136
+ yield* DatabaseService.add(functionObj);
137
+
138
+ const enabledTrigger = Obj.make(FunctionTrigger, {
139
+ function: Ref.make(functionObj),
140
+ enabled: true,
141
+ spec: {
142
+ kind: 'timer',
143
+ cron: '* * * * *',
144
+ },
145
+ });
146
+
147
+ const disabledTrigger = Obj.make(FunctionTrigger, {
148
+ function: Ref.make(functionObj),
149
+ enabled: false,
150
+ spec: {
151
+ kind: 'timer',
152
+ cron: '* * * * *',
153
+ },
154
+ });
155
+
156
+ yield* DatabaseService.add(enabledTrigger);
157
+ yield* DatabaseService.add(disabledTrigger);
158
+
159
+ const dispatcher = yield* TriggerDispatcher;
160
+ yield* dispatcher.refreshTriggers();
161
+
162
+ // Manually test invocation of enabled vs disabled
163
+ yield* dispatcher.advanceTime(Duration.minutes(1));
164
+ const results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['timer'] });
165
+
166
+ // Enabled should succeed
167
+ expect(results.length).toBe(1);
168
+ expect(results[0].triggerId).toBe(enabledTrigger.id);
169
+ expect(Exit.isSuccess(results[0].result)).toBe(true);
170
+ }, Effect.provide(TestTriggerDispatcherLayer)),
171
+ );
172
+
173
+ it.effect(
174
+ 'cron triggers are invoked periodically on schedule',
175
+ Effect.fnUntraced(function* ({ expect }) {
176
+ const functionObj = serializeFunction(reply);
177
+ yield* DatabaseService.add(functionObj);
178
+
179
+ // cron every 5 minutes
180
+ const trigger = Obj.make(FunctionTrigger, {
181
+ function: Ref.make(functionObj),
182
+ enabled: true,
183
+ spec: {
184
+ kind: 'timer',
185
+ cron: '*/5 * * * *',
186
+ },
187
+ });
188
+ yield* DatabaseService.add(trigger);
189
+
190
+ // now = 15:01
191
+ const dispatcher = yield* TriggerDispatcher;
192
+ yield* dispatcher.refreshTriggers(); // next execution = 15:05
193
+
194
+ // advance 1 minute; now = 15:02 -- trigger should not be invoked
195
+ yield* dispatcher.advanceTime(Duration.minutes(1));
196
+ let results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['timer'] });
197
+ expect(results.length).toBe(0);
198
+
199
+ // advance 4 more minutes; now = 15:06 -- trigger should be invoked
200
+ yield* dispatcher.advanceTime(Duration.minutes(4));
201
+ results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['timer'] });
202
+ expect(results.length).toBe(1);
203
+
204
+ // advance 2 more minutes; now = 15:08 -- trigger should not be invoked
205
+ yield* dispatcher.advanceTime(Duration.minutes(2));
206
+ results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['timer'] });
207
+ expect(results.length).toBe(0);
208
+
209
+ // advance 3 more minutes; now = 15:11 -- trigger should be invoked
210
+ yield* dispatcher.advanceTime(Duration.minutes(3));
211
+ results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['timer'] });
212
+ expect(results.length).toBe(1);
213
+ }, Effect.provide(TestTriggerDispatcherLayer)),
214
+ );
215
+ });
216
+
217
+ describe('Dynamic Trigger Management', () => {
218
+ it.effect(
219
+ 'should handle trigger updates dynamically',
220
+ Effect.fnUntraced(function* ({ expect }) {
221
+ const dispatcher = yield* TriggerDispatcher;
222
+ yield* dispatcher.refreshTriggers();
223
+
224
+ // Initially no triggers in database
225
+
226
+ // Add a trigger dynamically
227
+ const functionObj = serializeFunction(reply);
228
+ yield* DatabaseService.add(functionObj);
229
+ const trigger = Obj.make(FunctionTrigger, {
230
+ function: Ref.make(functionObj),
231
+ enabled: true,
232
+ spec: {
233
+ kind: 'timer',
234
+ cron: '* * * * *', // Every minute
235
+ },
236
+ });
237
+ yield* DatabaseService.add(trigger);
238
+
239
+ // Can invoke the trigger
240
+ const result = yield* dispatcher.invokeTrigger({ trigger, event: { tick: 0 } });
241
+ expect(Exit.isSuccess(result.result)).toBe(true);
242
+ }, Effect.provide(TestTriggerDispatcherLayer)),
243
+ );
244
+ });
245
+
246
+ describe('Cron Patterns', () => {
247
+ it.effect(
248
+ 'should support Effect cron expressions',
249
+ Effect.fnUntraced(function* ({ expect }) {
250
+ const functionObj = serializeFunction(reply);
251
+ yield* DatabaseService.add(functionObj);
252
+
253
+ const validPatterns = [
254
+ '* * * * *', // Every minute
255
+ '0 * * * *', // Every hour
256
+ '0 0 * * *', // Daily
257
+ '0 0 * * 1', // Every Monday
258
+ '0 9-17 * * *', // Every hour from 9 AM to 5 PM
259
+ ];
260
+
261
+ const dispatcher = yield* TriggerDispatcher;
262
+
263
+ // Test that valid patterns can be invoked
264
+ for (const cron of validPatterns) {
265
+ const trigger = Obj.make(FunctionTrigger, {
266
+ function: Ref.make(functionObj),
267
+ enabled: true,
268
+ spec: {
269
+ kind: 'timer',
270
+ cron,
271
+ },
272
+ });
273
+ yield* DatabaseService.add(trigger);
274
+
275
+ const result = yield* dispatcher.invokeTrigger({ trigger, event: { tick: 0 } });
276
+ expect(Exit.isSuccess(result.result)).toBe(true);
277
+ }
278
+ }, Effect.provide(TestTriggerDispatcherLayer)),
279
+ );
280
+
281
+ it.effect(
282
+ 'should handle invalid cron expressions gracefully',
283
+ Effect.fnUntraced(function* ({ expect }) {
284
+ const functionObj = serializeFunction(reply);
285
+ yield* DatabaseService.add(functionObj);
286
+
287
+ // Test with an invalid pattern
288
+ const trigger = Obj.make(FunctionTrigger, {
289
+ function: Ref.make(functionObj),
290
+ enabled: true,
291
+ spec: {
292
+ kind: 'timer',
293
+ cron: 'invalid-cron',
294
+ },
295
+ });
296
+ yield* DatabaseService.add(trigger);
297
+
298
+ const dispatcher = yield* TriggerDispatcher;
299
+ yield* dispatcher.refreshTriggers();
300
+
301
+ // Can still invoke manually even with invalid cron
302
+ const result = yield* dispatcher.invokeScheduledTriggers({ kinds: ['timer'] });
303
+ expect(result.length).toBe(0);
304
+ }, Effect.provide(TestTriggerDispatcherLayer)),
305
+ );
306
+ });
307
+
308
+ describe('Natural Time Control', () => {
309
+ it.effect(
310
+ 'should start and stop dispatcher',
311
+ Effect.fnUntraced(
312
+ function* () {
313
+ const dispatcher = yield* TriggerDispatcher;
314
+ yield* dispatcher.start();
315
+ yield* dispatcher.stop();
316
+ },
317
+ Effect.provide(Layer.provideMerge(TriggerDispatcher.layer({ timeControl: 'natural' }), TestLayer)),
318
+ ),
319
+ );
320
+ });
321
+
322
+ describe('Queue Triggers', () => {
323
+ it.effect(
324
+ 'should invoke scheduled queue triggers',
325
+ Effect.fnUntraced(function* ({ expect }) {
326
+ const queue = yield* QueueService.createQueue();
327
+ const functionObj = serializeFunction(reply);
328
+ yield* DatabaseService.add(functionObj);
329
+ const trigger = Obj.make(FunctionTrigger, {
330
+ function: Ref.make(functionObj),
331
+ enabled: true,
332
+ spec: {
333
+ kind: 'queue',
334
+ queue: queue.dxn.toString(),
335
+ },
336
+ });
337
+ yield* DatabaseService.add(trigger);
338
+ yield* QueueService.append(queue, [
339
+ Obj.make(DataType.Person, {
340
+ fullName: 'John Doe',
341
+ }),
342
+ ]);
343
+
344
+ const dispatcher = yield* TriggerDispatcher;
345
+ const results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['queue'] });
346
+ expect(results.length).toBe(1);
347
+ expect(results[0].triggerId).toBe(trigger.id);
348
+ expect(Exit.isSuccess(results[0].result)).toBe(true);
349
+ }, Effect.provide(TestTriggerDispatcherLayer)),
350
+ );
351
+
352
+ it.effect(
353
+ 'triggers are invoked one by one',
354
+ Effect.fnUntraced(function* ({ expect }) {
355
+ const queue = yield* QueueService.createQueue();
356
+ const functionObj = serializeFunction(reply);
357
+ yield* DatabaseService.add(functionObj);
358
+ const trigger = Obj.make(FunctionTrigger, {
359
+ function: Ref.make(functionObj),
360
+ enabled: true,
361
+ spec: {
362
+ kind: 'queue',
363
+ queue: queue.dxn.toString(),
364
+ },
365
+ });
366
+ yield* DatabaseService.add(trigger);
367
+ yield* QueueService.append(queue, [
368
+ Obj.make(DataType.Person, {
369
+ fullName: 'John Doe',
370
+ }),
371
+ Obj.make(DataType.Person, {
372
+ fullName: 'Jane Smith',
373
+ }),
374
+ ]);
375
+
376
+ const dispatcher = yield* TriggerDispatcher;
377
+
378
+ {
379
+ const results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['queue'] });
380
+ expect(results.length).toBe(1);
381
+ expect(results[0].triggerId).toBe(trigger.id);
382
+ expect(Exit.isSuccess(results[0].result)).toBe(true);
383
+ }
384
+
385
+ {
386
+ const results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['queue'] });
387
+ expect(results.length).toBe(1);
388
+ expect(results[0].triggerId).toBe(trigger.id);
389
+ expect(Exit.isSuccess(results[0].result)).toBe(true);
390
+ }
391
+
392
+ {
393
+ const results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['queue'] });
394
+ expect(results.length).toBe(0);
395
+ }
396
+ }, Effect.provide(TestTriggerDispatcherLayer)),
397
+ );
398
+
399
+ it.effect(
400
+ 'builds input from pattern',
401
+ Effect.fnUntraced(function* ({ expect }) {
402
+ const queue = yield* QueueService.createQueue();
403
+ const functionObj = serializeFunction(reply);
404
+ yield* DatabaseService.add(functionObj);
405
+ const trigger = Obj.make(FunctionTrigger, {
406
+ function: Ref.make(functionObj),
407
+ enabled: true,
408
+ spec: {
409
+ kind: 'queue',
410
+ queue: queue.dxn.toString(),
411
+ },
412
+ input: {
413
+ instructions: 'Please process the queue item.',
414
+ input: '{{event.item}}',
415
+ triggerId: '{{trigger.id}}',
416
+ },
417
+ });
418
+ yield* DatabaseService.add(trigger);
419
+ const person = Obj.make(DataType.Person, {
420
+ fullName: 'John Doe',
421
+ });
422
+ yield* QueueService.append(queue, [person]);
423
+
424
+ const dispatcher = yield* TriggerDispatcher;
425
+ const results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['queue'] });
426
+ expect(results.length).toBe(1);
427
+ expect(results[0].triggerId).toBe(trigger.id);
428
+ const exit = results[0].result;
429
+ invariant(Exit.isSuccess(exit));
430
+ expect(exit.value).to.deep.include({
431
+ instructions: 'Please process the queue item.',
432
+ input: {
433
+ id: person.id,
434
+ fullName: 'John Doe',
435
+ },
436
+ triggerId: trigger.id,
437
+ });
438
+ }, Effect.provide(TestTriggerDispatcherLayer)),
439
+ );
440
+ });
441
+
442
+ describe('Database Triggers (Subscription)', () => {
443
+ it.effect(
444
+ 'should invoke triggers on object creation',
445
+ Effect.fnUntraced(function* ({ expect }) {
446
+ const functionObj = serializeFunction(reply);
447
+ yield* DatabaseService.add(functionObj);
448
+
449
+ // Create a subscription trigger that watches for DataType.Person objects
450
+ const trigger = Obj.make(FunctionTrigger, {
451
+ function: Ref.make(functionObj),
452
+ enabled: true,
453
+ spec: {
454
+ kind: 'subscription',
455
+ query: Query.select(Filter.type(DataType.Person)).ast,
456
+ },
457
+ });
458
+ yield* DatabaseService.add(trigger);
459
+
460
+ const dispatcher = yield* TriggerDispatcher;
461
+ yield* dispatcher.refreshTriggers();
462
+
463
+ // Create a new Person object - this should trigger the subscription
464
+ const person = Obj.make(DataType.Person, {
465
+ fullName: 'Alice Smith',
466
+ });
467
+ yield* DatabaseService.add(person);
468
+
469
+ // Invoke scheduled triggers to check if subscription fires
470
+ const results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
471
+
472
+ // Should have triggered for the new person
473
+ expect(results.length).toBe(1);
474
+ expect(results[0].triggerId).toBe(trigger.id);
475
+ expect(Exit.isSuccess(results[0].result)).toBe(true);
476
+ }, Effect.provide(TestTriggerDispatcherLayer)),
477
+ );
478
+
479
+ it.effect(
480
+ 'should invoke triggers on object updates',
481
+ Effect.fnUntraced(function* ({ expect }) {
482
+ const functionObj = serializeFunction(reply);
483
+ yield* DatabaseService.add(functionObj);
484
+
485
+ // Create a person object first
486
+ const person = Obj.make(DataType.Person, {
487
+ fullName: 'Bob Jones',
488
+ });
489
+ yield* DatabaseService.add(person);
490
+
491
+ // Create a subscription trigger
492
+ const trigger = Obj.make(FunctionTrigger, {
493
+ function: Ref.make(functionObj),
494
+ enabled: true,
495
+ spec: {
496
+ kind: 'subscription',
497
+ query: Query.select(Filter.type(DataType.Person)).ast,
498
+ },
499
+ });
500
+ yield* DatabaseService.add(trigger);
501
+
502
+ const dispatcher = yield* TriggerDispatcher;
503
+ yield* dispatcher.refreshTriggers();
504
+
505
+ // Initial check - should trigger for existing object
506
+ let results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
507
+ expect(results.length).toBe(1);
508
+
509
+ // Update the person object
510
+ person.fullName = 'Robert Jones';
511
+ yield* DatabaseService.flush();
512
+
513
+ // Should trigger again for the update
514
+ results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
515
+ expect(results.length).toBe(1);
516
+ expect(results[0].triggerId).toBe(trigger.id);
517
+ expect(Exit.isSuccess(results[0].result)).toBe(true);
518
+ }, Effect.provide(TestTriggerDispatcherLayer)),
519
+ );
520
+
521
+ it.effect(
522
+ 'should not invoke triggers for unchanged objects',
523
+ Effect.fnUntraced(function* ({ expect }) {
524
+ const functionObj = serializeFunction(reply);
525
+ yield* DatabaseService.add(functionObj);
526
+
527
+ // Create a subscription trigger first
528
+ const trigger = Obj.make(FunctionTrigger, {
529
+ function: Ref.make(functionObj),
530
+ enabled: true,
531
+ spec: {
532
+ kind: 'subscription',
533
+ query: Query.select(Filter.type(DataType.Person)).ast,
534
+ },
535
+ });
536
+ yield* DatabaseService.add(trigger);
537
+
538
+ const dispatcher = yield* TriggerDispatcher;
539
+ yield* dispatcher.refreshTriggers();
540
+
541
+ // Create a person object
542
+ const person = Obj.make(DataType.Person, {
543
+ fullName: 'Charlie Brown',
544
+ });
545
+ yield* DatabaseService.add(person);
546
+
547
+ // First invocation - should trigger for new object
548
+ let results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
549
+ expect(results.length).toBe(1);
550
+
551
+ // Second invocation without any changes - should not trigger
552
+ results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
553
+ expect(results.length).toBe(0);
554
+
555
+ // Update the object
556
+ person.fullName = 'Charles Brown';
557
+ yield* DatabaseService.flush();
558
+
559
+ // Third invocation - should trigger for the update
560
+ results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
561
+ expect(results.length).toBe(1);
562
+
563
+ // Fourth invocation without changes - should not trigger
564
+ results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
565
+ expect(results.length).toBe(0);
566
+ }, Effect.provide(TestTriggerDispatcherLayer)),
567
+ );
568
+
569
+ it.effect(
570
+ 'should handle multiple object types with filters',
571
+ Effect.fnUntraced(function* ({ expect }) {
572
+ const functionObj = serializeFunction(reply);
573
+ yield* DatabaseService.add(functionObj);
574
+
575
+ // Create a subscription trigger that only watches for DataType.Task objects
576
+ const trigger = Obj.make(FunctionTrigger, {
577
+ function: Ref.make(functionObj),
578
+ enabled: true,
579
+ spec: {
580
+ kind: 'subscription',
581
+ query: Query.select(Filter.type(DataType.Task)).ast,
582
+ },
583
+ });
584
+ yield* DatabaseService.add(trigger);
585
+
586
+ const dispatcher = yield* TriggerDispatcher;
587
+ yield* dispatcher.refreshTriggers();
588
+
589
+ // Create a Person object - should NOT trigger
590
+ const person = Obj.make(DataType.Person, {
591
+ fullName: 'David Wilson',
592
+ });
593
+ yield* DatabaseService.add(person);
594
+
595
+ let results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
596
+ expect(results.length).toBe(0);
597
+
598
+ // Create a Task object - should trigger
599
+ const task = Obj.make(DataType.Task, {
600
+ title: 'Important task',
601
+ });
602
+ yield* DatabaseService.add(task);
603
+
604
+ results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
605
+ expect(results.length).toBe(1);
606
+ expect(results[0].triggerId).toBe(trigger.id);
607
+ expect(Exit.isSuccess(results[0].result)).toBe(true);
608
+ }, Effect.provide(TestTriggerDispatcherLayer)),
609
+ );
610
+
611
+ it.effect(
612
+ 'should pass correct event data to function',
613
+ Effect.fnUntraced(function* ({ expect }) {
614
+ const functionObj = serializeFunction(reply);
615
+ yield* DatabaseService.add(functionObj);
616
+
617
+ const person = Obj.make(DataType.Person, {
618
+ fullName: 'Eva Martinez',
619
+ });
620
+ yield* DatabaseService.add(person);
621
+
622
+ // Create a subscription trigger with input pattern
623
+ const trigger = Obj.make(FunctionTrigger, {
624
+ function: Ref.make(functionObj),
625
+ enabled: true,
626
+ spec: {
627
+ kind: 'subscription',
628
+ query: Query.select(Filter.type(DataType.Person)).ast,
629
+ },
630
+ input: {
631
+ objectId: '{{event.changedObjectId}}',
632
+ changeType: '{{event.type}}',
633
+ triggerId: '{{trigger.id}}',
634
+ },
635
+ });
636
+ yield* DatabaseService.add(trigger);
637
+
638
+ const dispatcher = yield* TriggerDispatcher;
639
+ const results = yield* dispatcher.invokeScheduledTriggers({ kinds: ['subscription'] });
640
+
641
+ expect(results.length).toBe(1);
642
+ const exit = results[0].result;
643
+ invariant(Exit.isSuccess(exit));
644
+ expect(exit.value).to.deep.include({
645
+ objectId: person.id,
646
+ changeType: 'unknown', // TODO: This should be 'create' or 'update'
647
+ triggerId: trigger.id,
648
+ });
649
+ }, Effect.provide(TestTriggerDispatcherLayer)),
650
+ );
651
+ });
652
+ });