@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.
Files changed (38) hide show
  1. package/dist/lib/browser/index.mjs +492 -146
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +488 -143
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/types/src/handler.d.ts +33 -12
  8. package/dist/types/src/handler.d.ts.map +1 -1
  9. package/dist/types/src/index.d.ts +1 -1
  10. package/dist/types/src/index.d.ts.map +1 -1
  11. package/dist/types/src/runtime/dev-server.d.ts +17 -6
  12. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  13. package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
  14. package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
  15. package/dist/types/src/runtime/scheduler.d.ts +55 -7
  16. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  17. package/dist/types/src/testing/test/handler.d.ts +3 -0
  18. package/dist/types/src/testing/test/handler.d.ts.map +1 -0
  19. package/dist/types/src/testing/test/index.d.ts +3 -0
  20. package/dist/types/src/testing/test/index.d.ts.map +1 -0
  21. package/dist/types/src/types.d.ts +182 -0
  22. package/dist/types/src/types.d.ts.map +1 -0
  23. package/dist/types/tools/schema.d.ts +2 -0
  24. package/dist/types/tools/schema.d.ts.map +1 -0
  25. package/package.json +20 -11
  26. package/schema/functions.json +183 -0
  27. package/src/handler.ts +56 -26
  28. package/src/index.ts +1 -1
  29. package/src/runtime/dev-server.test.ts +80 -0
  30. package/src/runtime/dev-server.ts +74 -40
  31. package/src/runtime/scheduler.test.ts +163 -9
  32. package/src/runtime/scheduler.ts +228 -64
  33. package/src/testing/test/handler.ts +9 -0
  34. package/src/testing/test/index.ts +7 -0
  35. package/src/types.ts +87 -0
  36. package/dist/types/src/manifest.d.ts +0 -26
  37. package/dist/types/src/manifest.d.ts.map +0 -1
  38. 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 Response } from '../handler';
16
- import { type FunctionDef, type FunctionManifest } from '../manifest';
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 _registrationId?: string;
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('/:name', async (req, res) => {
73
- const { name } = req.params;
84
+ app.post('/:path', async (req, res) => {
85
+ const { path } = req.params;
74
86
  try {
75
- log.info('calling', { name });
87
+ log.info('calling', { path });
76
88
  if (this._options.reload) {
77
- const { def } = this._handlers[name];
89
+ const { def } = this._handlers['/' + path];
78
90
  await this._load(def, true);
79
91
  }
80
92
 
81
- res.statusCode = await this._invoke(name, req.body);
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: { name } }) => ({ name })),
110
+ functions: this.functions.map(({ def: { id, path } }) => ({ id, path })),
98
111
  });
99
112
 
100
- log.info('registered', { registrationId, endpoint });
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?.close(async () => {
112
- if (this._registrationId) {
113
- await this._client.services.services.FunctionRegistryService!.unregister({
114
- registrationId: this._registrationId,
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
- log.info('unregistered', { registrationId: this._registrationId });
118
- this._registrationId = undefined;
119
- this._proxy = undefined;
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, flush = false) {
134
- const { id, name, handler } = def;
135
- const path = join(this._options.directory, handler);
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 (flush) {
164
+ if (force) {
140
165
  Object.keys(require.cache)
141
- .filter((key) => key.startsWith(path))
142
- .forEach((key) => delete require.cache[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(path);
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[name] = { def, handler: module.default };
178
+ this._handlers[path] = { def, handler: module.default };
152
179
  }
153
180
 
154
181
  /**
155
- * Invoke function handler.
182
+ * Invoke function.
156
183
  */
157
- private async _invoke(name: string, event: any) {
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, name });
162
- const { handler } = this._handlers[name];
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: 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 '../manifest';
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
- test.only('callback', async () => {
19
+ let client: Client;
20
+ before(async () => {
17
21
  const testBuilder = new TestBuilder();
18
- const client = new Client({ services: testBuilder.createLocal() });
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
- name: 'test',
35
+ path: '/test',
27
36
  handler: 'test',
28
37
  },
29
38
  ],
30
39
  triggers: [
31
40
  {
32
41
  function: 'example.com/function/test',
33
- schedule: '0/1 * * * * *', // Every 1s.
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
- return 200;
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
- expect(count).to.equal(3);
107
+ });
54
108
 
55
- await scheduler.stop();
56
- await client.destroy();
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
  });