@dxos/functions 0.5.3-main.6f2dfea → 0.5.3-main.77c09ab

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