@dxos/functions 0.5.2 → 0.5.3-main.088a2c8
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 +492 -146
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +488 -143
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- 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 +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/runtime/dev-server.d.ts +17 -6
- 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 +55 -7
- package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
- package/dist/types/src/testing/test/handler.d.ts +3 -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/types.d.ts +182 -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 +20 -11
- package/schema/functions.json +183 -0
- package/src/handler.ts +56 -26
- package/src/index.ts +1 -1
- package/src/runtime/dev-server.test.ts +80 -0
- package/src/runtime/dev-server.ts +74 -40
- package/src/runtime/scheduler.test.ts +163 -9
- package/src/runtime/scheduler.ts +228 -64
- package/src/testing/test/handler.ts +9 -0
- package/src/testing/test/index.ts +7 -0
- package/src/types.ts +87 -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
|
@@ -7,18 +7,18 @@ import { getPort } from 'get-port-please';
|
|
|
7
7
|
import type http from 'http';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
|
|
10
|
-
import { Trigger } from '@dxos/async';
|
|
10
|
+
import { Event, Trigger } from '@dxos/async';
|
|
11
11
|
import { type Client } from '@dxos/client';
|
|
12
12
|
import { invariant } from '@dxos/invariant';
|
|
13
13
|
import { log } from '@dxos/log';
|
|
14
14
|
|
|
15
|
-
import { type FunctionContext, type FunctionHandler, type
|
|
16
|
-
import { type FunctionDef, type FunctionManifest } from '../
|
|
15
|
+
import { type FunctionContext, type FunctionEvent, type FunctionHandler, type FunctionResponse } from '../handler';
|
|
16
|
+
import { type FunctionDef, type FunctionManifest } from '../types';
|
|
17
17
|
|
|
18
18
|
export type DevServerOptions = {
|
|
19
|
-
port?: number;
|
|
20
|
-
directory: string;
|
|
21
19
|
manifest: FunctionManifest;
|
|
20
|
+
baseDir: string;
|
|
21
|
+
port?: number;
|
|
22
22
|
reload?: boolean;
|
|
23
23
|
dataDir?: string;
|
|
24
24
|
};
|
|
@@ -32,16 +32,24 @@ export class DevServer {
|
|
|
32
32
|
|
|
33
33
|
private _server?: http.Server;
|
|
34
34
|
private _port?: number;
|
|
35
|
-
private
|
|
35
|
+
private _functionServiceRegistration?: string;
|
|
36
36
|
private _proxy?: string;
|
|
37
37
|
private _seq = 0;
|
|
38
38
|
|
|
39
|
+
public readonly update = new Event<number>();
|
|
40
|
+
|
|
39
41
|
// prettier-ignore
|
|
40
42
|
constructor(
|
|
41
43
|
private readonly _client: Client,
|
|
42
44
|
private readonly _options: DevServerOptions,
|
|
43
45
|
) {}
|
|
44
46
|
|
|
47
|
+
get stats() {
|
|
48
|
+
return {
|
|
49
|
+
seq: this._seq,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
get endpoint() {
|
|
46
54
|
invariant(this._port);
|
|
47
55
|
return `http://localhost:${this._port}`;
|
|
@@ -66,19 +74,24 @@ export class DevServer {
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
async start() {
|
|
77
|
+
invariant(!this._server);
|
|
78
|
+
log.info('starting...');
|
|
79
|
+
|
|
80
|
+
// TODO(burdon): Move to hono.
|
|
69
81
|
const app = express();
|
|
70
82
|
app.use(express.json());
|
|
71
83
|
|
|
72
|
-
app.post('/:
|
|
73
|
-
const {
|
|
84
|
+
app.post('/:path', async (req, res) => {
|
|
85
|
+
const { path } = req.params;
|
|
74
86
|
try {
|
|
75
|
-
log.info('calling', {
|
|
87
|
+
log.info('calling', { path });
|
|
76
88
|
if (this._options.reload) {
|
|
77
|
-
const { def } = this._handlers[
|
|
89
|
+
const { def } = this._handlers['/' + path];
|
|
78
90
|
await this._load(def, true);
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
// TODO(burdon): Get function context.
|
|
94
|
+
res.statusCode = await this.invoke('/' + path, req.body);
|
|
82
95
|
res.end();
|
|
83
96
|
} catch (err: any) {
|
|
84
97
|
log.catch(err);
|
|
@@ -94,72 +107,95 @@ export class DevServer {
|
|
|
94
107
|
// Register functions.
|
|
95
108
|
const { registrationId, endpoint } = await this._client.services.services.FunctionRegistryService!.register({
|
|
96
109
|
endpoint: this.endpoint,
|
|
97
|
-
functions: this.functions.map(({ def: {
|
|
110
|
+
functions: this.functions.map(({ def: { id, path } }) => ({ id, path })),
|
|
98
111
|
});
|
|
99
112
|
|
|
100
|
-
log.info('registered', {
|
|
101
|
-
this._registrationId = registrationId;
|
|
113
|
+
log.info('registered', { endpoint });
|
|
102
114
|
this._proxy = endpoint;
|
|
115
|
+
this._functionServiceRegistration = registrationId;
|
|
103
116
|
} catch (err: any) {
|
|
104
117
|
await this.stop();
|
|
105
118
|
throw new Error('FunctionRegistryService not available (check plugin is configured).');
|
|
106
119
|
}
|
|
120
|
+
|
|
121
|
+
log.info('started', { port: this._port });
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
async stop() {
|
|
125
|
+
invariant(this._server);
|
|
126
|
+
log.info('stopping...');
|
|
127
|
+
|
|
110
128
|
const trigger = new Trigger();
|
|
111
|
-
this._server
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
this._server.close(async () => {
|
|
130
|
+
log.info('server stopped');
|
|
131
|
+
try {
|
|
132
|
+
if (this._functionServiceRegistration) {
|
|
133
|
+
invariant(this._client.services.services.FunctionRegistryService);
|
|
134
|
+
await this._client.services.services.FunctionRegistryService.unregister({
|
|
135
|
+
registrationId: this._functionServiceRegistration,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
log.info('unregistered', { registrationId: this._functionServiceRegistration });
|
|
139
|
+
this._functionServiceRegistration = undefined;
|
|
140
|
+
this._proxy = undefined;
|
|
141
|
+
}
|
|
116
142
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
143
|
+
trigger.wake();
|
|
144
|
+
} catch (err) {
|
|
145
|
+
trigger.throw(err as Error);
|
|
120
146
|
}
|
|
121
|
-
|
|
122
|
-
trigger.wake();
|
|
123
147
|
});
|
|
124
148
|
|
|
125
149
|
await trigger.wait();
|
|
126
150
|
this._port = undefined;
|
|
127
151
|
this._server = undefined;
|
|
152
|
+
log.info('stopped');
|
|
128
153
|
}
|
|
129
154
|
|
|
130
155
|
/**
|
|
131
156
|
* Load function.
|
|
132
157
|
*/
|
|
133
|
-
private async _load(def: FunctionDef,
|
|
134
|
-
const { id,
|
|
135
|
-
const
|
|
136
|
-
log.info('loading', { id });
|
|
158
|
+
private async _load(def: FunctionDef, force = false) {
|
|
159
|
+
const { id, path, handler } = def;
|
|
160
|
+
const filePath = join(this._options.baseDir, handler);
|
|
161
|
+
log.info('loading', { id, force });
|
|
137
162
|
|
|
138
163
|
// Remove from cache.
|
|
139
|
-
if (
|
|
164
|
+
if (force) {
|
|
140
165
|
Object.keys(require.cache)
|
|
141
|
-
.filter((key) => key.startsWith(
|
|
142
|
-
.forEach((key) =>
|
|
166
|
+
.filter((key) => key.startsWith(filePath))
|
|
167
|
+
.forEach((key) => {
|
|
168
|
+
delete require.cache[key];
|
|
169
|
+
});
|
|
143
170
|
}
|
|
144
171
|
|
|
145
172
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
146
|
-
const module = require(
|
|
173
|
+
const module = require(filePath);
|
|
147
174
|
if (typeof module.default !== 'function') {
|
|
148
175
|
throw new Error(`Handler must export default function: ${id}`);
|
|
149
176
|
}
|
|
150
177
|
|
|
151
|
-
this._handlers[
|
|
178
|
+
this._handlers[path] = { def, handler: module.default };
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
/**
|
|
155
|
-
* Invoke function
|
|
182
|
+
* Invoke function.
|
|
156
183
|
*/
|
|
157
|
-
|
|
184
|
+
public async invoke(path: string, data: any): Promise<number> {
|
|
158
185
|
const seq = ++this._seq;
|
|
159
186
|
const now = Date.now();
|
|
160
187
|
|
|
161
|
-
log.info('req', { seq,
|
|
162
|
-
const
|
|
188
|
+
log.info('req', { seq, path });
|
|
189
|
+
const statusCode = await this._invoke(path, { data });
|
|
190
|
+
|
|
191
|
+
log.info('res', { seq, path, statusCode, duration: Date.now() - now });
|
|
192
|
+
this.update.emit(statusCode);
|
|
193
|
+
return statusCode;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async _invoke(path: string, event: FunctionEvent) {
|
|
197
|
+
const { handler } = this._handlers[path] ?? {};
|
|
198
|
+
invariant(handler, `invalid path: ${path}`);
|
|
163
199
|
|
|
164
200
|
const context: FunctionContext = {
|
|
165
201
|
client: this._client,
|
|
@@ -167,7 +203,7 @@ export class DevServer {
|
|
|
167
203
|
};
|
|
168
204
|
|
|
169
205
|
let statusCode = 200;
|
|
170
|
-
const response:
|
|
206
|
+
const response: FunctionResponse = {
|
|
171
207
|
status: (code: number) => {
|
|
172
208
|
statusCode = code;
|
|
173
209
|
return response;
|
|
@@ -175,8 +211,6 @@ export class DevServer {
|
|
|
175
211
|
};
|
|
176
212
|
|
|
177
213
|
await handler({ context, event, response });
|
|
178
|
-
log.info('res', { seq, name, statusCode, duration: Date.now() - now });
|
|
179
|
-
|
|
180
214
|
return statusCode;
|
|
181
215
|
}
|
|
182
216
|
}
|
|
@@ -3,34 +3,45 @@
|
|
|
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, type WebhookTrigger } 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.createLocalClientServices() });
|
|
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
|
{
|
|
25
34
|
id: 'example.com/function/test',
|
|
26
|
-
|
|
35
|
+
path: '/test',
|
|
27
36
|
handler: 'test',
|
|
28
37
|
},
|
|
29
38
|
],
|
|
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,160 @@ 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
|
+
});
|
|
45
67
|
|
|
46
|
-
|
|
68
|
+
test('webhook', async () => {
|
|
69
|
+
const manifest: FunctionManifest = {
|
|
70
|
+
functions: [
|
|
71
|
+
{
|
|
72
|
+
id: 'example.com/function/test',
|
|
73
|
+
path: '/test',
|
|
74
|
+
handler: 'test',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
triggers: [
|
|
78
|
+
{
|
|
79
|
+
function: 'example.com/function/test',
|
|
80
|
+
webhook: {
|
|
81
|
+
method: 'GET',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
|
|
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
|
+
});
|
|
98
|
+
|
|
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
|
+
});
|
|
51
105
|
|
|
52
106
|
await done.wait();
|
|
53
|
-
|
|
107
|
+
});
|
|
54
108
|
|
|
55
|
-
|
|
56
|
-
|
|
109
|
+
test('websocket', async () => {
|
|
110
|
+
const manifest: FunctionManifest = {
|
|
111
|
+
functions: [
|
|
112
|
+
{
|
|
113
|
+
id: 'example.com/function/test',
|
|
114
|
+
path: '/test',
|
|
115
|
+
handler: 'test',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
triggers: [
|
|
119
|
+
{
|
|
120
|
+
function: 'example.com/function/test',
|
|
121
|
+
websocket: {
|
|
122
|
+
// url: 'https://hub.dxos.network/api/mailbox/test',
|
|
123
|
+
url: 'http://localhost:8081',
|
|
124
|
+
init: {
|
|
125
|
+
type: 'sync',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const done = new Trigger();
|
|
133
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
134
|
+
callback: async (data) => {
|
|
135
|
+
done.wake();
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await scheduler.start();
|
|
140
|
+
after(async () => {
|
|
141
|
+
await scheduler.stop();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Test server.
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
const wss = new WebSocket.Server({ port: 8081 });
|
|
147
|
+
wss.on('connection', (ws: WebSocket) => {
|
|
148
|
+
ws.on('message', (data) => {
|
|
149
|
+
const info = JSON.parse(new TextDecoder().decode(data as ArrayBuffer));
|
|
150
|
+
expect(info.type).to.equal('sync');
|
|
151
|
+
done.wake();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}, 500);
|
|
155
|
+
|
|
156
|
+
await done.wait();
|
|
157
|
+
});
|
|
158
|
+
|
|
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
|
+
|
|
165
|
+
const manifest: FunctionManifest = {
|
|
166
|
+
functions: [
|
|
167
|
+
{
|
|
168
|
+
id: 'example.com/function/test',
|
|
169
|
+
path: '/test',
|
|
170
|
+
handler: 'test',
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
triggers: [
|
|
174
|
+
{
|
|
175
|
+
function: 'example.com/function/test',
|
|
176
|
+
subscription: {
|
|
177
|
+
spaceKey: client.spaces.default.key.toHex(),
|
|
178
|
+
filter: [
|
|
179
|
+
{
|
|
180
|
+
type: TestType.typename,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
let count = 0;
|
|
189
|
+
const done = new Trigger();
|
|
190
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
191
|
+
callback: async () => {
|
|
192
|
+
if (++count === 2) {
|
|
193
|
+
done.wake();
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await scheduler.start();
|
|
199
|
+
after(async () => {
|
|
200
|
+
await scheduler.stop();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// TODO(burdon): Query for Expando?
|
|
204
|
+
setTimeout(() => {
|
|
205
|
+
const space = client.spaces.default;
|
|
206
|
+
const object = create(TestType, { title: 'Hello world!' });
|
|
207
|
+
space.db.add(object);
|
|
208
|
+
}, 100);
|
|
209
|
+
|
|
210
|
+
await done.wait();
|
|
57
211
|
});
|
|
58
212
|
});
|