@dxos/functions 0.5.3-main.f752aaa → 0.5.3-main.f9b873d

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 (81) hide show
  1. package/dist/lib/browser/index.mjs +802 -429
  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 +787 -426
  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 +2 -0
  10. package/dist/types/src/index.d.ts.map +1 -1
  11. package/dist/types/src/registry/function-registry.d.ts +24 -0
  12. package/dist/types/src/registry/function-registry.d.ts.map +1 -0
  13. package/dist/types/src/registry/function-registry.test.d.ts +2 -0
  14. package/dist/types/src/registry/function-registry.test.d.ts.map +1 -0
  15. package/dist/types/src/registry/index.d.ts +2 -0
  16. package/dist/types/src/registry/index.d.ts.map +1 -0
  17. package/dist/types/src/runtime/dev-server.d.ts +16 -13
  18. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  19. package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
  20. package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
  21. package/dist/types/src/runtime/scheduler.d.ts +12 -27
  22. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  23. package/dist/types/src/testing/functions-integration.test.d.ts +2 -0
  24. package/dist/types/src/testing/functions-integration.test.d.ts.map +1 -0
  25. package/dist/types/src/testing/index.d.ts +4 -0
  26. package/dist/types/src/testing/index.d.ts.map +1 -0
  27. package/dist/types/src/testing/setup.d.ts +5 -0
  28. package/dist/types/src/testing/setup.d.ts.map +1 -0
  29. package/dist/types/src/testing/test/handler.d.ts +4 -0
  30. package/dist/types/src/testing/test/handler.d.ts.map +1 -0
  31. package/dist/types/src/testing/test/index.d.ts +3 -0
  32. package/dist/types/src/testing/test/index.d.ts.map +1 -0
  33. package/dist/types/src/testing/types.d.ts +9 -0
  34. package/dist/types/src/testing/types.d.ts.map +1 -0
  35. package/dist/types/src/testing/util.d.ts +3 -0
  36. package/dist/types/src/testing/util.d.ts.map +1 -0
  37. package/dist/types/src/trigger/index.d.ts +2 -0
  38. package/dist/types/src/trigger/index.d.ts.map +1 -0
  39. package/dist/types/src/trigger/trigger-registry.d.ts +40 -0
  40. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -0
  41. package/dist/types/src/trigger/trigger-registry.test.d.ts +2 -0
  42. package/dist/types/src/trigger/trigger-registry.test.d.ts.map +1 -0
  43. package/dist/types/src/trigger/type/index.d.ts +5 -0
  44. package/dist/types/src/trigger/type/index.d.ts.map +1 -0
  45. package/dist/types/src/trigger/type/subscription-trigger.d.ts +4 -0
  46. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -0
  47. package/dist/types/src/trigger/type/timer-trigger.d.ts +4 -0
  48. package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -0
  49. package/dist/types/src/trigger/type/webhook-trigger.d.ts +4 -0
  50. package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -0
  51. package/dist/types/src/trigger/type/websocket-trigger.d.ts +13 -0
  52. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -0
  53. package/dist/types/src/types.d.ts +129 -101
  54. package/dist/types/src/types.d.ts.map +1 -1
  55. package/package.json +18 -13
  56. package/schema/functions.json +128 -101
  57. package/src/handler.ts +54 -31
  58. package/src/index.ts +2 -0
  59. package/src/registry/function-registry.test.ts +105 -0
  60. package/src/registry/function-registry.ts +84 -0
  61. package/src/registry/index.ts +5 -0
  62. package/src/runtime/dev-server.test.ts +60 -0
  63. package/src/runtime/dev-server.ts +104 -52
  64. package/src/runtime/scheduler.test.ts +56 -73
  65. package/src/runtime/scheduler.ts +79 -271
  66. package/src/testing/functions-integration.test.ts +99 -0
  67. package/src/testing/index.ts +7 -0
  68. package/src/testing/setup.ts +45 -0
  69. package/src/testing/test/handler.ts +15 -0
  70. package/src/testing/test/index.ts +7 -0
  71. package/src/testing/types.ts +9 -0
  72. package/src/testing/util.ts +16 -0
  73. package/src/trigger/index.ts +5 -0
  74. package/src/trigger/trigger-registry.test.ts +229 -0
  75. package/src/trigger/trigger-registry.ts +176 -0
  76. package/src/trigger/type/index.ts +8 -0
  77. package/src/trigger/type/subscription-trigger.ts +73 -0
  78. package/src/trigger/type/timer-trigger.ts +44 -0
  79. package/src/trigger/type/webhook-trigger.ts +47 -0
  80. package/src/trigger/type/websocket-trigger.ts +91 -0
  81. package/src/types.ts +57 -32
@@ -7,18 +7,19 @@ 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
+ import { Context } from '@dxos/context';
12
13
  import { invariant } from '@dxos/invariant';
13
14
  import { log } from '@dxos/log';
14
15
 
15
- import { type FunctionContext, type FunctionHandler, type Response } from '../handler';
16
- import { type FunctionDef, type FunctionManifest } from '../types';
16
+ import { type FunctionContext, type FunctionEvent, type FunctionHandler, type FunctionResponse } from '../handler';
17
+ import { type FunctionRegistry } from '../registry';
18
+ import { type FunctionDef } from '../types';
17
19
 
