@dxos/functions 0.5.4 → 0.5.5-main.23d7ea6

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 (41) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/browser/chunk-ERL6PHMU.mjs +1084 -0
  3. package/dist/lib/browser/chunk-ERL6PHMU.mjs.map +7 -0
  4. package/dist/lib/browser/index.mjs +17 -1075
  5. package/dist/lib/browser/index.mjs.map +4 -4
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/testing/index.mjs +150 -0
  8. package/dist/lib/browser/testing/index.mjs.map +7 -0
  9. package/dist/lib/node/chunk-INM6XAL7.cjs +1097 -0
  10. package/dist/lib/node/chunk-INM6XAL7.cjs.map +7 -0
  11. package/dist/lib/node/index.cjs +15 -1069
  12. package/dist/lib/node/index.cjs.map +4 -4
  13. package/dist/lib/node/meta.json +1 -1
  14. package/dist/lib/node/testing/index.cjs +171 -0
  15. package/dist/lib/node/testing/index.cjs.map +7 -0
  16. package/dist/types/src/function/function-registry.d.ts +1 -0
  17. package/dist/types/src/function/function-registry.d.ts.map +1 -1
  18. package/dist/types/src/runtime/dev-server.d.ts +1 -0
  19. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  20. package/dist/types/src/testing/index.d.ts +1 -0
  21. package/dist/types/src/testing/index.d.ts.map +1 -1
  22. package/dist/types/src/testing/manifest.d.ts +3 -0
  23. package/dist/types/src/testing/manifest.d.ts.map +1 -0
  24. package/dist/types/src/testing/plugin-init.d.ts +6 -0
  25. package/dist/types/src/testing/plugin-init.d.ts.map +1 -0
  26. package/dist/types/src/testing/setup.d.ts +11 -1
  27. package/dist/types/src/testing/setup.d.ts.map +1 -1
  28. package/dist/types/src/testing/util.d.ts +2 -0
  29. package/dist/types/src/testing/util.d.ts.map +1 -1
  30. package/package.json +23 -14
  31. package/src/function/function-registry.test.ts +14 -1
  32. package/src/function/function-registry.ts +10 -0
  33. package/src/runtime/dev-server.test.ts +42 -24
  34. package/src/runtime/dev-server.ts +17 -13
  35. package/src/runtime/scheduler.test.ts +4 -2
  36. package/src/testing/functions-integration.test.ts +8 -45
  37. package/src/testing/index.ts +1 -0
  38. package/src/testing/manifest.ts +15 -0
  39. package/src/testing/plugin-init.ts +20 -0
  40. package/src/testing/setup.ts +64 -6
  41. package/src/testing/util.ts +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/functions",
3
- "version": "0.5.4",
3
+ "version": "0.5.5-main.23d7ea6",
4
4
  "description": "Functions API and runtime.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -18,6 +18,12 @@
18
18
  "node": "./dist/lib/node/types.cjs",
19
19
  "default": "./dist/lib/node/types.cjs",
20
20
  "types": "./dist/types/src/types.d.ts"
21
+ },
22
+ "./testing": {
23
+ "import": "./dist/lib/browser/testing/index.mjs",
24
+ "require": "./dist/lib/node/testing/index.cjs",
25
+ "node": "./dist/lib/node/testing/index.cjs",
26
+ "types": "./dist/types/src/testing/index.d.ts"
21
27
  }
22
28
  },
23
29
  "types": "dist/types/src/index.d.ts",
@@ -25,6 +31,9 @@
25
31
  "*": {
26
32
  "types": [
27
33
  "dist/types/src/types.d.ts"
34
+ ],
35
+ "testing": [
36
+ "dist/types/src/testing/index.d.ts"
28
37
  ]
29
38
  }
30
39
  },
@@ -40,23 +49,23 @@
40
49
  "express": "^4.19.2",
41
50
  "get-port-please": "^3.1.1",
42
51
  "ws": "^8.14.2",
