@dxos/functions 0.5.3-main.f752aaa → 0.5.3-next.57eca40

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 (95) hide show
  1. package/dist/lib/browser/chunk-366QG6IX.mjs +81 -0
  2. package/dist/lib/browser/chunk-366QG6IX.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +825 -471
  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 +12 -0
  7. package/dist/lib/browser/types.mjs.map +7 -0
  8. package/dist/lib/node/chunk-3VSJ57ZZ.cjs +97 -0
  9. package/dist/lib/node/chunk-3VSJ57ZZ.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +805 -461
  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 +33 -0
  14. package/dist/lib/node/types.cjs.map +7 -0
  15. package/dist/types/src/function/function-registry.d.ts +24 -0
  16. package/dist/types/src/function/function-registry.d.ts.map +1 -0
  17. package/dist/types/src/function/function-registry.test.d.ts +2 -0
  18. package/dist/types/src/function/function-registry.test.d.ts.map +1 -0
  19. package/dist/types/src/function/index.d.ts +2 -0
  20. package/dist/types/src/function/index.d.ts.map +1 -0
  21. package/dist/types/src/handler.d.ts +33 -12
  22. package/dist/types/src/handler.d.ts.map +1 -1
  23. package/dist/types/src/index.d.ts +2 -0
  24. package/dist/types/src/index.d.ts.map +1 -1
  25. package/dist/types/src/runtime/dev-server.d.ts +16 -13
  26. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  27. package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
  28. package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
  29. package/dist/types/src/runtime/scheduler.d.ts +13 -27
  30. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  31. package/dist/types/src/testing/functions-integration.test.d.ts +2 -0
  32. package/dist/types/src/testing/functions-integration.test.d.ts.map +1 -0
  33. package/dist/types/src/testing/index.d.ts +4 -0
  34. package/dist/types/src/testing/index.d.ts.map +1 -0
  35. package/dist/types/src/testing/setup.d.ts +5 -0
  36. package/dist/types/src/testing/setup.d.ts.map +1 -0
  37. package/dist/types/src/testing/test/handler.d.ts +4 -0
  38. package/dist/types/src/testing/test/handler.d.ts.map +1 -0
  39. package/dist/types/src/testing/test/index.d.ts +3 -0
  40. package/dist/types/src/testing/test/index.d.ts.map +1 -0
  41. package/dist/types/src/testing/types.d.ts +9 -0
  42. package/dist/types/src/testing/types.d.ts.map +1 -0
  43. package/dist/types/src/testing/util.d.ts +3 -0
  44. package/dist/types/src/testing/util.d.ts.map +1 -0
  45. package/dist/types/src/trigger/index.d.ts +2 -0
  46. package/dist/types/src/trigger/index.d.ts.map +1 -0
  47. package/dist/types/src/trigger/trigger-registry.d.ts +40 -0
  48. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -0
  49. package/dist/types/src/trigger/trigger-registry.test.d.ts +2 -0
  50. package/dist/types/src/trigger/trigger-registry.test.d.ts.map +1 -0
  51. package/dist/types/src/trigger/type/index.d.ts +5 -0
  52. package/dist/types/src/trigger/type/index.d.ts.map +1 -0
  53. package/dist/types/src/trigger/type/subscription-trigger.d.ts +4 -0
  54. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -0
  55. package/dist/types/src/trigger/type/timer-trigger.d.ts +4 -0
  56. package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -0
  57. package/dist/types/src/trigger/type/webhook-trigger.d.ts +4 -0
  58. package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -0
  59. package/dist/types/src/trigger/type/websocket-trigger.d.ts +13 -0
  60. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -0
  61. package/dist/types/src/types.d.ts +143 -101
  62. package/dist/types/src/types.d.ts.map +1 -1
  63. package/dist/types/src/util.d.ts +15 -0
  64. package/dist/types/src/util.d.ts.map +1 -0
  65. package/dist/types/src/util.test.d.ts +2 -0
  66. package/dist/types/src/util.test.d.ts.map +1 -0
  67. package/package.json +33 -15
  68. package/schema/functions.json +140 -104
  69. package/src/function/function-registry.test.ts +105 -0
  70. package/src/function/function-registry.ts +90 -0
  71. package/src/function/index.ts +5 -0
  72. package/src/handler.ts +54 -31
  73. package/src/index.ts +2 -0
  74. package/src/runtime/dev-server.test.ts +60 -0
  75. package/src/runtime/dev-server.ts +104 -53
  76. package/src/runtime/scheduler.test.ts +56 -73
  77. package/src/runtime/scheduler.ts +87 -271
  78. package/src/testing/functions-integration.test.ts +99 -0
  79. package/src/testing/index.ts +7 -0
  80. package/src/testing/setup.ts +45 -0
  81. package/src/testing/test/handler.ts +15 -0
  82. package/src/testing/test/index.ts +7 -0
  83. package/src/testing/types.ts +9 -0
  84. package/src/testing/util.ts +16 -0
  85. package/src/trigger/index.ts +5 -0
  86. package/src/trigger/trigger-registry.test.ts +255 -0
  87. package/src/trigger/trigger-registry.ts +189 -0
  88. package/src/trigger/type/index.ts +8 -0
  89. package/src/trigger/type/subscription-trigger.ts +80 -0
  90. package/src/trigger/type/timer-trigger.ts +44 -0
  91. package/src/trigger/type/webhook-trigger.ts +47 -0
  92. package/src/trigger/type/websocket-trigger.ts +91 -0
  93. package/src/types.ts +59 -32
  94. package/src/util.test.ts +43 -0
  95. package/src/util.ts +48 -0
@@ -1,53 +1,147 @@
1
1
  import "@dxos/node-std/globals";
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined")
6
- return require.apply(this, arguments);
7
- throw Error('Dynamic require of "' + x + '" is not supported');
8
- });
9
-
10
- // inject-globals:@inject-globals
11
2
  import {
12
- global,
13
- Buffer as Buffer2,
14
- process
15
- } from "@dxos/node-std/inject-globals";
3
+ FunctionDef,
4
+ FunctionManifestSchema,
5
+ FunctionTrigger,
6
+ __require
7
+ } from "./chunk-366QG6IX.mjs";
16
8
 
17
- // packages/core/functions/src/handler.ts
18
- import { PublicKey } from "@dxos/client";
9
+ // packages/core/functions/src/function/function-registry.ts
10
+ import { Event } from "@dxos/async";
11
+ import { create, Filter } from "@dxos/client/echo";
12
+ import { Resource } from "@dxos/context";
13
+ import { PublicKey } from "@dxos/keys";
19
14
  import { log } from "@dxos/log";
