@dxos/functions 0.5.3-main.37bbd91 → 0.5.3-main.3b535c7
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 +471 -825
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +461 -805
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/handler.d.ts +12 -33
- package/dist/types/src/handler.d.ts.map +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 +13 -16
- package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
- package/dist/types/src/runtime/scheduler.d.ts +27 -13
- package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +101 -143
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +15 -33
- package/schema/functions.json +104 -140
- package/src/handler.ts +31 -54
- package/src/index.ts +0 -2
- package/src/runtime/dev-server.ts +53 -104
- package/src/runtime/scheduler.test.ts +73 -56
- package/src/runtime/scheduler.ts +271 -87
- package/src/types.ts +32 -59
- 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/runtime/dev-server.test.d.ts +0 -2
- package/dist/types/src/runtime/dev-server.test.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/test/handler.d.ts +0 -4
- package/dist/types/src/testing/test/handler.d.ts.map +0 -1
- package/dist/types/src/testing/test/index.d.ts +0 -3
- package/dist/types/src/testing/test/index.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/runtime/dev-server.test.ts +0 -60
- 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/test/handler.ts +0 -15
- package/src/testing/test/index.ts +0 -7
- 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 { FunctionRegistry } from '../function';
|
|
16
|
-
import { createInitializedClients, TestType, triggerWebhook } from '../testing';
|
|
17
|
-
import { TriggerRegistry } from '../trigger';
|
|
14
|
+
import { Scheduler } from './scheduler';
|
|
18
15
|
import { type FunctionManifest } 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.createLocal() });
|
|
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
|
+
name: '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,49 +69,52 @@ describe('scheduler', () => {
|
|
|
67
69
|
const manifest: FunctionManifest = {
|
|
68
70
|
functions: [
|
|
69
71
|
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
id: 'example.com/function/test',
|
|
73
|
+
name: 'test',
|
|
72
74
|
handler: 'test',
|
|
73
75
|
},
|
|
74
76
|
],
|
|
75
77
|
triggers: [
|
|
76
78
|
{
|
|
77
79
|
function: 'example.com/function/test',
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
method: 'GET',
|
|
80
|
+
webhook: {
|
|
81
|
+
port: 8080,
|
|
81
82
|
},
|
|
82
83
|
},
|
|
83
84
|
],
|
|
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
|
-
const space = await client.spaces.create();
|
|
91
|
-
await scheduler.register(space, manifest);
|
|
92
|
-
await scheduler.start();
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
await scheduler.start();
|
|
95
|
+
after(async () => {
|
|
96
|
+
await scheduler.stop();
|
|
97
|
+
});
|
|
95
98
|
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
void fetch('http://localhost:8080');
|
|
101
|
+
});
|
|
96
102
|
await done.wait();
|
|
97
103
|
});
|
|
98
104
|
|
|
99
|
-
test('websocket', async () => {
|
|
105
|
+
test.only('websocket', async () => {
|
|
100
106
|
const manifest: FunctionManifest = {
|
|
101
107
|
functions: [
|
|
102
108
|
{
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
id: 'example.com/function/test',
|
|
110
|
+
name: 'test',
|
|
105
111
|
handler: 'test',
|
|
106
112
|
},
|
|
107
113
|
],
|
|
108
114
|
triggers: [
|
|
109
115
|
{
|
|
110
116
|
function: 'example.com/function/test',
|
|
111
|
-
|
|
112
|
-
type: 'websocket',
|
|
117
|
+
websocket: {
|
|
113
118
|
// url: 'https://hub.dxos.network/api/mailbox/test',
|
|
114
119
|
url: 'http://localhost:8081',
|
|
115
120
|
init: {
|
|
@@ -121,11 +126,16 @@ describe('scheduler', () => {
|
|
|
121
126
|
};
|
|
122
127
|
|
|
123
128
|
const done = new Trigger();
|
|
124
|
-
const scheduler =
|
|
125
|
-
|
|
129
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
130
|
+
callback: async (data) => {
|
|
131
|
+
done.wake();
|
|
132
|
+
},
|
|
126
133
|
});
|
|
127
|
-
|
|
134
|
+
|
|
128
135
|
await scheduler.start();
|
|
136
|
+
after(async () => {
|
|
137
|
+
await scheduler.stop();
|
|
138
|
+
});
|
|
129
139
|
|
|
130
140
|
// Test server.
|
|
131
141
|
setTimeout(() => {
|
|
@@ -143,20 +153,29 @@ describe('scheduler', () => {
|
|
|
143
153
|
});
|
|
144
154
|
|
|
145
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
|
+
|
|
146
161
|
const manifest: FunctionManifest = {
|
|
147
162
|
functions: [
|
|
148
163
|
{
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
id: 'example.com/function/test',
|
|
165
|
+
name: 'test',
|
|
151
166
|
handler: 'test',
|
|
152
167
|
},
|
|
153
168
|
],
|
|
154
169
|
triggers: [
|
|
155
170
|
{
|
|
156
171
|
function: 'example.com/function/test',
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
filter: [
|
|
172
|
+
subscription: {
|
|
173
|
+
spaceKey: client.spaces.default.key.toHex(),
|
|
174
|
+
filter: [
|
|
175
|
+
{
|
|
176
|
+
type: TestType.typename,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
160
179
|
},
|
|
161
180
|
},
|
|
162
181
|
],
|
|
@@ -164,14 +183,20 @@ describe('scheduler', () => {
|
|
|
164
183
|
|
|
165
184
|
let count = 0;
|
|
166
185
|
const done = new Trigger();
|
|
167
|
-
const scheduler =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
186
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
187
|
+
callback: async () => {
|
|
188
|
+
if (++count === 2) {
|
|
189
|
+
done.wake();
|
|
190
|
+
}
|
|
191
|
+
},
|
|
171
192
|
});
|
|
172
|
-
|
|
193
|
+
|
|
173
194
|
await scheduler.start();
|
|
195
|
+
after(async () => {
|
|
196
|
+
await scheduler.stop();
|
|
197
|
+
});
|
|
174
198
|
|
|
199
|
+
// TODO(burdon): Query for Expando?
|
|
175
200
|
setTimeout(() => {
|
|
176
201
|
const space = client.spaces.default;
|
|
177
202
|
const object = create(TestType, { title: 'Hello world!' });
|
|
@@ -180,12 +205,4 @@ describe('scheduler', () => {
|
|
|
180
205
|
|
|
181
206
|
await done.wait();
|
|
182
207
|
});
|
|
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
208
|
});
|
package/src/runtime/scheduler.ts
CHANGED
|
@@ -2,19 +2,31 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { CronJob } from 'cron';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import WebSocket from 'ws';
|
|
6
8
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
+
import { TextV0Type } from '@braneframe/types';
|
|
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';
|
|
9
13
|
import { Context } from '@dxos/context';
|
|
14
|
+
import { invariant } from '@dxos/invariant';
|
|
10
15
|
import { log } from '@dxos/log';
|
|
16
|
+
import { ComplexMap } from '@dxos/util';
|
|
11
17
|
|
|
12
|
-
import { type
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
import { type FunctionSubscriptionEvent } from '../handler';
|
|
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';
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
type Callback = (data: FunctionSubscriptionEvent) => Promise<void>;
|
|
18
30
|
|
|
19
31
|
export type SchedulerOptions = {
|
|
20
32
|
endpoint?: string;
|
|
@@ -22,118 +34,290 @@ export type SchedulerOptions = {
|
|
|
22
34
|
};
|
|
23
35
|
|
|
24
36
|
/**
|
|
25
|
-
* The scheduler triggers function
|
|
37
|
+
* The scheduler triggers function exectuion based on various triggers.
|
|
26
38
|
*/
|
|
27
39
|
export class Scheduler {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
// Map of mounted functions.
|
|
41
|
+
private readonly _mounts = new ComplexMap<
|
|
42
|
+
{ id: string; spaceKey: PublicKey },
|
|
43
|
+
{ ctx: Context; trigger: FunctionTrigger }
|
|
44
|
+
>(({ id, spaceKey }) => `${spaceKey.toHex()}:${id}`);
|
|
31
45
|
|
|
32
46
|
constructor(
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
private readonly _client: Client,
|
|
48
|
+
private readonly _manifest: FunctionManifest,
|
|
35
49
|
private readonly _options: SchedulerOptions = {},
|
|
36
|
-
) {
|
|
37
|
-
this.functions.registered.on(async ({ space, added }) => {
|
|
38
|
-
await this._safeActivateTriggers(space, this.triggers.getInactiveTriggers(space), added);
|
|
39
|
-
});
|
|
40
|
-
this.triggers.registered.on(async ({ space, triggers }) => {
|
|
41
|
-
await this._safeActivateTriggers(space, triggers, this.functions.getFunctions(space));
|
|
42
|
-
});
|
|
43
|
-
}
|
|
50
|
+
) {}
|
|
44
51
|
|
|
45
52
|
async start() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
this._client.spaces.subscribe(async (spaces) => {
|
|
54
|
+
for (const space of spaces) {
|
|
55
|
+
await space.waitUntilReady();
|
|
56
|
+
for (const trigger of this._manifest.triggers ?? []) {
|
|
57
|
+
await this.mount(new Context(), space, trigger);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
async stop() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
for (const { id, spaceKey } of this._mounts.keys()) {
|
|
65
|
+
await this.unmount(id, spaceKey);
|
|
66
|
+
}
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
69
|
+
private async mount(ctx: Context, space: Space, trigger: FunctionTrigger) {
|
|
70
|
+
const key = { id: trigger.function, spaceKey: space.key };
|
|
71
|
+
const def = this._manifest.functions.find((config) => config.id === trigger.function);
|
|
72
|
+
invariant(def, `Function not found: ${trigger.function}`);
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await Promise.all(mountTasks).catch(log.catch);
|
|
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
|
+
}
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
log.info('function is not found for trigger', { fnTrigger });
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
83
|
+
//
|
|
84
|
+
// Triggers types.
|
|
85
|
+
//
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
meta: fnTrigger.meta,
|
|
86
|
-
data: { ...args, spaceKey: space.key },
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
});
|
|
87
|
+
if (trigger.timer) {
|
|
88
|
+
await this._createTimer(ctx, space, def, trigger.timer);
|
|
89
|
+
}
|
|
90
90
|
|
|
91
|
-
|
|
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
|
+
}
|
|
98
|
+
|
|
99
|
+
if (trigger.subscription) {
|
|
100
|
+
await this._createSubscription(ctx, space, def, trigger.subscription);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
private async
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const payload = Object.assign({}, meta && ({ meta } satisfies FunctionEventMeta<TMeta>), data);
|
|
105
|
+
private async unmount(id: string, spaceKey: PublicKey) {
|
|
106
|
+
const key = { id, spaceKey };
|
|
107
|
+
const { ctx } = this._mounts.get(key) ?? {};
|
|
108
|
+
if (ctx) {
|
|
109
|
+
this._mounts.delete(key);
|
|
110
|
+
await ctx.dispose();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
103
113
|
|
|
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 });
|
|
104
118
|
const { endpoint, callback } = this._options;
|
|
105
119
|
if (endpoint) {
|
|
106
120
|
// TODO(burdon): Move out of scheduler (generalize as callback).
|
|
107
|
-
|
|
108
|
-
log.info('exec', { function: def.uri, url, triggerType: trigger.spec.type });
|
|
109
|
-
const response = await fetch(url, {
|
|
121
|
+
await fetch(`${this._options.endpoint}/${def.name}`, {
|
|
110
122
|
method: 'POST',
|
|
111
123
|
headers: {
|
|
112
124
|
'Content-Type': 'application/json',
|
|
113
125
|
},
|
|
114
|
-
body: JSON.stringify(
|
|
126
|
+
body: JSON.stringify(data),
|
|
115
127
|
});
|
|
116
|
-
|
|
117
|
-
status = response.status;
|
|
118
128
|
} else if (callback) {
|
|
119
|
-
|
|
120
|
-
status = (await callback(payload)) ?? 200;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Check errors.
|
|
124
|
-
if (status && status >= 400) {
|
|
125
|
-
throw new Error(`Response: ${status}`);
|
|
129
|
+
await callback(data);
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
// const result = await response.json();
|
|
129
|
-
log.info('done', { function: def.
|
|
133
|
+
log.info('done', { function: def.id });
|
|
130
134
|
} catch (err: any) {
|
|
131
|
-
log.error('error', { function: def.
|
|
132
|
-
|
|
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
|
+
|
|
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
|
+
}
|
|
133
260
|
}
|
|
134
261
|
|
|
135
|
-
|
|
262
|
+
ctx.onDispose(() => {
|
|
263
|
+
ws?.close();
|
|
264
|
+
});
|
|
136
265
|
}
|
|
137
|
-
}
|
|
138
266
|
|
|
139
|
-
|
|
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
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|