43
- "@braneframe/types": "0.5.4",
44
- "@dxos/async": "0.5.4",
45
- "@dxos/client": "0.5.4",
46
- "@dxos/echo-db": "0.5.4",
47
- "@dxos/echo-schema": "0.5.4",
48
- "@dxos/keys": "0.5.4",
49
- "@dxos/context": "0.5.4",
50
- "@dxos/invariant": "0.5.4",
51
- "@dxos/log": "0.5.4",
52
- "@dxos/node-std": "0.5.4",
53
- "@dxos/protocols": "0.5.4",
54
- "@dxos/util": "0.5.4"
52
+ "@braneframe/types": "0.5.5-main.23d7ea6",
53
+ "@dxos/client": "0.5.5-main.23d7ea6",
54
+ "@dxos/async": "0.5.5-main.23d7ea6",
55
+ "@dxos/echo-schema": "0.5.5-main.23d7ea6",
56
+ "@dxos/context": "0.5.5-main.23d7ea6",
57
+ "@dxos/echo-db": "0.5.5-main.23d7ea6",
58
+ "@dxos/invariant": "0.5.5-main.23d7ea6",
59
+ "@dxos/keys": "0.5.5-main.23d7ea6",
60
+ "@dxos/log": "0.5.5-main.23d7ea6",
61
+ "@dxos/protocols": "0.5.5-main.23d7ea6",
62
+ "@dxos/node-std": "0.5.5-main.23d7ea6",
63
+ "@dxos/util": "0.5.5-main.23d7ea6"
55
64
  },
56
65
  "devDependencies": {
57
66
  "@types/express": "^4.17.17",
58
67
  "@types/ws": "^7.4.0",
59
- "@dxos/agent": "0.5.4"
68
+ "@dxos/agent": "0.5.5-main.23d7ea6"
60
69
  },
