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