@dxos/functions 0.8.2-main.2f9c567 → 0.8.2-main.36232bc

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 (168) hide show
  1. package/dist/lib/browser/bundler/index.mjs +3 -0
  2. package/dist/lib/browser/bundler/index.mjs.map +1 -1
  3. package/dist/lib/browser/chunk-2YE6S7XY.mjs +360 -0
  4. package/dist/lib/browser/chunk-2YE6S7XY.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-7CHDHCV3.mjs +482 -0
  6. package/dist/lib/browser/chunk-7CHDHCV3.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-LT4LR4VU.mjs +72 -0
  8. package/dist/lib/browser/chunk-LT4LR4VU.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-XRCXIG74.mjs +12 -0
  10. package/dist/lib/browser/chunk-XRCXIG74.mjs.map +7 -0
  11. package/dist/lib/browser/edge/index.mjs +7 -63
  12. package/dist/lib/browser/edge/index.mjs.map +4 -4
  13. package/dist/lib/browser/index.mjs +99 -367
  14. package/dist/lib/browser/index.mjs.map +4 -4
  15. package/dist/lib/browser/meta.json +1 -1
  16. package/dist/lib/browser/testing/index.mjs +670 -0
  17. package/dist/lib/browser/testing/index.mjs.map +7 -0
  18. package/dist/lib/browser/types/index.mjs +51 -0
  19. package/dist/lib/browser/types/index.mjs.map +7 -0
  20. package/dist/lib/node/bundler/index.cjs +1 -0
  21. package/dist/lib/node/bundler/index.cjs.map +1 -1
  22. package/dist/lib/node/chunk-FBIUZ7SD.cjs +496 -0
  23. package/dist/lib/node/chunk-FBIUZ7SD.cjs.map +7 -0
  24. package/dist/lib/node/chunk-JEQ2X3Z6.cjs +34 -0
  25. package/dist/lib/node/chunk-JEQ2X3Z6.cjs.map +7 -0
  26. package/dist/lib/node/chunk-NXZNXVT3.cjs +94 -0
  27. package/dist/lib/node/chunk-NXZNXVT3.cjs.map +7 -0
  28. package/dist/lib/node/chunk-SV5NRE5L.cjs +395 -0
  29. package/dist/lib/node/chunk-SV5NRE5L.cjs.map +7 -0
  30. package/dist/lib/node/edge/index.cjs +5 -65
  31. package/dist/lib/node/edge/index.cjs.map +4 -4
  32. package/dist/lib/node/index.cjs +94 -382
  33. package/dist/lib/node/index.cjs.map +4 -4
  34. package/dist/lib/node/meta.json +1 -1
  35. package/dist/lib/node/testing/index.cjs +687 -0
  36. package/dist/lib/node/testing/index.cjs.map +7 -0
  37. package/dist/lib/node/types/index.cjs +72 -0
  38. package/dist/lib/node/types/index.cjs.map +7 -0
  39. package/dist/lib/node-esm/bundler/index.mjs +1 -0
  40. package/dist/lib/node-esm/bundler/index.mjs.map +1 -1
  41. package/dist/lib/node-esm/chunk-3XMJFSID.mjs +360 -0
  42. package/dist/lib/node-esm/chunk-3XMJFSID.mjs.map +7 -0
  43. package/dist/lib/node-esm/chunk-C6YINTWG.mjs +482 -0
  44. package/dist/lib/node-esm/chunk-C6YINTWG.mjs.map +7 -0
  45. package/dist/lib/node-esm/chunk-DHGBFXSZ.mjs +12 -0
  46. package/dist/lib/node-esm/chunk-DHGBFXSZ.mjs.map +7 -0
  47. package/dist/lib/node-esm/chunk-O2SXVYU5.mjs +72 -0
  48. package/dist/lib/node-esm/chunk-O2SXVYU5.mjs.map +7 -0
  49. package/dist/lib/node-esm/edge/index.mjs +6 -64
  50. package/dist/lib/node-esm/edge/index.mjs.map +4 -4
  51. package/dist/lib/node-esm/index.mjs +97 -367
  52. package/dist/lib/node-esm/index.mjs.map +4 -4
  53. package/dist/lib/node-esm/meta.json +1 -1
  54. package/dist/lib/node-esm/testing/index.mjs +670 -0
  55. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  56. package/dist/lib/node-esm/types/index.mjs +51 -0
  57. package/dist/lib/node-esm/types/index.mjs.map +7 -0
  58. package/dist/types/src/browser/index.d.ts +2 -0
  59. package/dist/types/src/browser/index.d.ts.map +1 -0
  60. package/dist/types/src/edge/index.d.ts.map +1 -1
  61. package/dist/types/src/function/function-registry.d.ts +25 -0
  62. package/dist/types/src/function/function-registry.d.ts.map +1 -0
  63. package/dist/types/src/function/function-registry.test.d.ts +2 -0
  64. package/dist/types/src/function/function-registry.test.d.ts.map +1 -0
  65. package/dist/types/src/function/index.d.ts +2 -0
  66. package/dist/types/src/function/index.d.ts.map +1 -0
  67. package/dist/types/src/handler.d.ts +61 -12
  68. package/dist/types/src/handler.d.ts.map +1 -1
  69. package/dist/types/src/index.d.ts +3 -3
  70. package/dist/types/src/index.d.ts.map +1 -1
  71. package/dist/types/src/runtime/dev-server.d.ts +52 -0
  72. package/dist/types/src/runtime/dev-server.d.ts.map +1 -0
  73. package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
  74. package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
  75. package/dist/types/src/runtime/index.d.ts +3 -0
  76. package/dist/types/src/runtime/index.d.ts.map +1 -0
  77. package/dist/types/src/runtime/scheduler.d.ts +34 -0
  78. package/dist/types/src/runtime/scheduler.d.ts.map +1 -0
  79. package/dist/types/src/runtime/scheduler.test.d.ts +2 -0
  80. package/dist/types/src/runtime/scheduler.test.d.ts.map +1 -0
  81. package/dist/types/src/testing/functions-integration.test.d.ts +2 -0
  82. package/dist/types/src/testing/functions-integration.test.d.ts.map +1 -0
  83. package/dist/types/src/testing/index.d.ts +5 -0
  84. package/dist/types/src/testing/index.d.ts.map +1 -0
  85. package/dist/types/src/testing/manifest.d.ts +3 -0
  86. package/dist/types/src/testing/manifest.d.ts.map +1 -0
  87. package/dist/types/src/testing/plugin-init.d.ts +6 -0
  88. package/dist/types/src/testing/plugin-init.d.ts.map +1 -0
  89. package/dist/types/src/testing/setup.d.ts +15 -0
  90. package/dist/types/src/testing/setup.d.ts.map +1 -0
  91. package/dist/types/src/testing/test/handler.d.ts +4 -0
  92. package/dist/types/src/testing/test/handler.d.ts.map +1 -0
  93. package/dist/types/src/testing/test/index.d.ts +3 -0
  94. package/dist/types/src/testing/test/index.d.ts.map +1 -0
  95. package/dist/types/src/testing/types.d.ts +10 -0
  96. package/dist/types/src/testing/types.d.ts.map +1 -0
  97. package/dist/types/src/testing/util.d.ts +5 -0
  98. package/dist/types/src/testing/util.d.ts.map +1 -0
  99. package/dist/types/src/trigger/index.d.ts +3 -0
  100. package/dist/types/src/trigger/index.d.ts.map +1 -0
  101. package/dist/types/src/trigger/trigger-registry.d.ts +38 -0
  102. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -0
  103. package/dist/types/src/trigger/trigger-registry.test.d.ts +2 -0
  104. package/dist/types/src/trigger/trigger-registry.test.d.ts.map +1 -0
  105. package/dist/types/src/trigger/type/index.d.ts +3 -0
  106. package/dist/types/src/trigger/type/index.d.ts.map +1 -0
  107. package/dist/types/src/trigger/type/subscription-trigger.d.ts +4 -0
  108. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -0
  109. package/dist/types/src/trigger/type/timer-trigger.d.ts +4 -0
  110. package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -0
  111. package/dist/types/src/trigger/type/webhook-trigger.d.ts +4 -0
  112. package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -0
  113. package/dist/types/src/types/index.d.ts +5 -0
  114. package/dist/types/src/types/index.d.ts.map +1 -0
  115. package/dist/types/src/types/schema.d.ts +53 -0
  116. package/dist/types/src/types/schema.d.ts.map +1 -0
  117. package/dist/types/src/types/trace.d.ts +146 -0
  118. package/dist/types/src/types/trace.d.ts.map +1 -0
  119. package/dist/types/src/{types.d.ts → types/types.d.ts} +49 -191
  120. package/dist/types/src/types/types.d.ts.map +1 -0
  121. package/dist/types/src/types/url.d.ts.map +1 -0
  122. package/dist/types/tools/schema.d.ts +2 -0
  123. package/dist/types/tools/schema.d.ts.map +1 -0
  124. package/package.json +36 -20
  125. package/schema/functions.json +211 -0
  126. package/src/browser/index.ts +5 -0
  127. package/src/edge/index.ts +0 -4
  128. package/src/function/function-registry.test.ts +118 -0
  129. package/src/function/function-registry.ts +104 -0
  130. package/src/function/index.ts +5 -0
  131. package/src/handler.ts +118 -14
  132. package/src/index.ts +5 -4
  133. package/src/runtime/dev-server.test.ts +79 -0
  134. package/src/runtime/dev-server.ts +240 -0
  135. package/src/runtime/index.ts +6 -0
  136. package/src/runtime/scheduler.test.ts +152 -0
  137. package/src/runtime/scheduler.ts +170 -0
  138. package/src/testing/functions-integration.test.ts +65 -0
  139. package/src/testing/index.ts +8 -0
  140. package/src/testing/manifest.ts +15 -0
  141. package/src/testing/plugin-init.ts +20 -0
  142. package/src/testing/setup.ts +109 -0
  143. package/src/testing/test/handler.ts +15 -0
  144. package/src/testing/test/index.ts +7 -0
  145. package/src/testing/types.ts +9 -0
  146. package/src/testing/util.ts +26 -0
  147. package/src/translations.ts +1 -1
  148. package/src/trigger/index.ts +6 -0
  149. package/src/trigger/trigger-registry.test.ts +278 -0
  150. package/src/trigger/trigger-registry.ts +218 -0
  151. package/src/trigger/type/index.ts +7 -0
  152. package/src/trigger/type/subscription-trigger.ts +84 -0
  153. package/src/trigger/type/timer-trigger.ts +48 -0
  154. package/src/trigger/type/webhook-trigger.ts +48 -0
  155. package/src/types/index.ts +8 -0
  156. package/src/types/schema.ts +46 -0
  157. package/src/{trace.ts → types/trace.ts} +31 -33
  158. package/src/types/types.ts +163 -0
  159. package/dist/types/src/schema.d.ts +0 -57
  160. package/dist/types/src/schema.d.ts.map +0 -1
  161. package/dist/types/src/trace.d.ts +0 -148
  162. package/dist/types/src/trace.d.ts.map +0 -1
  163. package/dist/types/src/types.d.ts.map +0 -1
  164. package/dist/types/src/url.d.ts.map +0 -1
  165. package/src/schema.ts +0 -53
  166. package/src/types.ts +0 -214
  167. /package/dist/types/src/{url.d.ts → types/url.d.ts} +0 -0
  168. /package/src/{url.ts → types/url.ts} +0 -0
