@dxos/functions 0.3.8-main.152ec6f → 0.3.8-main.1b6c849

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.
@@ -5,6 +5,7 @@ export type DevServerOptions = {
5
5
  port?: number;
6
6
  directory: string;
7
7
  manifest: FunctionManifest;
8
+ reload?: boolean;
8
9
  };
9
10
  /**
10
11
  * Functions dev server provides a local HTTP server for testing functions.
@@ -17,6 +18,7 @@ export declare class DevServer {
17
18
  private _port?;
18
19
  private _registrationId?;
19
20
  private _proxy?;
21
+ private _seq;
20
22
  constructor(_client: Client, _options: DevServerOptions);
21
23
  get endpoint(): string;
22
24
  get proxy(): string | undefined;
@@ -27,5 +29,13 @@ export declare class DevServer {
27
29
  initialize(): Promise<void>;
28
30
  start(): Promise<void>;
29
31
  stop(): Promise<void>;
32
+ /**
33
+ * Load function.
34
+ */
35
+ private _load;
36
+ /**
37
+ * Invoke function handler.
38
+ */
39
+ private _invoke;
30
40
  }
31
41
  //# sourceMappingURL=dev-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../../../src/runtime/dev-server.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAI3C,OAAO,EAAwB,KAAK,eAAe,EAAiB,MAAM,YAAY,CAAC;AACvF,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;CAC5B,CAAC;AAEF;;GAEG;AAEH,qBAAa,SAAS;IAUlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAV3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2E;IAErG,OAAO,CAAC,OAAO,CAAC,CAAc;IAC9B,OAAO,CAAC,KAAK,CAAC,CAAS;IACvB,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,MAAM,CAAC,CAAS;gBAIL,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,gBAAgB;IAG7C,IAAI,QAAQ,WAGX;IAED,IAAI,KAAK,uBAER;IAED,IAAI,SAAS;;;QAEZ;IAEK,UAAU;IAsBV,KAAK;IA0DL,IAAI;CAoBX"}
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../../../src/runtime/dev-server.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAI3C,OAAO,EAAwB,KAAK,eAAe,EAAiB,MAAM,YAAY,CAAC;AACvF,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,qBAAa,SAAS;IAYlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAX3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2E;IAErG,OAAO,CAAC,OAAO,CAAC,CAAc;IAC9B,OAAO,CAAC,KAAK,CAAC,CAAS;IACvB,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,IAAI,CAAK;gBAIE,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,gBAAgB;IAG7C,IAAI,QAAQ,WAGX;IAED,IAAI,KAAK,uBAER;IAED,IAAI,SAAS;;;QAEZ;IAEK,UAAU;IAUV,KAAK;IAwCL,IAAI;IAqBV;;OAEG;YACW,KAAK;IAqBnB;;OAEG;YACW,OAAO;CAwBtB"}
@@ -1,3 +1,3 @@
1
1
  export * from './dev-server';
2
- export * from './trigger-manager';
2
+ export * from './scheduler';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/runtime/index.ts"],"names":[],"mappings":"AAIA,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/runtime/index.ts"],"names":[],"mappings":"AAIA,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC"}
@@ -1,19 +1,22 @@
1
1
  import { type Client } from '@dxos/client';
2
2
  import { type FunctionManifest } from '../manifest';
