@dxos/functions 0.5.2 → 0.5.3-main.088a2c8

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 (38) hide show
  1. package/dist/lib/browser/index.mjs +492 -146
  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 +488 -143
  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 +1 -1
  10. package/dist/types/src/index.d.ts.map +1 -1
  11. package/dist/types/src/runtime/dev-server.d.ts +17 -6
  12. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  13. package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
  14. package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
  15. package/dist/types/src/runtime/scheduler.d.ts +55 -7
  16. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  17. package/dist/types/src/testing/test/handler.d.ts +3 -0
  18. package/dist/types/src/testing/test/handler.d.ts.map +1 -0
  19. package/dist/types/src/testing/test/index.d.ts +3 -0
  20. package/dist/types/src/testing/test/index.d.ts.map +1 -0
  21. package/dist/types/src/types.d.ts +182 -0
  22. package/dist/types/src/types.d.ts.map +1 -0
  23. package/dist/types/tools/schema.d.ts +2 -0
  24. package/dist/types/tools/schema.d.ts.map +1 -0
  25. package/package.json +20 -11
  26. package/schema/functions.json +183 -0
  27. package/src/handler.ts +56 -26
  28. package/src/index.ts +1 -1
  29. package/src/runtime/dev-server.test.ts +80 -0
  30. package/src/runtime/dev-server.ts +74 -40
  31. package/src/runtime/scheduler.test.ts +163 -9
  32. package/src/runtime/scheduler.ts +228 -64
  33. package/src/testing/test/handler.ts +9 -0
  34. package/src/testing/test/index.ts +7 -0
  35. package/src/types.ts +87 -0
  36. package/dist/types/src/manifest.d.ts +0 -26
  37. package/dist/types/src/manifest.d.ts.map +0 -1
  38. package/src/manifest.ts +0 -42
@@ -29,6 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  var node_exports = {};
30
30
  __export(node_exports, {
31
31
  DevServer: () => DevServer,
32
+ FunctionManifestSchema: () => FunctionManifestSchema,
32
33
  Scheduler: () => Scheduler,
33
34
  subscriptionHandler: () => subscriptionHandler
34
35
  });
@@ -43,6 +44,10 @@ var import_async = require("@dxos/async");
43
44
  var import_invariant = require("@dxos/invariant");
44
45
  var import_log2 = require("@dxos/log");
45
46
  var import_cron = require("cron");
47
+ var import_get_port_please2 = require("get-port-please");
48
+ var import_node_http = __toESM(require("node:http"));
49
+ var import_node_path2 = __toESM(require("node:path"));
50
+ var import_ws = __toESM(require("ws"));
46
51
  var import_types = require("@braneframe/types");
47
52
  var import_async2 = require("@dxos/async");
48
53
  var import_echo = require("@dxos/client/echo");
@@ -50,6 +55,7 @@ var import_context = require("@dxos/context");
50
55
  var import_invariant2 = require("@dxos/invariant");
51
56
  var import_log3 = require("@dxos/log");
52
57
  var import_util2 = require("@dxos/util");
58
+ var S = __toESM(require("@effect/schema/Schema"));
53
59
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
54
60
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
55
61
  }) : x)(function(x) {
@@ -59,16 +65,16 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
59
65
  });
60
66
  var __dxlog_file = "/home/runner/work/dxos/dxos/packages/core/functions/src/handler.ts";
