@dxos/functions 0.5.3-main.3d65399 → 0.5.3-main.423e628

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 (97) hide show
  1. package/dist/lib/browser/chunk-P3HPDHNI.mjs +86 -0
  2. package/dist/lib/browser/chunk-P3HPDHNI.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +885 -304
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types.mjs +14 -0
  7. package/dist/lib/browser/types.mjs.map +7 -0
  8. package/dist/lib/node/chunk-KTLM3JNV.cjs +103 -0
  9. package/dist/lib/node/chunk-KTLM3JNV.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +871 -297
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/types.cjs +35 -0
  14. package/dist/lib/node/types.cjs.map +7 -0
  15. package/dist/types/src/browser/index.d.ts +2 -0
  16. package/dist/types/src/browser/index.d.ts.map +1 -0
  17. package/dist/types/src/function/function-registry.d.ts +24 -0
  18. package/dist/types/src/function/function-registry.d.ts.map +1 -0
  19. package/dist/types/src/function/function-registry.test.d.ts +2 -0
  20. package/dist/types/src/function/function-registry.test.d.ts.map +1 -0
  21. package/dist/types/src/function/index.d.ts +2 -0
  22. package/dist/types/src/function/index.d.ts.map +1 -0
  23. package/dist/types/src/handler.d.ts +33 -12
  24. package/dist/types/src/handler.d.ts.map +1 -1
  25. package/dist/types/src/index.d.ts +3 -1
  26. package/dist/types/src/index.d.ts.map +1 -1
  27. package/dist/types/src/runtime/dev-server.d.ts +15 -7
  28. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  29. package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
  30. package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
  31. package/dist/types/src/runtime/scheduler.d.ts +15 -15
  32. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  33. package/dist/types/src/testing/functions-integration.test.d.ts +2 -0
  34. package/dist/types/src/testing/functions-integration.test.d.ts.map +1 -0
  35. package/dist/types/src/testing/index.d.ts +4 -0
  36. package/dist/types/src/testing/index.d.ts.map +1 -0
  37. package/dist/types/src/testing/setup.d.ts +5 -0
  38. package/dist/types/src/testing/setup.d.ts.map +1 -0
  39. package/dist/types/src/testing/test/handler.d.ts +4 -0
  40. package/dist/types/src/testing/test/handler.d.ts.map +1 -0
  41. package/dist/types/src/testing/test/index.d.ts +3 -0
  42. package/dist/types/src/testing/test/index.d.ts.map +1 -0
  43. package/dist/types/src/testing/types.d.ts +9 -0
  44. package/dist/types/src/testing/types.d.ts.map +1 -0
  45. package/dist/types/src/testing/util.d.ts +3 -0
  46. package/dist/types/src/testing/util.d.ts.map +1 -0
  47. package/dist/types/src/trigger/index.d.ts +2 -0
  48. package/dist/types/src/trigger/index.d.ts.map +1 -0
  49. package/dist/types/src/trigger/trigger-registry.d.ts +40 -0
  50. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -0
  51. package/dist/types/src/trigger/trigger-registry.test.d.ts +2 -0
  52. package/dist/types/src/trigger/trigger-registry.test.d.ts.map +1 -0
  53. package/dist/types/src/trigger/type/index.d.ts +5 -0
  54. package/dist/types/src/trigger/type/index.d.ts.map +1 -0
  55. package/dist/types/src/trigger/type/subscription-trigger.d.ts +4 -0
  56. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -0
  57. package/dist/types/src/trigger/type/timer-trigger.d.ts +4 -0
  58. package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -0
  59. package/dist/types/src/trigger/type/webhook-trigger.d.ts +4 -0
  60. package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -0
  61. package/dist/types/src/trigger/type/websocket-trigger.d.ts +13 -0
  62. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -0
  63. package/dist/types/src/types.d.ts +203 -0
  64. package/dist/types/src/types.d.ts.map +1 -0
  65. package/dist/types/tools/schema.d.ts +2 -0
  66. package/dist/types/tools/schema.d.ts.map +1 -0
  67. package/package.json +38 -13
  68. package/schema/functions.json +206 -0
  69. package/src/browser/index.ts +5 -0
  70. package/src/function/function-registry.test.ts +105 -0
  71. package/src/function/function-registry.ts +90 -0
  72. package/src/function/index.ts +5 -0
  73. package/src/handler.ts +56 -26
  74. package/src/index.ts +3 -1
  75. package/src/runtime/dev-server.test.ts +60 -0
  76. package/src/runtime/dev-server.ts +104 -53
  77. package/src/runtime/scheduler.test.ts +154 -21
  78. package/src/runtime/scheduler.ts +87 -150
  79. package/src/testing/functions-integration.test.ts +99 -0
  80. package/src/testing/index.ts +7 -0
  81. package/src/testing/setup.ts +45 -0
  82. package/src/testing/test/handler.ts +15 -0
  83. package/src/testing/test/index.ts +7 -0
  84. package/src/testing/types.ts +9 -0
  85. package/src/testing/util.ts +16 -0
  86. package/src/trigger/index.ts +5 -0
  87. package/src/trigger/trigger-registry.test.ts +255 -0
  88. package/src/trigger/trigger-registry.ts +201 -0
  89. package/src/trigger/type/index.ts +8 -0
  90. package/src/trigger/type/subscription-trigger.ts +80 -0
  91. package/src/trigger/type/timer-trigger.ts +44 -0
  92. package/src/trigger/type/webhook-trigger.ts +47 -0
  93. package/src/trigger/type/websocket-trigger.ts +91 -0
  94. package/src/types.ts +112 -0
  95. package/dist/types/src/manifest.d.ts +0 -26
  96. package/dist/types/src/manifest.d.ts.map +0 -1
  97. package/src/manifest.ts +0 -42
