@dxos/functions 0.5.3-main.f752aaa → 0.5.3-main.f9b873d

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