@dxos/functions 0.5.3-main.6f2dfea → 0.5.3-main.79e0565

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