@@ -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 '../manifest';
16
+ import { type FunctionRegistry } from '../function';
17
+ import { type FunctionContext, type FunctionEvent, type FunctionHandler, type FunctionResponse } from '../handler';
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,36 @@ 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
 
39
- // prettier-ignore
42
+ public readonly update = new Event<number>();
43
+
40
44
  constructor(
41
45
  private readonly _client: Client,
46
+ private readonly _functionsRegistry: FunctionRegistry,
42
47
  private readonly _options: DevServerOptions,
43
- ) {}
48
+ ) {
49
+ this._functionsRegistry.registered.on(async ({ added }) => {
50
+ added.forEach((def) => this._load(def));
51
+ await this._safeUpdateRegistration();
52
+ log('new functions loaded', { added });
53
+ });
54
+ }
55
+
56
+ get stats() {
57
+ return {
58
+ seq: this._seq,
59
+ };
60
+ }
44
61
 
45
62
  get endpoint() {
46
63
  invariant(this._port);
@@ -55,30 +72,26 @@ export class DevServer {
55
72
  return Object.values(this._handlers);
56
73
  }
57
74
 
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
75
  async start() {
76
+ invariant(!this._server);
77
+ log.info('starting...');
78
+ this._ctx = createContext();
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,110 @@ 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 })),
98
110
  });
99
111
 
100
- log.info('registered', { registrationId, endpoint });
101
- this._registrationId = registrationId;
112
+ log.info('registered', { endpoint });
102
113
  this._proxy = endpoint;
114
+ this._functionServiceRegistration = registrationId;
115
+
116
+ // Open after registration, so that it can be updated with the list of function definitions.
117
+ await this._functionsRegistry.open(this._ctx);
103
118
  } catch (err: any) {
104
119
  await this.stop();
105
120
  throw new Error('FunctionRegistryService not available (check plugin is configured).');
106
121
  }
122
+
123
+ log.info('started', { port: this._port });
107
124
  }
108
125
 
109
126
  async stop() {
127
+ invariant(this._server);
128
+ log.info('stopping...');
129
+
110
130
  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
- });
131
+ this._server.close(async () => {
132
+ log.info('server stopped');
133
+ try {
134
+ if (this._functionServiceRegistration) {
135
+ invariant(this._client.services.services.FunctionRegistryService);
136
+ await this._client.services.services.FunctionRegistryService.unregister({
137
+ registrationId: this._functionServiceRegistration,
138
+ });
139
+
140
+ log.info('unregistered', { registrationId: this._functionServiceRegistration });
141
+ this._functionServiceRegistration = undefined;
142
+ this._proxy = undefined;
143
+ }
116
144
 
117
- log.info('unregistered', { registrationId: this._registrationId });
118
- this._registrationId = undefined;
119
- this._proxy = undefined;
145
+ trigger.wake();
146
+ } catch (err) {
147
+ trigger.throw(err as Error);
120
148
  }
121
-
122
- trigger.wake();
123
149
  });
124
150
 
125
151
  await trigger.wait();
126
152
  this._port = undefined;
127
153
  this._server = undefined;
154
+ log.info('stopped');
128
155
  }
129
156
 
