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

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 (92) hide show
  1. package/dist/lib/browser/chunk-4D4I3YMJ.mjs +86 -0
  2. package/dist/lib/browser/chunk-4D4I3YMJ.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +884 -457
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types.mjs +14 -0
  7. package/dist/lib/browser/types.mjs.map +7 -0
  8. package/dist/lib/node/chunk-3UYUR5N5.cjs +103 -0
  9. package/dist/lib/node/chunk-3UYUR5N5.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +868 -447
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/types.cjs +35 -0
  14. package/dist/lib/node/types.cjs.map +7 -0
  15. package/dist/types/src/browser/index.d.ts +2 -0
  16. package/dist/types/src/browser/index.d.ts.map +1 -0
  17. package/dist/types/src/function/function-registry.d.ts +24 -0
  18. package/dist/types/src/function/function-registry.d.ts.map +1 -0
  19. package/dist/types/src/function/function-registry.test.d.ts +2 -0
  20. package/dist/types/src/function/function-registry.test.d.ts.map +1 -0
  21. package/dist/types/src/function/index.d.ts +2 -0
  22. package/dist/types/src/function/index.d.ts.map +1 -0
  23. package/dist/types/src/handler.d.ts +33 -12
  24. package/dist/types/src/handler.d.ts.map +1 -1
  25. package/dist/types/src/index.d.ts +2 -0
  26. package/dist/types/src/index.d.ts.map +1 -1
  27. package/dist/types/src/runtime/dev-server.d.ts +16 -13
  28. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  29. package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
  30. package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
  31. package/dist/types/src/runtime/scheduler.d.ts +13 -27
  32. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  33. package/dist/types/src/testing/functions-integration.test.d.ts +2 -0
  34. package/dist/types/src/testing/functions-integration.test.d.ts.map +1 -0
  35. package/dist/types/src/testing/index.d.ts +4 -0
  36. package/dist/types/src/testing/index.d.ts.map +1 -0
  37. package/dist/types/src/testing/setup.d.ts +5 -0
  38. package/dist/types/src/testing/setup.d.ts.map +1 -0
  39. package/dist/types/src/testing/test/handler.d.ts +4 -0
  40. package/dist/types/src/testing/test/handler.d.ts.map +1 -0
  41. package/dist/types/src/testing/test/index.d.ts +3 -0
  42. package/dist/types/src/testing/test/index.d.ts.map +1 -0
  43. package/dist/types/src/testing/types.d.ts +9 -0
  44. package/dist/types/src/testing/types.d.ts.map +1 -0
  45. package/dist/types/src/testing/util.d.ts +3 -0
  46. package/dist/types/src/testing/util.d.ts.map +1 -0
  47. package/dist/types/src/trigger/index.d.ts +2 -0
  48. package/dist/types/src/trigger/index.d.ts.map +1 -0
  49. package/dist/types/src/trigger/trigger-registry.d.ts +37 -0
  50. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -0
  51. package/dist/types/src/trigger/trigger-registry.test.d.ts +2 -0
  52. package/dist/types/src/trigger/trigger-registry.test.d.ts.map +1 -0
  53. package/dist/types/src/trigger/type/index.d.ts +5 -0
  54. package/dist/types/src/trigger/type/index.d.ts.map +1 -0
  55. package/dist/types/src/trigger/type/subscription-trigger.d.ts +4 -0
  56. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -0
  57. package/dist/types/src/trigger/type/timer-trigger.d.ts +4 -0
  58. package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -0
  59. package/dist/types/src/trigger/type/webhook-trigger.d.ts +4 -0
  60. package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -0
  61. package/dist/types/src/trigger/type/websocket-trigger.d.ts +13 -0
  62. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -0
  63. package/dist/types/src/types.d.ts +162 -107
  64. package/dist/types/src/types.d.ts.map +1 -1
  65. package/package.json +33 -15
  66. package/schema/functions.json +144 -103
  67. package/src/browser/index.ts +5 -0
  68. package/src/function/function-registry.test.ts +105 -0
  69. package/src/function/function-registry.ts +90 -0
  70. package/src/function/index.ts +5 -0
  71. package/src/handler.ts +54 -31
  72. package/src/index.ts +2 -0
  73. package/src/runtime/dev-server.test.ts +60 -0
  74. package/src/runtime/dev-server.ts +104 -53
  75. package/src/runtime/scheduler.test.ts +61 -73
  76. package/src/runtime/scheduler.ts +91 -270
  77. package/src/testing/functions-integration.test.ts +100 -0
  78. package/src/testing/index.ts +7 -0
  79. package/src/testing/setup.ts +43 -0
  80. package/src/testing/test/handler.ts +15 -0
  81. package/src/testing/test/index.ts +7 -0
  82. package/src/testing/types.ts +9 -0
  83. package/src/testing/util.ts +16 -0
  84. package/src/trigger/index.ts +5 -0
  85. package/src/trigger/trigger-registry.test.ts +272 -0
  86. package/src/trigger/trigger-registry.ts +211 -0
  87. package/src/trigger/type/index.ts +8 -0
  88. package/src/trigger/type/subscription-trigger.ts +86 -0
  89. package/src/trigger/type/timer-trigger.ts +45 -0
  90. package/src/trigger/type/webhook-trigger.ts +48 -0
  91. package/src/trigger/type/websocket-trigger.ts +92 -0
  92. package/src/types.ts +87 -48
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
@@ -2,6 +2,8 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './function';
5
6
  export * from './handler';
