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