130
157
  /**
131
158
  * Load function.
132
159
  */
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 });
160
+ private async _load(def: FunctionDef, force?: boolean | undefined) {
161
+ const { uri, route, handler } = def;
162
+ const filePath = join(this._options.baseDir, handler);
163
+ log.info('loading', { uri, force });
137
164
 
138
165
  // Remove from cache.
139
- if (flush) {
166
+ if (force) {
140
167
  Object.keys(require.cache)
141
- .filter((key) => key.startsWith(path))
142
- .forEach((key) => delete require.cache[key]);
168
+ .filter((key) => key.startsWith(filePath))
169
+ .forEach((key) => {
170
+ delete require.cache[key];
171
+ });
143
172
  }
144
173
 
174
+ // TODO(burdon): Import types.
145
175
  // eslint-disable-next-line @typescript-eslint/no-var-requires
146
- const module = require(path);
176
+ const module = require(filePath);
147
177
  if (typeof module.default !== 'function') {
148
- throw new Error(`Handler must export default function: ${id}`);
178
+ throw new Error(`Handler must export default function: ${uri}`);
149
179
  }
150
180
 
151
- this._handlers[name] = { def, handler: module.default };
181
+ this._handlers[route] = { def, handler: module.default };
182
+ }
183
+
184
+ private async _safeUpdateRegistration(): Promise<void> {
185
+ invariant(this._functionServiceRegistration);
186
+ try {
187
+ await this._client.services.services.FunctionRegistryService!.updateRegistration({
188
+ registrationId: this._functionServiceRegistration,
189
+ functions: this.functions.map(({ def: { id, route } }) => ({ id, route })),
190
+ });
191
+ } catch (e) {
192
+ log.catch(e);
193
+ }
152
194
  }
153
195
 
154
196
  /**
155
- * Invoke function handler.
197
+ * Invoke function.
156
198
  */
157
- private async _invoke(name: string, event: any) {
199
+ public async invoke(path: string, data: any): Promise<number> {
158
200
  const seq = ++this._seq;
159
201
  const now = Date.now();
160
202
 
161
- log.info('req', { seq, name });
162
- const { handler } = this._handlers[name];
203
+ log.info('req', { seq, path });
204
+ const statusCode = await this._invoke(path, { data });
205
+
206
+ log.info('res', { seq, path, statusCode, duration: Date.now() - now });
207
+ this.update.emit(statusCode);
208
+ return statusCode;
209
+ }
210
+
211
+ private async _invoke(path: string, event: FunctionEvent) {
212
+ const { handler } = this._handlers[path] ?? {};
213
+ invariant(handler, `invalid path: ${path}`);
163
214
 
164
215
  const context: FunctionContext = {
165
216
  client: this._client,
@@ -167,7 +218,7 @@ export class DevServer {
167
218
  };
168
219
 
169
220
  let statusCode = 200;
170
- const response: Response = {
221
+ const response: FunctionResponse = {
171
222
  status: (code: number) => {
172
223
  statusCode = code;
173
224
  return response;
@@ -175,8 +226,8 @@ export class DevServer {
175
226
  };
176
227
 
177
228
  await handler({ context, event, response });
178
- log.info('res', { seq, name, statusCode, duration: Date.now() - now });
179
-
180
229
  return statusCode;
181
230
  }
182
231
  }
232
+
233
+ const createContext = () => new Context({ name: 'DevServer' });
@@ -3,56 +3,189 @@
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
- import { Client } from '@dxos/client';
9
+ import { type Client } from '@dxos/client';
9
10
  import { TestBuilder } from '@dxos/client/testing';
11
+ import { create } from '@dxos/echo-schema';
10
12
  import { describe, test } from '@dxos/test';
11
13
 
12
- import { Scheduler } from './scheduler';
13
- import { type FunctionManifest } from '../manifest';
14
+ import { Scheduler, type SchedulerOptions } from './scheduler';
15
+ import { FunctionRegistry } from '../function';
16
+ import { createInitializedClients, TestType, triggerWebhook } from '../testing';
17
+ import { TriggerRegistry } from '../trigger';
18
+ import { type FunctionManifest } from '../types';
14
19
 
20
+ // TODO(burdon): Test we can add and remove triggers.
15
21
  describe('scheduler', () => {
16
- test.only('callback', async () => {
17
- const testBuilder = new TestBuilder();
18
- const client = new Client({ services: testBuilder.createLocal() });
19
- await client.initialize();
20
- await client.halo.createIdentity();
22
+ let testBuilder: TestBuilder;
23
+ let client: Client;
24
+ before(async () => {
25
+ testBuilder = new TestBuilder();
26
+ client = (await createInitializedClients(testBuilder, 1))[0];
27
+ });
28
+ after(async () => {
29
+ await testBuilder.destroy();
30
+ });
21
31
 
32
+ test('timer', async () => {
22
33
  const manifest: FunctionManifest = {
23
34
  functions: [
24
35
  {
25
- id: 'example.com/function/test',
26
- name: 'test',
36
+ uri: 'example.com/function/test',
37
+ route: '/test',
27
38
  handler: 'test',
28
39
  },
29
40
  ],
30
41
  triggers: [
31
42
  {
32
43
  function: 'example.com/function/test',
33
- schedule: '0/1 * * * * *', // Every 1s.
44
+ spec: {
45
+ type: 'timer',
46
+ cron: '0/1 * * * * *', // Every 1s.
47
+ },
34
48
  },
35
49
  ],
36
50
  };
37
51
 
38
52
  let count = 0;
39
53
  const done = new Trigger();
40
- const scheduler = new Scheduler(client, manifest, {
41
- callback: async () => {
42
- if (++count === 3) {
43
- done.wake();
44
- }
54
+ const scheduler = createScheduler(async () => {
55
+ if (++count === 3) {
56
+ done.wake();
57
+ }
58
+ });
59
+ await scheduler.register(client.spaces.default, manifest);
60
+ await scheduler.start();
61
+
62
+ await done.wait({ timeout: 5_000 });
63
+ expect(count).to.equal(3);
64
+ });
65
+
66
+ test('webhook', async () => {
67
+ const manifest: FunctionManifest = {
68
+ functions: [
69
+ {
70
+ uri: 'example.com/function/test',
71
+ route: '/test',
72
+ handler: 'test',
73
+ },
74
+ ],
75
+ triggers: [
76
+ {
77
+ function: 'example.com/function/test',
78
+ spec: {
79
+ type: 'webhook',
80
+ method: 'GET',
81
+ },
82
+ },
83
+ ],
84
+ };
45
85
 
46
- return 200;
47
- },
86
+ const done = new Trigger();
87
+ const scheduler = createScheduler(async () => {
88
+ done.wake();
48
89
  });
90
+ const space = await client.spaces.create();
91
+ await scheduler.register(space, manifest);
92
+ await scheduler.start();
93
+
94
+ setTimeout(async () => triggerWebhook(space, manifest.functions![0].uri));
95
+
96
+ await done.wait();
97
+ });
98
+
99
+ test('websocket', async () => {
100
+ const manifest: FunctionManifest = {
101
+ functions: [
102
+ {
103
+ uri: 'example.com/function/test',
104
+ route: '/test',
105
+ handler: 'test',
106
+ },
107
+ ],
108
+ triggers: [
109
+ {
110
+ function: 'example.com/function/test',
111
+ spec: {
112
+ type: 'websocket',
113
+ // url: 'https://hub.dxos.network/api/mailbox/test',
114
+ url: 'http://localhost:8081',
115
+ init: {
116
+ type: 'sync',
117
+ },
118
+ },
119
+ },
120
+ ],
121
+ };
49
122
 
123
+ const done = new Trigger();
124
+ const scheduler = createScheduler(async () => {
125
+ done.wake();
126
+ });
127
+ await scheduler.register(client.spaces.default, manifest);
50
128
  await scheduler.start();
51
129
 
130
+ // Test server.
131
+ setTimeout(() => {
132
+ const wss = new WebSocket.Server({ port: 8081 });
133
+ wss.on('connection', (ws: WebSocket) => {
134
+ ws.on('message', (data) => {
135
+ const info = JSON.parse(new TextDecoder().decode(data as ArrayBuffer));
136
+ expect(info.type).to.equal('sync');
137
+ done.wake();
138
+ });
139
+ });
140
+ }, 500);
141
+
52
142
  await done.wait();
53
- expect(count).to.equal(3);
143
+ });
144
+
145
+ test('subscription', async () => {
146
+ const manifest: FunctionManifest = {
147
+ functions: [
148
+ {
149
+ uri: 'example.com/function/test',
150
+ route: '/test',
151
+ handler: 'test',
152
+ },
153
+ ],
154
+ triggers: [
155
+ {
156
+ function: 'example.com/function/test',
157
+ spec: {
158
+ type: 'subscription',
159
+ filter: [{ type: TestType.typename }],
160
+ },
161
+ },
162
+ ],
163
+ };
54
164
 
55
- await scheduler.stop();
56
- await client.destroy();
165
+ let count = 0;
166
+ const done = new Trigger();
167
+ const scheduler = createScheduler(async () => {
168
+ if (++count === 2) {
169
+ done.wake();
170
+ }
171
+ });
172
+ await scheduler.register(client.spaces.default, manifest);
173
+ await scheduler.start();
174
+
175
+ setTimeout(() => {
176
+ const space = client.spaces.default;
177
+ const object = create(TestType, { title: 'Hello world!' });
178
+ space.db.add(object);
179
+ }, 100);
180
+
181
+ await done.wait();
57
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
+ };
58
191
  });