@dxos/functions 0.5.4 → 0.5.5-main.1aa8b60

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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/browser/{chunk-4D4I3YMJ.mjs → chunk-ERWZ4JUZ.mjs} +31 -31
  3. package/dist/lib/browser/{chunk-4D4I3YMJ.mjs.map → chunk-ERWZ4JUZ.mjs.map} +2 -2
  4. package/dist/lib/browser/chunk-SXZ5DYJG.mjs +1089 -0
  5. package/dist/lib/browser/chunk-SXZ5DYJG.mjs.map +7 -0
  6. package/dist/lib/browser/index.mjs +18 -1076
  7. package/dist/lib/browser/index.mjs.map +4 -4
  8. package/dist/lib/browser/meta.json +1 -1
  9. package/dist/lib/browser/testing/index.mjs +151 -0
  10. package/dist/lib/browser/testing/index.mjs.map +7 -0
  11. package/dist/lib/browser/types.mjs +1 -1
  12. package/dist/lib/node/{chunk-3UYUR5N5.cjs → chunk-BLLSDTKZ.cjs} +34 -34
  13. package/dist/lib/node/{chunk-3UYUR5N5.cjs.map → chunk-BLLSDTKZ.cjs.map} +2 -2
  14. package/dist/lib/node/chunk-RPHL3ORN.cjs +1102 -0
  15. package/dist/lib/node/chunk-RPHL3ORN.cjs.map +7 -0
  16. package/dist/lib/node/index.cjs +20 -1074
  17. package/dist/lib/node/index.cjs.map +4 -4
  18. package/dist/lib/node/meta.json +1 -1
  19. package/dist/lib/node/testing/index.cjs +172 -0
  20. package/dist/lib/node/testing/index.cjs.map +7 -0
  21. package/dist/lib/node/types.cjs +5 -5
  22. package/dist/lib/node/types.cjs.map +1 -1
  23. package/dist/types/src/function/function-registry.d.ts +1 -0
  24. package/dist/types/src/function/function-registry.d.ts.map +1 -1
  25. package/dist/types/src/runtime/dev-server.d.ts +1 -0
  26. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  27. package/dist/types/src/testing/index.d.ts +1 -0
  28. package/dist/types/src/testing/index.d.ts.map +1 -1
  29. package/dist/types/src/testing/manifest.d.ts +3 -0
  30. package/dist/types/src/testing/manifest.d.ts.map +1 -0
  31. package/dist/types/src/testing/plugin-init.d.ts +6 -0
  32. package/dist/types/src/testing/plugin-init.d.ts.map +1 -0
  33. package/dist/types/src/testing/setup.d.ts +11 -1
  34. package/dist/types/src/testing/setup.d.ts.map +1 -1
  35. package/dist/types/src/testing/util.d.ts +2 -0
  36. package/dist/types/src/testing/util.d.ts.map +1 -1
  37. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -1
  38. package/dist/types/src/types.d.ts +27 -38
  39. package/dist/types/src/types.d.ts.map +1 -1
  40. package/package.json +24 -15
  41. package/src/function/function-registry.test.ts +14 -1
  42. package/src/function/function-registry.ts +10 -0
  43. package/src/runtime/dev-server.test.ts +42 -24
  44. package/src/runtime/dev-server.ts +17 -13
  45. package/src/runtime/scheduler.test.ts +4 -2
  46. package/src/testing/functions-integration.test.ts +8 -45
  47. package/src/testing/index.ts +1 -0
  48. package/src/testing/manifest.ts +15 -0
  49. package/src/testing/plugin-init.ts +20 -0
  50. package/src/testing/setup.ts +65 -6
  51. package/src/testing/types.ts +1 -1
  52. package/src/testing/util.ts +10 -0
  53. package/src/trigger/type/websocket-trigger.ts +12 -8
  54. package/src/types.ts +31 -31
@@ -1,3 +1,5 @@
1
+ import type { Client } from '@dxos/client';
1
2
  import { type Space } from '@dxos/client/echo';
2
3
  export declare const triggerWebhook: (space: Space, uri: string) => Promise<void>;
