@dxos/functions 0.5.3-main.f1ddd61 → 0.5.3-main.f752aaa
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 +268 -77
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +265 -75
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/handler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/runtime/dev-server.d.ts +7 -2
- package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
- package/dist/types/src/runtime/scheduler.d.ts +19 -5
- package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +160 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/tools/schema.d.ts +2 -0
- package/dist/types/tools/schema.d.ts.map +1 -0
- package/package.json +18 -11
- package/schema/functions.json +170 -0
- package/src/handler.ts +7 -0
- package/src/index.ts +1 -1
- package/src/runtime/dev-server.ts +1 -1
- package/src/runtime/scheduler.test.ts +158 -8
- package/src/runtime/scheduler.ts +174 -58
- package/src/types.ts +76 -0
- package/dist/types/src/manifest.d.ts +0 -26
- package/dist/types/src/manifest.d.ts.map +0 -1
- package/src/manifest.ts +0 -42
|
@@ -3,22 +3,31 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { expect } from 'chai';
|
|
6
|
+
import WebSocket from 'ws';
|
|
6
7
|
|
|
7
8
|
import { Trigger } from '@dxos/async';
|
|
8
9
|
import { Client } from '@dxos/client';
|
|
9
10
|
import { TestBuilder } from '@dxos/client/testing';
|
|
11
|
+
import { create, S, TypedObject } from '@dxos/echo-schema';
|
|
10
12
|
import { describe, test } from '@dxos/test';
|
|
11
13
|
|
|
12
14
|
import { Scheduler } from './scheduler';
|
|
13
|
-
import { type FunctionManifest } from '../
|
|
15
|
+
import { type FunctionManifest } from '../types';
|
|
14
16
|
|
|
17
|
+
// TODO(burdon): Test we can add and remove triggers.
|
|
15
18
|
describe('scheduler', () => {
|
|
16
|
-
|
|
19
|
+
let client: Client;
|
|
20
|
+
before(async () => {
|
|
17
21
|
const testBuilder = new TestBuilder();
|
|
18
|
-
|
|
22
|
+
client = new Client({ services: testBuilder.createLocal() });
|
|
19
23
|
await client.initialize();
|
|
20
24
|
await client.halo.createIdentity();
|
|
25
|
+
});
|
|
26
|
+
after(async () => {
|
|
27
|
+
await client.destroy();
|
|
28
|
+
});
|
|
21
29
|
|
|
30
|
+
test('timer', async () => {
|
|
22
31
|
const manifest: FunctionManifest = {
|
|
23
32
|
functions: [
|
|
24
33
|
{
|
|
@@ -30,7 +39,9 @@ describe('scheduler', () => {
|
|
|
30
39
|
triggers: [
|
|
31
40
|
{
|
|
32
41
|
function: 'example.com/function/test',
|
|
33
|
-
|
|
42
|
+
timer: {
|
|
43
|
+
cron: '0/1 * * * * *', // Every 1s.
|
|
44
|
+
},
|
|
34
45
|
},
|
|
35
46
|
],
|
|
36
47
|
};
|
|
@@ -42,17 +53,156 @@ describe('scheduler', () => {
|
|
|
42
53
|
if (++count === 3) {
|
|
43
54
|
done.wake();
|
|
44
55
|
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await scheduler.start();
|
|
60
|
+
after(async () => {
|
|
61
|
+
await scheduler.stop();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await done.wait({ timeout: 5_000 });
|
|
65
|
+
expect(count).to.equal(3);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('webhook', async () => {
|
|
69
|
+
const manifest: FunctionManifest = {
|
|
70
|
+
functions: [
|
|
71
|
+
{
|
|
72
|
+
id: 'example.com/function/test',
|
|
73
|
+
name: 'test',
|
|
74
|
+
handler: 'test',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
triggers: [
|
|
78
|
+
{
|
|
79
|
+
function: 'example.com/function/test',
|
|
80
|
+
webhook: {
|
|
81
|
+
port: 8080,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
45
86
|
|
|
46
|
-
|
|
87
|
+
const done = new Trigger();
|
|
88
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
89
|
+
callback: async () => {
|
|
90
|
+
done.wake();
|
|
47
91
|
},
|
|
48
92
|
});
|
|
49
93
|
|
|
50
94
|
await scheduler.start();
|
|
95
|
+
after(async () => {
|
|
96
|
+
await scheduler.stop();
|
|
97
|
+
});
|
|
51
98
|
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
void fetch('http://localhost:8080');
|
|
101
|
+
});
|
|
52
102
|
await done.wait();
|
|
53
|
-
|
|
103
|
+
});
|
|
54
104
|
|
|
55
|
-
|
|
56
|
-
|
|
105
|
+
test.only('websocket', async () => {
|
|
106
|
+
const manifest: FunctionManifest = {
|
|
107
|
+
functions: [
|
|
108
|
+
{
|
|
109
|
+
id: 'example.com/function/test',
|
|
110
|
+
name: 'test',
|
|
111
|
+
handler: 'test',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
triggers: [
|
|
115
|
+
{
|
|
116
|
+
function: 'example.com/function/test',
|
|
117
|
+
websocket: {
|
|
118
|
+
// url: 'https://hub.dxos.network/api/mailbox/test',
|
|
119
|
+
url: 'http://localhost:8081',
|
|
120
|
+
init: {
|
|
121
|
+
type: 'sync',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const done = new Trigger();
|
|
129
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
130
|
+
callback: async (data) => {
|
|
131
|
+
done.wake();
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await scheduler.start();
|
|
136
|
+
after(async () => {
|
|
137
|
+
await scheduler.stop();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Test server.
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
const wss = new WebSocket.Server({ port: 8081 });
|
|
143
|
+
wss.on('connection', (ws: WebSocket) => {
|
|
144
|
+
ws.on('message', (data) => {
|
|
145
|
+
const info = JSON.parse(new TextDecoder().decode(data as ArrayBuffer));
|
|
146
|
+
expect(info.type).to.equal('sync');
|
|
147
|
+
done.wake();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}, 500);
|
|
151
|
+
|
|
152
|
+
await done.wait();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('subscription', async () => {
|
|
156
|
+
class TestType extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
157
|
+
title: S.string,
|
|
158
|
+
}) {}
|
|
159
|
+
client.addSchema(TestType);
|
|
160
|
+
|
|
161
|
+
const manifest: FunctionManifest = {
|
|
162
|
+
functions: [
|
|
163
|
+
{
|
|
164
|
+
id: 'example.com/function/test',
|
|
165
|
+
name: 'test',
|
|
166
|
+
handler: 'test',
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
triggers: [
|
|
170
|
+
{
|
|
171
|
+
function: 'example.com/function/test',
|
|
172
|
+
subscription: {
|
|
173
|
+
spaceKey: client.spaces.default.key.toHex(),
|
|
174
|
+
filter: [
|
|
175
|
+
{
|
|
176
|
+
type: TestType.typename,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
let count = 0;
|
|
185
|
+
const done = new Trigger();
|
|
186
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
187
|
+
callback: async () => {
|
|
188
|
+
if (++count === 2) {
|
|
189
|
+
done.wake();
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await scheduler.start();
|
|
195
|
+
after(async () => {
|
|
196
|
+
await scheduler.stop();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// TODO(burdon): Query for Expando?
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
const space = client.spaces.default;
|
|
202
|
+
const object = create(TestType, { title: 'Hello world!' });
|
|
203
|
+
space.db.add(object);
|
|
204
|
+
}, 100);
|
|
205
|
+
|
|
206
|
+
await done.wait();
|
|
57
207
|
});
|
|
58
208
|
});
|
package/src/runtime/scheduler.ts
CHANGED
|
@@ -3,30 +3,39 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { CronJob } from 'cron';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import WebSocket from 'ws';
|
|
6
8
|
|
|
7
9
|
import { TextV0Type } from '@braneframe/types';
|
|
8
|
-
import { debounce, DeferredTask } from '@dxos/async';
|
|
10
|
+
import { debounce, DeferredTask, sleep, Trigger } from '@dxos/async';
|
|
9
11
|
import { type Client, type PublicKey } from '@dxos/client';
|
|
10
|
-
import {
|
|
12
|
+
import { createSubscription, Filter, getAutomergeObjectCore, type Query, type Space } from '@dxos/client/echo';
|
|
11
13
|
import { Context } from '@dxos/context';
|
|
12
14
|
import { invariant } from '@dxos/invariant';
|
|
13
15
|
import { log } from '@dxos/log';
|
|
14
16
|
import { ComplexMap } from '@dxos/util';
|
|
15
17
|
|
|
16
18
|
import { type FunctionSubscriptionEvent } from '../handler';
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type FunctionDef,
|
|
21
|
+
type FunctionManifest,
|
|
22
|
+
type FunctionTrigger,
|
|
23
|
+
type SubscriptionTrigger,
|
|
24
|
+
type TimerTrigger,
|
|
25
|
+
type WebhookTrigger,
|
|
26
|
+
type WebsocketTrigger,
|
|
27
|
+
} from '../types';
|
|
18
28
|
|
|
19
|
-
type Callback = (data: FunctionSubscriptionEvent) => Promise<
|
|
29
|
+
type Callback = (data: FunctionSubscriptionEvent) => Promise<void>;
|
|
20
30
|
|
|
21
|
-
type SchedulerOptions = {
|
|
31
|
+
export type SchedulerOptions = {
|
|
22
32
|
endpoint?: string;
|
|
23
33
|
callback?: Callback;
|
|
24
34
|
};
|
|
25
35
|
|
|
26
36
|
/**
|
|
27
|
-
*
|
|
37
|
+
* The scheduler triggers function exectuion based on various triggers.
|
|
28
38
|
*/
|
|
29
|
-
// TODO(burdon): Create tests.
|
|
30
39
|
export class Scheduler {
|
|
31
40
|
// Map of mounted functions.
|
|
32
41
|
private readonly _mounts = new ComplexMap<
|
|
@@ -62,7 +71,7 @@ export class Scheduler {
|
|
|
62
71
|
const def = this._manifest.functions.find((config) => config.id === trigger.function);
|
|
63
72
|
invariant(def, `Function not found: ${trigger.function}`);
|
|
64
73
|
|
|
65
|
-
// Currently supports only one trigger declaration per function.
|
|
74
|
+
// TODO(burdon): Currently supports only one trigger declaration per function.
|
|
66
75
|
const exists = this._mounts.get(key);
|
|
67
76
|
if (!exists) {
|
|
68
77
|
this._mounts.set(key, { ctx, trigger });
|
|
@@ -71,14 +80,24 @@ export class Scheduler {
|
|
|
71
80
|
return;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
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);
|
|
77
97
|
}
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this._createSubscription(ctx, space, def, triggerSubscription);
|
|
99
|
+
if (trigger.subscription) {
|
|
100
|
+
await this._createSubscription(ctx, space, def, trigger.subscription);
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
103
|
}
|
|
@@ -92,19 +111,51 @@ export class Scheduler {
|
|
|
92
111
|
}
|
|
93
112
|
}
|
|
94
113
|
|
|
95
|
-
|
|
114
|
+
// TODO(burdon): Pass in Space key (common context).
|
|
115
|
+
private async _execFunction(def: FunctionDef, data: any) {
|
|
116
|
+
try {
|
|
117
|
+
log.info('exec', { function: def.id });
|
|
118
|
+
const { endpoint, callback } = this._options;
|
|
119
|
+
if (endpoint) {
|
|
120
|
+
// TODO(burdon): Move out of scheduler (generalize as callback).
|
|
121
|
+
await fetch(`${this._options.endpoint}/${def.name}`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify(data),
|
|
127
|
+
});
|
|
128
|
+
} else if (callback) {
|
|
129
|
+
await callback(data);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// const result = await response.json();
|
|
133
|
+
log.info('done', { function: def.id });
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
log.error('error', { function: def.id, error: err.message });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
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
|
+
|
|
96
150
|
const task = new DeferredTask(ctx, async () => {
|
|
97
|
-
await this._execFunction(def, {
|
|
98
|
-
space: space.key,
|
|
99
|
-
});
|
|
151
|
+
await this._execFunction(def, { space: space.key });
|
|
100
152
|
});
|
|
101
153
|
|
|
102
|
-
invariant(trigger.schedule);
|
|
103
154
|
let last = 0;
|
|
104
155
|
let run = 0;
|
|
105
156
|
// https://www.npmjs.com/package/cron#constructor
|
|
106
157
|
const job = CronJob.from({
|
|
107
|
-
cronTime:
|
|
158
|
+
cronTime: cron,
|
|
108
159
|
runOnInit: false,
|
|
109
160
|
onTick: () => {
|
|
110
161
|
// TODO(burdon): Check greater than 30s (use cron-parser).
|
|
@@ -122,18 +173,109 @@ export class Scheduler {
|
|
|
122
173
|
ctx.onDispose(() => job.stop());
|
|
123
174
|
}
|
|
124
175
|
|
|
125
|
-
|
|
126
|
-
|
|
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 });
|
|
127
272
|
const objectIds = new Set<string>();
|
|
128
273
|
const task = new DeferredTask(ctx, async () => {
|
|
129
|
-
await this._execFunction(def, {
|
|
130
|
-
space: space.key,
|
|
131
|
-
objects: Array.from(objectIds),
|
|
132
|
-
});
|
|
274
|
+
await this._execFunction(def, { space: space.key, objects: Array.from(objectIds) });
|
|
133
275
|
});
|
|
134
276
|
|
|
135
277
|
// TODO(burdon): Don't fire initially.
|
|
136
|
-
// TODO(burdon):
|
|
278
|
+
// TODO(burdon): Subscription is called THREE times.
|
|
137
279
|
const subscriptions: (() => void)[] = [];
|
|
138
280
|
const subscription = createSubscription(({ added, updated }) => {
|
|
139
281
|
log.info('updated', { added: added.length, updated: updated.length });
|
|
@@ -150,13 +292,13 @@ export class Scheduler {
|
|
|
150
292
|
|
|
151
293
|
// TODO(burdon): Create queue. Only allow one invocation per trigger at a time?
|
|
152
294
|
// TODO(burdon): Disable trigger if keeps failing.
|
|
153
|
-
const {
|
|
295
|
+
const { filter, options: { deep, delay } = {} } = trigger;
|
|
154
296
|
const update = ({ objects }: Query) => {
|
|
155
297
|
subscription.update(objects);
|
|
156
298
|
|
|
157
299
|
// TODO(burdon): Hack to monitor changes to Document's text object.
|
|
158
300
|
if (deep) {
|
|
159
|
-
log.info('update', {
|
|
301
|
+
log.info('update', { objects: objects.length });
|
|
160
302
|
for (const object of objects) {
|
|
161
303
|
const content = object.content;
|
|
162
304
|
if (content instanceof TextV0Type) {
|
|
@@ -168,40 +310,14 @@ export class Scheduler {
|
|
|
168
310
|
}
|
|
169
311
|
};
|
|
170
312
|
|
|
313
|
+
// TODO(burdon): Is Filter.or implemented?
|
|
171
314
|
// TODO(burdon): [Bug]: all callbacks are fired on the first mutation.
|
|
172
315
|
// TODO(burdon): [Bug]: not updated when document is deleted (either top or hierarchically).
|
|
173
|
-
const query = space.db.query(Filter.typename(type, props));
|
|
174
|
-
subscriptions.push(query.subscribe(delay ? debounce(update, delay
|
|
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));
|
|
175
318
|
|
|
176
319
|
ctx.onDispose(() => {
|
|
177
320
|
subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
178
321
|
});
|
|
179
322
|
}
|
|
180
|
-
|
|
181
|
-
private async _execFunction(def: FunctionDef, data: any) {
|
|
182
|
-
try {
|
|
183
|
-
log('request', { function: def.id });
|
|
184
|
-
const { endpoint, callback } = this._options;
|
|
185
|
-
let status = 0;
|
|
186
|
-
if (endpoint) {
|
|
187
|
-
// TODO(burdon): Move out of scheduler (generalize as callback).
|
|
188
|
-
const response = await fetch(`${this._options.endpoint}/${def.name}`, {
|
|
189
|
-
method: 'POST',
|
|
190
|
-
headers: {
|
|
191
|
-
'Content-Type': 'application/json',
|
|
192
|
-
},
|
|
193
|
-
body: JSON.stringify(data),
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
status = response.status;
|
|
197
|
-
} else if (callback) {
|
|
198
|
-
status = await callback(data);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// const result = await response.json();
|
|
202
|
-
log('result', { function: def.id, result: status });
|
|
203
|
-
} catch (err: any) {
|
|
204
|
-
log.error('error', { function: def.id, error: err.message });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
323
|
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as S from '@effect/schema/Schema';
|
|
6
|
+
|
|
7
|
+
const TimerTriggerSchema = S.struct({
|
|
8
|
+
cron: S.string,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const WebhookTriggerSchema = S.struct({
|
|
12
|
+
port: S.number,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const WebsocketTriggerSchema = S.struct({
|
|
16
|
+
url: S.string,
|
|
17
|
+
init: S.optional(S.record(S.string, S.any)),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const SubscriptionTriggerSchema = S.struct({
|
|
21
|
+
spaceKey: S.optional(S.string),
|
|
22
|
+
// TODO(burdon): Define query DSL.
|
|
23
|
+
filter: S.array(
|
|
24
|
+
S.struct({
|
|
25
|
+
type: S.string,
|
|
26
|
+
props: S.optional(S.record(S.string, S.any)),
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
options: S.optional(
|
|
30
|
+
S.struct({
|
|
31
|
+
// Watch changes to object (not just creation).
|
|
32
|
+
deep: S.optional(S.boolean),
|
|
33
|
+
// Debounce changes (delay in ms).
|
|
34
|
+
delay: S.optional(S.number),
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const FunctionTriggerSchema = S.struct({
|
|
40
|
+
function: S.string.pipe(S.description('Function ID/URI.')),
|
|
41
|
+
|
|
42
|
+
timer: S.optional(TimerTriggerSchema),
|
|
43
|
+
webhook: S.optional(WebhookTriggerSchema),
|
|
44
|
+
websocket: S.optional(WebsocketTriggerSchema),
|
|
45
|
+
subscription: S.optional(SubscriptionTriggerSchema),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export type TimerTrigger = S.Schema.Type<typeof TimerTriggerSchema>;
|
|
49
|
+
export type WebhookTrigger = S.Schema.Type<typeof WebhookTriggerSchema>;
|
|
50
|
+
export type WebsocketTrigger = S.Schema.Type<typeof WebsocketTriggerSchema>;
|
|
51
|
+
export type SubscriptionTrigger = S.Schema.Type<typeof SubscriptionTriggerSchema>;
|
|
52
|
+
export type FunctionTrigger = S.Schema.Type<typeof FunctionTriggerSchema>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Function definition.
|
|
56
|
+
*/
|
|
57
|
+
// TODO(burdon): Name vs. path?
|
|
58
|
+
const FunctionDefSchema = S.struct({
|
|
59
|
+
id: S.string,
|
|
60
|
+
description: S.optional(S.string),
|
|
61
|
+
name: S.string,
|
|
62
|
+
// TODO(burdon): NPM/GitHub URL?
|
|
63
|
+
handler: S.string,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type FunctionDef = S.Schema.Type<typeof FunctionDefSchema>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Function manifest file.
|
|
70
|
+
*/
|
|
71
|
+
export const FunctionManifestSchema = S.struct({
|
|
72
|
+
functions: S.mutable(S.array(FunctionDefSchema)),
|
|
73
|
+
triggers: S.mutable(S.array(FunctionTriggerSchema)),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export type FunctionManifest = S.Schema.Type<typeof FunctionManifestSchema>;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export type FunctionDef = {
|
|
2
|
-
id: string;
|
|
3
|
-
name: string;
|
|
4
|
-
handler: string;
|
|
5
|
-
description?: string;
|
|
6
|
-
};
|
|
7
|
-
export type TriggerSubscription = {
|
|
8
|
-
type: string;
|
|
9
|
-
spaceKey: string;
|
|
10
|
-
props?: Record<string, any>;
|
|
11
|
-
deep?: boolean;
|
|
12
|
-
delay?: number;
|
|
13
|
-
};
|
|
14
|
-
export type FunctionTrigger = {
|
|
15
|
-
function: string;
|
|
16
|
-
schedule?: string;
|
|
17
|
-
subscriptions?: TriggerSubscription[];
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Function manifest file.
|
|
21
|
-
*/
|
|
22
|
-
export type FunctionManifest = {
|
|
23
|
-
functions: FunctionDef[];
|
|
24
|
-
triggers: FunctionTrigger[];
|
|
25
|
-
};
|
|
26
|
-
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/manifest.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,WAAW,GAAG;IAExB,EAAE,EAAE,MAAM,CAAC;IAEX,IAAI,EAAE,MAAM,CAAC;IAEb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAGF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAKF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACvC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC"}
|
package/src/manifest.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// Lambda-like function definitions.
|
|
6
|
-
// See: https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml/#functions
|
|
7
|
-
|
|
8
|
-
export type FunctionDef = {
|
|
9
|
-
// FQ function name.
|
|
10
|
-
id: string;
|
|
11
|
-
// URL path.
|
|
12
|
-
name: string;
|
|
13
|
-
// File path of handler.
|
|
14
|
-
handler: string;
|
|
15
|
-
description?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// TODO(burdon): Query DSL.
|
|
19
|
-
export type TriggerSubscription = {
|
|
20
|
-
type: string;
|
|
21
|
-
spaceKey: string;
|
|
22
|
-
props?: Record<string, any>;
|
|
23
|
-
deep?: boolean; // Watch changes to object (not just creation).
|
|
24
|
-
delay?: number;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// TODO(burdon): Generalize binding.
|
|
28
|
-
// https://www.npmjs.com/package/aws-lambda
|
|
29
|
-
// https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html
|
|
30
|
-
export type FunctionTrigger = {
|
|
31
|
-
function: string;
|
|
32
|
-
schedule?: string;
|
|
33
|
-
subscriptions?: TriggerSubscription[];
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Function manifest file.
|
|
38
|
-
*/
|
|
39
|
-
export type FunctionManifest = {
|
|
40
|
-
functions: FunctionDef[];
|
|
41
|
-
triggers: FunctionTrigger[];
|
|
42
|
-
};
|