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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/lib/browser/index.mjs +802 -429
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +787 -426
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/types/src/handler.d.ts +33 -12
  8. package/dist/types/src/handler.d.ts.map +1 -1
  9. package/dist/types/src/index.d.ts +2 -0
  10. package/dist/types/src/index.d.ts.map +1 -1
  11. package/dist/types/src/registry/function-registry.d.ts +24 -0
  12. package/dist/types/src/registry/function-registry.d.ts.map +1 -0
  13. package/dist/types/src/registry/function-registry.test.d.ts +2 -0
  14. package/dist/types/src/registry/function-registry.test.d.ts.map +1 -0
  15. package/dist/types/src/registry/index.d.ts +2 -0
  16. package/dist/types/src/registry/index.d.ts.map +1 -0
  17. package/dist/types/src/runtime/dev-server.d.ts +16 -13
  18. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  19. package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
  20. package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
  21. package/dist/types/src/runtime/scheduler.d.ts +12 -27
  22. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  23. package/dist/types/src/testing/functions-integration.test.d.ts +2 -0
  24. package/dist/types/src/testing/functions-integration.test.d.ts.map +1 -0
  25. package/dist/types/src/testing/index.d.ts +4 -0
  26. package/dist/types/src/testing/index.d.ts.map +1 -0
  27. package/dist/types/src/testing/setup.d.ts +5 -0
  28. package/dist/types/src/testing/setup.d.ts.map +1 -0
  29. package/dist/types/src/testing/test/handler.d.ts +4 -0
  30. package/dist/types/src/testing/test/handler.d.ts.map +1 -0
  31. package/dist/types/src/testing/test/index.d.ts +3 -0
  32. package/dist/types/src/testing/test/index.d.ts.map +1 -0
  33. package/dist/types/src/testing/types.d.ts +9 -0
  34. package/dist/types/src/testing/types.d.ts.map +1 -0
  35. package/dist/types/src/testing/util.d.ts +3 -0
  36. package/dist/types/src/testing/util.d.ts.map +1 -0
  37. package/dist/types/src/trigger/index.d.ts +2 -0
  38. package/dist/types/src/trigger/index.d.ts.map +1 -0
  39. package/dist/types/src/trigger/trigger-registry.d.ts +40 -0
  40. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -0
  41. package/dist/types/src/trigger/trigger-registry.test.d.ts +2 -0
  42. package/dist/types/src/trigger/trigger-registry.test.d.ts.map +1 -0
  43. package/dist/types/src/trigger/type/index.d.ts +5 -0
  44. package/dist/types/src/trigger/type/index.d.ts.map +1 -0
  45. package/dist/types/src/trigger/type/subscription-trigger.d.ts +4 -0
  46. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -0
  47. package/dist/types/src/trigger/type/timer-trigger.d.ts +4 -0
  48. package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -0
  49. package/dist/types/src/trigger/type/webhook-trigger.d.ts +4 -0
  50. package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -0
  51. package/dist/types/src/trigger/type/websocket-trigger.d.ts +13 -0
  52. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -0
  53. package/dist/types/src/types.d.ts +129 -101
  54. package/dist/types/src/types.d.ts.map +1 -1
  55. package/package.json +18 -13
  56. package/schema/functions.json +128 -101
  57. package/src/handler.ts +54 -31
  58. package/src/index.ts +2 -0
  59. package/src/registry/function-registry.test.ts +105 -0
  60. package/src/registry/function-registry.ts +84 -0
  61. package/src/registry/index.ts +5 -0
  62. package/src/runtime/dev-server.test.ts +60 -0
  63. package/src/runtime/dev-server.ts +104 -52
  64. package/src/runtime/scheduler.test.ts +56 -73
  65. package/src/runtime/scheduler.ts +79 -271
  66. package/src/testing/functions-integration.test.ts +99 -0
  67. package/src/testing/index.ts +7 -0
  68. package/src/testing/setup.ts +45 -0
  69. package/src/testing/test/handler.ts +15 -0
  70. package/src/testing/test/index.ts +7 -0
  71. package/src/testing/types.ts +9 -0
  72. package/src/testing/util.ts +16 -0
  73. package/src/trigger/index.ts +5 -0
  74. package/src/trigger/trigger-registry.test.ts +229 -0
  75. package/src/trigger/trigger-registry.ts +176 -0
  76. package/src/trigger/type/index.ts +8 -0
  77. package/src/trigger/type/subscription-trigger.ts +73 -0
  78. package/src/trigger/type/timer-trigger.ts +44 -0
  79. package/src/trigger/type/webhook-trigger.ts +47 -0
  80. package/src/trigger/type/websocket-trigger.ts +91 -0
  81. package/src/types.ts +57 -32
