@dxos/functions 0.5.3-main.f752aaa → 0.5.3-main.f9b873d
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/index.mjs +802 -429
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +787 -426
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/handler.d.ts +33 -12
- package/dist/types/src/handler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/registry/function-registry.d.ts +24 -0
- package/dist/types/src/registry/function-registry.d.ts.map +1 -0
- package/dist/types/src/registry/function-registry.test.d.ts +2 -0
- package/dist/types/src/registry/function-registry.test.d.ts.map +1 -0
- package/dist/types/src/registry/index.d.ts +2 -0
- package/dist/types/src/registry/index.d.ts.map +1 -0
- package/dist/types/src/runtime/dev-server.d.ts +16 -13
- package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
- package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
- package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
- package/dist/types/src/runtime/scheduler.d.ts +12 -27
- package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
- package/dist/types/src/testing/functions-integration.test.d.ts +2 -0
- package/dist/types/src/testing/functions-integration.test.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +4 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/setup.d.ts +5 -0
- package/dist/types/src/testing/setup.d.ts.map +1 -0
- package/dist/types/src/testing/test/handler.d.ts +4 -0
- package/dist/types/src/testing/test/handler.d.ts.map +1 -0
- package/dist/types/src/testing/test/index.d.ts +3 -0
- package/dist/types/src/testing/test/index.d.ts.map +1 -0
- package/dist/types/src/testing/types.d.ts +9 -0
- package/dist/types/src/testing/types.d.ts.map +1 -0
- package/dist/types/src/testing/util.d.ts +3 -0
- package/dist/types/src/testing/util.d.ts.map +1 -0
- package/dist/types/src/trigger/index.d.ts +2 -0
- package/dist/types/src/trigger/index.d.ts.map +1 -0
- package/dist/types/src/trigger/trigger-registry.d.ts +40 -0
- package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -0
- package/dist/types/src/trigger/trigger-registry.test.d.ts +2 -0
- package/dist/types/src/trigger/trigger-registry.test.d.ts.map +1 -0
- package/dist/types/src/trigger/type/index.d.ts +5 -0
- package/dist/types/src/trigger/type/index.d.ts.map +1 -0
- package/dist/types/src/trigger/type/subscription-trigger.d.ts +4 -0
- package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -0
- package/dist/types/src/trigger/type/timer-trigger.d.ts +4 -0
- package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -0
- package/dist/types/src/trigger/type/webhook-trigger.d.ts +4 -0
- package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -0
- package/dist/types/src/trigger/type/websocket-trigger.d.ts +13 -0
- package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +129 -101
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +18 -13
- package/schema/functions.json +128 -101
- package/src/handler.ts +54 -31
- package/src/index.ts +2 -0
- package/src/registry/function-registry.test.ts +105 -0
- package/src/registry/function-registry.ts +84 -0
- package/src/registry/index.ts +5 -0
- package/src/runtime/dev-server.test.ts +60 -0
- package/src/runtime/dev-server.ts +104 -52
- package/src/runtime/scheduler.test.ts +56 -73
- package/src/runtime/scheduler.ts +79 -271
- package/src/testing/functions-integration.test.ts +99 -0
- package/src/testing/index.ts +7 -0
- package/src/testing/setup.ts +45 -0
- package/src/testing/test/handler.ts +15 -0
- package/src/testing/test/index.ts +7 -0
- package/src/testing/types.ts +9 -0
- package/src/testing/util.ts +16 -0
- package/src/trigger/index.ts +5 -0
- package/src/trigger/trigger-registry.test.ts +229 -0
- package/src/trigger/trigger-registry.ts +176 -0
- package/src/trigger/type/index.ts +8 -0
- package/src/trigger/type/subscription-trigger.ts +73 -0
- package/src/trigger/type/timer-trigger.ts +44 -0
- package/src/trigger/type/webhook-trigger.ts +47 -0
- package/src/trigger/type/websocket-trigger.ts +91 -0
- package/src/types.ts +57 -32
package/src/runtime/scheduler.ts
CHANGED
|
@@ -2,31 +2,18 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import http from 'node:http';
|
|
7
|
-
import WebSocket from 'ws';
|
|
5
|
+
import path from 'node:path';
|
|
8
6
|
|
|
9
|
-
import {
|
|
10
|
-
import { debounce, DeferredTask, sleep, Trigger } from '@dxos/async';
|
|
11
|
-
import { type Client, type PublicKey } from '@dxos/client';
|
|
12
|
-
import { createSubscription, Filter, getAutomergeObjectCore, type Query, type Space } from '@dxos/client/echo';
|
|
7
|
+
import { type Space } from '@dxos/client/echo';
|
|
13
8
|
import { Context } from '@dxos/context';
|
|
14
|
-
import { invariant } from '@dxos/invariant';
|
|
15
9
|
import { log } from '@dxos/log';
|
|
16
|
-
import { ComplexMap } from '@dxos/util';
|
|
17
10
|
|
|
18
|
-
import { type
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
type FunctionTrigger,
|
|
23
|
-
type SubscriptionTrigger,
|
|
24
|
-
type TimerTrigger,
|
|
25
|
-
type WebhookTrigger,
|
|
26
|
-
type WebsocketTrigger,
|
|
27
|
-
} from '../types';
|
|
11
|
+
import { type FunctionEventMeta } from '../handler';
|
|
12
|
+
import { type FunctionRegistry } from '../registry';
|
|
13
|
+
import { type TriggerRegistry } from '../trigger';
|
|
14
|
+
import { type FunctionDef, type FunctionManifest, type FunctionTrigger } from '../types';
|
|
28
15
|
|
|
29
|
-
type Callback = (data:
|
|
16
|
+
export type Callback = (data: any) => Promise<void | number>;
|
|
30
17
|
|
|
31
18
|
export type SchedulerOptions = {
|
|
32
19
|
endpoint?: string;
|
|
@@ -34,290 +21,111 @@ export type SchedulerOptions = {
|
|
|
34
21
|
};
|
|
35
22
|
|
|
36
23
|
/**
|
|
37
|
-
* The scheduler triggers function
|
|
24
|
+
* The scheduler triggers function execution based on various triggers.
|
|
38
25
|
*/
|
|
39
26
|
export class Scheduler {
|
|
40
|
-
|
|
41
|
-
private readonly _mounts = new ComplexMap<
|
|
42
|
-
{ id: string; spaceKey: PublicKey },
|
|
43
|
-
{ ctx: Context; trigger: FunctionTrigger }
|
|
44
|
-
>(({ id, spaceKey }) => `${spaceKey.toHex()}:${id}`);
|
|
27
|
+
private _ctx = createContext();
|
|
45
28
|
|
|
46
29
|
constructor(
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
public readonly functions: FunctionRegistry,
|
|
31
|
+
public readonly triggers: TriggerRegistry,
|
|
49
32
|
private readonly _options: SchedulerOptions = {},
|
|
50
|
-
) {
|
|
33
|
+
) {
|
|
34
|
+
this.functions.onFunctionsRegistered.on(async ({ space, newFunctions }) => {
|
|
35
|
+
await this._safeActivateTriggers(space, this.triggers.getInactiveTriggers(space), newFunctions);
|
|
36
|
+
});
|
|
37
|
+
this.triggers.registered.on(async ({ space, triggers }) => {
|
|
38
|
+
await this._safeActivateTriggers(space, triggers, this.functions.getFunctions(space));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
51
41
|
|
|
52
42
|
async start() {
|
|
53
|
-
this.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await this.mount(new Context(), space, trigger);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
});
|
|
43
|
+
await this._ctx.dispose();
|
|
44
|
+
this._ctx = createContext();
|
|
45
|
+
await this.functions.open(this._ctx);
|
|
46
|
+
await this.triggers.open(this._ctx);
|
|
61
47
|
}
|
|
62
48
|
|
|
63
49
|
async stop() {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
await this._ctx.dispose();
|
|
51
|
+
await this.functions.close();
|
|
52
|
+
await this.triggers.close();
|
|
67
53
|
}
|
|
68
54
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// TODO(burdon): Currently supports only one trigger declaration per function.
|
|
75
|
-
const exists = this._mounts.get(key);
|
|
76
|
-
if (!exists) {
|
|
77
|
-
this._mounts.set(key, { ctx, trigger });
|
|
78
|
-
log('mount', { space: space.key, trigger });
|
|
79
|
-
if (ctx.disposed) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
// Triggers types.
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
if (trigger.timer) {
|
|
88
|
-
await this._createTimer(ctx, space, def, trigger.timer);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (trigger.webhook) {
|
|
92
|
-
await this._createWebhook(ctx, space, def, trigger.webhook);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (trigger.websocket) {
|
|
96
|
-
await this._createWebsocket(ctx, space, def, trigger.websocket);
|
|
97
|
-
}
|
|
55
|
+
public async register(space: Space, manifest: FunctionManifest) {
|
|
56
|
+
await this.functions.register(space, manifest);
|
|
57
|
+
await this.triggers.register(space, manifest);
|
|
58
|
+
}
|
|
98
59
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
60
|
+
private async _safeActivateTriggers(
|
|
61
|
+
space: Space,
|
|
62
|
+
triggers: FunctionTrigger[],
|
|
63
|
+
functions: FunctionDef[],
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const mountTasks = triggers.map((trigger) => {
|
|
66
|
+
return this.activate(space, functions, trigger);
|
|
67
|
+
});
|
|
68
|
+
await Promise.all(mountTasks).catch(log.catch);
|
|
103
69
|
}
|
|
104
70
|
|
|
105
|
-
private async
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
await ctx.dispose();
|
|
71
|
+
private async activate(space: Space, functions: FunctionDef[], fnTrigger: FunctionTrigger) {
|
|
72
|
+
const definition = functions.find((def) => def.uri === fnTrigger.function);
|
|
73
|
+
if (!definition) {
|
|
74
|
+
log.info('function is not found for trigger', { fnTrigger });
|
|
75
|
+
return;
|
|
111
76
|
}
|
|
77
|
+
|
|
78
|
+
await this.triggers.activate({ space }, fnTrigger, async (args) => {
|
|
79
|
+
return this._execFunction(definition, {
|
|
80
|
+
meta: fnTrigger.meta,
|
|
81
|
+
data: { ...args, spaceKey: space.key },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
log('activated trigger', { space: space.key, trigger: fnTrigger });
|
|
112
85
|
}
|
|
113
86
|
|
|
114
|
-
|
|
115
|
-
|
|
87
|
+
private async _execFunction<TData, TMeta>(
|
|
88
|
+
def: FunctionDef,
|
|
89
|
+
{ data, meta }: { data: TData; meta?: TMeta },
|
|
90
|
+
): Promise<number> {
|
|
91
|
+
let status = 0;
|
|
116
92
|
try {
|
|
117
|
-
|
|
93
|
+
// TODO(burdon): Pass in Space key (common context)?
|
|
94
|
+
const payload = Object.assign({}, meta && ({ meta } satisfies FunctionEventMeta<TMeta>), data);
|
|
95
|
+
|
|
118
96
|
const { endpoint, callback } = this._options;
|
|
119
97
|
if (endpoint) {
|
|
120
98
|
// TODO(burdon): Move out of scheduler (generalize as callback).
|
|
121
|
-
|
|
99
|
+
const url = path.join(endpoint, def.route);
|
|
100
|
+
log.info('exec', { function: def.uri, url });
|
|
101
|
+
const response = await fetch(url, {
|
|
122
102
|
method: 'POST',
|
|
123
103
|
headers: {
|
|
124
104
|
'Content-Type': 'application/json',
|
|
125
105
|
},
|
|
126
|
-
body: JSON.stringify(
|
|
106
|
+
body: JSON.stringify(payload),
|
|
127
107
|
});
|
|
108
|
+
|
|
109
|
+
status = response.status;
|
|
128
110
|
} else if (callback) {
|
|
129
|
-
|
|
111
|
+
log.info('exec', { function: def.uri });
|
|
112
|
+
status = (await callback(payload)) ?? 200;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check errors.
|
|
116
|
+
if (status && status >= 400) {
|
|
117
|
+
throw new Error(`Response: ${status}`);
|
|
130
118
|
}
|
|
131
119
|
|
|
132
120
|
// const result = await response.json();
|
|
133
|
-
log.info('done', { function: def.
|
|
121
|
+
log.info('done', { function: def.uri, status });
|
|
134
122
|
} catch (err: any) {
|
|
135
|
-
log.error('error', { function: def.
|
|
123
|
+
log.error('error', { function: def.uri, error: err.message });
|
|
124
|
+
status = 500;
|
|
136
125
|
}
|
|
137
|
-
}
|
|
138
126
|
|
|
139
|
-
|
|
140
|
-
// Triggers
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Cron timer.
|
|
145
|
-
*/
|
|
146
|
-
private async _createTimer(ctx: Context, space: Space, def: FunctionDef, trigger: TimerTrigger) {
|
|
147
|
-
log.info('timer', { space: space.key, trigger });
|
|
148
|
-
const { cron } = trigger;
|
|
149
|
-
|
|
150
|
-
const task = new DeferredTask(ctx, async () => {
|
|
151
|
-
await this._execFunction(def, { space: space.key });
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
let last = 0;
|
|
155
|
-
let run = 0;
|
|
156
|
-
// https://www.npmjs.com/package/cron#constructor
|
|
157
|
-
const job = CronJob.from({
|
|
158
|
-
cronTime: cron,
|
|
159
|
-
runOnInit: false,
|
|
160
|
-
onTick: () => {
|
|
161
|
-
// TODO(burdon): Check greater than 30s (use cron-parser).
|
|
162
|
-
const now = Date.now();
|
|
163
|
-
const delta = last ? now - last : 0;
|
|
164
|
-
last = now;
|
|
165
|
-
|
|
166
|
-
run++;
|
|
167
|
-
log.info('tick', { space: space.key.truncate(), count: run, delta });
|
|
168
|
-
task.schedule();
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
job.start();
|
|
173
|
-
ctx.onDispose(() => job.stop());
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Webhook.
|
|
178
|
-
*/
|
|
179
|
-
private async _createWebhook(ctx: Context, space: Space, def: FunctionDef, trigger: WebhookTrigger) {
|
|
180
|
-
log.info('webhook', { space: space.key, trigger });
|
|
181
|
-
const { port } = trigger;
|
|
182
|
-
|
|
183
|
-
// TODO(burdon): POST JSON.
|
|
184
|
-
const server = http.createServer(async (req, res) => {
|
|
185
|
-
await this._execFunction(def, { space: space.key });
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
server.listen(port, () => {
|
|
189
|
-
log.info('started webhook', { port });
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
ctx.onDispose(() => {
|
|
193
|
-
server.close();
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Websocket.
|
|
199
|
-
*/
|
|
200
|
-
private async _createWebsocket(
|
|
201
|
-
ctx: Context,
|
|
202
|
-
space: Space,
|
|
203
|
-
def: FunctionDef,
|
|
204
|
-
trigger: WebsocketTrigger,
|
|
205
|
-
options: {
|
|
206
|
-
retryDelay: number;
|
|
207
|
-
maxAttempts: number;
|
|
208
|
-
} = {
|
|
209
|
-
retryDelay: 2,
|
|
210
|
-
maxAttempts: 5,
|
|
211
|
-
},
|
|
212
|
-
) {
|
|
213
|
-
log.info('websocket', { space: space.key, trigger });
|
|
214
|
-
const { url } = trigger;
|
|
215
|
-
|
|
216
|
-
let ws: WebSocket;
|
|
217
|
-
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
|
|
218
|
-
const open = new Trigger<boolean>();
|
|
219
|
-
|
|
220
|
-
ws = new WebSocket(url);
|
|
221
|
-
Object.assign(ws, {
|
|
222
|
-
onopen: () => {
|
|
223
|
-
log.info('opened', { url });
|
|
224
|
-
if (trigger.init) {
|
|
225
|
-
ws.send(new TextEncoder().encode(JSON.stringify(trigger.init)));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
open.wake(true);
|
|
229
|
-
},
|
|
230
|
-
|
|
231
|
-
onclose: () => {
|
|
232
|
-
log.info('closed', { url });
|
|
233
|
-
open.wake(false);
|
|
234
|
-
},
|
|
235
|
-
|
|
236
|
-
onerror: (event) => {
|
|
237
|
-
log.catch(event.error, { url });
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
onmessage: async (event) => {
|
|
241
|
-
try {
|
|
242
|
-
const data = JSON.parse(new TextDecoder().decode(event.data as Uint8Array));
|
|
243
|
-
await this._execFunction(def, { space: space.key, data });
|
|
244
|
-
} catch (err) {
|
|
245
|
-
log.catch(err, { url });
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
} satisfies Partial<WebSocket>);
|
|
249
|
-
|
|
250
|
-
const isOpen = await open.wait();
|
|
251
|
-
if (isOpen) {
|
|
252
|
-
break;
|
|
253
|
-
} else {
|
|
254
|
-
const wait = Math.pow(attempt, 2) * options.retryDelay;
|
|
255
|
-
if (attempt < options.maxAttempts) {
|
|
256
|
-
log.warn(`failed to connect; trying again in ${wait}s`, { attempt });
|
|
257
|
-
await sleep(wait * 1_000);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
ctx.onDispose(() => {
|
|
263
|
-
ws?.close();
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* ECHO subscription.
|
|
269
|
-
*/
|
|
270
|
-
private async _createSubscription(ctx: Context, space: Space, def: FunctionDef, trigger: SubscriptionTrigger) {
|
|
271
|
-
log.info('subscription', { space: space.key, trigger });
|
|
272
|
-
const objectIds = new Set<string>();
|
|
273
|
-
const task = new DeferredTask(ctx, async () => {
|
|
274
|
-
await this._execFunction(def, { space: space.key, objects: Array.from(objectIds) });
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// TODO(burdon): Don't fire initially.
|
|
278
|
-
// TODO(burdon): Subscription is called THREE times.
|
|
279
|
-
const subscriptions: (() => void)[] = [];
|
|
280
|
-
const subscription = createSubscription(({ added, updated }) => {
|
|
281
|
-
log.info('updated', { added: added.length, updated: updated.length });
|
|
282
|
-
for (const object of added) {
|
|
283
|
-
objectIds.add(object.id);
|
|
284
|
-
}
|
|
285
|
-
for (const object of updated) {
|
|
286
|
-
objectIds.add(object.id);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
task.schedule();
|
|
290
|
-
});
|
|
291
|
-
subscriptions.push(() => subscription.unsubscribe());
|
|
292
|
-
|
|
293
|
-
// TODO(burdon): Create queue. Only allow one invocation per trigger at a time?
|
|
294
|
-
// TODO(burdon): Disable trigger if keeps failing.
|
|
295
|
-
const { filter, options: { deep, delay } = {} } = trigger;
|
|
296
|
-
const update = ({ objects }: Query) => {
|
|
297
|
-
subscription.update(objects);
|
|
298
|
-
|
|
299
|
-
// TODO(burdon): Hack to monitor changes to Document's text object.
|
|
300
|
-
if (deep) {
|
|
301
|
-
log.info('update', { objects: objects.length });
|
|
302
|
-
for (const object of objects) {
|
|
303
|
-
const content = object.content;
|
|
304
|
-
if (content instanceof TextV0Type) {
|
|
305
|
-
subscriptions.push(
|
|
306
|
-
getAutomergeObjectCore(content).updates.on(debounce(() => subscription.update([object]), 1_000)),
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// TODO(burdon): Is Filter.or implemented?
|
|
314
|
-
// TODO(burdon): [Bug]: all callbacks are fired on the first mutation.
|
|
315
|
-
// TODO(burdon): [Bug]: not updated when document is deleted (either top or hierarchically).
|
|
316
|
-
const query = space.db.query(Filter.or(filter.map(({ type, props }) => Filter.typename(type, props))));
|
|
317
|
-
subscriptions.push(query.subscribe(delay ? debounce(update, delay) : update));
|
|
318
|
-
|
|
319
|
-
ctx.onDispose(() => {
|
|
320
|
-
subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
321
|
-
});
|
|
127
|
+
return status;
|
|
322
128
|
}
|
|
323
129
|
}
|
|
130
|
+
|
|
131
|
+
const createContext = () => new Context({ name: 'FunctionScheduler' });
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
import { Trigger, waitForCondition } from '@dxos/async';
|
|
9
|
+
import { type Client } from '@dxos/client';
|
|
10
|
+
import { create, type Space } from '@dxos/client/echo';
|
|
11
|
+
import { performInvitation, TestBuilder } from '@dxos/client/testing';
|
|
12
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
13
|
+
import { describe, test } from '@dxos/test';
|
|
14
|
+
|
|
15
|
+
import { setTestCallHandler } from './test/handler';
|
|
16
|
+
import { FunctionRegistry } from '../registry';
|
|
17
|
+
import { DevServer, Scheduler } from '../runtime';
|
|
18
|
+
import { createFunctionRuntime, createInitializedClients, TestType } from '../testing';
|
|
19
|
+
import { TriggerRegistry } from '../trigger';
|
|
20
|
+
import { FunctionDef, FunctionTrigger } from '../types';
|
|
21
|
+
|
|
22
|
+
describe('functions e2e', () => {
|
|
23
|
+
let testBuilder: TestBuilder;
|
|
24
|
+
before(async () => {
|
|
25
|
+
testBuilder = new TestBuilder();
|
|
26
|
+
});
|
|
27
|
+
after(async () => {
|
|
28
|
+
await testBuilder.destroy();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('a function gets triggered in response to another peer object creations', async () => {
|
|
32
|
+
// TODO(burdon): Create builder pattern.
|
|
33
|
+
const functionRuntime = await createFunctionRuntime(testBuilder);
|
|
34
|
+
const devServer = await startDevServer(functionRuntime);
|
|
35
|
+
const scheduler = await startScheduler(functionRuntime, devServer);
|
|
36
|
+
|
|
37
|
+
const app = (await createInitializedClients(testBuilder, 1))[0];
|
|
38
|
+
const space = await app.spaces.create();
|
|
39
|
+
await inviteMember(space, functionRuntime);
|
|
40
|
+
|
|
41
|
+
const uri = 'example.com/function/test';
|
|
42
|
+
space.db.add(create(FunctionDef, { uri, route: '/test', handler: 'test' }));
|
|
43
|
+
const triggerMeta: FunctionTrigger['meta'] = { name: 'DXOS' };
|
|
44
|
+
space.db.add(
|
|
45
|
+
create(FunctionTrigger, {
|
|
46
|
+
function: uri,
|
|
47
|
+
meta: triggerMeta,
|
|
48
|
+
spec: {
|
|
49
|
+
type: 'subscription',
|
|
50
|
+
filter: [{ type: TestType.typename }],
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const called = new Trigger<any>();
|
|
56
|
+
setTestCallHandler(async (args) => {
|
|
57
|
+
called.wake(args.event.data);
|
|
58
|
+
return args.response.status(200);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await waitTriggersReplicated(space, scheduler);
|
|
62
|
+
const addedObject = space.db.add(create(TestType, { title: '42' }));
|
|
63
|
+
|
|
64
|
+
const callArgs = await called.wait();
|
|
65
|
+
expect(callArgs.meta).to.deep.eq(triggerMeta);
|
|
66
|
+
expect(callArgs.objects).to.deep.eq([addedObject.id]);
|
|
67
|
+
expect(callArgs.spaceKey).to.eq(space.key.toHex());
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const waitTriggersReplicated = async (space: Space, scheduler: Scheduler) => {
|
|
71
|
+
await waitForCondition({ condition: () => scheduler.triggers.getActiveTriggers(space).length > 0 });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// TODO(burdon): Factor out utils to builder pattern.
|
|
75
|
+
|
|
76
|
+
const startScheduler = async (client: Client, devServer: DevServer) => {
|
|
77
|
+
const functionRegistry = new FunctionRegistry(client);
|
|
78
|
+
const triggerRegistry = new TriggerRegistry(client);
|
|
79
|
+
const scheduler = new Scheduler(functionRegistry, triggerRegistry, { endpoint: devServer.endpoint });
|
|
80
|
+
await scheduler.start();
|
|
81
|
+
testBuilder.ctx.onDispose(() => scheduler.stop());
|
|
82
|
+
return scheduler;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const startDevServer = async (client: Client) => {
|
|
86
|
+
const functionRegistry = new FunctionRegistry(client);
|
|
87
|
+
const server = new DevServer(client, functionRegistry, {
|
|
88
|
+
baseDir: path.join(__dirname, '../testing'),
|
|
89
|
+
});
|
|
90
|
+
await server.start();
|
|
91
|
+
testBuilder.ctx.onDispose(() => server.stop());
|
|
92
|
+
return server;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const inviteMember = async (host: Space, guest: Client) => {
|
|
96
|
+
const [{ invitation: hostInvitation }] = await Promise.all(performInvitation({ host, guest: guest.spaces }));
|
|
97
|
+
expect(hostInvitation?.state).to.eq(Invitation.State.SUCCESS);
|
|
98
|
+
};
|
|
99
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { FunctionsPlugin } from '@dxos/agent';
|
|
6
|
+
import { Client, Config } from '@dxos/client';
|
|
7
|
+
import { type TestBuilder } from '@dxos/client/testing';
|
|
8
|
+
import { range } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
import { TestType } from './types';
|
|
11
|
+
import { FunctionDef, FunctionTrigger } from '../types';
|
|
12
|
+
|
|
13
|
+
// TODO(burdon): Create TestBuilder.
|
|
14
|
+
|
|
15
|
+
export const createInitializedClients = async (testBuilder: TestBuilder, count: number = 1, config?: Config) => {
|
|
16
|
+
const clients = range(count).map(() => new Client({ config, services: testBuilder.createLocalClientServices() }));
|
|
17
|
+
testBuilder.ctx.onDispose(() => Promise.all(clients.map((c) => c.destroy())));
|
|
18
|
+
return Promise.all(
|
|
19
|
+
clients.map(async (client, index) => {
|
|
20
|
+
await client.initialize();
|
|
21
|
+
await client.halo.createIdentity({ displayName: `Peer ${index}` });
|
|
22
|
+
client.addSchema(TestType, FunctionDef, FunctionTrigger);
|
|
23
|
+
return client;
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const createFunctionRuntime = async (testBuilder: TestBuilder): Promise<Client> => {
|
|
29
|
+
const config = new Config({
|
|
30
|
+
runtime: {
|
|
31
|
+
agent: {
|
|
32
|
+
plugins: [{ id: 'dxos.org/agent/plugin/functions', config: { port: 8080 } }],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const client = (await createInitializedClients(testBuilder, 1, config))[0];
|
|
38
|
+
|
|
39
|
+
// TODO(burdon): Better way to configure plugin? (Rationalize chess.test).
|
|
40
|
+
const functionsPlugin = new FunctionsPlugin();
|
|
41
|
+
await functionsPlugin.initialize({ client, clientServices: client.services });
|
|
42
|
+
await functionsPlugin.open();
|
|
43
|
+
testBuilder.ctx.onDispose(() => functionsPlugin.close());
|
|
44
|
+
return client;
|
|
45
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type FunctionHandler } from '../../handler';
|
|
6
|
+
|
|
7
|
+
let callHandler: FunctionHandler<any> = async ({ response }) => response.status(200);
|
|
8
|
+
|
|
9
|
+
export const setTestCallHandler = (handler: FunctionHandler<any>) => {
|
|
10
|
+
callHandler = handler;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const handler: FunctionHandler<any> = async (args) => {
|
|
14
|
+
return callHandler(args);
|
|
15
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Filter, type Space } from '@dxos/client/echo';
|
|
6
|
+
import { invariant } from '@dxos/invariant';
|
|
7
|
+
|
|
8
|
+
import { FunctionTrigger } from '../types';
|
|
9
|
+
|
|
10
|
+
export const triggerWebhook = async (space: Space, uri: string) => {
|
|
11
|
+
const trigger = (
|
|
12
|
+
await space.db.query(Filter.schema(FunctionTrigger, (t: FunctionTrigger) => t.function === uri)).run()
|
|
13
|
+
).objects[0];
|
|
14
|
+
invariant(trigger.spec.type === 'webhook');
|
|
15
|
+
void fetch(`http://localhost:${trigger.spec.port}`);
|
|
16
|
+
};
|