4
+ export declare const inviteMember: (host: Space, guest: Client) => Promise<void>;
3
5
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../../src/testing/util.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAKvD,eAAO,MAAM,cAAc,UAAiB,KAAK,OAAO,MAAM,kBAM7D,CAAC"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../../src/testing/util.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAU,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAOvD,eAAO,MAAM,cAAc,UAAiB,KAAK,OAAO,MAAM,kBAM7D,CAAC;AAEF,eAAO,MAAM,YAAY,SAAgB,KAAK,SAAS,MAAM,kBAK5D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"websocket-trigger.d.ts","sourceRoot":"","sources":["../../../../../src/trigger/type/websocket-trigger.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEhF,UAAU,uBAAuB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,cAAc,CAAC,gBAAgB,EAAE,uBAAuB,CAoE5F,CAAC"}
1
+ {"version":3,"file":"websocket-trigger.d.ts","sourceRoot":"","sources":["../../../../../src/trigger/type/websocket-trigger.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEhF,UAAU,uBAAuB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,cAAc,CAAC,gBAAgB,EAAE,uBAAuB,CAwE5F,CAAC"}
@@ -6,44 +6,33 @@ import { S } from '@dxos/echo-schema';
6
6
  * https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
7
7
  */
8
8
  export type FunctionTriggerType = 'subscription' | 'timer' | 'webhook' | 'websocket';
9
- declare const SubscriptionTriggerSchema: S.mutable<S.struct<{
10
- type: S.literal<["subscription"]>;
11
- filter: S.array<S.struct<{
12
- type: S.$string;
13
- props: S.PropertySignature<"?:", {
14
- readonly [x: string]: any;
15
- } | undefined, never, "?:", {
16
- readonly [x: string]: any;
17
- } | undefined, never>;
9
+ declare const SubscriptionTriggerSchema: S.mutable<S.Struct<{
10
+ type: S.Literal<["subscription"]>;
11
+ filter: S.Array$<S.Struct<{
12
+ type: typeof S.String;
13
+ props: S.optional<S.Record$<typeof S.String, typeof S.Any>>;
14
+ }>>;
15
+ options: S.optional<S.Struct<{
16
+ deep: S.optional<typeof S.Boolean>;
17
+ delay: S.optional<typeof S.Number>;
18
18
  }>>;
19
- options: S.PropertySignature<"?:", {
20
- readonly deep?: boolean | undefined;
21
- readonly delay?: number | undefined;
22
- } | undefined, never, "?:", {
23
- readonly deep?: boolean | undefined;
24
- readonly delay?: number | undefined;
25
- } | undefined, never>;
26
19
  }>>;
27
20
  export type SubscriptionTrigger = S.Schema.Type<typeof SubscriptionTriggerSchema>;
28
- declare const TimerTriggerSchema: S.mutable<S.struct<{
29
- type: S.literal<["timer"]>;
30
- cron: S.$string;
21
+ declare const TimerTriggerSchema: S.mutable<S.Struct<{
22
+ type: S.Literal<["timer"]>;
23
+ cron: typeof S.String;
31
24
  }>>;
32
25
  export type TimerTrigger = S.Schema.Type<typeof TimerTriggerSchema>;
33
- declare const WebhookTriggerSchema: S.mutable<S.struct<{
34
- type: S.literal<["webhook"]>;
35
- method: S.$string;
36
- port: S.PropertySignature<"?:", number | undefined, never, "?:", number | undefined, never>;
26
+ declare const WebhookTriggerSchema: S.mutable<S.Struct<{
27
+ type: S.Literal<["webhook"]>;
28
+ method: typeof S.String;
29
+ port: S.optional<typeof S.Number>;
37
30
  }>>;
38
31
  export type WebhookTrigger = S.Schema.Type<typeof WebhookTriggerSchema>;
39
- declare const WebsocketTriggerSchema: S.mutable<S.struct<{
40
- type: S.literal<["websocket"]>;
41
- url: S.$string;
42
- init: S.PropertySignature<"?:", {
43
- readonly [x: string]: any;
44
- } | undefined, never, "?:", {
45
- readonly [x: string]: any;
46
- } | undefined, never>;
32
+ declare const WebsocketTriggerSchema: S.mutable<S.Struct<{
33
+ type: S.Literal<["websocket"]>;
34
+ url: typeof S.String;
35
+ init: S.optional<S.Record$<typeof S.String, typeof S.Any>>;
47
36
  }>>;
48
37
  export type WebsocketTrigger = S.Schema.Type<typeof WebsocketTriggerSchema>;
49
38
  export type TriggerSpec = TimerTrigger | WebhookTrigger | WebsocketTrigger | SubscriptionTrigger;
@@ -103,8 +92,8 @@ export declare class FunctionTrigger extends FunctionTrigger_base {
103
92
  /**
104
93
  * Function manifest file.
105
94
  */
106
- export declare const FunctionManifestSchema: S.struct<{
107
- functions: S.PropertySignature<"?:", ({
95
+ export declare const FunctionManifestSchema: S.Struct<{
96
+ functions: S.optional<S.mutable<S.Array$<S.Schema<{
108
97
  handler: string;
109
98
  uri: string;
110
99
  description?: string | undefined;
@@ -116,7 +105,7 @@ export declare const FunctionManifestSchema: S.struct<{
116
105
  readonly source: string;
117
106
  }[];
118
107
  } | undefined;
119
- })[] | undefined, never, "?:", ({
108
+ }, {
120
109
  handler: string;
121
110
  uri: string;
122
111
  description?: string | undefined;
@@ -128,8 +117,8 @@ export declare const FunctionManifestSchema: S.struct<{
128
117
  readonly source: string;
129
118
  }[];
130
119
  } | undefined;
131
- })[] | undefined, never>;
132
- triggers: S.PropertySignature<"?:", ({
120
+ }, never>>>>;
121
+ triggers: S.optional<S.mutable<S.Array$<S.Schema<{
133
122
  function: string;
134
123
  enabled?: boolean | undefined;
135
124
  meta?: {
@@ -168,7 +157,7 @@ export declare const FunctionManifestSchema: S.struct<{
168
157
  readonly source: string;
169
158
  }[];
170
159
  } | undefined;
171
- })[] | undefined, never, "?:", ({
160
+ }, {
172
161
  function: string;
173
162
  enabled?: boolean | undefined;
174
163
  meta?: {
@@ -207,7 +196,7 @@ export declare const FunctionManifestSchema: S.struct<{
207
196
  readonly source: string;
208
197
  }[];
209
198
  } | undefined;
210
- })[] | undefined, never>;
199
+ }, never>>>>;
211
200
  }>;
212
201
  export type FunctionManifest = S.Schema.Type<typeof FunctionManifestSchema>;
213
202
  export declare const FUNCTION_SCHEMA: (typeof FunctionDef | typeof FunctionTrigger)[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAIA,OAAO,EAAa,CAAC,EAAe,MAAM,mBAAmB,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAErF,QAAA,MAAM,yBAAyB;;;;;;;;;;;;;;;;;GAmB9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,QAAA,MAAM,kBAAkB;;;GAKvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,QAAA,MAAM,oBAAoB;;;;GAOzB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,QAAA,MAAM,sBAAsB;;;;;;;;GAM3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAS5E,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,cAAc,GAAG,gBAAgB,GAAG,mBAAmB,CAAC;;;;;;;;;AAEjG;;GAEG;AACH,qBAAa,WAAY,SAAQ,gBAQ/B;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEL;;GAEG;AACH,qBAAa,eAAgB,SAAQ,oBASnC;CAAG;AAEL;;GAEG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAG5E,eAAO,MAAM,eAAe,iDAAiC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAIA,OAAO,EAAa,CAAC,EAAe,MAAM,mBAAmB,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAErF,QAAA,MAAM,yBAAyB;;;;;;;;;;GAmB9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,QAAA,MAAM,kBAAkB;;;GAKvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,QAAA,MAAM,oBAAoB;;;;GAOzB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,QAAA,MAAM,sBAAsB;;;;GAM3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAS5E,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,cAAc,GAAG,gBAAgB,GAAG,mBAAmB,CAAC;;;;;;;;;AAEjG;;GAEG;AACH,qBAAa,WAAY,SAAQ,gBAQ/B;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEL;;GAEG;AACH,qBAAa,eAAgB,SAAQ,oBASnC;CAAG;AAEL;;GAEG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAG5E,eAAO,MAAM,eAAe,iDAAiC,CAAC"}
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.1aa8b60",
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
  },
@@ -36,27 +45,27 @@
36
45
  "dependencies": {
37
46
  "@preact/signals-core": "^1.6.0",
38
47
  "cron": "^3.1.6",
39
- "effect": "^2.4.19",
48
+ "effect": "^3.2.7",
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.1aa8b60",
53
+ "@dxos/async": "0.5.5-main.1aa8b60",
54
+ "@dxos/client": "0.5.5-main.1aa8b60",
55
+ "@dxos/context": "0.5.5-main.1aa8b60",
56
+ "@dxos/invariant": "0.5.5-main.1aa8b60",
57
+ "@dxos/echo-db": "0.5.5-main.1aa8b60",
58
+ "@dxos/keys": "0.5.5-main.1aa8b60",
59
+ "@dxos/echo-schema": "0.5.5-main.1aa8b60",
60
+ "@dxos/log": "0.5.5-main.1aa8b60",
61
+ "@dxos/node-std": "0.5.5-main.1aa8b60",
62
+ "@dxos/util": "0.5.5-main.1aa8b60",
63
+ "@dxos/protocols": "0.5.5-main.1aa8b60"
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.1aa8b60"
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
+ };