18
20
  export type DevServerOptions = {
21
+ baseDir: string;
19
22
  port?: number;
20
- directory: string;
21
- manifest: FunctionManifest;
22
23
  reload?: boolean;
23
24
  dataDir?: string;
24
25
  };
@@ -27,20 +28,37 @@ export type DevServerOptions = {
27
28
  * Functions dev server provides a local HTTP server for testing functions.
28
29
  */
29
30
  export class DevServer {
31
+ private _ctx = createContext();
32
+
30
33
  // Function handlers indexed by name (URL path).
31
34
  private readonly _handlers: Record<string, { def: FunctionDef; handler: FunctionHandler<any> }> = {};
32
35
 
33
36
  private _server?: http.Server;
34
37
  private _port?: number;
35
- private _registrationId?: string;
38
+ private _functionServiceRegistration?: string;
36
39
  private _proxy?: string;
37
40
  private _seq = 0;
38
41
 
42
+ public readonly update = new Event<number>();
43
+
39
44
  // prettier-ignore
40
45
  constructor(
41
46
  private readonly _client: Client,
47
+ private readonly _functionsRegistry: FunctionRegistry,
42
48
  private readonly _options: DevServerOptions,
43
- ) {}
49
+ ) {
50
+ this._functionsRegistry.onFunctionsRegistered.on(async ({ newFunctions }) => {
51
+ newFunctions.forEach((def) => this._load(def));
52
+ await this._safeUpdateRegistration();
53
+ log('new functions loaded', { newFunctions });
54
+ });
55
+ }
56
+
57
+ get stats() {
58
+ return {
59
+ seq: this._seq,
60
+ };
61
+ }
44
62
 
45
63
  get endpoint() {
46
64
  invariant(this._port);
@@ -55,30 +73,26 @@ export class DevServer {
55
73
  return Object.values(this._handlers);
56
74
  }
57
75
 
58
- async initialize() {
59
- for (const def of this._options.manifest.functions) {
60
- try {
61
- await this._load(def);
62
- } catch (err) {
63
- log.error('parsing function (check manifest)', err);
64
- }
65
- }
66
- }
67
-
68
76
  async start() {
77
+ invariant(!this._server);
78
+ log.info('starting...');
79
+ this._ctx = createContext();
80
+
81
+ // TODO(burdon): Move to hono.
69
82
  const app = express();
70
83
  app.use(express.json());
71
84
 
72
- app.post('/:name', async (req, res) => {
73
- const { name } = req.params;
85
+ app.post('/:path', async (req, res) => {
86
+ const { path } = req.params;
74
87
  try {
75
- log.info('calling', { name });
88
+ log.info('calling', { path });
76
89
  if (this._options.reload) {
77
- const { def } = this._handlers[name];
90
+ const { def } = this._handlers['/' + path];
78
91
  await this._load(def, true);
79
92
  }
80
93
 
81
- res.statusCode = await this._invoke(name, req.body);
94
+ // TODO(burdon): Get function context.
95
+ res.statusCode = await this.invoke('/' + path, req.body);
82
96
  res.end();
83
97
  } catch (err: any) {
84
98
  log.catch(err);
@@ -94,72 +108,110 @@ export class DevServer {
94
108
  // Register functions.
95
109
  const { registrationId, endpoint } = await this._client.services.services.FunctionRegistryService!.register({
96
110
  endpoint: this.endpoint,
97
- functions: this.functions.map(({ def: { name } }) => ({ name })),
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;
116
+
117
+ // Open after registration, so that it can be updated with the list of function definitions.
118
+ await this._functionsRegistry.open(this._ctx);
103
119
  } catch (err: any) {
104
120
  await this.stop();
105
121
  throw new Error('FunctionRegistryService not available (check plugin is configured).');
106
122
  }
123
+
124
+ log.info('started', { port: this._port });
107
125
  }
108
126
 
109
127
  async stop() {
128
+ invariant(this._server);
129
+ log.info('stopping...');
130
+
110
131
  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
- });
132
+ this._server.close(async () => {
133
+ log.info('server stopped');
134
+ try {
135
+ if (this._functionServiceRegistration) {
136
+ invariant(this._client.services.services.FunctionRegistryService);
137
+ await this._client.services.services.FunctionRegistryService.unregister({
138
+ registrationId: this._functionServiceRegistration,
139
+ });
140
+
141
+ log.info('unregistered', { registrationId: this._functionServiceRegistration });
142
+ this._functionServiceRegistration = undefined;
143
+ this._proxy = undefined;
144
+ }
116
145
 
117
- log.info('unregistered', { registrationId: this._registrationId });
118
- this._registrationId = undefined;
119
- this._proxy = undefined;
146
+ trigger.wake();
147
+ } catch (err) {
148
+ trigger.throw(err as Error);
120
149
  }
121
-
122
- trigger.wake();
123
150
  });
124
151
 
125
152
  await trigger.wait();
126
153
  this._port = undefined;
127
154
  this._server = undefined;
155
+ log.info('stopped');
128
156
  }
129
157
 
130
158
  /**
131
159
  * Load function.
132
160
  */
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 });
161
+ private async _load(def: FunctionDef, force = false) {
162
+ const { uri, route, handler } = def;
163
+ const filePath = join(this._options.baseDir, handler);
164
+ log.info('loading', { uri, force });
137
165
 
138
166
  // Remove from cache.
139
- if (flush) {
167
+ if (force) {
140
168
  Object.keys(require.cache)
141
- .filter((key) => key.startsWith(path))
142
- .forEach((key) => delete require.cache[key]);
169
+ .filter((key) => key.startsWith(filePath))
170
+ .forEach((key) => {
171
+ delete require.cache[key];
172
+ });
143
173
  }
144
174
 
175
+ // TODO(burdon): Import types.
145
176
  // eslint-disable-next-line @typescript-eslint/no-var-requires
146
- const module = require(path);
177
+ const module = require(filePath);
147
178
  if (typeof module.default !== 'function') {
148
- throw new Error(`Handler must export default function: ${id}`);
179
+ throw new Error(`Handler must export default function: ${uri}`);
149
180
  }
150
181
 
151
- this._handlers[name] = { def, handler: module.default };
182
+ this._handlers[route] = { def, handler: module.default };
183
+ }
184
+
185
+ private async _safeUpdateRegistration(): Promise<void> {
186
+ invariant(this._functionServiceRegistration);
187
+ try {
188
+ await this._client.services.services.FunctionRegistryService!.updateRegistration({
189
+ registrationId: this._functionServiceRegistration,
190
+ functions: this.functions.map(({ def: { id, route } }) => ({ id, route })),
191
+ });
192
+ } catch (e) {
193
+ log.catch(e);
194
+ }
152
195
  }
153
196
 
154
197
  /**
155
- * Invoke function handler.
198
+ * Invoke function.
156
199
  */
157
- private async _invoke(name: string, event: any) {
200
+ public async invoke(path: string, data: any): Promise<number> {
158
201
  const seq = ++this._seq;
159
202
  const now = Date.now();
160
203
 
161
- log.info('req', { seq, name });
162
- const { handler } = this._handlers[name];
204
+ log.info('req', { seq, path });
205
+ const statusCode = await this._invoke(path, { data });
206
+
207
+ log.info('res', { seq, path, statusCode, duration: Date.now() - now });
208
+ this.update.emit(statusCode);
209
+ return statusCode;
210
+ }
211
+
212
+ private async _invoke(path: string, event: FunctionEvent) {
213
+ const { handler } = this._handlers[path] ?? {};
214
+ invariant(handler, `invalid path: ${path}`);
163
215
 
164
216
  const context: FunctionContext = {
165
217
  client: this._client,
@@ -167,7 +219,7 @@ export class DevServer {
167
219
  };
168
220
 
169
221
  let statusCode = 200;
170
- const response: Response = {
222
+ const response: FunctionResponse = {
171
223
  status: (code: number) => {
172
224
  statusCode = code;
173
225
  return response;
@@ -175,8 +227,8 @@ export class DevServer {
175
227
  };
176
228
 
177
229
  await handler({ context, event, response });
178
- log.info('res', { seq, name, statusCode, duration: Date.now() - now });
179
-
180
230
  return statusCode;
181
231
  }
182
232
  }
233
+
234
+ const createContext = () => new Context({ name: 'DevServer' });
@@ -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, S, TypedObject } from '@dxos/echo-schema';
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 '../registry';
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
- const testBuilder = new TestBuilder();
22
- client = new Client({ services: testBuilder.createLocal() });
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 client.destroy();
29
+ await testBuilder.destroy();
28
30
  });
29
31
 
30
32
  test('timer', async () => {
31
33
  const manifest: FunctionManifest = {
32
34
  functions: [
33
35
  {
34
- id: 'example.com/function/test',
35
- name: 'test',
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
- timer: {
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 = new Scheduler(client, manifest, {
52
- callback: async () => {
53
- if (++count === 3) {
54
- done.wake();
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
- id: 'example.com/function/test',
73
- name: 'test',
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
- webhook: {
81
- port: 8080,
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 = new Scheduler(client, manifest, {
89
- callback: async () => {
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
- void fetch('http://localhost:8080');
101
- });
94
+ setTimeout(async () => triggerWebhook(space, manifest.functions![0].uri));
95
+
102
96
  await done.wait();
103
97
  });
104
98
 
105
- test.only('websocket', async () => {
99
+ test('websocket', async () => {
106
100
  const manifest: FunctionManifest = {
107
101
  functions: [
108
102
  {
109
- id: 'example.com/function/test',
110
- name: 'test',
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
- websocket: {
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 = new Scheduler(client, manifest, {
130
- callback: async (data) => {
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
- id: 'example.com/function/test',
165
- name: 'test',
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
- subscription: {
173
- spaceKey: client.spaces.default.key.toHex(),
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 = new Scheduler(client, manifest, {
187
- callback: async () => {
188
- if (++count === 2) {
189
- done.wake();
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
  });