@dxos/functions 0.3.7 → 0.3.8-main.0ae9c21

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/functions",
3
- "version": "0.3.7",
3
+ "version": "0.3.8-main.0ae9c21",
4
4
  "description": "Functions SDK and runtime.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -20,15 +20,15 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "express": "^4.17.1",
23
- "portfinder": "^1.0.32",
24
- "@dxos/async": "0.3.7",
25
- "@dxos/client": "0.3.7",
26
- "@dxos/context": "0.3.7",
27
- "@dxos/echo-schema": "0.3.7",
28
- "@dxos/invariant": "0.3.7",
29
- "@dxos/log": "0.3.7",
30
- "@dxos/node-std": "0.3.7",
31
- "@dxos/util": "0.3.7"
23
+ "get-port-please": "^3.1.1",
24
+ "@dxos/async": "0.3.8-main.0ae9c21",
25
+ "@dxos/context": "0.3.8-main.0ae9c21",
26
+ "@dxos/invariant": "0.3.8-main.0ae9c21",
27
+ "@dxos/echo-schema": "0.3.8-main.0ae9c21",
28
+ "@dxos/log": "0.3.8-main.0ae9c21",
29
+ "@dxos/node-std": "0.3.8-main.0ae9c21",
30
+ "@dxos/util": "0.3.8-main.0ae9c21",
31
+ "@dxos/client": "0.3.8-main.0ae9c21"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/express": "^4.17.17"
package/src/handler.ts ADDED
@@ -0,0 +1,29 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Client } from '@dxos/client';
6
+
7
+ export interface Response {
8
+ status(code: number): Response;
9
+ succeed(data?: object): Response;
10
+ }
11
+
12
+ export interface FunctionContext {
13
+ client: Client;
14
+ status(code: number): Response;
15
+ }
16
+
17
+ /**
18
+ * Function handler.
19
+ */
20
+ export type FunctionHandler<T extends {}> = (params: {
21
+ context: FunctionContext;
22
+ event: T;
23
+ }) => Promise<Response | void>;
24
+
25
+ // TODO(burdon): Types.
26
+ export type FunctionSubscriptionEvent = {
27
+ space: string;
28
+ objects: string[];
29
+ };
package/src/index.ts CHANGED
@@ -2,5 +2,6 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './function';
5
+ export * from './handler';
6
+ export * from './manifest';
6
7
  export * from './runtime';
@@ -0,0 +1,36 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export type FunctionDef = {
6
+ // FQ function name.
7
+ id: string;
8
+ // URL path.
9
+ path: string;
10
+ // Path of handler.
11
+ handler: string;
12
+ description?: string;
13
+ };
14
+
15
+ export type TriggerSubscription = {
16
+ type: string;
17
+ spaceKey: string;
18
+ props?: Record<string, any>;
19
+ nested?: string[];
20
+ };
21
+
22
+ // TODO(burdon): Generalize binding.
23
+ // https://www.npmjs.com/package/aws-lambda
24
+ // https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html
25
+ export type FunctionTrigger = {
26
+ function: string;
27
+ subscription: TriggerSubscription;
28
+ };
29
+
30
+ /**
31
+ * Function manifest file.
32
+ */
33
+ export type FunctionManifest = {
34
+ functions: FunctionDef[];
35
+ triggers: FunctionTrigger[];
36
+ };
@@ -3,32 +3,35 @@
3
3
  //
4
4
 
5
5
  import express from 'express';
6
+ import { getPort } from 'get-port-please';
6
7
  import type http from 'http';
7
8
  import { join } from 'node:path';
8
- import { getPortPromise } from 'portfinder';
9
9
 
10
10
  import { Trigger } from '@dxos/async';
11
11
  import { type Client } from '@dxos/client';
12
+ import { invariant } from '@dxos/invariant';
12
13
  import { log } from '@dxos/log';
13
14
 
14
- import { type FunctionContext, type FunctionHandler, type FunctionsManifest, type Response } from '../function';
15
-
16
- const DEFAULT_PORT = 7000;
15
+ import { type FunctionContext, type FunctionHandler, type Response } from '../handler';
16
+ import { type FunctionDef, type FunctionManifest } from '../manifest';
17
17
 
18
18
  export type DevServerOptions = {
19
+ port?: number;
19
20
  directory: string;
20
- manifest: FunctionsManifest;
21
+ manifest: FunctionManifest;
21
22
  };
22
23
 
23
24
  /**
24
25
  * Functions dev server provides a local HTTP server for testing functions.
25
26
  */
