@dxos/functions 0.5.3-main.6f2dfea → 0.5.3-main.79e0565

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 (82) hide show
  1. package/dist/lib/browser/index.mjs +764 -476
  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 +745 -471
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/types/src/function/function-registry.d.ts +24 -0
  8. package/dist/types/src/function/function-registry.d.ts.map +1 -0
  9. package/dist/types/src/function/function-registry.test.d.ts +2 -0
  10. package/dist/types/src/function/function-registry.test.d.ts.map +1 -0
  11. package/dist/types/src/function/index.d.ts +2 -0
  12. package/dist/types/src/function/index.d.ts.map +1 -0
  13. package/dist/types/src/handler.d.ts +32 -12
  14. package/dist/types/src/handler.d.ts.map +1 -1
  15. package/dist/types/src/index.d.ts +2 -0
  16. package/dist/types/src/index.d.ts.map +1 -1
  17. package/dist/types/src/runtime/dev-server.d.ts +7 -10
  18. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  19. package/dist/types/src/runtime/scheduler.d.ts +11 -59
  20. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  21. package/dist/types/src/testing/functions-integration.test.d.ts +2 -0
  22. package/dist/types/src/testing/functions-integration.test.d.ts.map +1 -0
  23. package/dist/types/src/testing/index.d.ts +4 -0
  24. package/dist/types/src/testing/index.d.ts.map +1 -0
  25. package/dist/types/src/testing/setup.d.ts +5 -0
  26. package/dist/types/src/testing/setup.d.ts.map +1 -0
  27. package/dist/types/src/testing/test/handler.d.ts +1 -0
  28. package/dist/types/src/testing/test/handler.d.ts.map +1 -1
  29. package/dist/types/src/testing/types.d.ts +9 -0
  30. package/dist/types/src/testing/types.d.ts.map +1 -0
  31. package/dist/types/src/testing/util.d.ts +3 -0
  32. package/dist/types/src/testing/util.d.ts.map +1 -0
  33. package/dist/types/src/trigger/index.d.ts +2 -0
  34. package/dist/types/src/trigger/index.d.ts.map +1 -0
  35. package/dist/types/src/trigger/trigger-registry.d.ts +40 -0
  36. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -0
  37. package/dist/types/src/trigger/trigger-registry.test.d.ts +2 -0
  38. package/dist/types/src/trigger/trigger-registry.test.d.ts.map +1 -0
  39. package/dist/types/src/trigger/type/index.d.ts +5 -0
  40. package/dist/types/src/trigger/type/index.d.ts.map +1 -0
  41. package/dist/types/src/trigger/type/subscription-trigger.d.ts +4 -0
  42. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -0
  43. package/dist/types/src/trigger/type/timer-trigger.d.ts +4 -0
  44. package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -0
  45. package/dist/types/src/trigger/type/webhook-trigger.d.ts +4 -0
  46. package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -0
  47. package/dist/types/src/trigger/type/websocket-trigger.d.ts +13 -0
  48. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -0
  49. package/dist/types/src/types.d.ts +131 -111
  50. package/dist/types/src/types.d.ts.map +1 -1
  51. package/dist/types/src/util.d.ts +15 -0
  52. package/dist/types/src/util.d.ts.map +1 -0
  53. package/dist/types/src/util.test.d.ts +2 -0
  54. package/dist/types/src/util.test.d.ts.map +1 -0
  55. package/package.json +14 -12
  56. package/schema/functions.json +140 -112
  57. package/src/function/function-registry.test.ts +105 -0
  58. package/src/function/function-registry.ts +90 -0
  59. package/src/function/index.ts +5 -0
  60. package/src/handler.ts +50 -27
  61. package/src/index.ts +2 -0
  62. package/src/runtime/dev-server.test.ts +15 -35
  63. package/src/runtime/dev-server.ts +40 -23
  64. package/src/runtime/scheduler.test.ts +54 -75
  65. package/src/runtime/scheduler.ts +75 -300
  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 +8 -2
  70. package/src/testing/types.ts +9 -0
  71. package/src/testing/util.ts +16 -0
  72. package/src/trigger/index.ts +5 -0
  73. package/src/trigger/trigger-registry.test.ts +255 -0
  74. package/src/trigger/trigger-registry.ts +189 -0
  75. package/src/trigger/type/index.ts +8 -0
  76. package/src/trigger/type/subscription-trigger.ts +80 -0
  77. package/src/trigger/type/timer-trigger.ts +44 -0
  78. package/src/trigger/type/webhook-trigger.ts +47 -0
  79. package/src/trigger/type/websocket-trigger.ts +91 -0
  80. package/src/types.ts +58 -40
  81. package/src/util.test.ts +43 -0
  82. package/src/util.ts +48 -0