61
67
  var subscriptionHandler = (handler) => {
62
- return ({ event, context, ...rest }) => {
68
+ return ({ event: { data }, context, ...rest }) => {
63
69
  const { client } = context;
64
- const space = event.space ? client.spaces.get(import_client.PublicKey.from(event.space)) : void 0;
65
- const objects = space && event.objects?.map((id) => space.db.getObjectById(id)).filter(import_util.nonNullable);
66
- if (!!event.space && !space) {
70
+ const space = data.spaceKey ? client.spaces.get(import_client.PublicKey.from(data.spaceKey)) : void 0;
71
+ const objects = space ? data.objects?.map((id) => space.db.getObjectById(id)).filter(import_util.nonNullable) : [];
72
+ if (!!data.spaceKey && !space) {
67
73
  import_log.log.warn("invalid space", {
68
- event
74
+ data
69
75
  }, {
70
76
  F: __dxlog_file,
71
- L: 61,
77
+ L: 91,
72
78
  S: void 0,
73
79
  C: (f, a) => f(...a)
74
80
  });
@@ -78,15 +84,18 @@ var subscriptionHandler = (handler) => {
78
84
  objects: objects?.length
79
85
  }, {
80
86
  F: __dxlog_file,
81
- L: 63,
87
+ L: 93,
82
88
  S: void 0,
83
89
  C: (f, a) => f(...a)
84
90
  });
85
91
  }
86
92
  return handler({
87
93
  event: {
88
- space,
89
- objects
94
+ data: {
95
+ ...data,
96
+ space,
97
+ objects
98
+ }
90
99
  },
91
100
  context,
92
101
  ...rest
@@ -101,11 +110,17 @@ var DevServer = class {
101
110
  this._options = _options;
102
111
  this._handlers = {};
103
112
  this._seq = 0;
113
+ this.update = new import_async.Event();
114
+ }
115
+ get stats() {
116
+ return {
117
+ seq: this._seq
118
+ };
104
119
  }
105
120
  get endpoint() {
106
121
  (0, import_invariant.invariant)(this._port, void 0, {
107
122
  F: __dxlog_file2,
108
- L: 46,
123
+ L: 54,
109
124
  S: this,
110
125
  A: [
111
126
  "this._port",
@@ -127,7 +142,7 @@ var DevServer = class {
127
142
  } catch (err) {
128
143
  import_log2.log.error("parsing function (check manifest)", err, {
129
144
  F: __dxlog_file2,
130
- L: 63,
145
+ L: 71,
131
146
  S: this,
132
147
  C: (f, a) => f(...a)
133
148
  });
@@ -135,29 +150,44 @@ var DevServer = class {
135
150
  }
136
151
  }
137
152
  async start() {
153
+ (0, import_invariant.invariant)(!this._server, void 0, {
154
+ F: __dxlog_file2,
155
+ L: 77,
156
+ S: this,
157
+ A: [
158
+ "!this._server",
159
+ ""
160
+ ]
161
+ });
162
+ import_log2.log.info("starting...", void 0, {
163
+ F: __dxlog_file2,
164
+ L: 78,
165
+ S: this,
166
+ C: (f, a) => f(...a)
167
+ });
138
168
  const app = (0, import_express.default)();
139
169
  app.use(import_express.default.json());
140
- app.post("/:name", async (req, res) => {
141
- const { name } = req.params;
170
+ app.post("/:path", async (req, res) => {
171
+ const { path: path2 } = req.params;
142
172
  try {
143
173
  import_log2.log.info("calling", {
144
- name
174
+ path: path2
145
175
  }, {
146
176
  F: __dxlog_file2,
147
- L: 75,
177
+ L: 87,
148
178
  S: this,
149
179
  C: (f, a) => f(...a)
150
180
  });
151
181
  if (this._options.reload) {
152
- const { def } = this._handlers[name];
182
+ const { def } = this._handlers["/" + path2];
153
183
  await this._load(def, true);
154
184
  }
155
- res.statusCode = await this._invoke(name, req.body);
185
+ res.statusCode = await this.invoke("/" + path2, req.body);
156
186
  res.end();
157
187
  } catch (err) {
158
188
  import_log2.log.catch(err, void 0, {
159
189
  F: __dxlog_file2,
160
- L: 84,
190
+ L: 97,
161
191
  S: this,
162
192
  C: (f, a) => f(...a)
163
193
  });
@@ -177,92 +207,170 @@ var DevServer = class {
177
207
  try {
178
208
  const { registrationId, endpoint } = await this._client.services.services.FunctionRegistryService.register({
179
209
  endpoint: this.endpoint,
180
- functions: this.functions.map(({ def: { name } }) => ({
181
- name
210
+ functions: this.functions.map(({ def: { id, path: path2 } }) => ({
211
+ id,
212
+ path: path2
182
213
  }))
183
214
  });
184
215
  import_log2.log.info("registered", {
185
- registrationId,
186
216
  endpoint
187
217
  }, {
188
218
  F: __dxlog_file2,
189
- L: 100,
219
+ L: 113,
190
220
  S: this,
191
221
  C: (f, a) => f(...a)
192
222
  });
193
- this._registrationId = registrationId;
194
223
  this._proxy = endpoint;
224
+ this._functionServiceRegistration = registrationId;
195
225
  } catch (err) {
196
226
  await this.stop();
197
227
  throw new Error("FunctionRegistryService not available (check plugin is configured).");
198
228
  }
229
+ import_log2.log.info("started", {
230
+ port: this._port
231
+ }, {
232
+ F: __dxlog_file2,
233
+ L: 121,
234
+ S: this,
235
+ C: (f, a) => f(...a)
236
+ });
199
237
  }
200
238
  async stop() {
239
+ (0, import_invariant.invariant)(this._server, void 0, {
240
+ F: __dxlog_file2,
241
+ L: 125,
242
+ S: this,
243
+ A: [
244
+ "this._server",
245
+ ""
246
+ ]
247
+ });
248
+ import_log2.log.info("stopping...", void 0, {
249
+ F: __dxlog_file2,
250
+ L: 126,
251
+ S: this,
252
+ C: (f, a) => f(...a)
253
+ });
201
254
  const trigger = new import_async.Trigger();
202
- this._server?.close(async () => {
203
- if (this._registrationId) {
204
- await this._client.services.services.FunctionRegistryService.unregister({
205
- registrationId: this._registrationId
206
- });
207
- import_log2.log.info("unregistered", {
208
- registrationId: this._registrationId
209
- }, {
210
- F: __dxlog_file2,
211
- L: 117,
212
- S: this,
213
- C: (f, a) => f(...a)
214
- });
215
- this._registrationId = void 0;
216
- this._proxy = void 0;
255
+ this._server.close(async () => {
256
+ import_log2.log.info("server stopped", void 0, {
257
+ F: __dxlog_file2,
258
+ L: 130,
259
+ S: this,
260
+ C: (f, a) => f(...a)
261
+ });
262
+ try {
263
+ if (this._functionServiceRegistration) {
264
+ (0, import_invariant.invariant)(this._client.services.services.FunctionRegistryService, void 0, {
265
+ F: __dxlog_file2,
266
+ L: 133,
267
+ S: this,
268
+ A: [
269
+ "this._client.services.services.FunctionRegistryService",
270
+ ""
271
+ ]
272
+ });
273
+ await this._client.services.services.FunctionRegistryService.unregister({
274
+ registrationId: this._functionServiceRegistration
275
+ });
276
+ import_log2.log.info("unregistered", {
277
+ registrationId: this._functionServiceRegistration
278
+ }, {
279
+ F: __dxlog_file2,
280
+ L: 138,
281
+ S: this,
282
+ C: (f, a) => f(...a)
283
+ });
284
+ this._functionServiceRegistration = void 0;
285
+ this._proxy = void 0;
286
+ }
287
+ trigger.wake();
288
+ } catch (err) {
289
+ trigger.throw(err);
217
290
  }
218
- trigger.wake();
219
291
  });
220
292
  await trigger.wait();
221
293
  this._port = void 0;
222
294
  this._server = void 0;
295
+ import_log2.log.info("stopped", void 0, {
296
+ F: __dxlog_file2,
297
+ L: 152,
298
+ S: this,
299
+ C: (f, a) => f(...a)
300
+ });
223
301
  }
224
302
  /**
225
303
  * Load function.
226
304
  */
227
- async _load(def, flush = false) {
228
- const { id, name, handler } = def;
229
- const path = (0, import_node_path.join)(this._options.directory, handler);
305
+ async _load(def, force = false) {
306
+ const { id, path: path2, handler } = def;
307
+ const filePath = (0, import_node_path.join)(this._options.baseDir, handler);
230
308
  import_log2.log.info("loading", {
231
- id
309
+ id,
310
+ force
232
311
  }, {
233
312
  F: __dxlog_file2,
234
- L: 136,
313
+ L: 161,
235
314
  S: this,
236
315
  C: (f, a) => f(...a)
237
316
  });
238
- if (flush) {
239
- Object.keys(__require.cache).filter((key) => key.startsWith(path)).forEach((key) => delete __require.cache[key]);
317
+ if (force) {
318
+ Object.keys(__require.cache).filter((key) => key.startsWith(filePath)).forEach((key) => {
319
+ delete __require.cache[key];
320
+ });
240
321
  }
241
- const module2 = __require(path);
322
+ const module2 = __require(filePath);
242
323
  if (typeof module2.default !== "function") {
243
324
  throw new Error(`Handler must export default function: ${id}`);
244
325
  }
245
- this._handlers[name] = {
326
+ this._handlers[path2] = {
246
327
  def,
247
328
  handler: module2.default
248
329
  };
249
330
  }
250
331
  /**
251
- * Invoke function handler.
332
+ * Invoke function.
252
333
  */
253
- async _invoke(name, event) {
334
+ async invoke(path2, data) {
254
335
  const seq = ++this._seq;
255
336
  const now = Date.now();
256
337
  import_log2.log.info("req", {
257
338
  seq,
258
- name
339
+ path: path2
259
340
  }, {
260
341
  F: __dxlog_file2,
261
- L: 161,
342
+ L: 188,
343
+ S: this,
344
+ C: (f, a) => f(...a)
345
+ });
346
+ const statusCode = await this._invoke(path2, {
347
+ data
348
+ });
349
+ import_log2.log.info("res", {
350
+ seq,
351
+ path: path2,
352
+ statusCode,
353
+ duration: Date.now() - now
354
+ }, {
355
+ F: __dxlog_file2,
356
+ L: 191,
262
357
  S: this,
263
358
  C: (f, a) => f(...a)
264
359
  });
265
- const { handler } = this._handlers[name];
360
+ this.update.emit(statusCode);
361
+ return statusCode;
362
+ }
363
+ async _invoke(path2, event) {
364
+ const { handler } = this._handlers[path2] ?? {};
365
+ (0, import_invariant.invariant)(handler, `invalid path: ${path2}`, {
366
+ F: __dxlog_file2,
367
+ L: 198,
368
+ S: this,
369
+ A: [
370
+ "handler",
371
+ "`invalid path: ${path}`"
372
+ ]
373
+ });
266
374
  const context = {
267
375
  client: this._client,
268
376
  dataDir: this._options.dataDir
@@ -279,17 +387,6 @@ var DevServer = class {
279
387
  event,
280
388
  response
281
389
  });
282
- import_log2.log.info("res", {
283
- seq,
284
- name,
285
- statusCode,
286
- duration: Date.now() - now
287
- }, {
288
- F: __dxlog_file2,
289
- L: 178,
290
- S: this,
291
- C: (f, a) => f(...a)
292
- });
293
390
  return statusCode;
294
391
  }
295
392
  };
@@ -299,7 +396,13 @@ var Scheduler = class {
299
396
  this._client = _client;
300
397
  this._manifest = _manifest;
301
398
  this._options = _options;
302
- this._mounts = new import_util2.ComplexMap(({ id, spaceKey }) => `${spaceKey.toHex()}:${id}`);
399
+ this._mounts = new import_util2.ComplexMap(({ spaceKey, id }) => `${spaceKey.toHex()}:${id}`);
400
+ }
401
+ get mounts() {
402
+ return Array.from(this._mounts.values()).reduce((acc, { trigger }) => {
403
+ acc.push(trigger);
404
+ return acc;
405
+ }, []);
303
406
  }
304
407
  async start() {
305
408
  this._client.spaces.subscribe(async (spaces) => {
@@ -316,15 +419,18 @@ var Scheduler = class {
316
419
  await this.unmount(id, spaceKey);
317
420
  }
318
421
  }
422
+ /**
423
+ * Mount trigger.
424
+ */
319
425
  async mount(ctx, space, trigger) {
320
426
  const key = {
321
- id: trigger.function,
322
- spaceKey: space.key
427
+ spaceKey: space.key,
428
+ id: trigger.function
323
429
  };
324
430
  const def = this._manifest.functions.find((config) => config.id === trigger.function);
325
431
  (0, import_invariant2.invariant)(def, `Function not found: ${trigger.function}`, {
326
432
  F: __dxlog_file3,
327
- L: 63,
433
+ L: 76,
328
434
  S: this,
329
435
  A: [
330
436
  "def",
@@ -342,18 +448,24 @@ var Scheduler = class {
342
448
  trigger
343
449
  }, {
344
450
  F: __dxlog_file3,
345
- L: 69,
451
+ L: 82,
346
452
  S: this,
347
453
  C: (f, a) => f(...a)
348
454
  });
349
455
  if (ctx.disposed) {
350
456
  return;
351
457
  }
352
- if (trigger.schedule) {
353
- this._createTimer(ctx, space, def, trigger);
458
+ if (trigger.timer) {
459
+ await this._createTimer(ctx, space, def, trigger);
460
+ }
461
+ if (trigger.webhook) {
462
+ await this._createWebhook(ctx, space, def, trigger);
463
+ }
464
+ if (trigger.websocket) {
465
+ await this._createWebsocket(ctx, space, def, trigger);
354
466
  }
355
- for (const triggerSubscription of trigger.subscriptions ?? []) {
356
- this._createSubscription(ctx, space, def, triggerSubscription);
467
+ if (trigger.subscription) {
468
+ await this._createSubscription(ctx, space, def, trigger);
357
469
  }
358
470
  }
359
471
  }
@@ -368,25 +480,95 @@ var Scheduler = class {
368
480
  await ctx.dispose();
369
481
  }
370
482
  }
371
- _createTimer(ctx, space, def, trigger) {
372
- const task = new import_async2.DeferredTask(ctx, async () => {
373
- await this._execFunction(def, {
374
- space: space.key
483
+ async _execFunction(def, trigger, data) {
484
+ let status = 0;
485
+ try {
486
+ const payload = Object.assign({}, {
487
+ meta: trigger.meta
488
+ }, data);
489
+ const { endpoint, callback } = this._options;
490
+ if (endpoint) {
491
+ const url = import_node_path2.default.join(endpoint, def.path);
492
+ import_log3.log.info("exec", {
493
+ function: def.id,
494
+ url
495
+ }, {
496
+ F: __dxlog_file3,
497
+ L: 128,
498
+ S: this,
499
+ C: (f, a) => f(...a)
500
+ });
501
+ const response = await fetch(url, {
502
+ method: "POST",
503
+ headers: {
504
+ "Content-Type": "application/json"
505
+ },
506
+ body: JSON.stringify(payload)
507
+ });
508
+ status = response.status;
509
+ } else if (callback) {
510
+ import_log3.log.info("exec", {
511
+ function: def.id
512
+ }, {
513
+ F: __dxlog_file3,
514
+ L: 139,
515
+ S: this,
516
+ C: (f, a) => f(...a)
517
+ });
518
+ status = await callback(payload) ?? 200;
519
+ }
520
+ if (status && status >= 400) {
521
+ throw new Error(`Response: ${status}`);
522
+ }
523
+ import_log3.log.info("done", {
524
+ function: def.id,
525
+ status
526
+ }, {
527
+ F: __dxlog_file3,
528
+ L: 149,
529
+ S: this,
530
+ C: (f, a) => f(...a)
375
531
  });
376
- });
377
- (0, import_invariant2.invariant)(trigger.schedule, void 0, {
532
+ } catch (err) {
533
+ import_log3.log.error("error", {
534
+ function: def.id,
535
+ error: err.message
536
+ }, {
537
+ F: __dxlog_file3,
538
+ L: 151,
539
+ S: this,
540
+ C: (f, a) => f(...a)
541
+ });
542
+ status = 500;
543
+ }
544
+ return status;
545
+ }
546
+ //
547
+ // Triggers
548
+ //
549
+ /**
550
+ * Cron timer.
551
+ */
552
+ async _createTimer(ctx, space, def, trigger) {
553
+ import_log3.log.info("timer", {
554
+ space: space.key,
555
+ trigger
556
+ }, {
378
557
  F: __dxlog_file3,
379
- L: 102,
558
+ L: 166,
380
559
  S: this,
381
- A: [
382
- "trigger.schedule",
383
- ""
384
- ]
560
+ C: (f, a) => f(...a)
561
+ });
562
+ const spec = trigger.timer;
563
+ const task = new import_async2.DeferredTask(ctx, async () => {
564
+ await this._execFunction(def, trigger, {
565
+ spaceKey: space.key
566
+ });
385
567
  });
386
568
  let last = 0;
387
569
  let run = 0;
388
570
  const job = import_cron.CronJob.from({
389
- cronTime: trigger.schedule,
571
+ cronTime: spec.cron,
390
572
  runOnInit: false,
391
573
  onTick: () => {
392
574
  const now = Date.now();
@@ -399,7 +581,7 @@ var Scheduler = class {
399
581
  delta
400
582
  }, {
401
583
  F: __dxlog_file3,
402
- L: 116,
584
+ L: 186,
403
585
  S: this,
404
586
  C: (f, a) => f(...a)
405
587
  });
@@ -409,20 +591,180 @@ var Scheduler = class {
409
591
  job.start();
410
592
  ctx.onDispose(() => job.stop());
411
593
  }
412
- _createSubscription(ctx, space, def, triggerSubscription) {
594
+ /**
595
+ * Webhook.
596
+ */
597
+ async _createWebhook(ctx, space, def, trigger) {
598
+ import_log3.log.info("webhook", {
599
+ space: space.key,
600
+ trigger
601
+ }, {
602
+ F: __dxlog_file3,
603
+ L: 199,
604
+ S: this,
605
+ C: (f, a) => f(...a)
606
+ });
607
+ const spec = trigger.webhook;
608
+ const server = import_node_http.default.createServer(async (req, res) => {
609
+ if (req.method !== spec.method) {
610
+ res.statusCode = 405;
611
+ return res.end();
612
+ }
613
+ res.statusCode = await this._execFunction(def, trigger, {
614
+ spaceKey: space.key
615
+ });
616
+ res.end();
617
+ });
618
+ const port = await (0, import_get_port_please2.getPort)({
619
+ random: true
620
+ });
621
+ server.listen(port, () => {
622
+ import_log3.log.info("started webhook", {
623
+ port
624
+ }, {
625
+ F: __dxlog_file3,
626
+ L: 223,
627
+ S: this,
628
+ C: (f, a) => f(...a)
629
+ });
630
+ spec.port = port;
631
+ });
632
+ ctx.onDispose(() => {
633
+ server.close();
634
+ });
635
+ }
636
+ /**
637
+ * Websocket.
638
+ * NOTE: The port must be unique, so the same hook cannot be used for multiple spaces.
639
+ */
640
+ async _createWebsocket(ctx, space, def, trigger, options = {
641
+ retryDelay: 2,
642
+ maxAttempts: 5
643
+ }) {
644
+ import_log3.log.info("websocket", {
645
+ space: space.key,
646
+ trigger
647
+ }, {
648
+ F: __dxlog_file3,
649
+ L: 249,
650
+ S: this,
651
+ C: (f, a) => f(...a)
652
+ });
653
+ const spec = trigger.websocket;
654
+ const { url, init } = spec;
655
+ let ws;
656
+ for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
657
+ const open = new import_async2.Trigger();
658
+ ws = new import_ws.default(url);
659
+ Object.assign(ws, {
660
+ onopen: () => {
661
+ import_log3.log.info("opened", {
662
+ url
663
+ }, {
664
+ F: __dxlog_file3,
665
+ L: 260,
666
+ S: this,
667
+ C: (f, a) => f(...a)
668
+ });
669
+ if (spec.init) {
670
+ ws.send(new TextEncoder().encode(JSON.stringify(init)));
671
+ }
672
+ open.wake(true);
673
+ },
674
+ onclose: (event) => {
675
+ import_log3.log.info("closed", {
676
+ url,
677
+ code: event.code
678
+ }, {
679
+ F: __dxlog_file3,
680
+ L: 269,
681
+ S: this,
682
+ C: (f, a) => f(...a)
683
+ });
684
+ if (event.code === 1006) {
685
+ setTimeout(async () => {
686
+ import_log3.log.info(`reconnecting in ${options.retryDelay}s...`, {
687
+ url
688
+ }, {
689
+ F: __dxlog_file3,
690
+ L: 274,
691
+ S: this,
692
+ C: (f, a) => f(...a)
693
+ });
694
+ await this._createWebsocket(ctx, space, def, trigger, options);
695
+ }, options.retryDelay * 1e3);
696
+ }
697
+ open.wake(false);
698
+ },
699
+ onerror: (event) => {
700
+ import_log3.log.catch(event.error, {
701
+ url
702
+ }, {
703
+ F: __dxlog_file3,
704
+ L: 283,
705
+ S: this,
706
+ C: (f, a) => f(...a)
707
+ });
708
+ },
709
+ onmessage: async (event) => {
710
+ try {
711
+ const data = JSON.parse(new TextDecoder().decode(event.data));
712
+ await this._execFunction(def, trigger, {
713
+ spaceKey: space.key,
714
+ data
715
+ });
716
+ } catch (err) {
717
+ import_log3.log.catch(err, {
718
+ url
719
+ }, {
720
+ F: __dxlog_file3,
721
+ L: 291,
722
+ S: this,
723
+ C: (f, a) => f(...a)
724
+ });
725
+ }
726
+ }
727
+ });
728
+ const isOpen = await open.wait();
729
+ if (isOpen) {
730
+ break;
731
+ } else {
732
+ const wait = Math.pow(attempt, 2) * options.retryDelay;
733
+ if (attempt < options.maxAttempts) {
734
+ import_log3.log.warn(`failed to connect; trying again in ${wait}s`, {
735
+ attempt
736
+ }, {
737
+ F: __dxlog_file3,
738
+ L: 302,
739
+ S: this,
740
+ C: (f, a) => f(...a)
741
+ });
742
+ await (0, import_async2.sleep)(wait * 1e3);
743
+ }
744
+ }
745
+ }
746
+ ctx.onDispose(() => {
747
+ ws?.close();
748
+ });
749
+ }
750
+ /**
751
+ * ECHO subscription.
752
+ */
753
+ async _createSubscription(ctx, space, def, trigger) {
413
754
  import_log3.log.info("subscription", {
414
755
  space: space.key,
415
- triggerSubscription
756
+ trigger
416
757
  }, {
417
758
  F: __dxlog_file3,
418
- L: 126,
759
+ L: 317,
419
760
  S: this,
420
761
  C: (f, a) => f(...a)
421
762
  });
763
+ const spec = trigger.subscription;
422
764
  const objectIds = /* @__PURE__ */ new Set();
423
765
  const task = new import_async2.DeferredTask(ctx, async () => {
424
- await this._execFunction(def, {
425
- space: space.key,
766
+ await this._execFunction(def, trigger, {
767
+ spaceKey: space.key,
426
768
  objects: Array.from(objectIds)
427
769
  });
428
770
  });
@@ -433,7 +775,7 @@ var Scheduler = class {
433
775
  updated: updated.length
434
776
  }, {
435
777
  F: __dxlog_file3,
436
- L: 139,
778
+ L: 329,
437
779
  S: this,
438
780
  C: (f, a) => f(...a)
439
781
  });
@@ -446,17 +788,15 @@ var Scheduler = class {
446
788
  task.schedule();
447
789
  });
448
790
  subscriptions.push(() => subscription.unsubscribe());
449
- const { type, props, deep, delay } = triggerSubscription;
791
+ const { filter, options: { deep, delay } = {} } = spec;
450
792
  const update = ({ objects }) => {
451
793
  subscription.update(objects);
452
794
  if (deep) {
453
795
  import_log3.log.info("update", {
454
- type,
455
- deep,
456
796
  objects: objects.length
457
797
  }, {
458
798
  F: __dxlog_file3,
459
- L: 159,
799
+ L: 349,
460
800
  S: this,
461
801
  C: (f, a) => f(...a)
462
802
  });
@@ -470,61 +810,66 @@ var Scheduler = class {
470
810
  }
471
811
  }
472
812
  };
473
- const query = space.db.query(import_echo.Filter.typename(type, props));
474
- subscriptions.push(query.subscribe(delay ? (0, import_async2.debounce)(update, delay * 1e3) : update));
813
+ const query = space.db.query(import_echo.Filter.or(filter.map(({ type, props }) => import_echo.Filter.typename(type, props))));
814
+ subscriptions.push(query.subscribe(delay ? (0, import_async2.debounce)(update, delay) : update));
475
815
  ctx.onDispose(() => {
476
816
  subscriptions.forEach((unsubscribe) => unsubscribe());
477
817
  });
478
818
  }
479
- async _execFunction(def, data) {
480
- try {
481
- (0, import_log3.log)("request", {
482
- function: def.id
483
- }, {
484
- F: __dxlog_file3,
485
- L: 183,
486
- S: this,
487
- C: (f, a) => f(...a)
488
- });
489
- const { endpoint, callback } = this._options;
490
- let status = 0;
491
- if (endpoint) {
492
- const response = await fetch(`${this._options.endpoint}/${def.name}`, {
493
- method: "POST",
494
- headers: {
495
- "Content-Type": "application/json"
496
- },
497
- body: JSON.stringify(data)
498
- });
499
- status = response.status;
500
- } else if (callback) {
501
- status = await callback(data);
502
- }
503
- (0, import_log3.log)("result", {
504
- function: def.id,
505
- result: status
506
- }, {
507
- F: __dxlog_file3,
508
- L: 202,
509
- S: this,
510
- C: (f, a) => f(...a)
511
- });
512
- } catch (err) {
513
- import_log3.log.error("error", {
514
- function: def.id,
515
- error: err.message
516
- }, {
517
- F: __dxlog_file3,
518
- L: 204,
519
- S: this,
520
- C: (f, a) => f(...a)
521
- });
522
- }
523
- }
524
819
  };
820
+ var TimerTriggerSchema = S.struct({
821
+ cron: S.string
822
+ });
823
+ var WebhookTriggerSchema = S.mutable(S.struct({
824
+ method: S.string,
825
+ // Assigned port.
826
+ port: S.optional(S.number)
827
+ }));
828
+ var WebsocketTriggerSchema = S.struct({
829
+ url: S.string,
830
+ init: S.optional(S.record(S.string, S.any))
831
+ });
832
+ var SubscriptionTriggerSchema = S.struct({
833
+ spaceKey: S.optional(S.string),
834
+ // TODO(burdon): Define query DSL.
835
+ filter: S.array(S.struct({
836
+ type: S.string,
837
+ props: S.optional(S.record(S.string, S.any))
838
+ })),
839
+ options: S.optional(S.struct({
840
+ // Watch changes to object (not just creation).
841
+ deep: S.optional(S.boolean),
842
+ // Debounce changes (delay in ms).
843
+ delay: S.optional(S.number)
844
+ }))
845
+ });
846
+ var FunctionTriggerSchema = S.struct({
847
+ function: S.string.pipe(S.description("Function ID/URI.")),
848
+ // Context passed to function.
849
+ meta: S.optional(S.record(S.string, S.any)),
850
+ // Triggers.
851
+ timer: S.optional(TimerTriggerSchema),
852
+ webhook: S.optional(WebhookTriggerSchema),
853
+ websocket: S.optional(WebsocketTriggerSchema),
854
+ subscription: S.optional(SubscriptionTriggerSchema)
855
+ });
856
+ var FunctionDefSchema = S.struct({
857
+ id: S.string,
858
+ // name: S.string,
859
+ description: S.optional(S.string),
860
+ // TODO(burdon): Rename route?
861
+ path: S.string,
862
+ // TODO(burdon): NPM/GitHub/Docker/CF URL?
863
+ handler: S.string
864
+ });
865
+ var FunctionManifestSchema = S.struct({
866
+ functions: S.mutable(S.array(FunctionDefSchema)),
867
+ triggers: S.optional(S.mutable(S.array(FunctionTriggerSchema)))
868
+ });
525
869
  // Annotate the CommonJS export names for ESM import in node:
526
870
  0 && (module.exports = {
527
871
  DevServer,
872
+ FunctionManifestSchema,
528
873
  Scheduler,
529
874
  subscriptionHandler
530
875
  });