3
- export type InvokeOptions = {
3
+ type SchedulerOptions = {
4
4
  endpoint: string;
5
5
  };
6
- export declare class TriggerManager {
6
+ /**
7
+ * Functions scheduler.
8
+ */
9
+ export declare class Scheduler {
7
10
  private readonly _client;
8
11
  private readonly _manifest;
9
- private readonly _invokeOptions;
12
+ private readonly _options;
10
13
  private readonly _mounts;
11
- private readonly _queries;
12
- constructor(_client: Client, _manifest: FunctionManifest, _invokeOptions: InvokeOptions);
14
+ constructor(_client: Client, _manifest: FunctionManifest, _options: SchedulerOptions);
13
15
  start(): Promise<void>;
14
16
  stop(): Promise<void>;
15
17
  private mount;
16
18
  private unmount;
17
19
  private execFunction;
18
20
  }
19
- //# sourceMappingURL=trigger-manager.d.ts.map
21
+ export {};
22
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../../../src/runtime/scheduler.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,MAAM,EAAkB,MAAM,cAAc,CAAC;AAQ3D,OAAO,EAAoB,KAAK,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAE5F,KAAK,gBAAgB,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AAEH,qBAAa,SAAS;IAQlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAR3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAG6B;gBAGlC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,gBAAgB,EAC3B,QAAQ,EAAE,gBAAgB;IAGvC,KAAK;IAWL,IAAI;YAMI,KAAK;YA+DL,OAAO;YASP,YAAY;CAiB3B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/functions",
3
- "version": "0.3.8-main.152ec6f",
3
+ "version": "0.3.8-main.1b6c849",
4
4
  "description": "Functions SDK and runtime.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -19,16 +19,17 @@
19
19
  "src"
20
20
  ],
21
21
  "dependencies": {
22
+ "cron": "^3.1.6",
22
23
  "express": "^4.17.1",
23
24
  "get-port-please": "^3.1.1",
24
- "@dxos/async": "0.3.8-main.152ec6f",
25
- "@dxos/client": "0.3.8-main.152ec6f",
26
- "@dxos/context": "0.3.8-main.152ec6f",
27
- "@dxos/invariant": "0.3.8-main.152ec6f",
28
- "@dxos/node-std": "0.3.8-main.152ec6f",
29
- "@dxos/echo-schema": "0.3.8-main.152ec6f",
30
- "@dxos/util": "0.3.8-main.152ec6f",
31
- "@dxos/log": "0.3.8-main.152ec6f"
25
+ "@dxos/async": "0.3.8-main.1b6c849",
26
+ "@dxos/client": "0.3.8-main.1b6c849",
27
+ "@dxos/context": "0.3.8-main.1b6c849",
28
+ "@dxos/node-std": "0.3.8-main.1b6c849",
29
+ "@dxos/echo-schema": "0.3.8-main.1b6c849",
30
+ "@dxos/invariant": "0.3.8-main.1b6c849",
31
+ "@dxos/log": "0.3.8-main.1b6c849",
32
+ "@dxos/util": "0.3.8-main.1b6c849"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/express": "^4.17.17"
package/src/handler.ts CHANGED
@@ -4,26 +4,25 @@
4
4
 
5
5
  import { type Client } from '@dxos/client';
6
6
 
7
+ // TODO(burdon): No response?
7
8
  export interface Response {
8
9
  status(code: number): Response;
9
- succeed(data?: object): Response;
10
10
  }
11
11
 
12
+ // TODO(burdon): Limit access to individual space?
12
13
  export interface FunctionContext {
13
14
  client: Client;
14
- status(code: number): Response;
15
15
  }
16
16
 
17
- /**
18
- * Function handler.
19
- */
17
+ // TODO(burdon): Model after http request. Ref Lambda/OpenFaaS.
18
+ // https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html
20
19
  export type FunctionHandler<T extends {}> = (params: {
21
- context: FunctionContext;
22
20
  event: T;
21
+ context: FunctionContext;
22
+ response: Response;
23
23
  }) => Promise<Response | void>;
24
24
 
25
- // TODO(burdon): Types.
26
25
  export type FunctionSubscriptionEvent = {
27
- space: string;
26
+ space: string; // TODO(burdon): Convert to PublicKey.
28
27
  objects: string[];
29
28
  };
package/src/manifest.ts CHANGED
@@ -2,12 +2,15 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ // Lambda-like function definitions.
6
+ // See: https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml/#functions
7
+
5
8
  export type FunctionDef = {
6
9
  // FQ function name.
7
10
  id: string;
8
11
  // URL path.
9
- path: string;
10
- // Path of handler.
12
+ name: string;
13
+ // File path of handler.
11
14
  handler: string;
12
15
  description?: string;
13
16
  };
@@ -24,7 +27,8 @@ export type TriggerSubscription = {
24
27
  // https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html
25
28
  export type FunctionTrigger = {
26
29
  function: string;
27
- subscription: TriggerSubscription;
30
+ schedule?: string;
31
+ subscription?: TriggerSubscription;
28
32
  };
29
33
 
30
34
  /**
@@ -19,19 +19,21 @@ export type DevServerOptions = {
19
19
  port?: number;
20
20
  directory: string;
21
21
  manifest: FunctionManifest;
22
+ reload?: boolean;
22
23
  };
23
24
 
24
25
  /**
25
26
  * Functions dev server provides a local HTTP server for testing functions.
26
27
  */
27
- // TODO(burdon): Reconcile with agent/functions dev dispatcher.
28
28
  export class DevServer {
29
+ // Function handlers indexed by name (URL path).
29
30
  private readonly _handlers: Record<string, { def: FunctionDef; handler: FunctionHandler<any> }> = {};
30
31
 
31
32
  private _server?: http.Server;
32
33
  private _port?: number;
33
34
  private _registrationId?: string;
34
35
  private _proxy?: string;
36
+ private _seq = 0;
35
37
 
36
38
  // prettier-ignore
37
39
  constructor(
@@ -54,20 +56,8 @@ export class DevServer {
54
56
 
55
57
  async initialize() {
56
58
  for (const def of this._options.manifest.functions) {
57
- const { id, path, handler: dir } = def;
58
59
  try {
59
- // eslint-disable-next-line @typescript-eslint/no-var-requires
60
- const module = require(join(this._options.directory, dir));
61
- const handler = module.default;
62
- if (typeof handler !== 'function') {
63
- throw new Error(`Handler must export default function: ${id}`);
64
- }
65
-
66
- if (this._handlers[path]) {
67
- log.warn(`Function already registered: ${id}`);
68
- }
69
-
70
- this._handlers[path] = { def, handler };
60
+ await this._load(def);
71
61
  } catch (err) {
72
62
  log.error('parsing function (check manifest)', err);
73
63
  }
@@ -80,47 +70,29 @@ export class DevServer {
80
70
 
81
71
  app.post('/:name', async (req, res) => {
82
72
  const { name } = req.params;
83
-
84
- const response: Response = {
85
- status: (code: number) => {
86
- res.statusCode = code;
87
- return response;
88
- },
89
-
90
- succeed: (result = {}) => {
91
- res.end(JSON.stringify(result));
92
- return response;
93
- },
94
- };
95
-
96
- const context: FunctionContext = {
97
- client: this._client,
98
- status: response.status.bind(response),
99
- };
100
-
101
- void (async () => {
102
- try {
103
- log(`invoking: ${name}`);
104
- const { handler } = this._handlers[name];
105
- const response = await handler({ context, event: req.body });
106
- log('done', { response });
107
- } catch (err: any) {
108
- res.statusCode = 500;
109
- res.end(err.message);
73
+ try {
74
+ if (this._options.reload) {
75
+ const { def } = this._handlers[name];
76
+ await this._load(def, true);
110
77
  }
111
- })();
78
+
79
+ res.statusCode = await this._invoke(name, req.body);
80
+ res.end();
81
+ } catch (err: any) {
82
+ log.error(err);
83
+ res.statusCode = 500;
84
+ res.end();
85
+ }
112
86
  });
113
87
 
114
- // TODO(burdon): Push down port management to agent.
115
88
  this._port = await getPort({ port: 7200, portRange: [7200, 7299] });
116
89
  this._server = app.listen(this._port);
117
90
 
118
- // TODO(burdon): Test during initialization.
119
91
  try {
120
92
  // Register functions.
121
93
  const { registrationId, endpoint } = await this._client.services.services.FunctionRegistryService!.register({
122
94
  endpoint: this.endpoint,
123
- functions: this.functions.map(({ def: { path } }) => ({ name: path })),
95
+ functions: this.functions.map(({ def: { name } }) => ({ name })),
124
96
  });
125
97
 
126
98
  log.info('registered', { registrationId, endpoint });
@@ -152,4 +124,56 @@ export class DevServer {
152
124
  this._port = undefined;
153
125
  this._server = undefined;
154
126
  }
127
+
128
+ /**
129
+ * Load function.
130
+ */
131
+ private async _load(def: FunctionDef, flush = false) {
132
+ const { id, name, handler } = def;
133
+ const path = join(this._options.directory, handler);
134
+ log.info('loading', { id });
135
+
136
+ // Remove from cache.
137
+ if (flush) {
138
+ Object.keys(require.cache)
139
+ .filter((key) => key.startsWith(path))
140
+ .forEach((key) => delete require.cache[key]);
141
+ }
142
+
143
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
144
+ const module = require(path);
145
+ if (typeof module.default !== 'function') {
146
+ throw new Error(`Handler must export default function: ${id}`);
147
+ }
148
+
149
+ this._handlers[name] = { def, handler: module.default };
150
+ }
151
+
152
+ /**
153
+ * Invoke function handler.
154
+ */
155
+ private async _invoke(name: string, event: any) {
156
+ const seq = ++this._seq;
157
+ const now = Date.now();
158
+
159
+ log.info('req', { seq, name });
160
+ const { handler } = this._handlers[name];
161
+
162
+ const context: FunctionContext = {
163
+ client: this._client,
164
+ };
165
+
166
+ let statusCode = 200;
167
+ const response: Response = {
168
+ status: (code: number) => {
169
+ statusCode = code;
170
+ return response;
171
+ },
172
+ };
173
+
174
+ await handler({ context, event, response });
175
+ log.info('res', { seq, name, statusCode, duration: Date.now() - now });
176
+
177
+ return statusCode;
178
+ }
155
179
  }
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './dev-server';
6
- export * from './trigger-manager';
6
+ export * from './scheduler';
@@ -0,0 +1,145 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CronJob } from 'cron';
6
+
7
+ import { DeferredTask } from '@dxos/async';
8
+ import { type Client, type PublicKey } from '@dxos/client';
9
+ import { type Space } from '@dxos/client/echo';
10
+ import { Context } from '@dxos/context';
11
+ import { Filter, createSubscription } from '@dxos/echo-schema';
12
+ import { invariant } from '@dxos/invariant';
13
+ import { log } from '@dxos/log';
14
+ import { ComplexMap } from '@dxos/util';
15
+
16
+ import { type FunctionDef, type FunctionManifest, type FunctionTrigger } from '../manifest';
17
+
18
+ type SchedulerOptions = {
19
+ endpoint: string;
20
+ };
21
+
22
+ /**
23
+ * Functions scheduler.
24
+ */
25
+ // TODO(burdon): Create tests.
26
+ export class Scheduler {
27
+ // Map of mounted functions.
28
+ private readonly _mounts = new ComplexMap<
29
+ { id: string; spaceKey: PublicKey },
30
+ { ctx: Context; trigger: FunctionTrigger }
31
+ >(({ id, spaceKey }) => `${spaceKey.toHex()}:${id}`);
32
+
33
+ constructor(
34
+ private readonly _client: Client,
35
+ private readonly _manifest: FunctionManifest,
36
+ private readonly _options: SchedulerOptions,
37
+ ) {}
38
+
39
+ async start() {
40
+ this._client.spaces.subscribe(async (spaces) => {
41
+ for (const space of spaces) {
42
+ await space.waitUntilReady();
43
+ for (const trigger of this._manifest.triggers ?? []) {
44
+ await this.mount(new Context(), space, trigger);
45
+ }
46
+ }
47
+ });
48
+ }
49
+
50
+ async stop() {
51
+ for (const { id, spaceKey } of this._mounts.keys()) {
52
+ await this.unmount(id, spaceKey);
53
+ }
54
+ }
55
+
56
+ private async mount(ctx: Context, space: Space, trigger: FunctionTrigger) {
57
+ const key = { id: trigger.function, spaceKey: space.key };
58
+ const def = this._manifest.functions.find((config) => config.id === trigger.function);
59
+ invariant(def, `Function not found: ${trigger.function}`);
60
+
61
+ const exists = this._mounts.get(key);
62
+ if (!exists) {
63
+ this._mounts.set(key, { ctx, trigger });
64
+ log('mount', { space: space.key, trigger });
65
+ if (ctx.disposed) {
66
+ return;
67
+ }
68
+
69
+ // Cron schedule.
70
+ if (trigger.schedule) {
71
+ const task = new DeferredTask(ctx, async () => {
72
+ await this.execFunction(def, {
73
+ space: space.key,
74
+ });
75
+ });
76
+
77
+ // TODO(burdon): Check greater than 30s min (use cron-parser).
78
+ const job = new CronJob(trigger.schedule, () => task.schedule());
79
+
80
+ job.start();
81
+ ctx.onDispose(() => job.stop());
82
+ }
83
+
84
+ // ECHO subscription.
85
+ if (trigger.subscription) {
86
+ const objectIds = new Set<string>();
87
+ const task = new DeferredTask(ctx, async () => {
88
+ await this.execFunction(def, {
89
+ space: space.key,
90
+ objects: Array.from(objectIds),
91
+ });
92
+ });
93
+
94
+ const subscription = createSubscription(({ added, updated }) => {
95
+ for (const object of added) {
96
+ objectIds.add(object.id);
97
+ }
98
+ for (const object of updated) {
99
+ objectIds.add(object.id);
100
+ }
101
+
102
+ task.schedule();
103
+ });
104
+
105
+ const { type, props } = trigger.subscription;
106
+ const query = space.db.query(Filter.typename(type, props));
107
+ const unsubscribe = query.subscribe(({ objects }) => {
108
+ subscription.update(objects);
109
+ }, true);
110
+
111
+ ctx.onDispose(() => {
112
+ subscription.unsubscribe();
113
+ unsubscribe();
114
+ });
115
+ }
116
+ }
117
+ }
118
+
119
+ private async unmount(id: string, spaceKey: PublicKey) {
120
+ const key = { id, spaceKey };
121
+ const { ctx } = this._mounts.get(key) ?? {};
122
+ if (ctx) {
123
+ this._mounts.delete(key);
124
+ await ctx.dispose();
125
+ }
126
+ }
127
+
128
+ private async execFunction(def: FunctionDef, data: any) {
129
+ try {
130
+ log('request', { function: def.id });
131
+ const response = await fetch(`${this._options.endpoint}/${def.name}`, {
132
+ method: 'POST',
133
+ headers: {
134
+ 'Content-Type': 'application/json',
135
+ },
136
+ body: JSON.stringify(data),
137
+ });
138
+
139
+ // const result = await response.json();
140
+ log('result', { function: def.id, result: response.status });
141
+ } catch (err: any) {
142
+ log.error('error', { function: def.id, error: err.message });
143
+ }
144
+ }
145
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"trigger-manager.d.ts","sourceRoot":"","sources":["../../../../src/runtime/trigger-manager.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAkB,MAAM,cAAc,CAAC;AAQ3D,OAAO,EAAE,KAAK,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAG1E,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qBAAa,cAAc;IASvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAVjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAGiC;IAEzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;gBAG1B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,gBAAgB,EAC3B,cAAc,EAAE,aAAa;IAG1C,KAAK;IAaL,IAAI;YAMI,KAAK;YAuDL,OAAO;YASP,YAAY;CAoB3B"}
@@ -1,138 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- import { DeferredTask } from '@dxos/async';
6
- import { type Client, type PublicKey } from '@dxos/client';
7
- import type { Query, Space } from '@dxos/client/echo';
8
- import { Context } from '@dxos/context';
9
- import { Filter, createSubscription } from '@dxos/echo-schema';
10
- import { invariant } from '@dxos/invariant';
11
- import { log } from '@dxos/log';
12
- import { ComplexMap } from '@dxos/util';
13
-
14
- import { type FunctionManifest, type FunctionTrigger } from '../manifest';
15
-
16
- // TODO(burdon): Rename.
17
- export type InvokeOptions = {
18
- endpoint: string;
19
- };
20
-
21
- export class TriggerManager {
22
- private readonly _mounts = new ComplexMap<
23
- { name: string; spaceKey: PublicKey },
24
- { ctx: Context; trigger: FunctionTrigger }
25
- >(({ name, spaceKey }) => `${spaceKey.toHex()}:${name}`);
26
-
27
- private readonly _queries = new Set<Query>();
28
-
29
- constructor(
30
- private readonly _client: Client,
31
- private readonly _manifest: FunctionManifest,
32
- private readonly _invokeOptions: InvokeOptions,
33
- ) {}
34
-
35
- async start() {
36
- // TODO(burdon): Make runtime configurable (via CLI)?
37
- this._client.spaces.subscribe(async (spaces) => {
38
- for (const space of spaces) {
39
- await space.waitUntilReady();
40
- for (const trigger of this._manifest.triggers ?? []) {
41
- // TODO(burdon): New context? Shared?
42
- await this.mount(new Context(), space, trigger);
43
- }
44
- }
45
- });
46
- }
47
-
48
- async stop() {
49
- for (const { name, spaceKey } of this._mounts.keys()) {
50
- await this.unmount(name, spaceKey);
51
- }
52
- }
53
-
54
- private async mount(ctx: Context, space: Space, trigger: FunctionTrigger) {
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}`);
58
- const exists = this._mounts.get(key);
59
- if (!exists) {
60
- this._mounts.set(key, { ctx, trigger });
61
- log('mount', { space: space.key, trigger });
62
- if (ctx.disposed) {
63
- return;
64
- }
65
-
66
- // TODO(burdon): Why DeferredTask? How to pass objectIds to function?
67
- const objectIds = new Set<string>();
68
- const task = new DeferredTask(ctx, async () => {
69
- await this.execFunction(this._invokeOptions, config.path, {
70
- space: space.key,
71
- objects: Array.from(objectIds),
72
- });
73
- });
74
-
75
- let count = 0;
76
- const subscription = createSubscription(({ added, updated }) => {
77
- for (const object of added) {
78
- objectIds.add(object.id);
79
- }
80
- for (const object of updated) {
81
- objectIds.add(object.id);
82
- }
83
-
84
- log('updated', {
85
- trigger,
86
- space: space.key,
87
- objects: objectIds.size,
88
- count,
89
- });
90
-
91
- task.schedule();
92
- count++;
93
- });
94
- // TODO(burdon): DSL for query (replace props).
95
- const query = space.db.query(Filter.typename(trigger.subscription.type, trigger.subscription.props));
96
- this._queries.add(query);
97
- const unsubscribe = query.subscribe(({ objects }) => {
98
- subscription.update(objects);
99
- }, true);
100
-
101
- ctx.onDispose(() => {
102
- subscription.unsubscribe();
103
- unsubscribe();
104
- this._queries.delete(query);
105
- });
106
- }
107
- }
108
-
109
- private async unmount(name: string, spaceKey: PublicKey) {
110
- const key = { name, spaceKey };
111
- const { ctx } = this._mounts.get(key) ?? {};
112
- if (ctx) {
113
- this._mounts.delete(key);
114
- await ctx.dispose();
115
- }
116
- }
117
-
118
- private async execFunction(options: InvokeOptions, functionName: string, data: any) {
119
- const { endpoint } = options;
120
- invariant(endpoint, 'Missing endpoint');
121
-
122
- try {
123
- log('invoke', { function: functionName });
124
- const url = `${endpoint}/${functionName}`;
125
- const res = await fetch(url, {
126
- method: 'POST',
127
- body: JSON.stringify(data),
128
- headers: {
129
- 'Content-Type': 'application/json',
130
- },
131
- });
132
-
133
- log('result', { function: functionName, result: await res.json() });
134
- } catch (err: any) {
135
- log.error('error', { function: functionName, error: err.message });
136
- }
137
- }
138
- }