61
70
  "publishConfig": {
62
71
  "access": "public"
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { expect } from 'chai';
6
6
 
7
- import { Trigger } from '@dxos/async';
7
+ import { sleep, Trigger } from '@dxos/async';
8
8
  import { type Client } from '@dxos/client';
9
9
  import { TestBuilder } from '@dxos/client/testing';
10
10
  import { Context } from '@dxos/context';
@@ -39,6 +39,19 @@ describe('function registry', () => {
39
39
  await testBuilder.destroy();
40
40
  });
41
41
 
42
+ test('getUniqueByUri', async () => {
43
+ const client = (await createInitializedClients(testBuilder))[0];
44
+ const registry = createRegistry(client);
45
+ await registry.open();
46
+ for (let i = 0; i < 2; i++) {
47
+ const space = await client.spaces.create();
48
+ await registry.register(space, testManifest.functions);
49
+ }
50
+ await sleep(10);
51
+ const definitions = registry.getUniqueByUri();
52
+ expect(definitions.length).to.eq(testManifest.functions?.length);
53
+ });
54
+
42
55
  describe('register', () => {
43
56
  test('creates new functions', async () => {
44
57
  const client = (await createInitializedClients(testBuilder))[0];
@@ -30,6 +30,16 @@ export class FunctionRegistry extends Resource {
30
30
  return this._functionBySpaceKey.get(space.key) ?? [];
31
31
  }
32
32
 
33
+ public getUniqueByUri(): FunctionDef[] {
34
+ const uniqueByUri = [...this._functionBySpaceKey.values()]
35
+ .flatMap((defs) => defs)
36
+ .reduce((acc, v) => {
37
+ acc.set(v.uri, v);
38
+ return acc;
39
+ }, new Map<string, FunctionDef>());
40
+ return [...uniqueByUri.values()];
41
+ }
42
+
33
43
  /**
34
44
  * Loads function definitions from the manifest into the space.
35
45
  * We first load all the definitions from the space to deduplicate by functionId.
@@ -3,24 +3,25 @@
3
3
  //
4
4
 
5
5
  import { expect } from 'chai';
6
+ import { getRandomPort } from 'get-port-please';
6
7
  import path from 'path';
7
8
 
8
- import { waitForCondition } from '@dxos/async';
9
+ import { sleep, waitForCondition } from '@dxos/async';
9
10
  import { type Client } from '@dxos/client';
10
11
  import { TestBuilder } from '@dxos/client/testing';
11
12
  import { describe, test } from '@dxos/test';
12
13
 
13
14
  import { DevServer } from './dev-server';
14
15
  import { FunctionRegistry } from '../function';
15
- import { createFunctionRuntime } from '../testing';
16
- import { type FunctionManifest } from '../types';
16
+ import { createFunctionRuntime, testFunctionManifest } from '../testing';
17
+ import { initFunctionsPlugin } from '../testing/plugin-init';
17
18
 
18
19
  describe('dev server', () => {
19
20
  let client: Client;
20
21
  let testBuilder: TestBuilder;
21
22
  before(async () => {
22
23
  testBuilder = new TestBuilder();
23
- client = await createFunctionRuntime(testBuilder);
24
+ client = await createFunctionRuntime(testBuilder, initFunctionsPlugin);
24
25
  expect(client.services.services.FunctionRegistryService).to.exist;
25
26
  });
26
27
 
@@ -28,33 +29,50 @@ describe('dev server', () => {
28
29
  await testBuilder.destroy();
29
30
  });
30
31
 
31
- test('start/stop', async () => {
32
- const manifest: FunctionManifest = {
33
- functions: [
34
- {
35
- uri: 'example.com/function/test',
36
- route: 'test',
37
- handler: 'test',
38
- },
39
- ],
40
- };
32
+ test('function registry open after dev server started', async () => {
33
+ const { registry, server, space } = await setupTest();
34
+ await registry.register(space, testFunctionManifest.functions);
35
+ await server.start();
36
+ await registry.open();
37
+ await waitForCondition({ condition: () => server.functions.length > 0 });
38
+ await expectTestFunctionInvocable(server);
39
+ });
40
+
41
+ test('function registry open before dev server started', async () => {
42
+ const { registry, server, space } = await setupTest();
43
+ await registry.register(space, testFunctionManifest.functions);
44
+ await registry.open();
45
+ await server.start();
46
+ await waitForCondition({ condition: () => server.functions.length > 0 });
47
+ await expectTestFunctionInvocable(server);
48
+ });
49
+
50
+ test('unsubscribes from functions after stopped', async () => {
51
+ const { registry, server, space } = await setupTest();
52
+ await registry.register(space, testFunctionManifest.functions);
53
+ await server.start();
54
+ await server.stop();
55
+ await registry.open();
56
+ await sleep(20);
57
+ expect(server.functions.length).to.eq(0);
58
+ });
41
59
 
60
+ const expectTestFunctionInvocable = async (server: DevServer) => {
61
+ const seq = server.stats.seq;
62
+ await server.invoke('test', {});
63
+ expect(server.stats.seq).to.eq(seq + 1);
64
+ };
65
+
66
+ const setupTest = async () => {
42
67
  const registry = new FunctionRegistry(client);
43
68
  const server = new DevServer(client, registry, {
44
69
  baseDir: path.join(__dirname, '../testing'),
70
+ port: await getRandomPort('127.0.0.1'),
45
71
  });
46
72
  const space = await client.spaces.create();
47
- await registry.register(space, manifest.functions);
48
- await server.start();
49
-
50
73
  // TODO(burdon): Doesn't shut down cleanly.
51
74
  // Error: invariant violation [this._client.services.services.FunctionRegistryService]
52
75
  testBuilder.ctx.onDispose(() => server.stop());
53
- expect(server).to.exist;
54
-
55
- await waitForCondition({ condition: () => server.functions.length > 0 });
56
-
57
- await server.invoke('test', {});
58
- expect(server.stats.seq).to.eq(1);
59
- });
76
+ return { registry, server, space };
77
+ };
60
78
  });
@@ -7,7 +7,7 @@ import { getPort } from 'get-port-please';
7
7
  import type http from 'http';
8
8
  import { join } from 'node:path';
9
9
 
10
- import { Event, Trigger } from '@dxos/async';
10
+ import { asyncTimeout, Event, Trigger } from '@dxos/async';
11
11
  import { type Client } from '@dxos/client';
12
12
  import { Context } from '@dxos/context';
13
13
  import { invariant } from '@dxos/invariant';
@@ -45,14 +45,7 @@ export class DevServer {
45
45
  private readonly _client: Client,
46
46
  private readonly _functionsRegistry: FunctionRegistry,
47
47
  private readonly _options: DevServerOptions,
48
- ) {
49
- // TODO(burdon): Add/remove listener in start/stop.
50
- this._functionsRegistry.registered.on(async ({ added }) => {
51
- added.forEach((def) => this._load(def));
52
- await this._safeUpdateRegistration();
53
- log('new functions loaded', { added });
54
- });
55
- }
48
+ ) {}
56
49
 
57
50
  get stats() {
58
51
  return {
@@ -92,7 +85,7 @@ export class DevServer {
92
85
  }
93
86
 
94
87
  // TODO(burdon): Get function context.
95
- res.statusCode = await this.invoke('/' + path, req.body);
88
+ res.statusCode = await asyncTimeout(this.invoke('/' + path, req.body), 20_000);
96
89
  res.end();
97
90
  } catch (err: any) {
98
91
  log.catch(err);
@@ -101,7 +94,7 @@ export class DevServer {
101
94
  }
102
95
  });
103
96
 
104
- this._port = await getPort({ host: 'localhost', port: 7200, portRange: [7200, 7299] });
97
+ this._port = this._options.port ?? (await getPort({ host: 'localhost', port: 7200, portRange: [7200, 7299] }));
105
98
  this._server = app.listen(this._port);
106
99
 
107
100
  try {
@@ -115,7 +108,8 @@ export class DevServer {
115
108
  this._functionServiceRegistration = registrationId;
116
109
 
117
110
  // Open after registration, so that it can be updated with the list of function definitions.
118
- await this._functionsRegistry.open(this._ctx);
111
+ await this._handleNewFunctions(this._functionsRegistry.getUniqueByUri());
112
+ this._ctx.onDispose(this._functionsRegistry.registered.on(({ added }) => this._handleNewFunctions(added)));
119
113
  } catch (err: any) {
120
114
  await this.stop();
121
115
  throw new Error('FunctionRegistryService not available (check plugin is configured).');
@@ -125,8 +119,12 @@ export class DevServer {
125
119
  }
126
120
 
127
121
  async stop() {
128
- invariant(this._server);
122
+ if (!this._server) {
123
+ return;
124
+ }
125
+
129
126
  log.info('stopping...');
127
+ await this._ctx.dispose();
130
128
 
131
129
  const trigger = new Trigger();
132
130
  this._server.close(async () => {
@@ -155,6 +153,12 @@ export class DevServer {
155
153
  log.info('stopped');
156
154
  }
157
155
 
156
+ private async _handleNewFunctions(newFunctions: FunctionDef[]) {
157
+ newFunctions.forEach((def) => this._load(def));
158
+ await this._safeUpdateRegistration();
159
+ log('new functions loaded', { newFunctions });
160
+ }
161
+
158
162
  /**
159
163
  * Load function.
160
164
  */
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  import { expect } from 'chai';
6
+ import { getRandomPort } from 'get-port-please';
6
7
  import WebSocket from 'ws';
7
8
 
8
9
  import { Trigger } from '@dxos/async';
@@ -108,6 +109,7 @@ describe('scheduler', () => {
108
109
  });
109
110
 
110
111
  test('websocket', async () => {
112
+ const port = await getRandomPort('127.0.0.1');
111
113
  const manifest: FunctionManifest = {
112
114
  functions: [
113
115
  {
@@ -123,7 +125,7 @@ describe('scheduler', () => {
123
125
  spec: {
124
126
  type: 'websocket',
125
127
  // url: 'https://hub.dxos.network/api/mailbox/test',
126
- url: 'http://localhost:8081',
128
+ url: `http://localhost:${port}`,
127
129
  init: {
128
130
  type: 'sync',
129
131
  },
@@ -141,7 +143,7 @@ describe('scheduler', () => {
141
143
 
142
144
  // Test server.
143
145
  setTimeout(() => {
144
- const wss = new WebSocket.Server({ port: 8081 });
146
+ const wss = new WebSocket.Server({ port });
145
147
  wss.on('connection', (ws: WebSocket) => {
146
148
  ws.on('message', (data) => {
147
149
  const info = JSON.parse(new TextDecoder().decode(data as ArrayBuffer));
@@ -3,20 +3,15 @@
3
3
  //
4
4
 
5
5
  import { expect } from 'chai';
6
- import path from 'path';
7
6
 
8
- import { Trigger, waitForCondition } from '@dxos/async';
9
- import { type Client } from '@dxos/client';
10
- import { create, type Space } from '@dxos/client/echo';
11
- import { performInvitation, TestBuilder } from '@dxos/client/testing';
12
- import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
7
+ import { Trigger } from '@dxos/async';
8
+ import { create } from '@dxos/client/echo';
9
+ import { TestBuilder } from '@dxos/client/testing';
13
10
  import { describe, test } from '@dxos/test';
14
11
 
12
+ import { initFunctionsPlugin } from './plugin-init';
15
13
  import { setTestCallHandler } from './test/handler';
16
- import { FunctionRegistry } from '../function';
17
- import { DevServer, Scheduler } from '../runtime';
18
- import { createFunctionRuntime, createInitializedClients, TestType } from '../testing';
19
- import { TriggerRegistry } from '../trigger';
14
+ import { createInitializedClients, inviteMember, startFunctionsHost, TestType } from '../testing';
20
15
  import { FunctionDef, FunctionTrigger } from '../types';
21
16
 
22
17
  describe('functions e2e', () => {
@@ -30,13 +25,11 @@ describe('functions e2e', () => {
30
25
 
31
26
  test('a function gets triggered in response to another peer object creations', async () => {
32
27
  // TODO(burdon): Create builder pattern.
33
- const functionRuntime = await createFunctionRuntime(testBuilder);
34
- const devServer = await startDevServer(functionRuntime);
35
- const scheduler = await startScheduler(functionRuntime, devServer);
28
+ const functionRuntime = await startFunctionsHost(testBuilder, initFunctionsPlugin);
36
29
 
37
30
  const app = (await createInitializedClients(testBuilder, 1))[0];
38
31
  const space = await app.spaces.create();
39
- await inviteMember(space, functionRuntime);
32
+ await inviteMember(space, functionRuntime.client);
40
33
 
41
34
  const uri = 'example.com/function/test';
42
35
  space.db.add(create(FunctionDef, { uri, route: '/test', handler: 'test' }));
@@ -59,7 +52,7 @@ describe('functions e2e', () => {
59
52
  return args.response.status(200);
60
53
  });
61
54
 
62
- await waitTriggersReplicated(space, scheduler);
55
+ await functionRuntime.waitHasActiveTriggers(space);
63
56
  const addedObject = space.db.add(create(TestType, { title: '42' }));
64
57
 
65
58
  const callArgs = await called.wait();
@@ -67,34 +60,4 @@ describe('functions e2e', () => {
67
60
  expect(callArgs.objects).to.deep.eq([addedObject.id]);
68
61
  expect(callArgs.spaceKey).to.eq(space.key.toHex());
69
62
  });
70
-
71
- const waitTriggersReplicated = async (space: Space, scheduler: Scheduler) => {
72
- await waitForCondition({ condition: () => scheduler.triggers.getActiveTriggers(space).length > 0 });
73
- };
74
-
75
- // TODO(burdon): Factor out utils to builder pattern.
76
-
77
- const startScheduler = async (client: Client, devServer: DevServer) => {
78
- const functionRegistry = new FunctionRegistry(client);
79
- const triggerRegistry = new TriggerRegistry(client);
80
- const scheduler = new Scheduler(functionRegistry, triggerRegistry, { endpoint: devServer.endpoint });
81
- await scheduler.start();
82
- testBuilder.ctx.onDispose(() => scheduler.stop());
83
- return scheduler;
84
- };
85
-
86
- const startDevServer = async (client: Client) => {
87
- const functionRegistry = new FunctionRegistry(client);
88
- const server = new DevServer(client, functionRegistry, {
89
- baseDir: path.join(__dirname, '../testing'),
90
- });
91
- await server.start();
92
- testBuilder.ctx.onDispose(() => server.stop());
93
- return server;
94
- };
95
-
96
- const inviteMember = async (host: Space, guest: Client) => {
97
- const [{ invitation: hostInvitation }] = await Promise.all(performInvitation({ host, guest: guest.spaces }));
98
- expect(hostInvitation?.state).to.eq(Invitation.State.SUCCESS);
99
- };
100
63
  });
@@ -5,3 +5,4 @@
5
5
  export * from './setup';
6
6
  export * from './types';
7
7
  export * from './util';
8
+ export * from './manifest';
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import type { FunctionManifest } from '../types';
6
+
7
+ export const testFunctionManifest: FunctionManifest = {
8
+ functions: [
9
+ {
10
+ uri: 'example.com/function/test',
11
+ route: 'test',
12
+ handler: 'test',
13
+ },
14
+ ],
15
+ };
@@ -0,0 +1,20 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { FunctionsPlugin } from '@dxos/agent';
6
+ import { type Client } from '@dxos/client';
7
+
8
+ import { type FunctionsPluginInitializer } from './setup';
9
+
10
+ /**
11
+ * Deliberately in a non-exported file to keep @dxos/agent as a devDependency.
12
+ */
13
+ export const initFunctionsPlugin: FunctionsPluginInitializer = async (client: Client) => {
14
+ const plugin = new FunctionsPlugin();
15
+ await plugin.initialize({ client, clientServices: client.services });
16
+ await plugin.open();
17
+ return {
18
+ close: () => plugin.close(),
19
+ };
20
+ };
@@ -2,14 +2,23 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { FunctionsPlugin } from '@dxos/agent';
5
+ import { getRandomPort } from 'get-port-please';
6
+ import path from 'node:path';
7
+
8
+ import { waitForCondition } from '@dxos/async';
6
9
  import { Client, Config } from '@dxos/client';
10
+ import { type Space } from '@dxos/client/echo';
7
11
  import { type TestBuilder } from '@dxos/client/testing';
8
12
  import { range } from '@dxos/util';
9
13
 
10
14
  import { TestType } from './types';
15
+ import { FunctionRegistry } from '../function';
16
+ import { DevServer, type DevServerOptions, Scheduler } from '../runtime';
17
+ import { TriggerRegistry } from '../trigger';
11
18
  import { FunctionDef, FunctionTrigger } from '../types';
12
19
 
20
+ export type FunctionsPluginInitializer = (client: Client) => Promise<{ close: () => Promise<void> }>;
21
+
13
22
  // TODO(burdon): Extend/wrap TestBuilder.
14
23
 
15
24
  export const createInitializedClients = async (testBuilder: TestBuilder, count: number = 1, config?: Config) => {
@@ -25,19 +34,68 @@ export const createInitializedClients = async (testBuilder: TestBuilder, count:
25
34
  );
26
35
  };
27
36
 
28
- export const createFunctionRuntime = async (testBuilder: TestBuilder): Promise<Client> => {
37
+ export const createFunctionRuntime = async (
38
+ testBuilder: TestBuilder,
39
+ pluginInitializer: FunctionsPluginInitializer,
40
+ ): Promise<Client> => {
41
+ const functionsPort = await getRandomPort('127.0.0.1');
29
42
  const config = new Config({
30
43
  runtime: {
31
44
  agent: {
32
- plugins: [{ id: 'dxos.org/agent/plugin/functions', config: { port: 8080 } }],
45
+ plugins: [{ id: 'dxos.org/agent/plugin/functions', config: { port: functionsPort } }],
33
46
  },
34
47
  },
35
48
  });
36
49
 
37
50
  const [client] = await createInitializedClients(testBuilder, 1, config);
38
- const plugin = new FunctionsPlugin();
39
- await plugin.initialize({ client, clientServices: client.services });
40
- await plugin.open();
51
+ const plugin = await pluginInitializer(client);
41
52
  testBuilder.ctx.onDispose(() => plugin.close());
42
53
  return client;
43
54
  };
55
+
56
+ export const startFunctionsHost = async (
57
+ testBuilder: TestBuilder,
58
+ pluginInitializer: FunctionsPluginInitializer,
59
+ options?: DevServerOptions,
60
+ ) => {
61
+ const functionRuntime = await createFunctionRuntime(testBuilder, pluginInitializer);
62
+ const functionsRegistry = new FunctionRegistry(functionRuntime);
63
+ const devServer = await startDevServer(testBuilder, functionRuntime, functionsRegistry, options);
64
+ const scheduler = await startScheduler(testBuilder, functionRuntime, devServer, functionsRegistry);
65
+ return {
66
+ scheduler,
67
+ client: functionRuntime,
68
+ waitHasActiveTriggers: async (space: Space) => {
69
+ await waitForCondition({ condition: () => scheduler.triggers.getActiveTriggers(space).length > 0 });
70
+ },
71
+ };
72
+ };
73
+
74
+ const startScheduler = async (
75
+ testBuilder: TestBuilder,
76
+ client: Client,
77
+ devServer: DevServer,
78
+ functionRegistry: FunctionRegistry,
79
+ ) => {
80
+ const triggerRegistry = new TriggerRegistry(client);
81
+ const scheduler = new Scheduler(functionRegistry, triggerRegistry, { endpoint: devServer.endpoint });
82
+ await scheduler.start();
83
+ testBuilder.ctx.onDispose(() => scheduler.stop());
84
+ return scheduler;
85
+ };
86
+
87
+ const startDevServer = async (
88
+ testBuilder: TestBuilder,
89
+ client: Client,
90
+ functionRegistry: FunctionRegistry,
91
+ options?: { baseDir?: string },
92
+ ) => {
93
+ const server = new DevServer(client, functionRegistry, {
94
+ baseDir: path.join(__dirname, '../testing'),
95
+ port: await getRandomPort('127.0.0.1'),
96
+ ...options,
97
+ });
98
+ await server.start();
99
+ testBuilder.ctx.onDispose(() => server.stop());
100
+ return server;
101
+ };
@@ -2,8 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import type { Client } from '@dxos/client';
5
6
  import { Filter, type Space } from '@dxos/client/echo';
7
+ import { performInvitation } from '@dxos/client/testing';
6
8
  import { invariant } from '@dxos/invariant';
9
+ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
7
10
 
8
11
  import { FunctionTrigger } from '../types';
9
12
 
@@ -14,3 +17,10 @@ export const triggerWebhook = async (space: Space, uri: string) => {
14
17
  invariant(trigger.spec.type === 'webhook');
15
18
  void fetch(`http://localhost:${trigger.spec.port}`);
16
19
  };
20
+
21
+ export const inviteMember = async (host: Space, guest: Client) => {
22
+ const [{ invitation: hostInvitation }] = await Promise.all(performInvitation({ host, guest: guest.spaces }));
23
+ if (hostInvitation?.state !== Invitation.State.SUCCESS) {
24
+ throw new Error(`Expected ${hostInvitation?.state} to be ${Invitation.State.SUCCESS}.`);
25
+ }
26
+ };