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