@dxos/functions 0.5.3-main.8b66fe2 → 0.5.3-main.8ffbbae
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 +493 -692
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +486 -675
- 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 -11
- 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 +111 -131
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +14 -30
- package/schema/functions.json +116 -139
- package/src/index.ts +0 -2
- package/src/runtime/dev-server.test.ts +35 -15
- package/src/runtime/dev-server.ts +20 -37
- package/src/runtime/scheduler.test.ts +75 -54
- package/src/runtime/scheduler.ts +298 -66
- package/src/testing/test/handler.ts +2 -8
- package/src/types.ts +42 -58
- package/dist/lib/browser/chunk-366QG6IX.mjs +0 -81
- package/dist/lib/browser/chunk-366QG6IX.mjs.map +0 -7
- package/dist/lib/browser/types.mjs +0 -12
- package/dist/lib/browser/types.mjs.map +0 -7
- package/dist/lib/node/chunk-3VSJ57ZZ.cjs +0 -97
- package/dist/lib/node/chunk-3VSJ57ZZ.cjs.map +0 -7
- package/dist/lib/node/types.cjs +0 -33
- package/dist/lib/node/types.cjs.map +0 -7
- package/dist/types/src/function/function-registry.d.ts +0 -24
- package/dist/types/src/function/function-registry.d.ts.map +0 -1
- package/dist/types/src/function/function-registry.test.d.ts +0 -2
- package/dist/types/src/function/function-registry.test.d.ts.map +0 -1
- package/dist/types/src/function/index.d.ts +0 -2
- package/dist/types/src/function/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/dist/types/src/util.d.ts +0 -15
- package/dist/types/src/util.d.ts.map +0 -1
- package/dist/types/src/util.test.d.ts +0 -2
- package/dist/types/src/util.test.d.ts.map +0 -1
- package/src/function/function-registry.test.ts +0 -105
- package/src/function/function-registry.ts +0 -90
- package/src/function/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 -255
- package/src/trigger/trigger-registry.ts +0 -189
- package/src/trigger/type/index.ts +0 -8
- package/src/trigger/type/subscription-trigger.ts +0 -80
- 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/util.test.ts +0 -43
- package/src/util.ts +0 -48
|
@@ -6,43 +6,40 @@ import { expect } from 'chai';
|
|
|
6
6
|
import WebSocket from 'ws';
|
|
7
7
|
|
|
8
8
|
import { Trigger } from '@dxos/async';
|
|
9
|
-
import {
|
|
9
|
+
import { Client } from '@dxos/client';
|
|
10
10
|
import { TestBuilder } from '@dxos/client/testing';
|
|
11
|
-
import { create } from '@dxos/echo-schema';
|
|
11
|
+
import { create, S, TypedObject } from '@dxos/echo-schema';
|
|
12
12
|
import { describe, test } from '@dxos/test';
|
|
13
13
|
|
|
14
|
-
import { Scheduler
|
|
15
|
-
import {
|
|
16
|
-
import { createInitializedClients, TestType, triggerWebhook } from '../testing';
|
|
17
|
-
import { TriggerRegistry } from '../trigger';
|
|
18
|
-
import { type FunctionManifest } from '../types';
|
|
14
|
+
import { Scheduler } from './scheduler';
|
|
15
|
+
import { type FunctionManifest, type WebhookTrigger } from '../types';
|
|
19
16
|
|
|
20
17
|
// TODO(burdon): Test we can add and remove triggers.
|
|
21
18
|
describe('scheduler', () => {
|
|
22
|
-
let testBuilder: TestBuilder;
|
|
23
19
|
let client: Client;
|
|
24
20
|
before(async () => {
|
|
25
|
-
testBuilder = new TestBuilder();
|
|
26
|
-
client = (
|
|
21
|
+
const testBuilder = new TestBuilder();
|
|
22
|
+
client = new Client({ services: testBuilder.createLocalClientServices() });
|
|
23
|
+
await client.initialize();
|
|
24
|
+
await client.halo.createIdentity();
|
|
27
25
|
});
|
|
28
26
|
after(async () => {
|
|
29
|
-
await
|
|
27
|
+
await client.destroy();
|
|
30
28
|
});
|
|
31
29
|
|
|
32
30
|
test('timer', async () => {
|
|
33
31
|
const manifest: FunctionManifest = {
|
|
34
32
|
functions: [
|
|
35
33
|
{
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
id: 'example.com/function/test',
|
|
35
|
+
path: '/test',
|
|
38
36
|
handler: 'test',
|
|
39
37
|
},
|
|
40
38
|
],
|
|
41
39
|
triggers: [
|
|
42
40
|
{
|
|
43
41
|
function: 'example.com/function/test',
|
|
44
|
-
|
|
45
|
-
type: 'timer',
|
|
42
|
+
timer: {
|
|
46
43
|
cron: '0/1 * * * * *', // Every 1s.
|
|
47
44
|
},
|
|
48
45
|
},
|
|
@@ -51,13 +48,18 @@ describe('scheduler', () => {
|
|
|
51
48
|
|
|
52
49
|
let count = 0;
|
|
53
50
|
const done = new Trigger();
|
|
54
|
-
const scheduler =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
52
|
+
callback: async () => {
|
|
53
|
+
if (++count === 3) {
|
|
54
|
+
done.wake();
|
|
55
|
+
}
|
|
56
|
+
},
|
|
58
57
|
});
|
|
59
|
-
|
|
58
|
+
|
|
60
59
|
await scheduler.start();
|
|
60
|
+
after(async () => {
|
|
61
|
+
await scheduler.stop();
|
|
62
|
+
});
|
|
61
63
|
|
|
62
64
|
await done.wait({ timeout: 5_000 });
|
|
63
65
|
expect(count).to.equal(3);
|
|
@@ -67,16 +69,15 @@ describe('scheduler', () => {
|
|
|
67
69
|
const manifest: FunctionManifest = {
|
|
68
70
|
functions: [
|
|
69
71
|
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
id: 'example.com/function/test',
|
|
73
|
+
path: '/test',
|
|
72
74
|
handler: 'test',
|
|
73
75
|
},
|
|
74
76
|
],
|
|
75
77
|
triggers: [
|
|
76
78
|
{
|
|
77
79
|
function: 'example.com/function/test',
|
|
78
|
-
|
|
79
|
-
type: 'webhook',
|
|
80
|
+
webhook: {
|
|
80
81
|
method: 'GET',
|
|
81
82
|
},
|
|
82
83
|
},
|
|
@@ -84,14 +85,23 @@ describe('scheduler', () => {
|
|
|
84
85
|
};
|
|
85
86
|
|
|
86
87
|
const done = new Trigger();
|
|
87
|
-
const scheduler =
|
|
88
|
-
|
|
88
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
89
|
+
callback: async () => {
|
|
90
|
+
done.wake();
|
|
91
|
+
},
|
|
89
92
|
});
|
|
90
|
-
|
|
91
|
-
await scheduler.register(space, manifest);
|
|
93
|
+
|
|
92
94
|
await scheduler.start();
|
|
95
|
+
after(async () => {
|
|
96
|
+
await scheduler.stop();
|
|
97
|
+
});
|
|
93
98
|
|
|
94
|
-
setTimeout(
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
const mount: WebhookTrigger = scheduler.mounts.find(
|
|
101
|
+
(mount) => mount.function === 'example.com/function/test',
|
|
102
|
+
)!.webhook!;
|
|
103
|
+
void fetch(`http://localhost:${mount.port}`);
|
|
104
|
+
});
|
|
95
105
|
|
|
96
106
|
await done.wait();
|
|
97
107
|
});
|
|
@@ -100,16 +110,15 @@ describe('scheduler', () => {
|
|
|
100
110
|
const manifest: FunctionManifest = {
|
|
101
111
|
functions: [
|
|
102
112
|
{
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
id: 'example.com/function/test',
|
|
114
|
+
path: '/test',
|
|
105
115
|
handler: 'test',
|
|
106
116
|
},
|
|
107
117
|
],
|
|
108
118
|
triggers: [
|
|
109
119
|
{
|
|
110
120
|
function: 'example.com/function/test',
|
|
111
|
-
|
|
112
|
-
type: 'websocket',
|
|
121
|
+
websocket: {
|
|
113
122
|
// url: 'https://hub.dxos.network/api/mailbox/test',
|
|
114
123
|
url: 'http://localhost:8081',
|
|
115
124
|
init: {
|
|
@@ -121,11 +130,16 @@ describe('scheduler', () => {
|
|
|
121
130
|
};
|
|
122
131
|
|
|
123
132
|
const done = new Trigger();
|
|
124
|
-
const scheduler =
|
|
125
|
-
|
|
133
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
134
|
+
callback: async (data) => {
|
|
135
|
+
done.wake();
|
|
136
|
+
},
|
|
126
137
|
});
|
|
127
|
-
|
|
138
|
+
|
|
128
139
|
await scheduler.start();
|
|
140
|
+
after(async () => {
|
|
141
|
+
await scheduler.stop();
|
|
142
|
+
});
|
|
129
143
|
|
|
130
144
|
// Test server.
|
|
131
145
|
setTimeout(() => {
|
|
@@ -143,20 +157,29 @@ describe('scheduler', () => {
|
|
|
143
157
|
});
|
|
144
158
|
|
|
145
159
|
test('subscription', async () => {
|
|
160
|
+
class TestType extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
161
|
+
title: S.string,
|
|
162
|
+
}) {}
|
|
163
|
+
client.addSchema(TestType);
|
|
164
|
+
|
|
146
165
|
const manifest: FunctionManifest = {
|
|
147
166
|
functions: [
|
|
148
167
|
{
|
|
149
|
-
|
|
150
|
-
|
|
168
|
+
id: 'example.com/function/test',
|
|
169
|
+
path: '/test',
|
|
151
170
|
handler: 'test',
|
|
152
171
|
},
|
|
153
172
|
],
|
|
154
173
|
triggers: [
|
|
155
174
|
{
|
|
156
175
|
function: 'example.com/function/test',
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
filter: [
|
|
176
|
+
subscription: {
|
|
177
|
+
spaceKey: client.spaces.default.key.toHex(),
|
|
178
|
+
filter: [
|
|
179
|
+
{
|
|
180
|
+
type: TestType.typename,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
160
183
|
},
|
|
161
184
|
},
|
|
162
185
|
],
|
|
@@ -164,14 +187,20 @@ describe('scheduler', () => {
|
|
|
164
187
|
|
|
165
188
|
let count = 0;
|
|
166
189
|
const done = new Trigger();
|
|
167
|
-
const scheduler =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
191
|
+
callback: async () => {
|
|
192
|
+
if (++count === 2) {
|
|
193
|
+
done.wake();
|
|
194
|
+
}
|
|
195
|
+
},
|
|
171
196
|
});
|
|
172
|
-
|
|
197
|
+
|
|
173
198
|
await scheduler.start();
|
|
199
|
+
after(async () => {
|
|
200
|
+
await scheduler.stop();
|
|
201
|
+
});
|
|
174
202
|
|
|
203
|
+
// TODO(burdon): Query for Expando?
|
|
175
204
|
setTimeout(() => {
|
|
176
205
|
const space = client.spaces.default;
|
|
177
206
|
const object = create(TestType, { title: 'Hello world!' });
|
|
@@ -180,12 +209,4 @@ describe('scheduler', () => {
|
|
|
180
209
|
|
|
181
210
|
await done.wait();
|
|
182
211
|
});
|
|
183
|
-
|
|
184
|
-
const createScheduler = (callback: SchedulerOptions['callback']) => {
|
|
185
|
-
const scheduler = new Scheduler(new FunctionRegistry(client), new TriggerRegistry(client), { callback });
|
|
186
|
-
after(async () => {
|
|
187
|
-
await scheduler.stop();
|
|
188
|
-
});
|
|
189
|
-
return scheduler;
|
|
190
|
-
};
|
|
191
212
|
});
|
package/src/runtime/scheduler.ts
CHANGED
|
@@ -2,16 +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 {
|
|
8
|
-
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';
|
|
9
15
|
import { Context } from '@dxos/context';
|
|
16
|
+
import { invariant } from '@dxos/invariant';
|
|
10
17
|
import { log } from '@dxos/log';
|
|
18
|
+
import { ComplexMap } from '@dxos/util';
|
|
11
19
|
|
|
12
|
-
import { type FunctionRegistry } from '../function';
|
|
13
20
|
import { type FunctionEventMeta } from '../handler';
|
|
14
|
-
import { type TriggerRegistry } from '../trigger';
|
|
15
21
|
import { type FunctionDef, type FunctionManifest, type FunctionTrigger } from '../types';
|
|
16
22
|
|
|
17
23
|
export type Callback = (data: any) => Promise<void | number>;
|
|
@@ -25,87 +31,101 @@ export type SchedulerOptions = {
|
|
|
25
31
|
* The scheduler triggers function execution based on various triggers.
|
|
26
32
|
*/
|
|
27
33
|
export class Scheduler {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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}`);
|
|
31
39
|
|
|
32
40
|
constructor(
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
private readonly _client: Client,
|
|
42
|
+
private readonly _manifest: FunctionManifest,
|
|
35
43
|
private readonly _options: SchedulerOptions = {},
|
|
36
|
-
) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
get mounts() {
|
|
47
|
+
return Array.from(this._mounts.values()).reduce<FunctionTrigger[]>((acc, { trigger }) => {
|
|
48
|
+
acc.push(trigger);
|
|
49
|
+
return acc;
|
|
50
|
+
}, []);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
async start() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
});
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
async stop() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
for (const { id, spaceKey } of this._mounts.keys()) {
|
|
66
|
+
await this.unmount(id, spaceKey);
|
|
67
|
+
}
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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}`);
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await Promise.all(mountTasks).catch(log.catch);
|
|
73
|
-
}
|
|
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
|
+
}
|
|
74
86
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
log.info('function is not found for trigger', { fnTrigger });
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
87
|
+
//
|
|
88
|
+
// Triggers types.
|
|
89
|
+
//
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
91
|
+
if (trigger.timer) {
|
|
92
|
+
await this._createTimer(ctx, space, def, trigger);
|
|
93
|
+
}
|
|
94
|
+
|
|
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
|
+
}
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
if (trigger.subscription) {
|
|
104
|
+
await this._createSubscription(ctx, space, def, trigger);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
92
107
|
}
|
|
93
108
|
|
|
94
|
-
private async
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async _execFunction<TData, TMeta>(def: FunctionDef, trigger: FunctionTrigger, data: TData): Promise<number> {
|
|
99
119
|
let status = 0;
|
|
100
120
|
try {
|
|
101
121
|
// TODO(burdon): Pass in Space key (common context)?
|
|
102
|
-
const payload = Object.assign({}, meta
|
|
122
|
+
const payload = Object.assign({}, { meta: trigger.meta as TMeta } satisfies FunctionEventMeta<TMeta>, data);
|
|
103
123
|
|
|
104
124
|
const { endpoint, callback } = this._options;
|
|
105
125
|
if (endpoint) {
|
|
106
126
|
// TODO(burdon): Move out of scheduler (generalize as callback).
|
|
107
|
-
const url = path.join(endpoint, def.
|
|
108
|
-
log.info('exec', { function: def.
|
|
127
|
+
const url = path.join(endpoint, def.path);
|
|
128
|
+
log.info('exec', { function: def.id, url });
|
|
109
129
|
const response = await fetch(url, {
|
|
110
130
|
method: 'POST',
|
|
111
131
|
headers: {
|
|
@@ -116,7 +136,7 @@ export class Scheduler {
|
|
|
116
136
|
|
|
117
137
|
status = response.status;
|
|
118
138
|
} else if (callback) {
|
|
119
|
-
log.info('exec', { function: def.
|
|
139
|
+
log.info('exec', { function: def.id });
|
|
120
140
|
status = (await callback(payload)) ?? 200;
|
|
121
141
|
}
|
|
122
142
|
|
|
@@ -126,14 +146,226 @@ export class Scheduler {
|
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
// const result = await response.json();
|
|
129
|
-
log.info('done', { function: def.
|
|
149
|
+
log.info('done', { function: def.id, status });
|
|
130
150
|
} catch (err: any) {
|
|
131
|
-
log.error('error', { function: def.
|
|
151
|
+
log.error('error', { function: def.id, error: err.message });
|
|
132
152
|
status = 500;
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
return status;
|
|
136
156
|
}
|
|
137
|
-
}
|
|
138
157
|
|
|
139
|
-
|
|
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
|
};
|