27
+ // TODO(burdon): Reconcile with agent/functions dev dispatcher.
26
28
  export class DevServer {
27
- private readonly _handlers: Record<string, FunctionHandler<any>> = {};
29
+ private readonly _handlers: Record<string, { def: FunctionDef; handler: FunctionHandler<any> }> = {};
28
30
 
29
31
  private _server?: http.Server;
30
32
  private _port?: number;
31
33
  private _registrationId?: string;
34
+ private _proxy?: string;
32
35
 
33
36
  // prettier-ignore
34
37
  constructor(
@@ -36,31 +39,37 @@ export class DevServer {
36
39
  private readonly _options: DevServerOptions,
37
40
  ) {}
38
41
 
39
- get port() {
40
- return this._port;
42
+ get endpoint() {
43
+ invariant(this._port);
44
+ return `http://localhost:${this._port}`;
41
45
  }
42
46
 
43
- get endpoint() {
44
- return this._port ? `http://localhost:${this._port}` : undefined;
47
+ get proxy() {
48
+ return this._proxy;
45
49
  }
46
50
 
47
51
  get functions() {
48
- return Object.keys(this._handlers);
52
+ return Object.values(this._handlers);
49
53
  }
50
54
 
51
55
  async initialize() {
52
- for (const { id } of this._options.manifest.functions) {
56
+ for (const def of this._options.manifest.functions) {
57
+ const { id, path, handler: dir } = def;
53
58
  try {
54
59
  // eslint-disable-next-line @typescript-eslint/no-var-requires
55
- const module = require(join(this._options.directory, id));
60
+ const module = require(join(this._options.directory, dir));
56
61
  const handler = module.default;
57
62
  if (typeof handler !== 'function') {
58
63
  throw new Error(`Handler must export default function: ${id}`);
59
64
  }
60
65
 
61
- this._handlers[id] = handler;
66
+ if (this._handlers[path]) {
67
+ log.warn(`Function already registered: ${id}`);
68
+ }
69
+
70
+ this._handlers[path] = { def, handler };
62
71
  } catch (err) {
63
- log.error('parsing function (check functions.yml manifest)', err);
72
+ log.error('parsing function (check manifest)', err);
64
73
  }
65
74
  }
66
75
  }
@@ -69,30 +78,32 @@ export class DevServer {
69
78
  const app = express();
70
79
  app.use(express.json());
71
80
 
72
- app.post('/:functionName', async (req, res) => {
73
- const functionName = req.params.functionName;
74
- log('invoke', { function: functionName, data: req.body });
81
+ app.post('/:name', async (req, res) => {
82
+ const { name } = req.params;
75
83
 
76
- const builder: Response = {
84
+ const response: Response = {
77
85
  status: (code: number) => {
78
86
  res.statusCode = code;
79
- return builder;
87
+ return response;
80
88
  },
89
+
81
90
  succeed: (result = {}) => {
82
91
  res.end(JSON.stringify(result));
83
- return builder;
92
+ return response;
84
93
  },
85
94
  };
86
95
 
87
96
  const context: FunctionContext = {
88
97
  client: this._client,
89
- status: builder.status.bind(builder),
98
+ status: response.status.bind(response),
90
99
  };
91
100
 
92
101
  void (async () => {
93
102
  try {
94
- // TODO(burdon): Typed event handler.
95
- await this._handlers[functionName]({ event: req.body, context });
103
+ log(`invoking: ${name}`);
104
+ const { handler } = this._handlers[name];
105
+ const response = await handler({ context, event: req.body });
106
+ log('done', { response });
96
107
  } catch (err: any) {
97
108
  res.statusCode = 500;
98
109
  res.end(err.message);
@@ -100,20 +111,24 @@ export class DevServer {
100
111
  })();
101
112
  });
102
113
 
103
- this._port = await getPortPromise({ startPort: DEFAULT_PORT });
114
+ // TODO(burdon): Push down port management to agent.
115
+ this._port = await getPort({ port: 7200, portRange: [7200, 7299] });
104
116
  this._server = app.listen(this._port);
105
117
 
106
- // TODO(burdon): Check plugin is registered.
107
- // TypeError: Cannot read properties of undefined (reading 'register')
118
+ // TODO(burdon): Test during initialization.
108
119
  try {
109
- const { registrationId } = await this._client.services.services.FunctionRegistryService!.register({
110
- endpoint: this.endpoint!,
111
- functions: this.functions.map((name) => ({ name })),
120
+ // Register functions.
121
+ const { registrationId, endpoint } = await this._client.services.services.FunctionRegistryService!.register({
122
+ endpoint: this.endpoint,
123
+ functions: this.functions.map(({ def: { path } }) => ({ name: path })),
112
124
  });
125
+
126
+ log.info('registered', { registrationId, endpoint });
113
127
  this._registrationId = registrationId;
128
+ this._proxy = endpoint;
114
129
  } catch (err: any) {
115
130
  await this.stop();
116
- throw new Error('FunctionRegistryService not available; check config (agent.plugins.functions).');
131
+ throw new Error('FunctionRegistryService not available (check plugin is configured).');
117
132
  }
118
133
  }
119
134
 
@@ -124,14 +139,17 @@ export class DevServer {
124
139
  await this._client.services.services.FunctionRegistryService!.unregister({
125
140
  registrationId: this._registrationId,
126
141
  });
142
+
143
+ log.info('unregistered', { registrationId: this._registrationId });
127
144
  this._registrationId = undefined;
145
+ this._proxy = undefined;
128
146
  }
129
147
 
130
148
  trigger.wake();
131
149
  });
132
150
 
133
151
  await trigger.wait();
134
- this._server = undefined;
135
152
  this._port = undefined;
153
+ this._server = undefined;
136
154
  }
137
155
  }
@@ -11,12 +11,11 @@ import { invariant } from '@dxos/invariant';
11
11
  import { log } from '@dxos/log';
12
12
  import { ComplexMap } from '@dxos/util';
13
13
 
14
- import { type FunctionTrigger } from '../function';
14
+ import { type FunctionManifest, type FunctionTrigger } from '../manifest';
15
15
 
16
16
  // TODO(burdon): Rename.
17
17
  export type InvokeOptions = {
18
18
  endpoint: string;
19
- runtime: string;
20
19
  };
21
20
 
22
21
  export class TriggerManager {
@@ -29,7 +28,7 @@ export class TriggerManager {
29
28
 
30
29
  constructor(
31
30
  private readonly _client: Client,
32
- private readonly _triggers: FunctionTrigger[],
31
+ private readonly _manifest: FunctionManifest,
33
32
  private readonly _invokeOptions: InvokeOptions,
34
33
  ) {}
35
34
 
@@ -38,7 +37,7 @@ export class TriggerManager {
38
37
  this._client.spaces.subscribe(async (spaces) => {
39
38
  for (const space of spaces) {
40
39
  await space.waitUntilReady();
41
- for (const trigger of this._triggers) {
40
+ for (const trigger of this._manifest.triggers ?? []) {
42
41
  // TODO(burdon): New context? Shared?
43
42
  await this.mount(new Context(), space, trigger);
44
43
  }
@@ -54,6 +53,8 @@ export class TriggerManager {
54
53
 
55
54
  private async mount(ctx: Context, space: Space, trigger: FunctionTrigger) {
56
55
  const key = { name: trigger.function, spaceKey: space.key };
56
+ const config = this._manifest.functions.find((config) => config.id === trigger.function);
57
+ invariant(config, `Function not found: ${trigger.function}`);
57
58
  const exists = this._mounts.get(key);
58
59
  if (!exists) {
59
60
  this._mounts.set(key, { ctx, trigger });
@@ -62,11 +63,10 @@ export class TriggerManager {
62
63
  return;
63
64
  }
64
65
 
65
- // TODO(burdon): Trigger binding.
66
66
  // TODO(burdon): Why DeferredTask? How to pass objectIds to function?
67
67
  const objectIds = new Set<string>();
68
68
  const task = new DeferredTask(ctx, async () => {
69
- await this.invokeFunction(this._invokeOptions, trigger.function, {
69
+ await this.execFunction(this._invokeOptions, config.path, {
70
70
  space: space.key,
71
71
  objects: Array.from(objectIds),
72
72
  });
@@ -115,14 +115,13 @@ export class TriggerManager {
115
115
  }
116
116
  }
117
117
 
118
- private async invokeFunction(options: InvokeOptions, functionName: string, data: any) {
119
- const { endpoint, runtime } = options;
118
+ private async execFunction(options: InvokeOptions, functionName: string, data: any) {
119
+ const { endpoint } = options;
120
120
  invariant(endpoint, 'Missing endpoint');
121
- invariant(runtime, 'Missing runtime');
122
121
 
123
122
  try {
124
123
  log('invoke', { function: functionName });
125
- const url = `${endpoint}/${runtime}/${functionName}`;
124
+ const url = `${endpoint}/${functionName}`;
126
125
  const res = await fetch(url, {
127
126
  method: 'POST',
128
127
  body: JSON.stringify(data),
@@ -1 +0,0 @@
1
- {"version":3,"file":"function.d.ts","sourceRoot":"","sources":["../../../src/function.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC/B,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;CAChC;AAMD,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE;IACnD,KAAK,EAAE,CAAC,CAAC;IACT,OAAO,EAAE,eAAe,CAAC;CAC1B,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAE/B,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,mBAAmB,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC"}
package/src/function.ts DELETED
@@ -1,51 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- import { type Client } from '@dxos/client';
6
-
7
- export interface Response {
8
- status(code: number): Response;
9
- succeed(data?: object): Response;
10
- }
11
-
12
- export interface FunctionContext {
13
- client: Client;
14
- status(code: number): Response;
15
- }
16
-
17
- // https://www.npmjs.com/package/aws-lambda
18
- // https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html
19
-
20
- // TODO(burdon): Types.
21
- export type FunctionSubscriptionEvent = {
22
- space: string;
23
- objects: string[];
24
- };
25
-
26
- export type FunctionHandler<T extends {}> = (params: {
27
- event: T;
28
- context: FunctionContext;
29
- }) => Promise<Response | void>;
30
-
31
- export type FunctionsManifest = {
32
- functions: FunctionConfig[];
33
- triggers: FunctionTrigger[];
34
- };
35
-
36
- export type FunctionConfig = {
37
- id: string;
38
- description?: string;
39
- };
40
-
41
- export type FunctionTrigger = {
42
- function: string;
43
- subscription: TriggerSubscription;
44
- };
45
-
46
- export type TriggerSubscription = {
47
- type: string;
48
- spaceKey: string;
49
- props?: Record<string, any>;
50
- nested?: string[];
51
- };