6
7
  export * from './runtime';
8
+ export * from './trigger';
7
9
  export * from './types';
@@ -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 '../function';
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.functions);
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
+ });
@@ -7,18 +7,19 @@ import { getPort } from 'get-port-please';
7
7
  import type http from 'http';
8
8
  import { join } from 'node:path';
9
9
 
10
- import { Trigger } from '@dxos/async';
10
+ import { Event, Trigger } from '@dxos/async';
11
11
  import { type Client } from '@dxos/client';
12
+ import { Context } from '@dxos/context';
12
13
  import { invariant } from '@dxos/invariant';
13
14
  import { log } from '@dxos/log';
14
15
 
15
- import { type FunctionContext, type FunctionHandler, type Response } from '../handler';
16
- import { type FunctionDef, type FunctionManifest } from '../types';
16
+ import { type FunctionRegistry } from '../function';
17
+ import { type FunctionContext, type FunctionEvent, type FunctionHandler, type FunctionResponse } from '../handler';
18
+ import { type FunctionDef } from '../types';
17
19
 
18
20
  export type DevServerOptions = {
21
+ baseDir: string;
19
22
  port?: number;
20
- directory: string;
21
- manifest: FunctionManifest;
22
23
  reload?: boolean;
23
24
  dataDir?: string;
24
25
  };