package/src/handler.ts CHANGED
@@ -6,10 +6,14 @@ import { Schema as S } from 'effect';
6
6
  import { type Effect } from 'effect';
7
7
 
8
8
  import { type AIServiceClient } from '@dxos/assistant';
9
- import type { CoreDatabase, EchoDatabase } from '@dxos/echo-db';
9
+ import { type Client, PublicKey } from '@dxos/client';
10
+ import { type Space } from '@dxos/client/echo';
11
+ import type { CoreDatabase, EchoDatabase, ReactiveEchoObject } from '@dxos/echo-db';
10
12
  import { type HasId } from '@dxos/echo-schema';
11
13
  import { type SpaceId, type DXN } from '@dxos/keys';
14
+ import { log } from '@dxos/log';
12
15
  import { type QueryResult } from '@dxos/protocols';
16
+ import { isNonNullable } from '@dxos/util';
13
17
 
14
18
  // TODO(burdon): Model after http request. Ref Lambda/OpenFaaS.
15
19
  // https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html
@@ -19,18 +23,13 @@ import { type QueryResult } from '@dxos/protocols';
19
23
  /**
20
24
  * Function handler.
21
25
  */
22
- export type FunctionHandler<TData = {}, TOutput = any> = (params: {
23
- /**
24
- * Services and context available to the function.
25
- */
26
+ export type FunctionHandler<TData = {}, TMeta = {}, TOutput = any> = (params: {
26
27
  context: FunctionContext;
27
-
28
+ event: FunctionEvent<TData, TMeta>;
28
29
  /**
29
- * Data passed as the input to the function.
30
- * Must match the function's input schema.
31
- * This will be the payload from the trigger or other data passed into the function in a workflow.
30
+ * @deprecated
32
31
  */
33
- data: TData;
32
+ response: FunctionResponse;
34
33
  }) => TOutput | Promise<TOutput> | Effect.Effect<TOutput, any>;
35
34
 
36
35
  /**
@@ -45,6 +44,17 @@ export interface FunctionContext {
45
44
  space: SpaceAPI | undefined;
46
45
 
47
46
  ai: AIServiceClient;
47
+
48
+ /**
49
+ * @deprecated
50
+ */
51
+ // TODO(burdon): Limit access to individual space.
52
+ client: Client;
53
+ /**
54
+ * @deprecated
55
+ */
56
+ // TODO(burdon): Replace with storage service abstraction.
57
+ dataDir?: string;
48
58
  }
49
59
 
50
60
  export interface FunctionContextAi {
@@ -52,6 +62,28 @@ export interface FunctionContextAi {
52
62
  run(model: string, inputs: any, options?: any): Promise<any>;
53
63
  }
54
64
 
65
+ /**
66
+ * Event payload.
67
+ */
68
+ // TODO(dmaretskyi): Update type definitions to match the actual payload.
69
+ export type FunctionEvent<TData = {}, TMeta = {}> = {
70
+ data: FunctionEventMeta<TMeta> & TData;
71
+ };
72
+
73
+ /**
74
+ * Metadata from trigger.
75
+ */
76
+ export type FunctionEventMeta<TMeta = {}> = {
77
+ meta: TMeta;
78
+ };
79
+
80
+ /**
81
+ * Function response.
82
+ */
83
+ export type FunctionResponse = {
84
+ status(code: number): FunctionResponse;
85
+ };
86
+
55
87
  //
56
88
  // API.
57
89
  //
@@ -77,19 +109,27 @@ export interface SpaceAPI {
77
109
  get queues(): QueuesAPI;
78
110
  }
79
111
 
112
+ // TODO(wittjosiah): Fix this.
80
113
  const __assertFunctionSpaceIsCompatibleWithTheClientSpace = () => {
81
- const _: SpaceAPI = {} as SpaceAPI;
114
+ // const _: SpaceAPI = {} as Space;
115
+ };
116
+
117
+ export type FunctionDefinition = {
118
+ description?: string;
119
+ inputSchema: S.Schema.AnyNoContext;
120
+ outputSchema?: S.Schema.AnyNoContext;
121
+ handler: FunctionHandler<any>;
82
122
  };
83
123
 
84
- export type FunctionDefinition<T = {}, O = any> = {
124
+ export type DefineFunctionParams<T, O = any> = {
85
125
  description?: string;
86
126
  inputSchema: S.Schema<T, any>;
87
127
  outputSchema?: S.Schema<O, any>;
88
- handler: FunctionHandler<T, O>;
128
+ handler: FunctionHandler<T, any, O>;
89
129
  };
90
130
 
91
131
  // TODO(dmaretskyi): Bind input type to function handler.
92
- export const defineFunction = <T, O>(params: FunctionDefinition<T, O>): FunctionDefinition<T, O> => {
132
+ export const defineFunction = <T, O>(params: DefineFunctionParams<T, O>): FunctionDefinition => {
93
133
  if (!S.isSchema(params.inputSchema)) {
94
134
  throw new Error('Input schema must be a valid schema');
95
135
  }
@@ -104,3 +144,67 @@ export const defineFunction = <T, O>(params: FunctionDefinition<T, O>): Function
104
144
  handler: params.handler,
105
145
  };
106
146
  };
147
+
148
+ //
149
+ // Subscription utils.
150
+ //
151
+
152
+ export type RawSubscriptionData = {
153
+ spaceKey?: string;
154
+ objects?: string[];
155
+ };
156
+
157
+ export type SubscriptionData = {
158
+ space?: Space;
159
+ objects?: ReactiveEchoObject<any>[];
160
+ };
161
+
162
+ /**
163
+ * Handler wrapper for subscription events; extracts space and objects.
164
+ *
165
+ * To test:
166
+ * ```
167
+ * curl -s -X POST -H "Content-Type: application/json" --data '{"space": "0446...1cbb"}' http://localhost:7100/dev/email-extractor
168
+ * ```
169
+ *
170
+ * NOTE: Get space key from devtools or `dx space list --json`
171
+ */
172
+ // TODO(burdon): Evolve into plugin definition like Composer.
173
+ export const subscriptionHandler = <TMeta>(
174
+ handler: FunctionHandler<SubscriptionData, TMeta>,
175
+ types?: S.Schema.AnyNoContext[],
176
+ ): FunctionHandler<RawSubscriptionData, TMeta> => {
177
+ return async ({ event: { data }, context, response, ...rest }) => {
178
+ const { client } = context;
179
+ const space = data.spaceKey ? client.spaces.get(PublicKey.from(data.spaceKey)) : undefined;
180
+ if (!space) {
181
+ log.error('Invalid space');
182
+ return response.status(500);
183
+ }
184
+
185
+ registerTypes(space, types);
186
+ const objects = space
187
+ ? data.objects
188
+ ?.map<ReactiveEchoObject<any> | undefined>((id) => space!.db.getObjectById(id))
189
+ .filter(isNonNullable)
190
+ : [];
191
+
192
+ if (!!data.spaceKey && !space) {
193
+ log.warn('invalid space', { data });
194
+ } else {
195
+ log.info('handler', { space: space?.key.truncate(), objects: objects?.length });
196
+ }
197
+
198
+ return handler({ event: { data: { ...data, space, objects } }, context, response, ...rest });
199
+ };
200
+ };
201
+
202
+ // TODO(burdon): Evolve types as part of function metadata.
203
+ const registerTypes = (space: Space, types: S.Schema.AnyNoContext[] = []) => {
204
+ const registry = space.db.graph.schemaRegistry;
205
+ for (const type of types) {
206
+ if (!registry.hasSchema(type)) {
207
+ registry.addSchema([type]);
208
+ }
209
+ }
210
+ };
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './edge';
6
+ export * from './function';
5
7
  export * from './handler';
6
- export * from './schema';
7
- export * from './trace';
8
+ // export * from './runtime';
9
+ export * from './trigger';
8
10
  export * from './types';
9
- export * from './url';
@@ -0,0 +1,79 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { getRandomPort } from 'get-port-please';
6
+ import path from 'node:path';
7
+ import { afterAll, beforeAll, describe, expect, test } from 'vitest';
8
+
9
+ import { sleep, waitForCondition } from '@dxos/async';
10
+ import { type Client } from '@dxos/client';
11
+ import { TestBuilder } from '@dxos/client/testing';
12
+
13
+ import { DevServer } from './dev-server';
14
+ import { FunctionRegistry } from '../function';
15
+ import { createFunctionRuntime, testFunctionManifest } from '../testing';
16
+ import { initFunctionsPlugin } from '../testing/plugin-init';
17
+
18
+ // TODO(wittjosiah): Doesn't work in vitest.
19
+ describe.skip('dev server', () => {
20
+ let client: Client;
21
+ let testBuilder: TestBuilder;
22
+
23
+ beforeAll(async () => {
24
+ testBuilder = new TestBuilder();
25
+ client = await createFunctionRuntime(testBuilder, initFunctionsPlugin);
26
+ expect(client.services.services.FunctionRegistryService).to.exist;
27
+ });
28
+
29
+ afterAll(async () => {
30
+ await testBuilder.destroy();
31
+ });
32
+
33
+ test('function registry open after dev server started', async () => {
34
+ const { space, registry, server } = await setupTest();
35
+ await registry.register(space, testFunctionManifest.functions);
36
+ await server.start();
37
+ await registry.open();
38
+ await waitForCondition({ condition: () => server.functions.length > 0 });
39
+ await expectTestFunctionInvocable(server);
40
+ });
41
+
42
+ test('function registry open before dev server started', async () => {
43
+ const { space, registry, server } = await setupTest();
44
+ await registry.register(space, testFunctionManifest.functions);
45
+ await registry.open();
46
+ await server.start();
47
+ await waitForCondition({ condition: () => server.functions.length > 0 });
48
+ await expectTestFunctionInvocable(server);
49
+ });
50
+
51
+ test('unsubscribes from functions after stopped', async () => {
52
+ const { space, registry, server } = await setupTest();
53
+ await registry.register(space, testFunctionManifest.functions);
54
+ await server.start();
55
+ await server.stop();
56
+ await registry.open();
57
+ await sleep(20);
58
+ expect(server.functions.length).to.eq(0);
59
+ });
60
+
61
+ const expectTestFunctionInvocable = async (server: DevServer) => {
62
+ const seq = server.stats.seq;
63
+ await server.invoke('test', {});
64
+ expect(server.stats.seq).to.eq(seq + 1);
65
+ };
66
+
67
+ const setupTest = async () => {
68
+ const registry = new FunctionRegistry(client);
69
+ const server = new DevServer(client, registry, {
70
+ baseDir: path.join(__dirname, '../testing'),
71
+ port: await getRandomPort('127.0.0.1'),
72
+ });
73
+ const space = await client.spaces.create();
74
+ // TODO(burdon): Doesn't shut down cleanly.
75
+ // Error: invariant violation [this._client.services.services.FunctionRegistryService]
76
+ testBuilder.ctx.onDispose(() => server.stop());
77
+ return { registry, server, space };
78
+ };
79
+ });
@@ -0,0 +1,240 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import express from 'express';
6
+ import { getPort } from 'get-port-please';
7
+ import type http from 'http';
8
+ import { join } from 'node:path';
9
+
10
+ import { asyncTimeout, Event, Trigger } from '@dxos/async';
11
+ import { type Client } from '@dxos/client';
12
+ import { Context } from '@dxos/context';
13
+ import { invariant } from '@dxos/invariant';
14
+ import { log } from '@dxos/log';
15
+
16
+ import { type FunctionRegistry } from '../function';
17
+ import { type FunctionContext, type FunctionEvent, type FunctionHandler, type FunctionResponse } from '../handler';
18
+ import { type FunctionDef } from '../types';
19
+
20
+ const FN_TIMEOUT = 20_000;
21
+
22
+ export type DevServerOptions = {
23
+ baseDir: string;
24
+ port?: number;
25
+ reload?: boolean;
26
+ dataDir?: string;
27
+ };
28
+
29
+ /**
30
+ * Functions dev server provides a local HTTP server for loading and invoking functions.
31
+ * Functions are executed in the context of an authenticated client.
32
+ */
33
+ export class DevServer {
34
+ private _ctx = createContext();
35
+
36
+ // Function handlers indexed by name (URL path).
37
+ private readonly _handlers: Record<string, { def: FunctionDef; handler: FunctionHandler<any> }> = {};
38
+
39
+ private _server?: http.Server;
40
+ private _port?: number;
41
+ private _functionServiceRegistration?: string;
42
+ private _proxy?: string;
43
+ private _seq = 0;
44
+
45
+ public readonly update = new Event<number>();
46
+
47
+ constructor(
48
+ private readonly _client: Client,
49
+ private readonly _functionsRegistry: FunctionRegistry,
50
+ private readonly _options: DevServerOptions,
51
+ ) {}
52
+
53
+ get stats() {
54
+ return {
55
+ seq: this._seq,
56
+ };
57
+ }
58
+
59
+ get endpoint() {
60
+ invariant(this._port);
61
+ return `http://localhost:${this._port}`;
62
+ }
63
+
64
+ get proxy() {
65
+ return this._proxy;
66
+ }
67
+
68
+ get functions() {
69
+ return Object.values(this._handlers);
70
+ }
71
+
72
+ async start() {
73
+ invariant(!this._server);
74
+ log.info('starting...');
75
+ this._ctx = createContext();
76
+
77
+ // TODO(burdon): Change to hono.
78
+ const app = express();
79
+ app.use(express.json());
80
+
81
+ app.post('/:path', async (req, res) => {
82
+ const { path } = req.params;
83
+ try {
84
+ log.info('calling', { path });
85
+ if (this._options.reload) {
86
+ const { def } = this._handlers['/' + path];
87
+ await this._load(def, true);
88
+ }
89
+
90
+ // TODO(burdon): Get function context.
91
+ res.statusCode = await asyncTimeout(this.invoke('/' + path, req.body), FN_TIMEOUT);
92
+ res.end();
93
+ } catch (err: any) {
94
+ log.catch(err);
95
+ res.statusCode = 500;
96
+ res.end();
97
+ }
98
+ });
99
+
100
+ this._port = this._options.port ?? (await getPort({ host: 'localhost', port: 7200, portRange: [7200, 7299] }));
101
+ this._server = app.listen(this._port);
102
+
103
+ try {
104
+ // Register functions.
105
+ const { registrationId, endpoint } = await this._client.services.services.FunctionRegistryService!.register({
106
+ endpoint: this.endpoint,
107
+ });
108
+
109
+ log.info('registered', { endpoint });
110
+ this._proxy = endpoint;
111
+ this._functionServiceRegistration = registrationId;
112
+
113
+ // Open after registration, so that it can be updated with the list of function definitions.
114
+ await this._handleNewFunctions(this._functionsRegistry.getUniqueByUri());
115
+ this._ctx.onDispose(this._functionsRegistry.registered.on(({ added }) => this._handleNewFunctions(added)));
116
+ } catch (err: any) {
117
+ await this.stop();
118
+ throw new Error('FunctionRegistryService not available (check plugin is configured).');
119
+ }
120
+
121
+ log.info('started', { port: this._port });
122
+ }
123
+
124
+ async stop() {
125
+ if (!this._server) {
126
+ return;
127
+ }
128
+
129
+ log.info('stopping...');
130
+ await this._ctx.dispose();
131
+
132
+ const trigger = new Trigger();
133
+ this._server.close(async () => {
134
+ log.info('server stopped');
135
+ try {
136
+ if (this._functionServiceRegistration) {
137
+ invariant(this._client.services.services.FunctionRegistryService);
138
+ await this._client.services.services.FunctionRegistryService.unregister({
139
+ registrationId: this._functionServiceRegistration,
140
+ });
141
+
142
+ log.info('unregistered', { registrationId: this._functionServiceRegistration });
143
+ this._functionServiceRegistration = undefined;
144
+ this._proxy = undefined;
145
+ }
146
+
147
+ trigger.wake();
148
+ } catch (err) {
149
+ trigger.throw(err as Error);
150
+ }
151
+ });
152
+
153
+ await trigger.wait();
154
+ this._port = undefined;
155
+ this._server = undefined;
156
+ log.info('stopped');
157
+ }
158
+
159
+ private async _handleNewFunctions(newFunctions: FunctionDef[]) {
160
+ newFunctions.forEach((def) => this._load(def));
161
+ await this._safeUpdateRegistration();
162
+ log('new functions loaded', { newFunctions });
163
+ }
164
+
165
+ /**
166
+ * Load function.
167
+ */
168
+ private async _load(def: FunctionDef, force?: boolean | undefined) {
169
+ const { uri, route, handler } = def;
170
+ const filePath = join(this._options.baseDir, handler);
171
+ log.info('loading', { uri, force });
172
+
173
+ // Remove from cache.
174
+ if (force) {
175
+ Object.keys(require.cache)
176
+ .filter((key) => key.startsWith(filePath))
177
+ .forEach((key) => {
178
+ delete require.cache[key];
179
+ });
180
+ }
181
+
182
+ // TODO(burdon): Import types.
183
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
184
+ const module = require(filePath);
185
+ if (typeof module.default !== 'function') {
186
+ throw new Error(`Handler must export default function: ${uri}`);
187
+ }
188
+
189
+ this._handlers[route] = { def, handler: module.default };
190
+ }
191
+
192
+ private async _safeUpdateRegistration(): Promise<void> {
193
+ invariant(this._functionServiceRegistration);
194
+ try {
195
+ await this._client.services.services.FunctionRegistryService!.updateRegistration({
196
+ registrationId: this._functionServiceRegistration,
197
+ functions: this.functions.map(({ def: { id, route } }) => ({ id, route })),
198
+ });
199
+ } catch (err) {
200
+ log.catch(err);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Invoke function.
206
+ */
207
+ public async invoke(path: string, data: any): Promise<number> {
208
+ const seq = ++this._seq;
209
+ const now = Date.now();
210
+
211
+ log.info('req', { seq, path });
212
+ const statusCode = await this._invoke(path, { data });
213
+
214
+ log.info('res', { seq, path, statusCode, duration: Date.now() - now });
215
+ this.update.emit(statusCode);
216
+ return statusCode;
217
+ }
218
+
219
+ private async _invoke(path: string, event: FunctionEvent) {
220
+ const { handler } = this._handlers[path] ?? {};
221
+ invariant(handler, `invalid path: ${path}`);
222
+ const context: FunctionContext = {
223
+ client: this._client,
224
+ dataDir: this._options.dataDir,
225
+ } as any;
226
+
227
+ let statusCode = 200;
228
+ const response: FunctionResponse = {
229
+ status: (code: number) => {
230
+ statusCode = code;
231
+ return response;
232
+ },
233
+ };
234
+
235
+ await handler({ context, event, response });
236
+ return statusCode;
237
+ }
238
+ }
239
+
240
+ const createContext = () => new Context({ name: 'DevServer' });
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './dev-server';
6
+ export * from './scheduler';
@@ -0,0 +1,152 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { afterAll, beforeAll, describe, expect, test } from 'vitest';
6
+
7
+ import { Trigger } from '@dxos/async';
8
+ import { type Client } from '@dxos/client';
9
+ import { TestBuilder } from '@dxos/client/testing';
10
+ import { live } from '@dxos/live-object';
11
+
12
+ import { Scheduler, type SchedulerOptions } from './scheduler';
13
+ import { FunctionRegistry } from '../function';
14
+ import { createInitializedClients, TestType, triggerWebhook } from '../testing';
15
+ import { TriggerRegistry } from '../trigger';
16
+ import { TriggerKind, type FunctionManifest } from '../types';
17
+
18
+ // TODO(burdon): Test we can add and remove triggers.
19
+ // Flaky: https://cloud.nx.app/runs/uqhKOBA6JQ/task/functions%3Atest
20
+ describe.skip('scheduler', () => {
21
+ let testBuilder: TestBuilder;
22
+ let client: Client;
23
+
24
+ beforeAll(async () => {
25
+ testBuilder = new TestBuilder();
26
+ client = (await createInitializedClients(testBuilder, 1))[0];
27
+ });
28
+
29
+ afterAll(async () => {
30
+ await testBuilder.destroy();
31
+ });
32
+
33
+ const createScheduler = (callback: SchedulerOptions['callback']) => {
34
+ const scheduler = new Scheduler(new FunctionRegistry(client), new TriggerRegistry(client), { callback });
35
+
36
+ afterAll(async () => {
37
+ await scheduler.stop();
38
+ });
39
+
40
+ return scheduler;
41
+ };
42
+
43
+ test('timer', async () => {
44
+ const manifest: FunctionManifest = {
45
+ functions: [
46
+ {
47
+ uri: 'example.com/function/test',
48
+ route: '/test',
49
+ handler: 'test',
50
+ },
51
+ ],
52
+ triggers: [
53
+ {
54
+ function: 'example.com/function/test',
55
+ enabled: true,
56
+ spec: {
57
+ type: TriggerKind.Timer,
58
+ cron: '0/1 * * * * *', // Every 1s.
59
+ },
60
+ },
61
+ ],
62
+ };
63
+
64
+ let count = 0;
65
+ const done = new Trigger();
66
+ const scheduler = createScheduler(async () => {
67
+ if (++count === 3) {
68
+ done.wake();
69
+ }
70
+ });
71
+ await scheduler.register(client.spaces.default, manifest);
72
+ await scheduler.start();
73
+
74
+ await done.wait({ timeout: 5_000 });
75
+ expect(count).to.equal(3);
76
+ });
77
+
78
+ // Flaky.
79
+ test.skip('webhook', async () => {
80
+ const manifest: FunctionManifest = {
81
+ functions: [
82
+ {
83
+ uri: 'example.com/function/test',
84
+ route: '/test',
85
+ handler: 'test',
86
+ },
87
+ ],
88
+ triggers: [
89
+ {
90
+ function: 'example.com/function/test',
91
+ enabled: true,
92
+ spec: {
93
+ type: TriggerKind.Webhook,
94
+ method: 'GET',
95
+ },
96
+ },
97
+ ],
98
+ };
99
+
100
+ const done = new Trigger();
101
+ const scheduler = createScheduler(async () => {
102
+ done.wake();
103
+ });
104
+ const space = await client.spaces.create();
105
+ await scheduler.register(space, manifest);
106
+ await scheduler.start();
107
+
108
+ setTimeout(async () => triggerWebhook(space, manifest.functions![0].uri));
109
+
110
+ await done.wait();
111
+ });
112
+
113
+ test('subscription', async () => {
114
+ const manifest: FunctionManifest = {
115
+ functions: [
116
+ {
117
+ uri: 'example.com/function/test',
118
+ route: '/test',
119
+ handler: 'test',
120
+ },
121
+ ],
122
+ triggers: [
123
+ {
124
+ function: 'example.com/function/test',
125
+ enabled: true,
126
+ spec: {
127
+ type: TriggerKind.Subscription,
128
+ filter: { type: TestType.typename },
129
+ },
130
+ },
131
+ ],
132
+ };
133
+
134
+ let count = 0;
135
+ const done = new Trigger();
136
+ const scheduler = createScheduler(async () => {
137
+ if (++count === 1) {
138
+ done.wake();
139
+ }
140
+ });
141
+ await scheduler.register(client.spaces.default, manifest);
142
+ await scheduler.start();
143
+
144
+ setTimeout(() => {
145
+ const space = client.spaces.default;
146
+ const object = live(TestType, { title: 'Hello world!' });
147
+ space.db.add(object);
148
+ }, 100);
149
+
150
+ await done.wait();
151
+ });
152
+ });