@@ -1,27 +1,24 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "type": "object",
4
- "required": [
5
- "functions",
6
- "triggers"
7
- ],
4
+ "required": [],
8
5
  "properties": {
9
6
  "functions": {
10
7
  "type": "array",
11
8
  "items": {
12
9
  "type": "object",
13
10
  "required": [
14
- "id",
15
- "name",
11
+ "uri",
12
+ "route",
16
13
  "handler"
17
14
  ],
18
15
  "properties": {
19
- "id": {
16
+ "uri": {
20
17
  "type": "string",
21
18
  "description": "a string",
22
19
  "title": "string"
23
20
  },
24
- "name": {
21
+ "route": {
25
22
  "type": "string",
26
23
  "description": "a string",
27
24
  "title": "string"
@@ -45,7 +42,8 @@
45
42
  "items": {
46
43
  "type": "object",
47
44
  "required": [
48
- "function"
45
+ "function",
46
+ "spec"
49
47
  ],
50
48
  "properties": {
51
49
  "function": {
@@ -53,113 +51,142 @@
53
51
  "description": "Function ID/URI.",
54
52
  "title": "string"
55
53
  },
56
- "timer": {
54
+ "meta": {
57
55
  "type": "object",
58
- "required": [
59
- "cron"
60
- ],
61
- "properties": {
62
- "cron": {
63
- "type": "string",
64
- "description": "a string",
65
- "title": "string"
66
- }
67
- },
68
- "additionalProperties": false
56
+ "required": [],
57
+ "properties": {},
58
+ "additionalProperties": {
59
+ "$id": "/schemas/any",
60
+ "title": "any"
61
+ }
69
62
  },
70
- "webhook": {
71
- "type": "object",
72
- "required": [
73
- "port"
74
- ],
75
- "properties": {
76
- "port": {
77
- "type": "number",
78
- "description": "a number",
79
- "title": "number"
80
- }
81
- },
82
- "additionalProperties": false
83
- },
84
- "websocket": {
85
- "type": "object",
86
- "required": [
87
- "url"
88
- ],
89
- "properties": {
90
- "url": {
91
- "type": "string",
92
- "description": "a string",
93
- "title": "string"
94
- },
95
- "init": {
63
+ "spec": {
64
+ "anyOf": [
65
+ {
96
66
  "type": "object",
97
- "required": [],
98
- "properties": {},
99
- "additionalProperties": {
100
- "$id": "/schemas/any",
101
- "title": "any"
102
- }
103
- }
104
- },
105
- "additionalProperties": false
106
- },
107
- "subscription": {
108
- "type": "object",
109
- "required": [
110
- "filter"
111
- ],
112
- "properties": {
113
- "spaceKey": {
114
- "type": "string",
115
- "description": "a string",
116
- "title": "string"
117
- },
118
- "filter": {
119
- "type": "array",
120
- "items": {
121
- "type": "object",
122
- "required": [
123
- "type"
124
- ],
125
- "properties": {
126
- "type": {
127
- "type": "string",
128
- "description": "a string",
129
- "title": "string"
130
- },
131
- "props": {
132
- "type": "object",
133
- "required": [],
134
- "properties": {},
135
- "additionalProperties": {
136
- "$id": "/schemas/any",
137
- "title": "any"
138
- }
139
- }
67
+ "required": [
68
+ "type",
69
+ "cron"
70
+ ],
71
+ "properties": {
72
+ "type": {
73
+ "const": "timer"
140
74
  },
141
- "additionalProperties": false
142
- }
75
+ "cron": {
76
+ "type": "string",
77
+ "description": "a string",
78
+ "title": "string"
79
+ }
80
+ },
81
+ "additionalProperties": false
143
82
  },
144
- "options": {
83
+ {
145
84
  "type": "object",
146
- "required": [],
85
+ "required": [
86
+ "type",
87
+ "method"
88
+ ],
147
89
  "properties": {
148
- "deep": {
149
- "type": "boolean",
150
- "description": "a boolean",
151
- "title": "boolean"
90
+ "type": {
91
+ "const": "webhook"
152
92
  },
153
- "delay": {
93
+ "method": {
94
+ "type": "string",
95
+ "description": "a string",
96
+ "title": "string"
97
+ },
98
+ "port": {
154
99
  "type": "number",
155
100
  "description": "a number",
156
101
  "title": "number"
157
102
  }
158
103
  },
159
104
  "additionalProperties": false
105
+ },
106
+ {
107
+ "type": "object",
108
+ "required": [
109
+ "type",
110
+ "url"
111
+ ],
112
+ "properties": {
113
+ "type": {
114
+ "const": "websocket"
115
+ },
116
+ "url": {
117
+ "type": "string",
118
+ "description": "a string",
119
+ "title": "string"
120
+ },
121
+ "init": {
122
+ "type": "object",
123
+ "required": [],
124
+ "properties": {},
125
+ "additionalProperties": {
126
+ "$id": "/schemas/any",
127
+ "title": "any"
128
+ }
129
+ }
130
+ },
131
+ "additionalProperties": false
132
+ },
133
+ {
134
+ "type": "object",
135
+ "required": [
136
+ "type",
137
+ "filter"
138
+ ],
139
+ "properties": {
140
+ "type": {
141
+ "const": "subscription"
142
+ },
143
+ "filter": {
144
+ "type": "array",
145
+ "items": {
146
+ "type": "object",
147
+ "required": [
148
+ "type"
149
+ ],
150
+ "properties": {
151
+ "type": {
152
+ "type": "string",
153
+ "description": "a string",
154
+ "title": "string"
155
+ },
156
+ "props": {
157
+ "type": "object",
158
+ "required": [],
159
+ "properties": {},
160
+ "additionalProperties": {
161
+ "$id": "/schemas/any",
162
+ "title": "any"
163
+ }
164
+ }
165
+ },
166
+ "additionalProperties": false
167
+ }
168
+ },
169
+ "options": {
170
+ "type": "object",
171
+ "required": [],
172
+ "properties": {
173
+ "deep": {
174
+ "type": "boolean",
175
+ "description": "a boolean",
176
+ "title": "boolean"
177
+ },
178
+ "delay": {
179
+ "type": "number",
180
+ "description": "a number",
181
+ "title": "number"
182
+ }
183
+ },
184
+ "additionalProperties": false
185
+ }
186
+ },
187
+ "additionalProperties": false
160
188
  }
161
- },
162
- "additionalProperties": false
189
+ ]
163
190
  }
164
191
  },
165
192
  "additionalProperties": false
package/src/handler.ts CHANGED
@@ -8,38 +8,61 @@ import { type EchoReactiveObject } from '@dxos/echo-schema';
8
8
  import { log } from '@dxos/log';
9
9
  import { nonNullable } from '@dxos/util';
10
10
 
11
- // TODO(burdon): Context?
12
- // Lambda-like function definitions.
13
- // See: https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml/#functions
14
- // https://www.npmjs.com/package/aws-lambda
11
+ // TODO(burdon): Model after http request. Ref Lambda/OpenFaaS.
15
12
  // https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html
13
+ // https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml/#functions
14
+ // https://www.npmjs.com/package/aws-lambda
16
15
 
17
- // TODO(burdon): No response?
18
- export interface Response {
19
- status(code: number): Response;
20
- }
16
+ /**
17
+ * Function handler.
18
+ */
19
+ export type FunctionHandler<TData = {}, TMeta = {}> = (params: {
20
+ context: FunctionContext;
21
+ event: FunctionEvent<TData, TMeta>;
22
+ response: FunctionResponse;
23
+ }) => Promise<FunctionResponse | void>;
21
24
 
22
- // TODO(burdon): Limit access to individual space?
25
+ /**
26
+ * Function context.
27
+ */
23
28
  export interface FunctionContext {
29
+ // TODO(burdon): Limit access to individual space.
24
30
  client: Client;
31
+ // TODO(burdon): Replace with storage service abstraction.
25
32
  dataDir?: string;
26
33
  }
27
34
 
28
- // TODO(burdon): Model after http request. Ref Lambda/OpenFaaS.
29
- // https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html
30
- export type FunctionHandler<T extends {}> = (params: {
31
- event: T;
32
- context: FunctionContext;
33
- response: Response;
34
- }) => Promise<Response | void>;
35
+ /**
36
+ * Event payload.
37
+ */
38
+ export type FunctionEvent<TData = {}, TMeta = {}> = {
39
+ data: FunctionEventMeta<TMeta> & TData;
40
+ };
41
+
42
+ /**
43
+ * Metadata from trigger.
44
+ */
45
+ export type FunctionEventMeta<TMeta = {}> = {
46
+ meta: TMeta;
47
+ };
48
+
49
+ /**
50
+ * Function response.
51
+ */
52
+ export interface FunctionResponse {
53
+ status(code: number): FunctionResponse;
54
+ }
55
+
56
+ //
57
+ // Subscription utils.
58
+ //
35
59
 
36
- export type FunctionSubscriptionEvent = {
37
- space?: string; // TODO(burdon): Convert to PublicKey.
60
+ export type RawSubscriptionData = {
61
+ spaceKey?: string;
38
62
  objects?: string[];
39
63
  };
40
64
 
41
- // TODO(burdon): ???
42
- export type FunctionSubscriptionEvent2 = {
65
+ export type SubscriptionData = {
43
66
  space?: Space;
44
67
  objects?: EchoReactiveObject<any>[];
45
68
  };
@@ -54,22 +77,22 @@ export type FunctionSubscriptionEvent2 = {
54
77
  *
55
78
  * NOTE: Get space key from devtools or `dx space list --json`
56
79
  */
57
- export const subscriptionHandler = (
58
- handler: FunctionHandler<FunctionSubscriptionEvent2>,
59
- ): FunctionHandler<FunctionSubscriptionEvent> => {
60
- return ({ event, context, ...rest }) => {
80
+ export const subscriptionHandler = <TMeta>(
81
+ handler: FunctionHandler<SubscriptionData, TMeta>,
82
+ ): FunctionHandler<RawSubscriptionData, TMeta> => {
83
+ return ({ event: { data }, context, ...rest }) => {
61
84
  const { client } = context;
62
- const space = event.space ? client.spaces.get(PublicKey.from(event.space)) : undefined;
63
- const objects =
64
- space &&
65
- event.objects?.map<EchoReactiveObject<any> | undefined>((id) => space!.db.getObjectById(id)).filter(nonNullable);
85
+ const space = data.spaceKey ? client.spaces.get(PublicKey.from(data.spaceKey)) : undefined;
86
+ const objects = space
87
+ ? data.objects?.map<EchoReactiveObject<any> | undefined>((id) => space!.db.getObjectById(id)).filter(nonNullable)
88
+ : [];
66
89
 
67
- if (!!event.space && !space) {
68
- log.warn('invalid space', { event });
90
+ if (!!data.spaceKey && !space) {
91
+ log.warn('invalid space', { data });
69
92
  } else {
70
93
  log.info('handler', { space: space?.key.truncate(), objects: objects?.length });
71
94
  }
72
95
 
73
- return handler({ event: { space, objects }, context, ...rest });
96
+ return handler({ event: { data: { ...data, space, objects } }, context, ...rest });
74
97
  };
75
98
  };
package/src/index.ts CHANGED
@@ -3,5 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './handler';
6
+ export * from './registry';
6
7
  export * from './runtime';
8
+ export * from './trigger';
7
9
  export * from './types';
@@ -0,0 +1,105 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { Trigger } from '@dxos/async';
8
+ import { type Client } from '@dxos/client';
9
+ import { TestBuilder } from '@dxos/client/testing';
10
+ import { Context } from '@dxos/context';
11
+ import { Filter } from '@dxos/echo-db';
12
+ import { create } from '@dxos/echo-schema';
13
+ import { describe, test } from '@dxos/test';
14
+ import { range } from '@dxos/util';
15
+
16
+ import { FunctionRegistry } from './function-registry';
17
+ import { createInitializedClients } from '../testing';
18
+ import { FunctionDef, type FunctionManifest } from '../types';
19
+
20
+ const testManifest: FunctionManifest = {
21
+ functions: [
22
+ {
23
+ uri: 'dxos.functions.test/hello',
24
+ route: '/hello',
25
+ handler: 'test',
26
+ },
27
+ ],
28
+ };
29
+
30
+ describe('function registry', () => {
31
+ let ctx: Context;
32
+ let testBuilder: TestBuilder;
33
+ beforeEach(async () => {
34
+ ctx = new Context();
35
+ testBuilder = new TestBuilder();
36
+ });
37
+ afterEach(async () => {
38
+ await ctx.dispose();
39
+ await testBuilder.destroy();
40
+ });
41
+
42
+ describe('register', () => {
43
+ test('creates new functions', async () => {
44
+ const client = (await createInitializedClients(testBuilder))[0];
45
+ const registry = createRegistry(client);
46
+ const space = await client.spaces.create();
47
+ await registry.register(space, testManifest);
48
+ const { objects: definitions } = await space.db.query(Filter.schema(FunctionDef)).run();
49
+ expect(definitions.length).to.eq(1);
50
+ expect(definitions[0].uri).to.eq(testManifest.functions?.[0]?.uri);
51
+ });
52
+
53
+ test('de-duplicates by function URI', async () => {
54
+ const client = (await createInitializedClients(testBuilder))[0];
55
+ const registry = createRegistry(client);
56
+ const space = await client.spaces.create();
57
+ const existing = space.db.add(create(FunctionDef, { ...testManifest.functions![0] }));
58
+ await registry.register(space, testManifest);
59
+ const { objects: definitions } = await space.db.query(Filter.schema(FunctionDef)).run();
60
+ expect(definitions.length).to.eq(1);
61
+ expect(definitions[0].uri).to.eq(existing.uri);
62
+ });
63
+ });
64
+
65
+ describe('onFunctionsRegistered', () => {
66
+ test('called with all registered when opened', async () => {
67
+ const client = (await createInitializedClients(testBuilder))[0];
68
+ const registry = createRegistry(client);
69
+ const space = await client.spaces.create();
70
+ const definitions = range(3, () => create(FunctionDef, { ...testManifest.functions![0] }));
71
+ definitions.forEach((def) => space.db.add(def));
72
+
73
+ const functionRegistered = new Trigger<FunctionDef[]>();
74
+ registry.onFunctionsRegistered.on((fn) => {
75
+ functionRegistered.wake(fn.newFunctions);
76
+ });
77
+ void registry.open(ctx);
78
+ const functions = await functionRegistered.wait();
79
+ const expected = definitions.map((def) => def.uri).sort();
80
+ expect(functions.map((fn) => fn.uri).sort()).to.deep.eq(expected);
81
+ });
82
+
83
+ test('called when a new functions is added', async () => {
84
+ const client = (await createInitializedClients(testBuilder))[0];
85
+ const registry = createRegistry(client);
86
+ const space = await client.spaces.create();
87
+
88
+ const functionRegistered = new Trigger<FunctionDef>();
89
+ registry.onFunctionsRegistered.on((fn) => {
90
+ expect(fn.newFunctions.length).to.eq(1);
91
+ functionRegistered.wake(fn.newFunctions[0]);
92
+ });
93
+ await registry.open(ctx);
94
+ await registry.register(space, testManifest);
95
+ const registered = await functionRegistered.wait();
96
+ expect(registered.uri).to.eq(testManifest.functions![0].uri);
97
+ });
98
+ });
99
+
100
+ const createRegistry = (client: Client) => {
101
+ const registry = new FunctionRegistry(client);
102
+ ctx.onDispose(() => registry.close());
103
+ return registry;
104
+ };
105
+ });
@@ -0,0 +1,84 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Event } from '@dxos/async';
6
+ import { type Client } from '@dxos/client';
7
+ import { create, Filter, type Space } from '@dxos/client/echo';
8
+ import { type Context, Resource } from '@dxos/context';
9
+ import { PublicKey } from '@dxos/keys';
10
+ import { ComplexMap } from '@dxos/util';
11
+
12
+ import { FunctionDef, type FunctionManifest } from '../types';
13
+
14
+ export type FunctionsRegisteredEvent = {
15
+ space: Space;
16
+ newFunctions: FunctionDef[];
17
+ };
18
+
19
+ export class FunctionRegistry extends Resource {
20
+ private readonly _functionBySpaceKey = new ComplexMap<PublicKey, FunctionDef[]>(PublicKey.hash);
21
+
22
+ public readonly onFunctionsRegistered = new Event<FunctionsRegisteredEvent>();
23
+
24
+ constructor(private readonly _client: Client) {
25
+ super();
26
+ }
27
+
28
+ public getFunctions(space: Space): FunctionDef[] {
29
+ return this._functionBySpaceKey.get(space.key) ?? [];
30
+ }
31
+
32
+ /**
33
+ * The method loads function definitions from the manifest into the space.
34
+ * We first load all the definitions from the space to deduplicate by functionId.
35
+ */
36
+ // TODO(burdon): This should not be space specific (they are static for the agent).
37
+ public async register(space: Space, manifest: FunctionManifest): Promise<void> {
38
+ if (!manifest.functions?.length) {
39
+ return;
40
+ }
41
+ if (!space.db.graph.runtimeSchemaRegistry.isSchemaRegistered(FunctionDef)) {
42
+ space.db.graph.runtimeSchemaRegistry.registerSchema(FunctionDef);
43
+ }
44
+
45
+ const { objects: existingDefinitions } = await space.db.query(Filter.schema(FunctionDef)).run();
46
+ const newDefinitions = getNewDefinitions(manifest.functions, existingDefinitions);
47
+ const reactiveObjects = newDefinitions.map((template) => create(FunctionDef, { ...template }));
48
+ reactiveObjects.forEach((obj) => space.db.add(obj));
49
+ }
50
+
51
+ protected override async _open(): Promise<void> {
52
+ const spaceListSubscription = this._client.spaces.subscribe(async (spaces) => {
53
+ for (const space of spaces) {
54
+ if (this._functionBySpaceKey.has(space.key)) {
55
+ continue;
56
+ }
57
+ const registered: FunctionDef[] = [];
58
+ this._functionBySpaceKey.set(space.key, registered);
59
+ await space.waitUntilReady();
60
+ if (this._ctx.disposed) {
61
+ break;
62
+ }
63
+
64
+ const functionsSubscription = space.db.query(Filter.schema(FunctionDef)).subscribe((definitions) => {
65
+ const newFunctions = getNewDefinitions(definitions.objects, registered);
66
+ if (newFunctions.length > 0) {
67
+ registered.push(...newFunctions);
68
+ this.onFunctionsRegistered.emit({ space, newFunctions });
69
+ }
70
+ });
71
+ this._ctx.onDispose(functionsSubscription);
72
+ }
73
+ });
74
+ this._ctx.onDispose(() => spaceListSubscription.unsubscribe());
75
+ }
76
+
77
+ protected override async _close(_: Context): Promise<void> {
78
+ this._functionBySpaceKey.clear();
79
+ }
80
+ }
81
+
82
+ const getNewDefinitions = <T extends { uri: string }>(candidateList: T[], existing: FunctionDef[]): T[] => {
83
+ return candidateList.filter((candidate) => existing.find((def) => def.uri === candidate.uri) == null);
84
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './function-registry';
@@ -0,0 +1,60 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+ import path from 'path';
7
+
8
+ import { waitForCondition } from '@dxos/async';
9
+ import { type Client } from '@dxos/client';
10
+ import { TestBuilder } from '@dxos/client/testing';
11
+ import { describe, test } from '@dxos/test';
12
+
13
+ import { DevServer } from './dev-server';
14
+ import { FunctionRegistry } from '../registry';
15
+ import { createFunctionRuntime } from '../testing';
16
+ import { type FunctionManifest } from '../types';
17
+
18
+ describe('dev server', () => {
19
+ let client: Client;
20
+ let testBuilder: TestBuilder;
21
+ before(async () => {
22
+ testBuilder = new TestBuilder();
23
+ client = await createFunctionRuntime(testBuilder);
24
+ expect(client.services.services.FunctionRegistryService).to.exist;
25
+ });
26
+
27
+ after(async () => {
28
+ await testBuilder.destroy();
29
+ });
30
+
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
+ };
41
+
42
+ const registry = new FunctionRegistry(client);
43
+ const server = new DevServer(client, registry, {
44
+ baseDir: path.join(__dirname, '../testing'),
45
+ });
46
+ const space = await client.spaces.create();
47
+ await registry.register(space, manifest);
48
+ await server.start();
49
+
50
+ // TODO(burdon): Doesn't shut down cleanly.
51
+ // Error: invariant violation [this._client.services.services.FunctionRegistryService]
52
+ 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
+ });
60
+ });