@@ -27,20 +28,37 @@ export type DevServerOptions = {
27
28
  * Functions dev server provides a local HTTP server for testing functions.
28
29
  */
29
30
  export class DevServer {
31
+ private _ctx = createContext();
32
+
30
33
  // Function handlers indexed by name (URL path).
31
34
  private readonly _handlers: Record<string, { def: FunctionDef; handler: FunctionHandler<any> }> = {};
32
35
 
33
36
  private _server?: http.Server;
34
37
  private _port?: number;
35
- private _registrationId?: string;
38
+ private _functionServiceRegistration?: string;
36
39
  private _proxy?: string;
37
40
  private _seq = 0;
38
41
 
39
- // prettier-ignore
42
+ public readonly update = new Event<number>();
43
+
40
44
  constructor(
41
45
  private readonly _client: Client,
46
+ private readonly _functionsRegistry: FunctionRegistry,
42
47
  private readonly _options: DevServerOptions,
43
- ) {}
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
+ }
56
+
57
+ get stats() {
58
+ return {
59
+ seq: this._seq,
60
+ };
61
+ }
44
62
 
45
63
  get endpoint() {
46
64
  invariant(this._port);
@@ -55,30 +73,26 @@ export class DevServer {
55
73
  return Object.values(this._handlers);
56
74
  }
57
75
 
58
- async initialize() {
59
- for (const def of this._options.manifest.functions) {
60
- try {
61
- await this._load(def);
62
- } catch (err) {
63
- log.error('parsing function (check manifest)', err);
64
- }
65
- }
66
- }
67
-
68
76
  async start() {
77
+ invariant(!this._server);
78
+ log.info('starting...');
79
+ this._ctx = createContext();
80
+
81
+ // TODO(burdon): Move to hono.
69
82
  const app = express();
70
83
  app.use(express.json());
71
84
 
72
- app.post('/:name', async (req, res) => {
73
- const { name } = req.params;
85
+ app.post('/:path', async (req, res) => {
86
+ const { path } = req.params;
74
87
  try {
75
- log.info('calling', { name });
88
+ log.info('calling', { path });
76
89
  if (this._options.reload) {
77
- const { def } = this._handlers[name];
90
+ const { def } = this._handlers['/' + path];
78
91
  await this._load(def, true);
79
92
  }
80
93
 
81
- res.statusCode = await this._invoke(name, req.body);
94
+ // TODO(burdon): Get function context.
95
+ res.statusCode = await this.invoke('/' + path, req.body);
82
96
  res.end();
83
97
  } catch (err: any) {
84
98
  log.catch(err);
@@ -94,80 +108,117 @@ export class DevServer {
94
108
  // Register functions.
95
109
  const { registrationId, endpoint } = await this._client.services.services.FunctionRegistryService!.register({
96
110
  endpoint: this.endpoint,
97
- functions: this.functions.map(({ def: { name } }) => ({ name })),
98
111
  });
99
112
 
100
- log.info('registered', { registrationId, endpoint });
101
- this._registrationId = registrationId;
113
+ log.info('registered', { endpoint });
102
114
  this._proxy = endpoint;
115
+ this._functionServiceRegistration = registrationId;
116
+
117
+ // Open after registration, so that it can be updated with the list of function definitions.
118
+ await this._functionsRegistry.open(this._ctx);
103
119
  } catch (err: any) {
104
120
  await this.stop();
105
121
  throw new Error('FunctionRegistryService not available (check plugin is configured).');
106
122
  }
123
+
124
+ log.info('started', { port: this._port });
107
125
  }
108
126
 
109
127
  async stop() {
128
+ invariant(this._server);
129
+ log.info('stopping...');
130
+
110
131
  const trigger = new Trigger();
111
- this._server?.close(async () => {
112
- if (this._registrationId) {
113
- await this._client.services.services.FunctionRegistryService!.unregister({
114
- registrationId: this._registrationId,
115
- });
132
+ this._server.close(async () => {
133
+ log.info('server stopped');
134
+ try {
135
+ if (this._functionServiceRegistration) {
136
+ invariant(this._client.services.services.FunctionRegistryService);
137
+ await this._client.services.services.FunctionRegistryService.unregister({
138
+ registrationId: this._functionServiceRegistration,
139
+ });
140
+
141
+ log.info('unregistered', { registrationId: this._functionServiceRegistration });
142
+ this._functionServiceRegistration = undefined;
143
+ this._proxy = undefined;
144
+ }
116
145
 
117
- log.info('unregistered', { registrationId: this._registrationId });
118
- this._registrationId = undefined;
119
- this._proxy = undefined;
146
+ trigger.wake();
147
+ } catch (err) {
148
+ trigger.throw(err as Error);
120
149
  }
121
-
122
- trigger.wake();
123
150
  });
124
151
 
125
152
  await trigger.wait();
126
153
  this._port = undefined;
127
154
  this._server = undefined;
155
+ log.info('stopped');
128
156
  }
129
157
 
130
158
  /**
131
159
  * Load function.
132
160
  */
133
- private async _load(def: FunctionDef, flush = false) {
134
- const { id, name, handler } = def;
135
- const path = join(this._options.directory, handler);
136
- log.info('loading', { id });
161
+ private async _load(def: FunctionDef, force?: boolean | undefined) {
162
+ const { uri, route, handler } = def;
163
+ const filePath = join(this._options.baseDir, handler);
164
+ log.info('loading', { uri, force });
137
165
 
138
166
  // Remove from cache.
139
- if (flush) {
167
+ if (force) {
140
168
  Object.keys(require.cache)
141
- .filter((key) => key.startsWith(path))
142
- .forEach((key) => delete require.cache[key]);
169
+ .filter((key) => key.startsWith(filePath))
170
+ .forEach((key) => {
171
+ delete require.cache[key];
172
+ });
143
173
  }
144
174
 
175
+ // TODO(burdon): Import types.
145
176
  // eslint-disable-next-line @typescript-eslint/no-var-requires
146
- const module = require(path);
177
+ const module = require(filePath);
147
178
  if (typeof module.default !== 'function') {
148
- throw new Error(`Handler must export default function: ${id}`);
179
+ throw new Error(`Handler must export default function: ${uri}`);
149
180
  }
150
181
 
151
- this._handlers[name] = { def, handler: module.default };
182
+ this._handlers[route] = { def, handler: module.default };
183
+ }
184
+
185
+ private async _safeUpdateRegistration(): Promise<void> {
186
+ invariant(this._functionServiceRegistration);
187
+ try {
188
+ await this._client.services.services.FunctionRegistryService!.updateRegistration({
189
+ registrationId: this._functionServiceRegistration,
190
+ functions: this.functions.map(({ def: { id, route } }) => ({ id, route })),
191
+ });
192
+ } catch (err) {
193
+ log.catch(err);
194
+ }
152
195
  }
153
196
 
154
197
  /**
155
- * Invoke function handler.
198
+ * Invoke function.
156
199
  */
157
- private async _invoke(name: string, event: any) {
200
+ public async invoke(path: string, data: any): Promise<number> {
158
201
  const seq = ++this._seq;
159
202
  const now = Date.now();
160
203
 
161
- log.info('req', { seq, name });
162
- const { handler } = this._handlers[name];
204
+ log.info('req', { seq, path });
205
+ const statusCode = await this._invoke(path, { data });
206
+
207
+ log.info('res', { seq, path, statusCode, duration: Date.now() - now });
208
+ this.update.emit(statusCode);
209
+ return statusCode;
210
+ }
163
211
 
212
+ private async _invoke(path: string, event: FunctionEvent) {
213
+ const { handler } = this._handlers[path] ?? {};
214
+ invariant(handler, `invalid path: ${path}`);
164
215
  const context: FunctionContext = {
165
216
  client: this._client,
166
217
  dataDir: this._options.dataDir,
167
218
  };
168
219
 
169
220
  let statusCode = 200;
170
- const response: Response = {
221
+ const response: FunctionResponse = {
171
222
  status: (code: number) => {
172
223
  statusCode = code;
173
224
  return response;
@@ -175,8 +226,8 @@ export class DevServer {
175
226
  };
176
227
 
177
228
  await handler({ context, event, response });
178
- log.info('res', { seq, name, statusCode, duration: Date.now() - now });
179
-
180
229
  return statusCode;
181
230
  }
182
231
  }
232
+
233
+ const createContext = () => new Context({ name: 'DevServer' });