15
+ import { ComplexMap } from "@dxos/util";
16
+
17
+ // packages/core/functions/src/util.ts
18
+ var diff = (previous, next, comparator) => {
19
+ const remaining = [
20
+ ...previous
21
+ ];
22
+ const result = {
23
+ added: [],
24
+ updated: [],
25
+ removed: remaining
26
+ };
27
+ for (const object of next) {
28
+ const index = remaining.findIndex((item) => comparator(item, object));
29
+ if (index === -1) {
30
+ result.added.push(object);
31
+ } else {
32
+ result.updated.push(remaining[index]);
33
+ remaining.splice(index, 1);
34
+ }
35
+ }
36
+ return result;
37
+ };
38
+ var intersection = (a, b, comparator) => a.filter((a2) => b.find((b2) => comparator(a2, b2)) !== void 0);
39
+
40
+ // packages/core/functions/src/function/function-registry.ts
41
+ var __dxlog_file = "/home/runner/work/dxos/dxos/packages/core/functions/src/function/function-registry.ts";
42
+ var FunctionRegistry = class extends Resource {
43
+ constructor(_client) {
44
+ super();
45
+ this._client = _client;
46
+ this._functionBySpaceKey = new ComplexMap(PublicKey.hash);
47
+ this.registered = new Event();
48
+ }
49
+ getFunctions(space) {
50
+ return this._functionBySpaceKey.get(space.key) ?? [];
51
+ }
52
+ /**
53
+ * Loads function definitions from the manifest into the space.
54
+ * We first load all the definitions from the space to deduplicate by functionId.
55
+ */
56
+ async register(space, functions) {
57
+ log("register", {
58
+ space: space.key,
59
+ functions: functions?.length ?? 0
60
+ }, {
61
+ F: __dxlog_file,
62
+ L: 39,
63
+ S: this,
64
+ C: (f, a) => f(...a)
65
+ });
66
+ if (!functions?.length) {
67
+ return;
68
+ }
69
+ if (!space.db.graph.runtimeSchemaRegistry.hasSchema(FunctionDef)) {
70
+ space.db.graph.runtimeSchemaRegistry.registerSchema(FunctionDef);
71
+ }
72
+ const { objects: existing } = await space.db.query(Filter.schema(FunctionDef)).run();
73
+ const { added, removed } = diff(existing, functions, (a, b) => a.uri === b.uri);
74
+ added.forEach((def) => space.db.add(create(FunctionDef, def)));
75
+ removed.forEach((def) => space.db.remove(def));
76
+ }
77
+ async _open() {
78
+ const spacesSubscription = this._client.spaces.subscribe(async (spaces) => {
79
+ for (const space of spaces) {
80
+ if (this._functionBySpaceKey.has(space.key)) {
81
+ continue;
82
+ }
83
+ const registered = [];
84
+ this._functionBySpaceKey.set(space.key, registered);
85
+ await space.waitUntilReady();
86
+ if (this._ctx.disposed) {
87
+ break;
88
+ }
89
+ this._ctx.onDispose(space.db.query(Filter.schema(FunctionDef)).subscribe(({ objects }) => {
90
+ const { added } = diff(registered, objects, (a, b) => a.uri === b.uri);
91
+ if (added.length > 0) {
92
+ registered.push(...added);
93
+ this.registered.emit({
94
+ space,
95
+ added
96
+ });
97
+ }
98
+ }));
99
+ }
100
+ });
101
+ this._ctx.onDispose(() => spacesSubscription.unsubscribe());
102
+ }
103
+ async _close(_) {
104
+ this._functionBySpaceKey.clear();
105
+ }
106
+ };
107
+
108
+ // packages/core/functions/src/handler.ts
109
+ import { PublicKey as PublicKey2 } from "@dxos/client";
110
+ import { log as log2 } from "@dxos/log";
20
111
  import { nonNullable } from "@dxos/util";
