@dxos/functions 0.5.3-main.61bbff4 → 0.5.3-main.64035f0
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 +424 -642
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +421 -627
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/runtime/dev-server.d.ts +10 -7
- package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
- package/dist/types/src/runtime/scheduler.d.ts +59 -10
- package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
- package/dist/types/src/testing/test/handler.d.ts +0 -1
- package/dist/types/src/testing/test/handler.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +112 -118
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +13 -16
- package/schema/functions.json +107 -121
- package/src/index.ts +0 -2
- package/src/runtime/dev-server.test.ts +35 -15
- package/src/runtime/dev-server.ts +18 -36
- package/src/runtime/scheduler.test.ts +75 -54
- package/src/runtime/scheduler.ts +298 -58
- package/src/testing/test/handler.ts +2 -8
- package/src/types.ts +42 -56
- package/dist/types/src/registry/function-registry.d.ts +0 -24
- package/dist/types/src/registry/function-registry.d.ts.map +0 -1
- package/dist/types/src/registry/function-registry.test.d.ts +0 -2
- package/dist/types/src/registry/function-registry.test.d.ts.map +0 -1
- package/dist/types/src/registry/index.d.ts +0 -2
- package/dist/types/src/registry/index.d.ts.map +0 -1
- package/dist/types/src/testing/functions-integration.test.d.ts +0 -2
- package/dist/types/src/testing/functions-integration.test.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -4
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/setup.d.ts +0 -5
- package/dist/types/src/testing/setup.d.ts.map +0 -1
- package/dist/types/src/testing/types.d.ts +0 -9
- package/dist/types/src/testing/types.d.ts.map +0 -1
- package/dist/types/src/testing/util.d.ts +0 -3
- package/dist/types/src/testing/util.d.ts.map +0 -1
- package/dist/types/src/trigger/index.d.ts +0 -2
- package/dist/types/src/trigger/index.d.ts.map +0 -1
- package/dist/types/src/trigger/trigger-registry.d.ts +0 -40
- package/dist/types/src/trigger/trigger-registry.d.ts.map +0 -1
- package/dist/types/src/trigger/trigger-registry.test.d.ts +0 -2
- package/dist/types/src/trigger/trigger-registry.test.d.ts.map +0 -1
- package/dist/types/src/trigger/type/index.d.ts +0 -5
- package/dist/types/src/trigger/type/index.d.ts.map +0 -1
- package/dist/types/src/trigger/type/subscription-trigger.d.ts +0 -4
- package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +0 -1
- package/dist/types/src/trigger/type/timer-trigger.d.ts +0 -4
- package/dist/types/src/trigger/type/timer-trigger.d.ts.map +0 -1
- package/dist/types/src/trigger/type/webhook-trigger.d.ts +0 -4
- package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +0 -1
- package/dist/types/src/trigger/type/websocket-trigger.d.ts +0 -13
- package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +0 -1
- package/src/registry/function-registry.test.ts +0 -105
- package/src/registry/function-registry.ts +0 -84
- package/src/registry/index.ts +0 -5
- package/src/testing/functions-integration.test.ts +0 -99
- package/src/testing/index.ts +0 -7
- package/src/testing/setup.ts +0 -45
- package/src/testing/types.ts +0 -9
- package/src/testing/util.ts +0 -16
- package/src/trigger/index.ts +0 -5
- package/src/trigger/trigger-registry.test.ts +0 -229
- package/src/trigger/trigger-registry.ts +0 -176
- package/src/trigger/type/index.ts +0 -8
- package/src/trigger/type/subscription-trigger.ts +0 -73
- package/src/trigger/type/timer-trigger.ts +0 -44
- package/src/trigger/type/webhook-trigger.ts +0 -47
- package/src/trigger/type/websocket-trigger.ts +0 -91
package/src/runtime/scheduler.ts
CHANGED
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { CronJob } from 'cron';
|
|
6
|
+
import { getPort } from 'get-port-please';
|
|
7
|
+
import http from 'node:http';
|
|
5
8
|
import path from 'node:path';
|
|
9
|
+
import WebSocket from 'ws';
|
|
6
10
|
|
|
7
|
-
import {
|
|
11
|
+
import { TextV0Type } from '@braneframe/types';
|
|
12
|
+
import { debounce, DeferredTask, sleep, Trigger } from '@dxos/async';
|
|
13
|
+
import { type Client, type PublicKey } from '@dxos/client';
|
|
14
|
+
import { createSubscription, Filter, getAutomergeObjectCore, type Query, type Space } from '@dxos/client/echo';
|
|
8
15
|
import { Context } from '@dxos/context';
|
|
16
|
+
import { invariant } from '@dxos/invariant';
|
|
9
17
|
import { log } from '@dxos/log';
|
|
18
|
+
import { ComplexMap } from '@dxos/util';
|
|
10
19
|
|
|
11
20
|
import { type FunctionEventMeta } from '../handler';
|
|
12
|
-
import { type FunctionRegistry } from '../registry';
|
|
13
|
-
import { type TriggerRegistry } from '../trigger';
|
|
14
21
|
import { type FunctionDef, type FunctionManifest, type FunctionTrigger } from '../types';
|
|
15
22
|
|
|
16
23
|
export type Callback = (data: any) => Promise<void | number>;
|
|
@@ -24,80 +31,101 @@ export type SchedulerOptions = {
|
|
|
24
31
|
* The scheduler triggers function execution based on various triggers.
|
|
25
32
|
*/
|
|
26
33
|
export class Scheduler {
|
|
27
|
-
|
|
34
|
+
// Map of mounted functions.
|
|
35
|
+
private readonly _mounts = new ComplexMap<
|
|
36
|
+
{ spaceKey: PublicKey; id: string },
|
|
37
|
+
{ ctx: Context; trigger: FunctionTrigger }
|
|
38
|
+
>(({ spaceKey, id }) => `${spaceKey.toHex()}:${id}`);
|
|
28
39
|
|
|
29
40
|
constructor(
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
private readonly _client: Client,
|
|
42
|
+
private readonly _manifest: FunctionManifest,
|
|
32
43
|
private readonly _options: SchedulerOptions = {},
|
|
33
|
-
) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
get mounts() {
|
|
47
|
+
return Array.from(this._mounts.values()).reduce<FunctionTrigger[]>((acc, { trigger }) => {
|
|
48
|
+
acc.push(trigger);
|
|
49
|
+
return acc;
|
|
50
|
+
}, []);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
async start() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
this._client.spaces.subscribe(async (spaces) => {
|
|
55
|
+
for (const space of spaces) {
|
|
56
|
+
await space.waitUntilReady();
|
|
57
|
+
for (const trigger of this._manifest.triggers ?? []) {
|
|
58
|
+
await this.mount(new Context(), space, trigger);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
async stop() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
for (const { id, spaceKey } of this._mounts.keys()) {
|
|
66
|
+
await this.unmount(id, spaceKey);
|
|
67
|
+
}
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Mount trigger.
|
|
72
|
+
*/
|
|
73
|
+
private async mount(ctx: Context, space: Space, trigger: FunctionTrigger) {
|
|
74
|
+
const key = { spaceKey: space.key, id: trigger.function };
|
|
75
|
+
const def = this._manifest.functions.find((config) => config.id === trigger.function);
|
|
76
|
+
invariant(def, `Function not found: ${trigger.function}`);
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
// TODO(burdon): Currently supports only one trigger declaration per function.
|
|
79
|
+
const exists = this._mounts.get(key);
|
|
80
|
+
if (!exists) {
|
|
81
|
+
this._mounts.set(key, { ctx, trigger });
|
|
82
|
+
log('mount', { space: space.key, trigger });
|
|
83
|
+
if (ctx.disposed) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//
|
|
88
|
+
// Triggers types.
|
|
89
|
+
//
|
|
90
|
+
|
|
91
|
+
if (trigger.timer) {
|
|
92
|
+
await this._createTimer(ctx, space, def, trigger);
|
|
93
|
+
}
|
|
70
94
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
if (trigger.webhook) {
|
|
96
|
+
await this._createWebhook(ctx, space, def, trigger);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (trigger.websocket) {
|
|
100
|
+
await this._createWebsocket(ctx, space, def, trigger);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (trigger.subscription) {
|
|
104
|
+
await this._createSubscription(ctx, space, def, trigger);
|
|
105
|
+
}
|
|
76
106
|
}
|
|
107
|
+
}
|
|
77
108
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
private async unmount(id: string, spaceKey: PublicKey) {
|
|
110
|
+
const key = { id, spaceKey };
|
|
111
|
+
const { ctx } = this._mounts.get(key) ?? {};
|
|
112
|
+
if (ctx) {
|
|
113
|
+
this._mounts.delete(key);
|
|
114
|
+
await ctx.dispose();
|
|
115
|
+
}
|
|
85
116
|
}
|
|
86
117
|
|
|
87
|
-
private async _execFunction<TData, TMeta>(
|
|
88
|
-
def: FunctionDef,
|
|
89
|
-
{ data, meta }: { data: TData; meta?: TMeta },
|
|
90
|
-
): Promise<number> {
|
|
118
|
+
private async _execFunction<TData, TMeta>(def: FunctionDef, trigger: FunctionTrigger, data: TData): Promise<number> {
|
|
91
119
|
let status = 0;
|
|
92
120
|
try {
|
|
93
121
|
// TODO(burdon): Pass in Space key (common context)?
|
|
94
|
-
const payload = Object.assign({}, meta
|
|
122
|
+
const payload = Object.assign({}, { meta: trigger.meta as TMeta } satisfies FunctionEventMeta<TMeta>, data);
|
|
95
123
|
|
|
96
124
|
const { endpoint, callback } = this._options;
|
|
97
125
|
if (endpoint) {
|
|
98
126
|
// TODO(burdon): Move out of scheduler (generalize as callback).
|
|
99
|
-
const url = path.join(endpoint, def.
|
|
100
|
-
log.info('exec', { function: def.
|
|
127
|
+
const url = path.join(endpoint, def.path);
|
|
128
|
+
log.info('exec', { function: def.id, url });
|
|
101
129
|
const response = await fetch(url, {
|
|
102
130
|
method: 'POST',
|
|
103
131
|
headers: {
|
|
@@ -108,7 +136,7 @@ export class Scheduler {
|
|
|
108
136
|
|
|
109
137
|
status = response.status;
|
|
110
138
|
} else if (callback) {
|
|
111
|
-
log.info('exec', { function: def.
|
|
139
|
+
log.info('exec', { function: def.id });
|
|
112
140
|
status = (await callback(payload)) ?? 200;
|
|
113
141
|
}
|
|
114
142
|
|
|
@@ -118,14 +146,226 @@ export class Scheduler {
|
|
|
118
146
|
}
|
|
119
147
|
|
|
120
148
|
// const result = await response.json();
|
|
121
|
-
log.info('done', { function: def.
|
|
149
|
+
log.info('done', { function: def.id, status });
|
|
122
150
|
} catch (err: any) {
|
|
123
|
-
log.error('error', { function: def.
|
|
151
|
+
log.error('error', { function: def.id, error: err.message });
|
|
124
152
|
status = 500;
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
return status;
|
|
128
156
|
}
|
|
129
|
-
}
|
|
130
157
|
|
|
131
|
-
|
|
158
|
+
//
|
|
159
|
+
// Triggers
|
|
160
|
+
//
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Cron timer.
|
|
164
|
+
*/
|
|
165
|
+
private async _createTimer(ctx: Context, space: Space, def: FunctionDef, trigger: FunctionTrigger) {
|
|
166
|
+
log.info('timer', { space: space.key, trigger });
|
|
167
|
+
const spec = trigger.timer!;
|
|
168
|
+
|
|
169
|
+
const task = new DeferredTask(ctx, async () => {
|
|
170
|
+
await this._execFunction(def, trigger, { spaceKey: space.key });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
let last = 0;
|
|
174
|
+
let run = 0;
|
|
175
|
+
// https://www.npmjs.com/package/cron#constructor
|
|
176
|
+
const job = CronJob.from({
|
|
177
|
+
cronTime: spec.cron,
|
|
178
|
+
runOnInit: false,
|
|
179
|
+
onTick: () => {
|
|
180
|
+
// TODO(burdon): Check greater than 30s (use cron-parser).
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
const delta = last ? now - last : 0;
|
|
183
|
+
last = now;
|
|
184
|
+
|
|
185
|
+
run++;
|
|
186
|
+
log.info('tick', { space: space.key.truncate(), count: run, delta });
|
|
187
|
+
task.schedule();
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
job.start();
|
|
192
|
+
ctx.onDispose(() => job.stop());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Webhook.
|
|
197
|
+
*/
|
|
198
|
+
private async _createWebhook(ctx: Context, space: Space, def: FunctionDef, trigger: FunctionTrigger) {
|
|
199
|
+
log.info('webhook', { space: space.key, trigger });
|
|
200
|
+
const spec = trigger.webhook!;
|
|
201
|
+
|
|
202
|
+
// TODO(burdon): Enable POST hook with payload.
|
|
203
|
+
const server = http.createServer(async (req, res) => {
|
|
204
|
+
if (req.method !== spec.method) {
|
|
205
|
+
res.statusCode = 405;
|
|
206
|
+
return res.end();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
res.statusCode = await this._execFunction(def, trigger, { spaceKey: space.key });
|
|
210
|
+
res.end();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// TODO(burdon): Not used.
|
|
214
|
+
// const DEF_PORT_RANGE = { min: 7500, max: 7599 };
|
|
215
|
+
// const portRange = Object.assign({}, trigger.port, DEF_PORT_RANGE) as WebhookTrigger['port'];
|
|
216
|
+
const port = await getPort({
|
|
217
|
+
random: true,
|
|
218
|
+
// portRange: [portRange!.min, portRange!.max],
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// TODO(burdon): Update trigger object with actual port.
|
|
222
|
+
server.listen(port, () => {
|
|
223
|
+
log.info('started webhook', { port });
|
|
224
|
+
spec.port = port;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
ctx.onDispose(() => {
|
|
228
|
+
server.close();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Websocket.
|
|
234
|
+
* NOTE: The port must be unique, so the same hook cannot be used for multiple spaces.
|
|
235
|
+
*/
|
|
236
|
+
private async _createWebsocket(
|
|
237
|
+
ctx: Context,
|
|
238
|
+
space: Space,
|
|
239
|
+
def: FunctionDef,
|
|
240
|
+
trigger: FunctionTrigger,
|
|
241
|
+
options: {
|
|
242
|
+
retryDelay: number;
|
|
243
|
+
maxAttempts: number;
|
|
244
|
+
} = {
|
|
245
|
+
retryDelay: 2,
|
|
246
|
+
maxAttempts: 5,
|
|
247
|
+
},
|
|
248
|
+
) {
|
|
249
|
+
log.info('websocket', { space: space.key, trigger });
|
|
250
|
+
const spec = trigger.websocket!;
|
|
251
|
+
const { url, init } = spec;
|
|
252
|
+
|
|
253
|
+
let ws: WebSocket;
|
|
254
|
+
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
|
|
255
|
+
const open = new Trigger<boolean>();
|
|
256
|
+
|
|
257
|
+
ws = new WebSocket(url);
|
|
258
|
+
Object.assign(ws, {
|
|
259
|
+
onopen: () => {
|
|
260
|
+
log.info('opened', { url });
|
|
261
|
+
if (spec.init) {
|
|
262
|
+
ws.send(new TextEncoder().encode(JSON.stringify(init)));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
open.wake(true);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
onclose: (event) => {
|
|
269
|
+
log.info('closed', { url, code: event.code });
|
|
270
|
+
// Reconnect if server closes (e.g., CF restart).
|
|
271
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
|
|
272
|
+
if (event.code === 1006) {
|
|
273
|
+
setTimeout(async () => {
|
|
274
|
+
log.info(`reconnecting in ${options.retryDelay}s...`, { url });
|
|
275
|
+
await this._createWebsocket(ctx, space, def, trigger, options);
|
|
276
|
+
}, options.retryDelay * 1_000);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
open.wake(false);
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
onerror: (event) => {
|
|
283
|
+
log.catch(event.error, { url });
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
onmessage: async (event) => {
|
|
287
|
+
try {
|
|
288
|
+
const data = JSON.parse(new TextDecoder().decode(event.data as Uint8Array));
|
|
289
|
+
await this._execFunction(def, trigger, { spaceKey: space.key, data });
|
|
290
|
+
} catch (err) {
|
|
291
|
+
log.catch(err, { url });
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
} satisfies Partial<WebSocket>);
|
|
295
|
+
|
|
296
|
+
const isOpen = await open.wait();
|
|
297
|
+
if (isOpen) {
|
|
298
|
+
break;
|
|
299
|
+
} else {
|
|
300
|
+
const wait = Math.pow(attempt, 2) * options.retryDelay;
|
|
301
|
+
if (attempt < options.maxAttempts) {
|
|
302
|
+
log.warn(`failed to connect; trying again in ${wait}s`, { attempt });
|
|
303
|
+
await sleep(wait * 1_000);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
ctx.onDispose(() => {
|
|
309
|
+
ws?.close();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* ECHO subscription.
|
|
315
|
+
*/
|
|
316
|
+
private async _createSubscription(ctx: Context, space: Space, def: FunctionDef, trigger: FunctionTrigger) {
|
|
317
|
+
log.info('subscription', { space: space.key, trigger });
|
|
318
|
+
const spec = trigger.subscription!;
|
|
319
|
+
|
|
320
|
+
const objectIds = new Set<string>();
|
|
321
|
+
const task = new DeferredTask(ctx, async () => {
|
|
322
|
+
await this._execFunction(def, trigger, { spaceKey: space.key, objects: Array.from(objectIds) });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// TODO(burdon): Don't fire initially?
|
|
326
|
+
// TODO(burdon): Create queue. Only allow one invocation per trigger at a time?
|
|
327
|
+
const subscriptions: (() => void)[] = [];
|
|
328
|
+
const subscription = createSubscription(({ added, updated }) => {
|
|
329
|
+
log.info('updated', { added: added.length, updated: updated.length });
|
|
330
|
+
for (const object of added) {
|
|
331
|
+
objectIds.add(object.id);
|
|
332
|
+
}
|
|
333
|
+
for (const object of updated) {
|
|
334
|
+
objectIds.add(object.id);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
task.schedule();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
subscriptions.push(() => subscription.unsubscribe());
|
|
341
|
+
|
|
342
|
+
// TODO(burdon): Disable trigger if keeps failing.
|
|
343
|
+
const { filter, options: { deep, delay } = {} } = spec;
|
|
344
|
+
const update = ({ objects }: Query) => {
|
|
345
|
+
subscription.update(objects);
|
|
346
|
+
|
|
347
|
+
// TODO(burdon): Hack to monitor changes to Document's text object.
|
|
348
|
+
if (deep) {
|
|
349
|
+
log.info('update', { objects: objects.length });
|
|
350
|
+
for (const object of objects) {
|
|
351
|
+
const content = object.content;
|
|
352
|
+
if (content instanceof TextV0Type) {
|
|
353
|
+
subscriptions.push(
|
|
354
|
+
getAutomergeObjectCore(content).updates.on(debounce(() => subscription.update([object]), 1_000)),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// TODO(burdon): Is Filter.or implemented?
|
|
362
|
+
// TODO(burdon): [Bug]: all callbacks are fired on the first mutation.
|
|
363
|
+
// TODO(burdon): [Bug]: not updated when document is deleted (either top or hierarchically).
|
|
364
|
+
const query = space.db.query(Filter.or(filter.map(({ type, props }) => Filter.typename(type, props))));
|
|
365
|
+
subscriptions.push(query.subscribe(delay ? debounce(update, delay) : update));
|
|
366
|
+
|
|
367
|
+
ctx.onDispose(() => {
|
|
368
|
+
subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { type FunctionHandler } from '../../handler';
|
|
6
6
|
|
|
7
|
-
|
|
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);
|
|
7
|
+
export const handler: FunctionHandler<any> = async ({ response }) => {
|
|
8
|
+
return response.status(200);
|
|
15
9
|
};
|
package/src/types.ts
CHANGED
|
@@ -2,21 +2,27 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import * as S from '@effect/schema/Schema';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const TimerTriggerSchema = S.struct({
|
|
8
|
+
cron: S.string,
|
|
9
|
+
});
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const WebhookTriggerSchema = S.mutable(
|
|
12
|
+
S.struct({
|
|
13
|
+
method: S.string,
|
|
14
|
+
// Assigned port.
|
|
15
|
+
port: S.optional(S.number),
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const WebsocketTriggerSchema = S.struct({
|
|
20
|
+
url: S.string,
|
|
21
|
+
init: S.optional(S.record(S.string, S.any)),
|
|
22
|
+
});
|
|
17
23
|
|
|
18
24
|
const SubscriptionTriggerSchema = S.struct({
|
|
19
|
-
|
|
25
|
+
spaceKey: S.optional(S.string),
|
|
20
26
|
// TODO(burdon): Define query DSL.
|
|
21
27
|
filter: S.array(
|
|
22
28
|
S.struct({
|
|
@@ -33,69 +39,49 @@ const SubscriptionTriggerSchema = S.struct({
|
|
|
33
39
|
}),
|
|
34
40
|
),
|
|
35
41
|
});
|
|
36
|
-
export type SubscriptionTrigger = S.Schema.Type<typeof SubscriptionTriggerSchema>;
|
|
37
42
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
cron: S.string,
|
|
41
|
-
});
|
|
42
|
-
export type TimerTrigger = S.Schema.Type<typeof TimerTriggerSchema>;
|
|
43
|
+
const FunctionTriggerSchema = S.struct({
|
|
44
|
+
function: S.string.pipe(S.description('Function ID/URI.')),
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
S.
|
|
46
|
-
type: S.literal('webhook'),
|
|
47
|
-
method: S.string,
|
|
48
|
-
// Assigned port.
|
|
49
|
-
port: S.optional(S.number),
|
|
50
|
-
}),
|
|
51
|
-
);
|
|
52
|
-
export type WebhookTrigger = S.Schema.Type<typeof WebhookTriggerSchema>;
|
|
46
|
+
// Context passed to function.
|
|
47
|
+
meta: S.optional(S.record(S.string, S.any)),
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
// Triggers.
|
|
50
|
+
timer: S.optional(TimerTriggerSchema),
|
|
51
|
+
webhook: S.optional(WebhookTriggerSchema),
|
|
52
|
+
websocket: S.optional(WebsocketTriggerSchema),
|
|
53
|
+
subscription: S.optional(SubscriptionTriggerSchema),
|
|
58
54
|
});
|
|
59
|
-
export type WebsocketTrigger = S.Schema.Type<typeof WebsocketTriggerSchema>;
|
|
60
55
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
export type TriggerSpec = TimerTrigger | WebhookTrigger | WebsocketTrigger | SubscriptionTrigger;
|
|
56
|
+
export type FunctionTrigger = S.Schema.Type<typeof FunctionTriggerSchema>;
|
|
57
|
+
|
|
58
|
+
export type TimerTrigger = S.Schema.Type<typeof TimerTriggerSchema>;
|
|
59
|
+
export type WebhookTrigger = S.Schema.Type<typeof WebhookTriggerSchema>;
|
|
60
|
+
export type WebsocketTrigger = S.Schema.Type<typeof WebsocketTriggerSchema>;
|
|
61
|
+
export type SubscriptionTrigger = S.Schema.Type<typeof SubscriptionTriggerSchema>;
|
|
68
62
|
|
|
69
63
|
/**
|
|
70
64
|
* Function definition.
|
|
71
65
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
uri: S.string,
|
|
66
|
+
// TODO(burdon): Name vs. path?
|
|
67
|
+
const FunctionDefSchema = S.struct({
|
|
68
|
+
id: S.string,
|
|
69
|
+
// name: S.string,
|
|
77
70
|
description: S.optional(S.string),
|
|
78
|
-
|
|
71
|
+
// TODO(burdon): Rename route?
|
|
72
|
+
path: S.string,
|
|
79
73
|
// TODO(burdon): NPM/GitHub/Docker/CF URL?
|
|
80
74
|
handler: S.string,
|
|
81
|
-
})
|
|
75
|
+
});
|
|
82
76
|
|
|
83
|
-
export
|
|
84
|
-
typename: 'dxos.org/type/FunctionTrigger',
|
|
85
|
-
version: '0.1.0',
|
|
86
|
-
})({
|
|
87
|
-
function: S.string.pipe(S.description('Function ID/URI.')),
|
|
88
|
-
// Context passed to a function.
|
|
89
|
-
meta: S.optional(S.record(S.string, S.any)),
|
|
90
|
-
spec: TriggerSpecSchema,
|
|
91
|
-
}) {}
|
|
77
|
+
export type FunctionDef = S.Schema.Type<typeof FunctionDefSchema>;
|
|
92
78
|
|
|
93
79
|
/**
|
|
94
80
|
* Function manifest file.
|
|
95
81
|
*/
|
|
96
82
|
export const FunctionManifestSchema = S.struct({
|
|
97
|
-
functions: S.
|
|
98
|
-
triggers: S.optional(S.mutable(S.array(
|
|
83
|
+
functions: S.mutable(S.array(FunctionDefSchema)),
|
|
84
|
+
triggers: S.optional(S.mutable(S.array(FunctionTriggerSchema))),
|
|
99
85
|
});
|
|
100
86
|
|
|
101
87
|
export type FunctionManifest = S.Schema.Type<typeof FunctionManifestSchema>;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Event } from '@dxos/async';
|
|
2
|
-
import { type Client } from '@dxos/client';
|
|
3
|
-
import { type Space } from '@dxos/client/echo';
|
|
4
|
-
import { type Context, Resource } from '@dxos/context';
|
|
5
|
-
import { FunctionDef, type FunctionManifest } from '../types';
|
|
6
|
-
export type FunctionsRegisteredEvent = {
|
|
7
|
-
space: Space;
|
|
8
|
-
newFunctions: FunctionDef[];
|
|
9
|
-
};
|
|
10
|
-
export declare class FunctionRegistry extends Resource {
|
|
11
|
-
private readonly _client;
|
|
12
|
-
private readonly _functionBySpaceKey;
|
|
13
|
-
readonly onFunctionsRegistered: Event<FunctionsRegisteredEvent>;
|
|
14
|
-
constructor(_client: Client);
|
|
15
|
-
getFunctions(space: Space): FunctionDef[];
|
|
16
|
-
/**
|
|
17
|
-
* The method loads function definitions from the manifest into the space.
|
|
18
|
-
* We first load all the definitions from the space to deduplicate by functionId.
|
|
19
|
-
*/
|
|
20
|
-
register(space: Space, manifest: FunctionManifest): Promise<void>;
|
|
21
|
-
protected _open(): Promise<void>;
|
|
22
|
-
protected _close(_: Context): Promise<void>;
|
|
23
|
-
}
|
|
24
|
-
//# sourceMappingURL=function-registry.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"function-registry.d.ts","sourceRoot":"","sources":["../../../../src/registry/function-registry.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAkB,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,KAAK,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAIvD,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE9D,MAAM,MAAM,wBAAwB,GAAG;IACrC,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,QAAQ;IAKhC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA4D;IAEhG,SAAgB,qBAAqB,kCAAyC;gBAEjD,OAAO,EAAE,MAAM;IAIrC,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW,EAAE;IAIhD;;;OAGG;IAEU,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;cAcrD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;cA0BtB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAG3D"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"function-registry.test.d.ts","sourceRoot":"","sources":["../../../../src/registry/function-registry.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/registry/index.ts"],"names":[],"mappings":"AAIA,cAAc,qBAAqB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"functions-integration.test.d.ts","sourceRoot":"","sources":["../../../../src/testing/functions-integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/testing/index.ts"],"names":[],"mappings":"AAIA,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC"}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { Client, Config } from '@dxos/client';
|
|
2
|
-
import { type TestBuilder } from '@dxos/client/testing';
|
|
3
|
-
export declare const createInitializedClients: (testBuilder: TestBuilder, count?: number, config?: Config) => Promise<Client[]>;
|
|
4
|
-
export declare const createFunctionRuntime: (testBuilder: TestBuilder) => Promise<Client>;
|
|
5
|
-
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../../src/testing/setup.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAQxD,eAAO,MAAM,wBAAwB,gBAAuB,WAAW,UAAS,MAAM,WAAe,MAAM,sBAW1G,CAAC;AAEF,eAAO,MAAM,qBAAqB,gBAAuB,WAAW,KAAG,QAAQ,MAAM,CAiBpF,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/testing/types.ts"],"names":[],"mappings":";;;;;AAMA,qBAAa,QAAS,SAAQ,aAE5B;CAAG"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../../src/testing/util.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAKvD,eAAO,MAAM,cAAc,UAAiB,KAAK,OAAO,MAAM,kBAM7D,CAAC"}
|