@@ -5,12 +5,14 @@
5
5
  import { expect } from 'chai';
6
6
  import path from 'path';
7
7
 
8
- import { FunctionsPlugin } from '@dxos/agent';
9
- import { Client, Config } from '@dxos/client';
8
+ import { waitForCondition } from '@dxos/async';
9
+ import { type Client } from '@dxos/client';
10
10
  import { TestBuilder } from '@dxos/client/testing';
11
- import { describe, openAndClose, test } from '@dxos/test';
11
+ import { describe, test } from '@dxos/test';
12
12
 
13
13
  import { DevServer } from './dev-server';
14
+ import { FunctionRegistry } from '../function';
15
+ import { createFunctionRuntime } from '../testing';
14
16
  import { type FunctionManifest } from '../types';
15
17
 
16
18
  describe('dev server', () => {
@@ -18,35 +20,10 @@ describe('dev server', () => {
18
20
  let testBuilder: TestBuilder;
19
21
  before(async () => {
20
22
  testBuilder = new TestBuilder();
21
- const config = new Config({
22
- runtime: {
23
- agent: {
24
- plugins: [
25
- {
26
- id: 'dxos.org/agent/plugin/functions',
27
- config: {
28
- port: 8080,
29
- },
30
- },
31
- ],
32
- },
33
- },
34
- });
35
-
36
- const services = testBuilder.createLocalClientServices();
37
- client = new Client({ config, services });
38
-
39
- await client.initialize();
40
- await client.halo.createIdentity();
41
- testBuilder.ctx.onDispose(() => client.destroy());
42
-
43
- // TODO(burdon): Better way to configure plugin? (Rationalize chess.test).
44
- const functionsPlugin = new FunctionsPlugin();
45
- await functionsPlugin.initialize({ client, clientServices: services });
46
- await openAndClose(functionsPlugin);
47
-
23
+ client = await createFunctionRuntime(testBuilder);
48
24
  expect(client.services.services.FunctionRegistryService).to.exist;
49
25
  });
26
+
50
27
  after(async () => {
51
28
  await testBuilder.destroy();
52
29
  });
@@ -55,18 +32,19 @@ describe('dev server', () => {
55
32
  const manifest: FunctionManifest = {
56
33
  functions: [
57
34
  {
58
- id: 'example.com/function/test',
59
- path: 'test',
35
+ uri: 'example.com/function/test',
36
+ route: 'test',
60
37
  handler: 'test',
61
38
  },
62
39
  ],
63
40
  };
64
41
 
65
- const server = new DevServer(client, {
66
- manifest,
42
+ const registry = new FunctionRegistry(client);
43
+ const server = new DevServer(client, registry, {
67
44
  baseDir: path.join(__dirname, '../testing'),
68
45
  });
69
- await server.initialize();
46
+ const space = await client.spaces.create();
47
+ await registry.register(space, manifest.functions);
70
48
  await server.start();
71
49
 
72
50
  // TODO(burdon): Doesn't shut down cleanly.
@@ -74,6 +52,8 @@ describe('dev server', () => {
74
52
  testBuilder.ctx.onDispose(() => server.stop());
75
53
  expect(server).to.exist;
76
54
 
55
+ await waitForCondition({ condition: () => server.functions.length > 0 });
56
+
77
57
  await server.invoke('test', {});
78
58
  expect(server.stats.seq).to.eq(1);
79
59
  });
@@ -9,14 +9,15 @@ import { join } from 'node:path';
9
9
 
10
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 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 = {
19
- manifest: FunctionManifest;
20
21
  baseDir: string;
21
22
  port?: number;
22
23
  reload?: boolean;
@@ -27,6 +28,8 @@ 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
 
@@ -38,11 +41,17 @@ export class DevServer {
38
41
 
39
42
  public readonly update = new Event<number>();
40
43
 
41
- // prettier-ignore
42
44
  constructor(
43
45
  private readonly _client: Client,
46
+ private readonly _functionsRegistry: FunctionRegistry,
44
47
  private readonly _options: DevServerOptions,
45
- ) {}
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
+ }
46
55
 
47
56
  get stats() {
48
57
  return {
@@ -63,19 +72,10 @@ export class DevServer {
63
72
  return Object.values(this._handlers);
64
73
  }
65
74
 
66
- async initialize() {
67
- for (const def of this._options.manifest.functions) {
68
- try {
69
- await this._load(def);
70
- } catch (err) {
71
- log.error('parsing function (check manifest)', err);
72
- }
73
- }
74
- }
75
-
76
75
  async start() {
77
76
  invariant(!this._server);
78
77
  log.info('starting...');
78
+ this._ctx = createContext();
79
79
 
80
80
  // TODO(burdon): Move to hono.
81
81
  const app = express();
@@ -107,12 +107,14 @@ export class DevServer {
107
107
  // Register functions.
108
108
  const { registrationId, endpoint } = await this._client.services.services.FunctionRegistryService!.register({
109
109
  endpoint: this.endpoint,
110
- functions: this.functions.map(({ def: { id, path } }) => ({ id, path })),
111
110
  });
112
111
 
113
112
  log.info('registered', { endpoint });
114
113
  this._proxy = endpoint;
115
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);
116
118
  } catch (err: any) {
117
119
  await this.stop();
118
120
  throw new Error('FunctionRegistryService not available (check plugin is configured).');
@@ -156,9 +158,9 @@ export class DevServer {
156
158
  * Load function.
157
159
  */
158
160
  private async _load(def: FunctionDef, force = false) {
159
- const { id, path, handler } = def;
161
+ const { uri, route, handler } = def;
160
162
  const filePath = join(this._options.baseDir, handler);
161
- log.info('loading', { id, force });
163
+ log.info('loading', { uri, force });
162
164
 
163
165
  // Remove from cache.
164
166
  if (force) {
@@ -169,13 +171,26 @@ export class DevServer {
169
171
  });
170
172
  }
171
173
 
174
+ // TODO(burdon): Import types.
172
175
  // eslint-disable-next-line @typescript-eslint/no-var-requires
173
176
  const module = require(filePath);
174
177
  if (typeof module.default !== 'function') {
175
- throw new Error(`Handler must export default function: ${id}`);
178
+ throw new Error(`Handler must export default function: ${uri}`);
176
179
  }
177
180
 
178
- this._handlers[path] = { 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
+ }
179
194
  }
180
195
 
181
196
  /**
@@ -186,14 +201,14 @@ export class DevServer {
186
201
  const now = Date.now();
187
202
 
188
203
  log.info('req', { seq, path });
189
- const statusCode = await this._invoke(path, data);
204
+ const statusCode = await this._invoke(path, { data });
190
205
 
191
206
  log.info('res', { seq, path, statusCode, duration: Date.now() - now });
192
207
  this.update.emit(statusCode);
193
208
  return statusCode;
194
209
  }
195
210
 
196
- private async _invoke(path: string, event: any) {
211
+ private async _invoke(path: string, event: FunctionEvent) {
197
212
  const { handler } = this._handlers[path] ?? {};
198
213
  invariant(handler, `invalid path: ${path}`);
199
214
 
@@ -203,7 +218,7 @@ export class DevServer {
203
218
  };
204
219
 
205
220
  let statusCode = 200;
206
- const response: Response = {
221
+ const response: FunctionResponse = {
207
222
  status: (code: number) => {
208
223
  statusCode = code;
209
224
  return response;
@@ -214,3 +229,5 @@ export class DevServer {
214
229
  return statusCode;
215
230
  }
216
231
  }
232
+
233
+ 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';
15
- import { type FunctionManifest, type WebhookTrigger } from '../types';
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';
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.createLocalClientServices() });
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
- path: '/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,15 +67,16 @@ describe('scheduler', () => {
69
67
  const manifest: FunctionManifest = {
70
68
  functions: [
71
69
  {
72
- id: 'example.com/function/test',
73
- path: '/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: {
78
+ spec: {
79
+ type: 'webhook',
81
80
  method: 'GET',
82
81
  },
83
82
  },
@@ -85,23 +84,14 @@ describe('scheduler', () => {
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
- const mount: WebhookTrigger = scheduler.mounts.find(
101
- (mount) => mount.function === 'example.com/function/test',
102
- )!.webhook!;
103
- void fetch(`http://localhost:${mount.port}`);
104
- });
94
+ setTimeout(async () => triggerWebhook(space, manifest.functions![0].uri));
105
95
 
106
96
  await done.wait();
107
97
  });
@@ -110,15 +100,16 @@ describe('scheduler', () => {
110
100
  const manifest: FunctionManifest = {
111
101
  functions: [
112
102
  {
113
- id: 'example.com/function/test',
114
- path: '/test',
103
+ uri: 'example.com/function/test',
104
+ route: '/test',
115
105
  handler: 'test',
116
106
  },
117
107
  ],
118
108
  triggers: [
119
109
  {
120
110
  function: 'example.com/function/test',
121
- websocket: {
111
+ spec: {
112
+ type: 'websocket',
122
113
  // url: 'https://hub.dxos.network/api/mailbox/test',
123
114
  url: 'http://localhost:8081',
124
115
  init: {
@@ -130,16 +121,11 @@ describe('scheduler', () => {
130
121
  };
131
122
 
132
123
  const done = new Trigger();
133
- const scheduler = new Scheduler(client, manifest, {
134
- callback: async (data) => {
135
- done.wake();
136
- },
124
+ const scheduler = createScheduler(async () => {
125
+ done.wake();
137
126
  });
138
-
127
+ await scheduler.register(client.spaces.default, manifest);
139
128
  await scheduler.start();
140
- after(async () => {
141
- await scheduler.stop();
142
- });
143
129
 
144
130
  // Test server.
145
131
  setTimeout(() => {
@@ -157,29 +143,20 @@ describe('scheduler', () => {
157
143
  });
158
144
 
159
145
  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
146
  const manifest: FunctionManifest = {
166
147
  functions: [
167
148
  {
168
- id: 'example.com/function/test',
169
- path: '/test',
149
+ uri: 'example.com/function/test',
150
+ route: '/test',
170
151
  handler: 'test',
171
152
  },
172
153
  ],
173
154
  triggers: [
174
155
  {
175
156
  function: 'example.com/function/test',
176
- subscription: {
177
- spaceKey: client.spaces.default.key.toHex(),
178
- filter: [
179
- {
180
- type: TestType.typename,
181
- },
182
- ],
157
+ spec: {
158
+ type: 'subscription',
159
+ filter: [{ type: TestType.typename }],
183
160
  },
184
161
  },
185
162
  ],
@@ -187,20 +164,14 @@ describe('scheduler', () => {
187
164
 
188
165
  let count = 0;
189
166
  const done = new Trigger();
190
- const scheduler = new Scheduler(client, manifest, {
191
- callback: async () => {
192
- if (++count === 2) {
193
- done.wake();
194
- }
195
- },
167
+ const scheduler = createScheduler(async () => {
168
+ if (++count === 2) {
169
+ done.wake();
170
+ }
196
171
  });
197
-
172
+ await scheduler.register(client.spaces.default, manifest);
198
173
  await scheduler.start();
199
- after(async () => {
200
- await scheduler.stop();
201
- });
202
174
 
203
- // TODO(burdon): Query for Expando?
204
175
  setTimeout(() => {
205
176
  const space = client.spaces.default;
206
177
  const object = create(TestType, { title: 'Hello world!' });
@@ -209,4 +180,12 @@ describe('scheduler', () => {
209
180
 
210
181
  await done.wait();
211
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
+ };
212
191
  });