21
- var __dxlog_file = "/home/runner/work/dxos/dxos/packages/core/functions/src/handler.ts";
112
+ var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/core/functions/src/handler.ts";
22
113
  var subscriptionHandler = (handler) => {
23
- return ({ event, context, ...rest }) => {
114
+ return ({ event: { data }, context, ...rest }) => {
24
115
  const { client } = context;
25
- const space = event.space ? client.spaces.get(PublicKey.from(event.space)) : void 0;
26
- const objects = space && event.objects?.map((id) => space.db.getObjectById(id)).filter(nonNullable);
27
- if (!!event.space && !space) {
28
- log.warn("invalid space", {
29
- event
116
+ const space = data.spaceKey ? client.spaces.get(PublicKey2.from(data.spaceKey)) : void 0;
117
+ const objects = space ? data.objects?.map((id) => space.db.getObjectById(id)).filter(nonNullable) : [];
118
+ if (!!data.spaceKey && !space) {
119
+ log2.warn("invalid space", {
120
+ data
30
121
  }, {
31
- F: __dxlog_file,
32
- L: 68,
122
+ F: __dxlog_file2,
123
+ L: 91,
33
124
  S: void 0,
34
125
  C: (f, a) => f(...a)
35
126
  });
36
127
  } else {
37
- log.info("handler", {
128
+ log2.info("handler", {
38
129
  space: space?.key.truncate(),
39
130
  objects: objects?.length
40
131
  }, {
41
- F: __dxlog_file,
42
- L: 70,
132
+ F: __dxlog_file2,
133
+ L: 93,
43
134
  S: void 0,
44
135
  C: (f, a) => f(...a)
45
136
  });
46
137
  }
47
138
  return handler({
48
139
  event: {
49
- space,
50
- objects
140
+ data: {
141
+ ...data,
142
+ space,
143
+ objects
144
+ }
51
145
  },
52
146
  context,
53
147
  ...rest
@@ -59,22 +153,42 @@ var subscriptionHandler = (handler) => {
59
153
  import express from "express";
60
154
  import { getPort } from "get-port-please";
61
155
  import { join } from "@dxos/node-std/path";
62
- import { Trigger } from "@dxos/async";
156
+ import { Event as Event2, Trigger } from "@dxos/async";
157
+ import { Context } from "@dxos/context";
63
158
  import { invariant } from "@dxos/invariant";
64
- import { log as log2 } from "@dxos/log";
65
- var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/core/functions/src/runtime/dev-server.ts";
159
+ import { log as log3 } from "@dxos/log";
160
+ var __dxlog_file3 = "/home/runner/work/dxos/dxos/packages/core/functions/src/runtime/dev-server.ts";
66
161
  var DevServer = class {
67
- // prettier-ignore
68
- constructor(_client, _options) {
162
+ constructor(_client, _functionsRegistry, _options) {
69
163
  this._client = _client;
164
+ this._functionsRegistry = _functionsRegistry;
70
165
  this._options = _options;
166
+ this._ctx = createContext();
71
167
  this._handlers = {};
72
168
  this._seq = 0;
169
+ this.update = new Event2();
170
+ this._functionsRegistry.registered.on(async ({ added }) => {
171
+ added.forEach((def) => this._load(def));
172
+ await this._safeUpdateRegistration();
173
+ log3("new functions loaded", {
174
+ added
175
+ }, {
176
+ F: __dxlog_file3,
177
+ L: 52,
178
+ S: this,
179
+ C: (f, a) => f(...a)
180
+ });
181
+ });
182
+ }
183
+ get stats() {
184
+ return {
185
+ seq: this._seq
186
+ };
73
187
  }
74
188
  get endpoint() {
75
189
  invariant(this._port, void 0, {
76
- F: __dxlog_file2,
77
- L: 46,
190
+ F: __dxlog_file3,
191
+ L: 63,
78
192
  S: this,
79
193
  A: [
80
194
  "this._port",
@@ -89,44 +203,46 @@ var DevServer = class {
89
203
  get functions() {
90
204
  return Object.values(this._handlers);
91
205
  }
92
- async initialize() {
93
- for (const def of this._options.manifest.functions) {
94
- try {
95
- await this._load(def);
96
- } catch (err) {
97
- log2.error("parsing function (check manifest)", err, {
98
- F: __dxlog_file2,
99
- L: 63,
100
- S: this,
101
- C: (f, a) => f(...a)
102
- });
103
- }
104
- }
105
- }
106
206
  async start() {
207
+ invariant(!this._server, void 0, {
208
+ F: __dxlog_file3,
209
+ L: 76,
210
+ S: this,
211
+ A: [
212
+ "!this._server",
213
+ ""
214
+ ]
215
+ });
216
+ log3.info("starting...", void 0, {
217
+ F: __dxlog_file3,
218
+ L: 77,
219
+ S: this,
220
+ C: (f, a) => f(...a)
221
+ });
222
+ this._ctx = createContext();
107
223
  const app = express();
108
224
  app.use(express.json());
109
- app.post("/:name", async (req, res) => {
110
- const { name } = req.params;
225
+ app.post("/:path", async (req, res) => {
226
+ const { path: path2 } = req.params;
111
227
  try {
112
- log2.info("calling", {
113
- name
228
+ log3.info("calling", {
229
+ path: path2
114
230
  }, {
115
- F: __dxlog_file2,
116
- L: 75,
231
+ F: __dxlog_file3,
232
+ L: 87,
117
233
  S: this,
118
234
  C: (f, a) => f(...a)
119
235
  });
120
236
  if (this._options.reload) {
121
- const { def } = this._handlers[name];
237
+ const { def } = this._handlers["/" + path2];
122
238
  await this._load(def, true);
123
239
  }
124
- res.statusCode = await this._invoke(name, req.body);
240
+ res.statusCode = await this.invoke("/" + path2, req.body);
125
241
  res.end();
126
242
  } catch (err) {
127
- log2.catch(err, void 0, {
128
- F: __dxlog_file2,
129
- L: 84,
243
+ log3.catch(err, void 0, {
244
+ F: __dxlog_file3,
245
+ L: 97,
130
246
  S: this,
131
247
  C: (f, a) => f(...a)
132
248
  });
@@ -145,93 +261,195 @@ var DevServer = class {
145
261
  this._server = app.listen(this._port);
146
262
  try {
147
263
  const { registrationId, endpoint } = await this._client.services.services.FunctionRegistryService.register({
148
- endpoint: this.endpoint,
149
- functions: this.functions.map(({ def: { name } }) => ({
150
- name
151
- }))
264
+ endpoint: this.endpoint
152
265
  });
153
- log2.info("registered", {
154
- registrationId,
266
+ log3.info("registered", {
155
267
  endpoint
156
268
  }, {
157
- F: __dxlog_file2,
158
- L: 100,
269
+ F: __dxlog_file3,
270
+ L: 112,
159
271
  S: this,
160
272
  C: (f, a) => f(...a)
161
273
  });
162
- this._registrationId = registrationId;
163
274
  this._proxy = endpoint;
275
+ this._functionServiceRegistration = registrationId;
276
+ await this._functionsRegistry.open(this._ctx);
164
277
  } catch (err) {
165
278
  await this.stop();
166
279
  throw new Error("FunctionRegistryService not available (check plugin is configured).");
167
280
  }
281
+ log3.info("started", {
282
+ port: this._port
283
+ }, {
284
+ F: __dxlog_file3,
285
+ L: 123,
286
+ S: this,
287
+ C: (f, a) => f(...a)
288
+ });
168
289
  }
169
290
  async stop() {
291
+ invariant(this._server, void 0, {
292
+ F: __dxlog_file3,
293
+ L: 127,
294
+ S: this,
295
+ A: [
296
+ "this._server",
297
+ ""
298
+ ]
299
+ });
300
+ log3.info("stopping...", void 0, {
301
+ F: __dxlog_file3,
302
+ L: 128,
303
+ S: this,
304
+ C: (f, a) => f(...a)
305
+ });
170
306
  const trigger = new Trigger();
171
- this._server?.close(async () => {
172
- if (this._registrationId) {
173
- await this._client.services.services.FunctionRegistryService.unregister({
174
- registrationId: this._registrationId
175
- });
176
- log2.info("unregistered", {
177
- registrationId: this._registrationId
178
- }, {
179
- F: __dxlog_file2,
180
- L: 117,
181
- S: this,
182
- C: (f, a) => f(...a)
183
- });
184
- this._registrationId = void 0;
185
- this._proxy = void 0;
307
+ this._server.close(async () => {
308
+ log3.info("server stopped", void 0, {
309
+ F: __dxlog_file3,
310
+ L: 132,
311
+ S: this,
312
+ C: (f, a) => f(...a)
313
+ });
314
+ try {
315
+ if (this._functionServiceRegistration) {
316
+ invariant(this._client.services.services.FunctionRegistryService, void 0, {
317
+ F: __dxlog_file3,
318
+ L: 135,
319
+ S: this,
320
+ A: [
321
+ "this._client.services.services.FunctionRegistryService",
322
+ ""
323
+ ]
324
+ });
325
+ await this._client.services.services.FunctionRegistryService.unregister({
326
+ registrationId: this._functionServiceRegistration
327
+ });
328
+ log3.info("unregistered", {
329
+ registrationId: this._functionServiceRegistration
330
+ }, {
331
+ F: __dxlog_file3,
332
+ L: 140,
333
+ S: this,
334
+ C: (f, a) => f(...a)
335
+ });
336
+ this._functionServiceRegistration = void 0;
337
+ this._proxy = void 0;
338
+ }
339
+ trigger.wake();
340
+ } catch (err) {
341
+ trigger.throw(err);
186
342
  }
187
- trigger.wake();
188
343
  });
189
344
  await trigger.wait();
190
345
  this._port = void 0;
191
346
  this._server = void 0;
347
+ log3.info("stopped", void 0, {
348
+ F: __dxlog_file3,
349
+ L: 154,
350
+ S: this,
351
+ C: (f, a) => f(...a)
352
+ });
192
353
  }
193
354
  /**
194
355
  * Load function.
195
356
  */
196
- async _load(def, flush = false) {
197
- const { id, name, handler } = def;
198
- const path = join(this._options.directory, handler);
199
- log2.info("loading", {
200
- id
357
+ async _load(def, force) {
358
+ const { uri, route, handler } = def;
359
+ const filePath = join(this._options.baseDir, handler);
360
+ log3.info("loading", {
361
+ uri,
362
+ force
201
363
  }, {
202
- F: __dxlog_file2,
203
- L: 136,
364
+ F: __dxlog_file3,
365
+ L: 163,
204
366
  S: this,
205
367
  C: (f, a) => f(...a)
206
368
  });
207
- if (flush) {
208
- Object.keys(__require.cache).filter((key) => key.startsWith(path)).forEach((key) => delete __require.cache[key]);
369
+ if (force) {
370
+ Object.keys(__require.cache).filter((key) => key.startsWith(filePath)).forEach((key) => {
371
+ delete __require.cache[key];
372
+ });
209
373
  }
210
- const module = __require(path);
374
+ const module = __require(filePath);
211
375
  if (typeof module.default !== "function") {
212
- throw new Error(`Handler must export default function: ${id}`);
376
+ throw new Error(`Handler must export default function: ${uri}`);
213
377
  }
214
- this._handlers[name] = {
378
+ this._handlers[route] = {
215
379
  def,
216
380
  handler: module.default
217
381
  };
218
382
  }
383
+ async _safeUpdateRegistration() {
384
+ invariant(this._functionServiceRegistration, void 0, {
385
+ F: __dxlog_file3,
386
+ L: 185,
387
+ S: this,
388
+ A: [
389
+ "this._functionServiceRegistration",
390
+ ""
391
+ ]
392
+ });
393
+ try {
394
+ await this._client.services.services.FunctionRegistryService.updateRegistration({
395
+ registrationId: this._functionServiceRegistration,
396
+ functions: this.functions.map(({ def: { id, route } }) => ({
397
+ id,
398
+ route
399
+ }))
400
+ });
401
+ } catch (e) {
402
+ log3.catch(e, void 0, {
403
+ F: __dxlog_file3,
404
+ L: 192,
405
+ S: this,
406
+ C: (f, a) => f(...a)
407
+ });
408
+ }
409
+ }
219
410
  /**
220
- * Invoke function handler.
411
+ * Invoke function.
221
412
  */
222
- async _invoke(name, event) {
413
+ async invoke(path2, data) {
223
414
  const seq = ++this._seq;
224
415
  const now = Date.now();
225
- log2.info("req", {
416
+ log3.info("req", {
226
417
  seq,
227
- name
418
+ path: path2
419
+ }, {
420
+ F: __dxlog_file3,
421
+ L: 203,
422
+ S: this,
423
+ C: (f, a) => f(...a)
424
+ });
425
+ const statusCode = await this._invoke(path2, {
426
+ data
427
+ });
428
+ log3.info("res", {
429
+ seq,
430
+ path: path2,
431
+ statusCode,
432
+ duration: Date.now() - now
228
433
  }, {
229
- F: __dxlog_file2,
230
- L: 161,
434
+ F: __dxlog_file3,
435
+ L: 206,
231
436
  S: this,
232
437
  C: (f, a) => f(...a)
233
438
  });
234
- const { handler } = this._handlers[name];
439
+ this.update.emit(statusCode);
440
+ return statusCode;
441
+ }
442
+ async _invoke(path2, event) {
443
+ const { handler } = this._handlers[path2] ?? {};
444
+ invariant(handler, `invalid path: ${path2}`, {
445
+ F: __dxlog_file3,
446
+ L: 213,
447
+ S: this,
448
+ A: [
449
+ "handler",
450
+ "`invalid path: ${path}`"
451
+ ]
452
+ });
235
453
  const context = {
236
454
  client: this._client,
237
455
  dataDir: this._options.dataDir
@@ -248,453 +466,589 @@ var DevServer = class {
248
466
  event,
249
467
  response
250
468
  });
251
- log2.info("res", {
252
- seq,
253
- name,
254
- statusCode,
255
- duration: Date.now() - now
256
- }, {
257
- F: __dxlog_file2,
258
- L: 178,
259
- S: this,
260
- C: (f, a) => f(...a)
261
- });
262
469
  return statusCode;
263
470
  }
264
471
  };
472
+ var createContext = () => new Context({
473
+ name: "DevServer"
474
+ });
265
475
 
266
476
  // packages/core/functions/src/runtime/scheduler.ts
267
- import { CronJob } from "cron";
268
- import http from "@dxos/node-std/http";
269
- import WebSocket from "ws";
270
- import { TextV0Type } from "@braneframe/types";
271
- import { debounce, DeferredTask, sleep, Trigger as Trigger2 } from "@dxos/async";
272
- import { createSubscription, Filter, getAutomergeObjectCore } from "@dxos/client/echo";
273
- import { Context } from "@dxos/context";
274
- import { invariant as invariant2 } from "@dxos/invariant";
275
- import { log as log3 } from "@dxos/log";
276
- import { ComplexMap } from "@dxos/util";
277
- var __dxlog_file3 = "/home/runner/work/dxos/dxos/packages/core/functions/src/runtime/scheduler.ts";
477
+ import path from "@dxos/node-std/path";
478
+ import { Mutex } from "@dxos/async";
479
+ import { Context as Context2 } from "@dxos/context";
480
+ import { log as log4 } from "@dxos/log";
481
+ var __dxlog_file4 = "/home/runner/work/dxos/dxos/packages/core/functions/src/runtime/scheduler.ts";
278
482
  var Scheduler = class {
279
- constructor(_client, _manifest, _options = {}) {
280
- this._client = _client;
281
- this._manifest = _manifest;
483
+ constructor(functions, triggers, _options = {}) {
484
+ this.functions = functions;
485
+ this.triggers = triggers;
282
486
  this._options = _options;
283
- this._mounts = new ComplexMap(({ id, spaceKey }) => `${spaceKey.toHex()}:${id}`);
487
+ this._ctx = createContext2();
488
+ this._callMutex = new Mutex();
489
+ this.functions.registered.on(async ({ space, added }) => {
490
+ await this._safeActivateTriggers(space, this.triggers.getInactiveTriggers(space), added);
491
+ });
492
+ this.triggers.registered.on(async ({ space, triggers: triggers2 }) => {
493
+ await this._safeActivateTriggers(space, triggers2, this.functions.getFunctions(space));
494
+ });
284
495
  }
285
496
  async start() {
286
- this._client.spaces.subscribe(async (spaces) => {
287
- for (const space of spaces) {
288
- await space.waitUntilReady();
289
- for (const trigger of this._manifest.triggers ?? []) {
290
- await this.mount(new Context(), space, trigger);
291
- }
292
- }
293
- });
497
+ await this._ctx.dispose();
498
+ this._ctx = createContext2();
499
+ await this.functions.open(this._ctx);
500
+ await this.triggers.open(this._ctx);
294
501
  }
295
502
  async stop() {
296
- for (const { id, spaceKey } of this._mounts.keys()) {
297
- await this.unmount(id, spaceKey);
298
- }
503
+ await this._ctx.dispose();
504
+ await this.functions.close();
505
+ await this.triggers.close();
299
506
  }
300
- async mount(ctx, space, trigger) {
301
- const key = {
302
- id: trigger.function,
303
- spaceKey: space.key
304
- };
305
- const def = this._manifest.functions.find((config) => config.id === trigger.function);
306
- invariant2(def, `Function not found: ${trigger.function}`, {
307
- F: __dxlog_file3,
308
- L: 72,
309
- S: this,
310
- A: [
311
- "def",
312
- "`Function not found: ${trigger.function}`"
313
- ]
507
+ // TODO(burdon): Remove and update registries directly.
508
+ async register(space, manifest) {
509
+ await this.functions.register(space, manifest.functions);
510
+ await this.triggers.register(space, manifest);
511
+ }
512
+ async _safeActivateTriggers(space, triggers, functions) {
513
+ const mountTasks = triggers.map((trigger) => {
514
+ return this.activate(space, functions, trigger);
314
515
  });
315
- const exists = this._mounts.get(key);
316
- if (!exists) {
317
- this._mounts.set(key, {
318
- ctx,
319
- trigger
320
- });
321
- log3("mount", {
322
- space: space.key,
323
- trigger
516
+ await Promise.all(mountTasks).catch(log4.catch);
517
+ }
518
+ async activate(space, functions, fnTrigger) {
519
+ const definition = functions.find((def) => def.uri === fnTrigger.function);
520
+ if (!definition) {
521
+ log4.info("function is not found for trigger", {
522
+ fnTrigger
324
523
  }, {
325
- F: __dxlog_file3,
524
+ F: __dxlog_file4,
326
525
  L: 78,
327
526
  S: this,
328
527
  C: (f, a) => f(...a)
329
528
  });
330
- if (ctx.disposed) {
331
- return;
332
- }
333
- if (trigger.timer) {
334
- await this._createTimer(ctx, space, def, trigger.timer);
335
- }
336
- if (trigger.webhook) {
337
- await this._createWebhook(ctx, space, def, trigger.webhook);
338
- }
339
- if (trigger.websocket) {
340
- await this._createWebsocket(ctx, space, def, trigger.websocket);
341
- }
342
- if (trigger.subscription) {
343
- await this._createSubscription(ctx, space, def, trigger.subscription);
344
- }
345
- }
346
- }
347
- async unmount(id, spaceKey) {
348
- const key = {
349
- id,
350
- spaceKey
351
- };
352
- const { ctx } = this._mounts.get(key) ?? {};
353
- if (ctx) {
354
- this._mounts.delete(key);
355
- await ctx.dispose();
529
+ return;
356
530
  }
531
+ await this.triggers.activate({
532
+ space
533
+ }, fnTrigger, async (args) => {
534
+ return this._callMutex.executeSynchronized(() => {
535
+ return this._execFunction(definition, fnTrigger, {
536
+ meta: fnTrigger.meta,
537
+ data: {
538
+ ...args,
539
+ spaceKey: space.key
540
+ }
541
+ });
542
+ });
543
+ });
544
+ log4("activated trigger", {
545
+ space: space.key,
546
+ trigger: fnTrigger
547
+ }, {
548
+ F: __dxlog_file4,
549
+ L: 91,
550
+ S: this,
551
+ C: (f, a) => f(...a)
552
+ });
357
553
  }
358
- // TODO(burdon): Pass in Space key (common context).
359
- async _execFunction(def, data) {
554
+ async _execFunction(def, trigger, { data, meta }) {
555
+ let status = 0;
360
556
  try {
361
- log3.info("exec", {
362
- function: def.id
363
- }, {
364
- F: __dxlog_file3,
365
- L: 117,
366
- S: this,
367
- C: (f, a) => f(...a)
368
- });
557
+ const payload = Object.assign({}, meta && {
558
+ meta
559
+ }, data);
369
560
  const { endpoint, callback } = this._options;
370
561
  if (endpoint) {
371
- await fetch(`${this._options.endpoint}/${def.name}`, {
562
+ const url = path.join(endpoint, def.route);
563
+ log4.info("exec", {
564
+ function: def.uri,
565
+ url,
566
+ triggerType: trigger.spec.type
567
+ }, {
568
+ F: __dxlog_file4,
569
+ L: 108,
570
+ S: this,
571
+ C: (f, a) => f(...a)
572
+ });
573
+ const response = await fetch(url, {
372
574
  method: "POST",
373
575
  headers: {
374
576
  "Content-Type": "application/json"
375
577
  },
376
- body: JSON.stringify(data)
578
+ body: JSON.stringify(payload)
377
579
  });
580
+ status = response.status;
378
581
  } else if (callback) {
379
- await callback(data);
582
+ log4.info("exec", {
583
+ function: def.uri
584
+ }, {
585
+ F: __dxlog_file4,
586
+ L: 119,
587
+ S: this,
588
+ C: (f, a) => f(...a)
589
+ });
590
+ status = await callback(payload) ?? 200;
591
+ }
592
+ if (status && status >= 400) {
593
+ throw new Error(`Response: ${status}`);
380
594
  }
381
- log3.info("done", {
382
- function: def.id
595
+ log4.info("done", {
596
+ function: def.uri,
597
+ status
383
598
  }, {
384
- F: __dxlog_file3,
385
- L: 133,
599
+ F: __dxlog_file4,
600
+ L: 129,
386
601
  S: this,
387
602
  C: (f, a) => f(...a)
388
603
  });
389
604
  } catch (err) {
390
- log3.error("error", {
391
- function: def.id,
605
+ log4.error("error", {
606
+ function: def.uri,
392
607
  error: err.message
393
608
  }, {
394
- F: __dxlog_file3,
395
- L: 135,
609
+ F: __dxlog_file4,
610
+ L: 131,
396
611
  S: this,
397
612
  C: (f, a) => f(...a)
398
613
  });
614
+ status = 500;
399
615
  }
616
+ return status;
400
617
  }
401
- //
402
- // Triggers
403
- //
404
- /**
405
- * Cron timer.
406
- */
407
- async _createTimer(ctx, space, def, trigger) {
408
- log3.info("timer", {
409
- space: space.key,
410
- trigger
618
+ };
619
+ var createContext2 = () => new Context2({
620
+ name: "FunctionScheduler"
621
+ });
622
+
623
+ // packages/core/functions/src/trigger/trigger-registry.ts
624
+ import { Event as Event3 } from "@dxos/async";
625
+ import { create as create2, Filter as Filter3, getMeta } from "@dxos/client/echo";
626
+ import { Context as Context3, Resource as Resource2 } from "@dxos/context";
627
+ import { ECHO_ATTR_META, foreignKey, foreignKeyEquals, splitMeta } from "@dxos/echo-schema";
628
+ import { invariant as invariant2 } from "@dxos/invariant";
629
+ import { PublicKey as PublicKey3 } from "@dxos/keys";
630
+ import { log as log9 } from "@dxos/log";
631
+ import { ComplexMap as ComplexMap2 } from "@dxos/util";
632
+
633
+ // packages/core/functions/src/trigger/type/subscription-trigger.ts
634
+ import { TextV0Type } from "@braneframe/types";
635
+ import { debounce, UpdateScheduler } from "@dxos/async";
636
+ import { createSubscription, Filter as Filter2, getAutomergeObjectCore } from "@dxos/echo-db";
637
+ import { log as log5 } from "@dxos/log";
638
+ var __dxlog_file5 = "/home/runner/work/dxos/dxos/packages/core/functions/src/trigger/type/subscription-trigger.ts";
639
+ var createSubscriptionTrigger = async (ctx, triggerCtx, spec, callback) => {
640
+ const objectIds = /* @__PURE__ */ new Set();
641
+ const task = new UpdateScheduler(ctx, async () => {
642
+ if (objectIds.size > 0) {
643
+ const objects = Array.from(objectIds);
644
+ objectIds.clear();
645
+ await callback({
646
+ objects
647
+ });
648
+ }
649
+ }, {
650
+ maxFrequency: 4
651
+ });
652
+ const subscriptions = [];
653
+ const subscription = createSubscription(({ added, updated }) => {
654
+ const sizeBefore = objectIds.size;
655
+ for (const object of added) {
656
+ objectIds.add(object.id);
657
+ }
658
+ for (const object of updated) {
659
+ objectIds.add(object.id);
660
+ }
661
+ if (objectIds.size > sizeBefore) {
662
+ log5.info("updated", {
663
+ added: added.length,
664
+ updated: updated.length
665
+ }, {
666
+ F: __dxlog_file5,
667
+ L: 45,
668
+ S: void 0,
669
+ C: (f, a) => f(...a)
670
+ });
671
+ task.trigger();
672
+ }
673
+ });
674
+ subscriptions.push(() => subscription.unsubscribe());
675
+ const { filter, options: { deep, delay } = {} } = spec;
676
+ const update = ({ objects }) => {
677
+ subscription.update(objects);
678
+ if (deep) {
679
+ log5.info("update", {
680
+ objects: objects.length
681
+ }, {
682
+ F: __dxlog_file5,
683
+ L: 59,
684
+ S: void 0,
685
+ C: (f, a) => f(...a)
686
+ });
687
+ for (const object of objects) {
688
+ const content = object.content;
689
+ if (content instanceof TextV0Type) {
690
+ subscriptions.push(getAutomergeObjectCore(content).updates.on(debounce(() => subscription.update([
691
+ object
692
+ ]), 1e3)));
693
+ }
694
+ }
695
+ }
696
+ };
697
+ const query = triggerCtx.space.db.query(Filter2.or(filter.map(({ type, props }) => Filter2.typename(type, props))));
698
+ subscriptions.push(query.subscribe(delay ? debounce(update, delay) : update));
699
+ ctx.onDispose(() => {
700
+ subscriptions.forEach((unsubscribe) => unsubscribe());
701
+ });
702
+ };
703
+
704
+ // packages/core/functions/src/trigger/type/timer-trigger.ts
705
+ import { CronJob } from "cron";
706
+ import { DeferredTask } from "@dxos/async";
707
+ import { log as log6 } from "@dxos/log";
708
+ var __dxlog_file6 = "/home/runner/work/dxos/dxos/packages/core/functions/src/trigger/type/timer-trigger.ts";
709
+ var createTimerTrigger = async (ctx, triggerContext, spec, callback) => {
710
+ const task = new DeferredTask(ctx, async () => {
711
+ await callback({});
712
+ });
713
+ let last = 0;
714
+ let run = 0;
715
+ const job = CronJob.from({
716
+ cronTime: spec.cron,
717
+ runOnInit: false,
718
+ onTick: () => {
719
+ const now = Date.now();
720
+ const delta = last ? now - last : 0;
721
+ last = now;
722
+ run++;
723
+ log6.info("tick", {
724
+ space: triggerContext.space.key.truncate(),
725
+ count: run,
726
+ delta
727
+ }, {
728
+ F: __dxlog_file6,
729
+ L: 37,
730
+ S: void 0,
731
+ C: (f, a) => f(...a)
732
+ });
733
+ task.schedule();
734
+ }
735
+ });
736
+ job.start();
737
+ ctx.onDispose(() => job.stop());
738
+ };
739
+
740
+ // packages/core/functions/src/trigger/type/webhook-trigger.ts
741
+ import { getPort as getPort2 } from "get-port-please";
742
+ import http from "@dxos/node-std/http";
743
+ import { log as log7 } from "@dxos/log";
744
+ var __dxlog_file7 = "/home/runner/work/dxos/dxos/packages/core/functions/src/trigger/type/webhook-trigger.ts";
745
+ var createWebhookTrigger = async (ctx, _, spec, callback) => {
746
+ const server = http.createServer(async (req, res) => {
747
+ if (req.method !== spec.method) {
748
+ res.statusCode = 405;
749
+ return res.end();
750
+ }
751
+ res.statusCode = await callback({});
752
+ res.end();
753
+ });
754
+ const port = await getPort2({
755
+ random: true
756
+ });
757
+ server.listen(port, () => {
758
+ log7.info("started webhook", {
759
+ port
411
760
  }, {
412
- F: __dxlog_file3,
413
- L: 147,
414
- S: this,
761
+ F: __dxlog_file7,
762
+ L: 40,
763
+ S: void 0,
415
764
  C: (f, a) => f(...a)
416
765
  });
417
- const { cron } = trigger;
418
- const task = new DeferredTask(ctx, async () => {
419
- await this._execFunction(def, {
420
- space: space.key
421
- });
422
- });
423
- let last = 0;
424
- let run = 0;
425
- const job = CronJob.from({
426
- cronTime: cron,
427
- runOnInit: false,
428
- onTick: () => {
429
- const now = Date.now();
430
- const delta = last ? now - last : 0;
431
- last = now;
432
- run++;
433
- log3.info("tick", {
434
- space: space.key.truncate(),
435
- count: run,
436
- delta
766
+ spec.port = port;
767
+ });
768
+ ctx.onDispose(() => {
769
+ server.close();
770
+ });
771
+ };
772
+
773
+ // packages/core/functions/src/trigger/type/websocket-trigger.ts
774
+ import WebSocket from "ws";
775
+ import { sleep, Trigger as Trigger2 } from "@dxos/async";
776
+ import { log as log8 } from "@dxos/log";
777
+ var __dxlog_file8 = "/home/runner/work/dxos/dxos/packages/core/functions/src/trigger/type/websocket-trigger.ts";
778
+ var createWebsocketTrigger = async (ctx, triggerCtx, spec, callback, options = {
779
+ retryDelay: 2,
780
+ maxAttempts: 5
781
+ }) => {
782
+ const { url, init } = spec;
783
+ let ws;
784
+ for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
785
+ const open = new Trigger2();
786
+ ws = new WebSocket(url);
787
+ Object.assign(ws, {
788
+ onopen: () => {
789
+ log8.info("opened", {
790
+ url
437
791
  }, {
438
- F: __dxlog_file3,
439
- L: 167,
440
- S: this,
792
+ F: __dxlog_file8,
793
+ L: 39,
794
+ S: void 0,
795
+ C: (f, a) => f(...a)
796
+ });
797
+ if (spec.init) {
798
+ ws.send(new TextEncoder().encode(JSON.stringify(init)));
799
+ }
800
+ open.wake(true);
801
+ },
802
+ onclose: (event) => {
803
+ log8.info("closed", {
804
+ url,
805
+ code: event.code
806
+ }, {
807
+ F: __dxlog_file8,
808
+ L: 48,
809
+ S: void 0,
810
+ C: (f, a) => f(...a)
811
+ });
812
+ if (event.code === 1006) {
813
+ setTimeout(async () => {
814
+ log8.info(`reconnecting in ${options.retryDelay}s...`, {
815
+ url
816
+ }, {
817
+ F: __dxlog_file8,
818
+ L: 53,
819
+ S: void 0,
820
+ C: (f, a) => f(...a)
821
+ });
822
+ await createWebsocketTrigger(ctx, triggerCtx, spec, callback, options);
823
+ }, options.retryDelay * 1e3);
824
+ }
825
+ open.wake(false);
826
+ },
827
+ onerror: (event) => {
828
+ log8.catch(event.error, {
829
+ url
830
+ }, {
831
+ F: __dxlog_file8,
832
+ L: 62,
833
+ S: void 0,
441
834
  C: (f, a) => f(...a)
442
835
  });
443
- task.schedule();
836
+ },
837
+ onmessage: async (event) => {
838
+ try {
839
+ log8.info("message", void 0, {
840
+ F: __dxlog_file8,
841
+ L: 67,
842
+ S: void 0,
843
+ C: (f, a) => f(...a)
844
+ });
845
+ const data = JSON.parse(new TextDecoder().decode(event.data));
846
+ await callback({
847
+ data
848
+ });
849
+ } catch (err) {
850
+ log8.catch(err, {
851
+ url
852
+ }, {
853
+ F: __dxlog_file8,
854
+ L: 71,
855
+ S: void 0,
856
+ C: (f, a) => f(...a)
857
+ });
858
+ }
444
859
  }
445
860
  });
446
- job.start();
447
- ctx.onDispose(() => job.stop());
861
+ const isOpen = await open.wait();
862
+ if (isOpen) {
863
+ break;
864
+ } else {
865
+ const wait = Math.pow(attempt, 2) * options.retryDelay;
866
+ if (attempt < options.maxAttempts) {
867
+ log8.warn(`failed to connect; trying again in ${wait}s`, {
868
+ attempt
869
+ }, {
870
+ F: __dxlog_file8,
871
+ L: 82,
872
+ S: void 0,
873
+ C: (f, a) => f(...a)
874
+ });
875
+ await sleep(wait * 1e3);
876
+ }
877
+ }
448
878
  }
449
- /**
450
- * Webhook.
451
- */
452
- async _createWebhook(ctx, space, def, trigger) {
453
- log3.info("webhook", {
454
- space: space.key,
879
+ ctx.onDispose(() => {
880
+ ws?.close();
881
+ });
882
+ };
883
+
884
+ // packages/core/functions/src/trigger/trigger-registry.ts
885
+ var __dxlog_file9 = "/home/runner/work/dxos/dxos/packages/core/functions/src/trigger/trigger-registry.ts";
886
+ var triggerHandlers = {
887
+ subscription: createSubscriptionTrigger,
888
+ timer: createTimerTrigger,
889
+ webhook: createWebhookTrigger,
890
+ websocket: createWebsocketTrigger
891
+ };
892
+ var TriggerRegistry = class extends Resource2 {
893
+ constructor(_client, _options) {
894
+ super();
895
+ this._client = _client;
896
+ this._options = _options;
897
+ this._triggersBySpaceKey = new ComplexMap2(PublicKey3.hash);
898
+ this.registered = new Event3();
899
+ this.removed = new Event3();
900
+ }
901
+ getActiveTriggers(space) {
902
+ return this._getTriggers(space, (t) => t.activationCtx != null);
903
+ }
904
+ getInactiveTriggers(space) {
905
+ return this._getTriggers(space, (t) => t.activationCtx == null);
906
+ }
907
+ async activate(triggerCtx, trigger, callback) {
908
+ log9("activate", {
909
+ space: triggerCtx.space.key,
455
910
  trigger
456
911
  }, {
457
- F: __dxlog_file3,
458
- L: 180,
912
+ F: __dxlog_file9,
913
+ L: 75,
459
914
  S: this,
460
915
  C: (f, a) => f(...a)
461
916
  });
462
- const { port } = trigger;
463
- const server = http.createServer(async (req, res) => {
464
- await this._execFunction(def, {
465
- space: space.key
466
- });
467
- });
468
- server.listen(port, () => {
469
- log3.info("started webhook", {
470
- port
471
- }, {
472
- F: __dxlog_file3,
473
- L: 189,
474
- S: this,
475
- C: (f, a) => f(...a)
476
- });
917
+ const activationCtx = new Context3({
918
+ name: `trigger_${trigger.function}`
477
919
  });
478
- ctx.onDispose(() => {
479
- server.close();
920
+ this._ctx.onDispose(() => activationCtx.dispose());
921
+ const registeredTrigger = this._triggersBySpaceKey.get(triggerCtx.space.key)?.find((reg) => reg.trigger.id === trigger.id);
922
+ invariant2(registeredTrigger, `Trigger is not registered: ${trigger.function}`, {
923
+ F: __dxlog_file9,
924
+ L: 81,
925
+ S: this,
926
+ A: [
927
+ "registeredTrigger",
928
+ "`Trigger is not registered: ${trigger.function}`"
929
+ ]
480
930
  });
931
+ registeredTrigger.activationCtx = activationCtx;
932
+ try {
933
+ const options = this._options?.[trigger.spec.type];
934
+ await triggerHandlers[trigger.spec.type](activationCtx, triggerCtx, trigger.spec, callback, options);
935
+ } catch (err) {
936
+ delete registeredTrigger.activationCtx;
937
+ throw err;
938
+ }
481
939
  }
482
940
  /**
483
- * Websocket.
941
+ * Loads triggers from the manifest into the space.
484
942
  */
485
- async _createWebsocket(ctx, space, def, trigger, options = {
486
- retryDelay: 2,
487
- maxAttempts: 5
488
- }) {
489
- log3.info("websocket", {
490
- space: space.key,
491
- trigger
943
+ async register(space, manifest) {
944
+ log9("register", {
945
+ space: space.key
492
946
  }, {
493
- F: __dxlog_file3,
494
- L: 213,
947
+ F: __dxlog_file9,
948
+ L: 97,
495
949
  S: this,
496
950
  C: (f, a) => f(...a)
497
951
  });
498
- const { url } = trigger;
499
- let ws;
500
- for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
501
- const open = new Trigger2();
502
- ws = new WebSocket(url);
503
- Object.assign(ws, {
504
- onopen: () => {
505
- log3.info("opened", {
506
- url
507
- }, {
508
- F: __dxlog_file3,
509
- L: 223,
510
- S: this,
511
- C: (f, a) => f(...a)
512
- });
513
- if (trigger.init) {
514
- ws.send(new TextEncoder().encode(JSON.stringify(trigger.init)));
515
- }
516
- open.wake(true);
517
- },
518
- onclose: () => {
519
- log3.info("closed", {
520
- url
521
- }, {
522
- F: __dxlog_file3,
523
- L: 232,
524
- S: this,
525
- C: (f, a) => f(...a)
526
- });
527
- open.wake(false);
528
- },
529
- onerror: (event) => {
530
- log3.catch(event.error, {
531
- url
532
- }, {
533
- F: __dxlog_file3,
534
- L: 237,
535
- S: this,
536
- C: (f, a) => f(...a)
537
- });
538
- },
539
- onmessage: async (event) => {
540
- try {
541
- const data = JSON.parse(new TextDecoder().decode(event.data));
542
- await this._execFunction(def, {
543
- space: space.key,
544
- data
545
- });
546
- } catch (err) {
547
- log3.catch(err, {
548
- url
549
- }, {
550
- F: __dxlog_file3,
551
- L: 245,
552
- S: this,
553
- C: (f, a) => f(...a)
554
- });
555
- }
952
+ if (!manifest.triggers?.length) {
953
+ return;
954
+ }
955
+ if (!space.db.graph.runtimeSchemaRegistry.hasSchema(FunctionTrigger)) {
956
+ space.db.graph.runtimeSchemaRegistry.registerSchema(FunctionTrigger);
957
+ }
958
+ const { objects: existing } = await space.db.query(Filter3.schema(FunctionTrigger)).run();
959
+ const { added, removed } = diff(existing, manifest.triggers, (a, b) => {
960
+ const keys = b[ECHO_ATTR_META]?.keys ?? [
961
+ foreignKey("manifest", [
962
+ b.function,
963
+ b.spec.type
964
+ ].join("-"))
965
+ ];
966
+ return intersection(getMeta(a)?.keys ?? [], keys, foreignKeyEquals).length > 0;
967
+ });
968
+ added.forEach((trigger) => {
969
+ const { meta, object } = splitMeta(trigger);
970
+ space.db.add(create2(FunctionTrigger, object, meta));
971
+ });
972
+ removed.forEach((trigger) => space.db.remove(trigger));
973
+ }
974
+ async _open() {
975
+ const spaceListSubscription = this._client.spaces.subscribe(async (spaces) => {
976
+ for (const space of spaces) {
977
+ if (this._triggersBySpaceKey.has(space.key)) {
978
+ continue;
556
979
  }
557
- });
558
- const isOpen = await open.wait();
559
- if (isOpen) {
560
- break;
561
- } else {
562
- const wait = Math.pow(attempt, 2) * options.retryDelay;
563
- if (attempt < options.maxAttempts) {
564
- log3.warn(`failed to connect; trying again in ${wait}s`, {
565
- attempt
566
- }, {
567
- F: __dxlog_file3,
568
- L: 256,
569
- S: this,
570
- C: (f, a) => f(...a)
571
- });
572
- await sleep(wait * 1e3);
980
+ const registered = [];
981
+ this._triggersBySpaceKey.set(space.key, registered);
982
+ await space.waitUntilReady();
983
+ if (this._ctx.disposed) {
984
+ break;
573
985
  }
986
+ const functionsSubscription = space.db.query(Filter3.schema(FunctionTrigger)).subscribe(async (triggers) => {
987
+ await this._handleRemovedTriggers(space, triggers.objects, registered);
988
+ this._handleNewTriggers(space, triggers.objects, registered);
989
+ });
990
+ this._ctx.onDispose(functionsSubscription);
574
991
  }
575
- }
576
- ctx.onDispose(() => {
577
- ws?.close();
578
992
  });
993
+ this._ctx.onDispose(() => spaceListSubscription.unsubscribe());
579
994
  }
580
- /**
581
- * ECHO subscription.
582
- */
583
- async _createSubscription(ctx, space, def, trigger) {
584
- log3.info("subscription", {
585
- space: space.key,
586
- trigger
587
- }, {
588
- F: __dxlog_file3,
589
- L: 271,
590
- S: this,
591
- C: (f, a) => f(...a)
592
- });
593
- const objectIds = /* @__PURE__ */ new Set();
594
- const task = new DeferredTask(ctx, async () => {
595
- await this._execFunction(def, {
596
- space: space.key,
597
- objects: Array.from(objectIds)
598
- });
995
+ async _close(_) {
996
+ this._triggersBySpaceKey.clear();
997
+ }
998
+ _handleNewTriggers(space, allTriggers, registered) {
999
+ const newTriggers = allTriggers.filter((candidate) => {
1000
+ return registered.find((reg) => reg.trigger.id === candidate.id) == null;
599
1001
  });
600
- const subscriptions = [];
601
- const subscription = createSubscription(({ added, updated }) => {
602
- log3.info("updated", {
603
- added: added.length,
604
- updated: updated.length
605
- }, {
606
- F: __dxlog_file3,
607
- L: 281,
1002
+ if (newTriggers.length > 0) {
1003
+ const newRegisteredTriggers = newTriggers.map((trigger) => ({
1004
+ trigger
1005
+ }));
1006
+ registered.push(...newRegisteredTriggers);
1007
+ log9("registered new triggers", () => ({
1008
+ spaceKey: space.key,
1009
+ functions: newTriggers.map((t) => t.function)
1010
+ }), {
1011
+ F: __dxlog_file9,
1012
+ L: 159,
608
1013
  S: this,
609
1014
  C: (f, a) => f(...a)
610
1015
  });
611
- for (const object of added) {
612
- objectIds.add(object.id);
613
- }
614
- for (const object of updated) {
615
- objectIds.add(object.id);
616
- }
617
- task.schedule();
618
- });
619
- subscriptions.push(() => subscription.unsubscribe());
620
- const { filter, options: { deep, delay } = {} } = trigger;
621
- const update = ({ objects }) => {
622
- subscription.update(objects);
623
- if (deep) {
624
- log3.info("update", {
625
- objects: objects.length
626
- }, {
627
- F: __dxlog_file3,
628
- L: 301,
629
- S: this,
630
- C: (f, a) => f(...a)
631
- });
632
- for (const object of objects) {
633
- const content = object.content;
634
- if (content instanceof TextV0Type) {
635
- subscriptions.push(getAutomergeObjectCore(content).updates.on(debounce(() => subscription.update([
636
- object
637
- ]), 1e3)));
638
- }
639
- }
1016
+ this.registered.emit({
1017
+ space,
1018
+ triggers: newTriggers
1019
+ });
1020
+ }
1021
+ }
1022
+ async _handleRemovedTriggers(space, allTriggers, registered) {
1023
+ const removed = [];
1024
+ for (let i = registered.length - 1; i >= 0; i--) {
1025
+ const wasRemoved = allTriggers.find((trigger) => trigger.id === registered[i].trigger.id) == null;
1026
+ if (wasRemoved) {
1027
+ const unregistered = registered.splice(i, 1)[0];
1028
+ await unregistered.activationCtx?.dispose();
1029
+ removed.push(unregistered.trigger);
640
1030
  }
641
- };
642
- const query = space.db.query(Filter.or(filter.map(({ type, props }) => Filter.typename(type, props))));
643
- subscriptions.push(query.subscribe(delay ? debounce(update, delay) : update));
644
- ctx.onDispose(() => {
645
- subscriptions.forEach((unsubscribe) => unsubscribe());
646
- });
1031
+ }
1032
+ if (removed.length > 0) {
1033
+ this.removed.emit({
1034
+ space,
1035
+ triggers: removed
1036
+ });
1037
+ }
1038
+ }
1039
+ _getTriggers(space, predicate) {
1040
+ const allSpaceTriggers = this._triggersBySpaceKey.get(space.key) ?? [];
1041
+ return allSpaceTriggers.filter(predicate).map((trigger) => trigger.trigger);
647
1042
  }
648
1043
  };
649
-
650
- // packages/core/functions/src/types.ts
651
- import * as S from "@effect/schema/Schema";
652
- var TimerTriggerSchema = S.struct({
653
- cron: S.string
654
- });
655
- var WebhookTriggerSchema = S.struct({
656
- port: S.number
657
- });
658
- var WebsocketTriggerSchema = S.struct({
659
- url: S.string,
660
- init: S.optional(S.record(S.string, S.any))
661
- });
662
- var SubscriptionTriggerSchema = S.struct({
663
- spaceKey: S.optional(S.string),
664
- // TODO(burdon): Define query DSL.
665
- filter: S.array(S.struct({
666
- type: S.string,
667
- props: S.optional(S.record(S.string, S.any))
668
- })),
669
- options: S.optional(S.struct({
670
- // Watch changes to object (not just creation).
671
- deep: S.optional(S.boolean),
672
- // Debounce changes (delay in ms).
673
- delay: S.optional(S.number)
674
- }))
675
- });
676
- var FunctionTriggerSchema = S.struct({
677
- function: S.string.pipe(S.description("Function ID/URI.")),
678
- timer: S.optional(TimerTriggerSchema),
679
- webhook: S.optional(WebhookTriggerSchema),
680
- websocket: S.optional(WebsocketTriggerSchema),
681
- subscription: S.optional(SubscriptionTriggerSchema)
682
- });
683
- var FunctionDefSchema = S.struct({
684
- id: S.string,
685
- description: S.optional(S.string),
686
- name: S.string,
687
- // TODO(burdon): NPM/GitHub URL?
688
- handler: S.string
689
- });
690
- var FunctionManifestSchema = S.struct({
691
- functions: S.mutable(S.array(FunctionDefSchema)),
692
- triggers: S.mutable(S.array(FunctionTriggerSchema))
693
- });
694
1044
  export {
695
1045
  DevServer,
1046
+ FunctionDef,
696
1047
  FunctionManifestSchema,
1048
+ FunctionRegistry,
1049
+ FunctionTrigger,
697
1050
  Scheduler,
1051
+ TriggerRegistry,
698
1052
  subscriptionHandler
699
1053
  };
700
1054
  //# sourceMappingURL=index.mjs.map