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

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 (92) hide show
  1. package/dist/lib/browser/chunk-4D4I3YMJ.mjs +86 -0
  2. package/dist/lib/browser/chunk-4D4I3YMJ.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +884 -457
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types.mjs +14 -0
  7. package/dist/lib/browser/types.mjs.map +7 -0
  8. package/dist/lib/node/chunk-3UYUR5N5.cjs +103 -0
  9. package/dist/lib/node/chunk-3UYUR5N5.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +868 -447
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/types.cjs +35 -0
  14. package/dist/lib/node/types.cjs.map +7 -0
  15. package/dist/types/src/browser/index.d.ts +2 -0
  16. package/dist/types/src/browser/index.d.ts.map +1 -0
  17. package/dist/types/src/function/function-registry.d.ts +24 -0
  18. package/dist/types/src/function/function-registry.d.ts.map +1 -0
  19. package/dist/types/src/function/function-registry.test.d.ts +2 -0
  20. package/dist/types/src/function/function-registry.test.d.ts.map +1 -0
  21. package/dist/types/src/function/index.d.ts +2 -0
  22. package/dist/types/src/function/index.d.ts.map +1 -0
  23. package/dist/types/src/handler.d.ts +33 -12
  24. package/dist/types/src/handler.d.ts.map +1 -1
  25. package/dist/types/src/index.d.ts +2 -0
  26. package/dist/types/src/index.d.ts.map +1 -1
  27. package/dist/types/src/runtime/dev-server.d.ts +16 -13
  28. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  29. package/dist/types/src/runtime/dev-server.test.d.ts +2 -0
  30. package/dist/types/src/runtime/dev-server.test.d.ts.map +1 -0
  31. package/dist/types/src/runtime/scheduler.d.ts +13 -27
  32. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  33. package/dist/types/src/testing/functions-integration.test.d.ts +2 -0
  34. package/dist/types/src/testing/functions-integration.test.d.ts.map +1 -0
  35. package/dist/types/src/testing/index.d.ts +4 -0
  36. package/dist/types/src/testing/index.d.ts.map +1 -0
  37. package/dist/types/src/testing/setup.d.ts +5 -0
  38. package/dist/types/src/testing/setup.d.ts.map +1 -0
  39. package/dist/types/src/testing/test/handler.d.ts +4 -0
  40. package/dist/types/src/testing/test/handler.d.ts.map +1 -0
  41. package/dist/types/src/testing/test/index.d.ts +3 -0
  42. package/dist/types/src/testing/test/index.d.ts.map +1 -0
  43. package/dist/types/src/testing/types.d.ts +9 -0
  44. package/dist/types/src/testing/types.d.ts.map +1 -0
  45. package/dist/types/src/testing/util.d.ts +3 -0
  46. package/dist/types/src/testing/util.d.ts.map +1 -0
  47. package/dist/types/src/trigger/index.d.ts +2 -0
  48. package/dist/types/src/trigger/index.d.ts.map +1 -0
  49. package/dist/types/src/trigger/trigger-registry.d.ts +37 -0
  50. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -0
  51. package/dist/types/src/trigger/trigger-registry.test.d.ts +2 -0
  52. package/dist/types/src/trigger/trigger-registry.test.d.ts.map +1 -0
  53. package/dist/types/src/trigger/type/index.d.ts +5 -0
  54. package/dist/types/src/trigger/type/index.d.ts.map +1 -0
  55. package/dist/types/src/trigger/type/subscription-trigger.d.ts +4 -0
  56. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -0
  57. package/dist/types/src/trigger/type/timer-trigger.d.ts +4 -0
  58. package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -0
  59. package/dist/types/src/trigger/type/webhook-trigger.d.ts +4 -0
  60. package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -0
  61. package/dist/types/src/trigger/type/websocket-trigger.d.ts +13 -0
  62. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -0
  63. package/dist/types/src/types.d.ts +162 -107
  64. package/dist/types/src/types.d.ts.map +1 -1
  65. package/package.json +33 -15
  66. package/schema/functions.json +144 -103
  67. package/src/browser/index.ts +5 -0
  68. package/src/function/function-registry.test.ts +105 -0
  69. package/src/function/function-registry.ts +90 -0
  70. package/src/function/index.ts +5 -0
  71. package/src/handler.ts +54 -31
  72. package/src/index.ts +2 -0
  73. package/src/runtime/dev-server.test.ts +60 -0
  74. package/src/runtime/dev-server.ts +104 -53
  75. package/src/runtime/scheduler.test.ts +61 -73
  76. package/src/runtime/scheduler.ts +91 -270
  77. package/src/testing/functions-integration.test.ts +100 -0
  78. package/src/testing/index.ts +7 -0
  79. package/src/testing/setup.ts +43 -0
  80. package/src/testing/test/handler.ts +15 -0
  81. package/src/testing/test/index.ts +7 -0
  82. package/src/testing/types.ts +9 -0
  83. package/src/testing/util.ts +16 -0
  84. package/src/trigger/index.ts +5 -0
  85. package/src/trigger/trigger-registry.test.ts +272 -0
  86. package/src/trigger/trigger-registry.ts +211 -0
  87. package/src/trigger/type/index.ts +8 -0
  88. package/src/trigger/type/subscription-trigger.ts +86 -0
  89. package/src/trigger/type/timer-trigger.ts +45 -0
  90. package/src/trigger/type/webhook-trigger.ts +48 -0
  91. package/src/trigger/type/websocket-trigger.ts +92 -0
  92. package/src/types.ts +87 -48
@@ -6,40 +6,53 @@ import { expect } from 'chai';
6
6
  import WebSocket from 'ws';
7
7
 
8
8
  import { Trigger } from '@dxos/async';
9
- import { Client } from '@dxos/client';
9
+ import { type Client } from '@dxos/client';
10
10
  import { TestBuilder } from '@dxos/client/testing';
11
- import { create, S, TypedObject } from '@dxos/echo-schema';
11
+ import { create } from '@dxos/echo-schema';
12
12
  import { describe, test } from '@dxos/test';
13
13
 
14
- import { Scheduler } from './scheduler';
14
+ import { Scheduler, type SchedulerOptions } from './scheduler';
15
+ import { FunctionRegistry } from '../function';
16
+ import { createInitializedClients, TestType, triggerWebhook } from '../testing';
17
+ import { TriggerRegistry } from '../trigger';
15
18
  import { type FunctionManifest } from '../types';
16
19
 
17
20
  // TODO(burdon): Test we can add and remove triggers.
18
21
  describe('scheduler', () => {
22
+ let testBuilder: TestBuilder;
19
23
  let client: Client;
20
24
  before(async () => {
21
- const testBuilder = new TestBuilder();
22
- client = new Client({ services: testBuilder.createLocal() });
23
- await client.initialize();
24
- await client.halo.createIdentity();
25
+ testBuilder = new TestBuilder();
26
+ client = (await createInitializedClients(testBuilder, 1))[0];
25
27
  });
26
28
  after(async () => {
27
- await client.destroy();
29
+ await testBuilder.destroy();
28
30
  });
29
31
 
32
+ const createScheduler = (callback: SchedulerOptions['callback']) => {
33
+ const scheduler = new Scheduler(new FunctionRegistry(client), new TriggerRegistry(client), { callback });
34
+ after(async () => {
35
+ await scheduler.stop();
36
+ });
37
+
38
+ return scheduler;
39
+ };
40
+
30
41
  test('timer', async () => {
31
42
  const manifest: FunctionManifest = {
32
43
  functions: [
33
44
  {
34
- id: 'example.com/function/test',
35
- name: 'test',
45
+ uri: 'example.com/function/test',
46
+ route: '/test',
36
47
  handler: 'test',
37
48
  },
38
49
  ],
39
50
  triggers: [
40
51
  {
41
52
  function: 'example.com/function/test',
42
- timer: {
53
+ enabled: true,
54
+ spec: {
55
+ type: 'timer',
43
56
  cron: '0/1 * * * * *', // Every 1s.
44
57
  },
45
58
  },
@@ -48,18 +61,13 @@ describe('scheduler', () => {
48
61
 
49
62
  let count = 0;
50
63
  const done = new Trigger();
51
- const scheduler = new Scheduler(client, manifest, {
52
- callback: async () => {
53
- if (++count === 3) {
54
- done.wake();
55
- }
56
- },
64
+ const scheduler = createScheduler(async () => {
65
+ if (++count === 3) {
66
+ done.wake();
67
+ }
57
68
  });
58
-
69
+ await scheduler.register(client.spaces.default, manifest);
59
70
  await scheduler.start();
60
- after(async () => {
61
- await scheduler.stop();
62
- });
63
71
 
64
72
  await done.wait({ timeout: 5_000 });
65
73
  expect(count).to.equal(3);
@@ -69,52 +77,51 @@ describe('scheduler', () => {
69
77
  const manifest: FunctionManifest = {
70
78
  functions: [
71
79
  {
72
- id: 'example.com/function/test',
73
- name: 'test',
80
+ uri: 'example.com/function/test',
81
+ route: '/test',
74
82
  handler: 'test',
75
83
  },
76
84
  ],
77
85
  triggers: [
78
86
  {
79
87
  function: 'example.com/function/test',
80
- webhook: {
81
- port: 8080,
88
+ enabled: true,
89
+ spec: {
90
+ type: 'webhook',
91
+ method: 'GET',
82
92
  },
83
93
  },
84
94
  ],
85
95
  };
86
96
 
87
97
  const done = new Trigger();
88
- const scheduler = new Scheduler(client, manifest, {
89
- callback: async () => {
90
- done.wake();
91
- },
98
+ const scheduler = createScheduler(async () => {
99
+ done.wake();
92
100
  });
93
-
101
+ const space = await client.spaces.create();
102
+ await scheduler.register(space, manifest);
94
103
  await scheduler.start();
95
- after(async () => {
96
- await scheduler.stop();
97
- });
98
104
 
99
- setTimeout(() => {
100
- void fetch('http://localhost:8080');
101
- });
105
+ setTimeout(async () => triggerWebhook(space, manifest.functions![0].uri));
106
+
102
107
  await done.wait();
103
108
  });
104
109
 
105
- test.only('websocket', async () => {
110
+ test('websocket', async () => {
106
111
  const manifest: FunctionManifest = {
107
112
  functions: [
108
113
  {
109
- id: 'example.com/function/test',
110
- name: 'test',
114
+ uri: 'example.com/function/test',
115
+ route: '/test',
111
116
  handler: 'test',
112
117
  },
113
118
  ],
114
119
  triggers: [
115
120
  {
116
121
  function: 'example.com/function/test',
117
- websocket: {
122
+ enabled: true,
123
+ spec: {
124
+ type: 'websocket',
118
125
  // url: 'https://hub.dxos.network/api/mailbox/test',
119
126
  url: 'http://localhost:8081',
120
127
  init: {
@@ -126,16 +133,11 @@ describe('scheduler', () => {
126
133
  };
127
134
 
128
135
  const done = new Trigger();
129
- const scheduler = new Scheduler(client, manifest, {
130
- callback: async (data) => {
131
- done.wake();
132
- },
136
+ const scheduler = createScheduler(async () => {
137
+ done.wake();
133
138
  });
134
-
139
+ await scheduler.register(client.spaces.default, manifest);
135
140
  await scheduler.start();
136
- after(async () => {
137
- await scheduler.stop();
138
- });
139
141
 
140
142
  // Test server.
141
143
  setTimeout(() => {
@@ -153,29 +155,21 @@ describe('scheduler', () => {
153
155
  });
154
156
 
155
157
  test('subscription', async () => {
156
- class TestType extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
157
- title: S.string,
158
- }) {}
159
- client.addSchema(TestType);
160
-
161
158
  const manifest: FunctionManifest = {
162
159
  functions: [
163
160
  {
164
- id: 'example.com/function/test',
165
- name: 'test',
161
+ uri: 'example.com/function/test',
162
+ route: '/test',
166
163
  handler: 'test',
167
164
  },
168
165
  ],
169
166
  triggers: [
170
167
  {
171
168
  function: 'example.com/function/test',
172
- subscription: {
173
- spaceKey: client.spaces.default.key.toHex(),
174
- filter: [
175
- {
176
- type: TestType.typename,
177
- },
178
- ],
169
+ enabled: true,
170
+ spec: {
171
+ type: 'subscription',
172
+ filter: [{ type: TestType.typename }],
179
173
  },
180
174
  },
181
175
  ],
@@ -183,20 +177,14 @@ describe('scheduler', () => {
183
177
 
184
178
  let count = 0;
185
179
  const done = new Trigger();
186
- const scheduler = new Scheduler(client, manifest, {
187
- callback: async () => {
188
- if (++count === 2) {
189
- done.wake();
190
- }
191
- },
180
+ const scheduler = createScheduler(async () => {
181
+ if (++count === 1) {
182
+ done.wake();
183
+ }
192
184
  });
193
-
185
+ await scheduler.register(client.spaces.default, manifest);
194
186
  await scheduler.start();
195
- after(async () => {
196
- await scheduler.stop();
197
- });
198
187
 
199
- // TODO(burdon): Query for Expando?
200
188
  setTimeout(() => {
201
189
  const space = client.spaces.default;
202
190
  const object = create(TestType, { title: 'Hello world!' });
@@ -2,31 +2,19 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { CronJob } from 'cron';
6
- import http from 'node:http';
7
- import WebSocket from 'ws';
5
+ import path from 'node:path';
8
6
 
9
- import { TextV0Type } from '@braneframe/types';
10
- import { debounce, DeferredTask, sleep, Trigger } from '@dxos/async';
11
- import { type Client, type PublicKey } from '@dxos/client';
12
- import { createSubscription, Filter, getAutomergeObjectCore, type Query, type Space } from '@dxos/client/echo';
7
+ import { Mutex } from '@dxos/async';
8
+ import { type Space } from '@dxos/client/echo';
13
9
  import { Context } from '@dxos/context';
14
- import { invariant } from '@dxos/invariant';
15
10
  import { log } from '@dxos/log';
16
- import { ComplexMap } from '@dxos/util';
17
11
 
18
- import { type FunctionSubscriptionEvent } from '../handler';
19
- import {
20
- type FunctionDef,
21
- type FunctionManifest,
22
- type FunctionTrigger,
23
- type SubscriptionTrigger,
24
- type TimerTrigger,
25
- type WebhookTrigger,
26
- type WebsocketTrigger,
27
- } from '../types';
12
+ import { type FunctionRegistry } from '../function';
13
+ import { type FunctionEventMeta } from '../handler';
14
+ import { type TriggerRegistry } from '../trigger';
15
+ import { type FunctionDef, type FunctionManifest, type FunctionTrigger } from '../types';
28
16
 
29
- type Callback = (data: FunctionSubscriptionEvent) => Promise<void>;
17
+ export type Callback = (data: any) => Promise<void | number>;
30
18
 
31
19
  export type SchedulerOptions = {
32
20
  endpoint?: string;
@@ -34,290 +22,123 @@ export type SchedulerOptions = {
34
22
  };
35
23
 
36
24
  /**
37
- * The scheduler triggers function exectuion based on various triggers.
25
+ * The scheduler triggers function execution based on various triggers.
38
26
  */
39
27
  export class Scheduler {
40
- // Map of mounted functions.
41
- private readonly _mounts = new ComplexMap<
42
- { id: string; spaceKey: PublicKey },
43
- { ctx: Context; trigger: FunctionTrigger }
44
- >(({ id, spaceKey }) => `${spaceKey.toHex()}:${id}`);
28
+ private _ctx = createContext();
29
+
30
+ private readonly _functionUriToCallMutex = new Map<string, Mutex>();
45
31
 
46
32
  constructor(
47
- private readonly _client: Client,
48
- private readonly _manifest: FunctionManifest,
33
+ public readonly functions: FunctionRegistry,
34
+ public readonly triggers: TriggerRegistry,
49
35
  private readonly _options: SchedulerOptions = {},
50
- ) {}
36
+ ) {
37
+ this.functions.registered.on(async ({ space, added }) => {
38
+ await this._safeActivateTriggers(space, this.triggers.getInactiveTriggers(space), added);
39
+ });
40
+ this.triggers.registered.on(async ({ space, triggers }) => {
41
+ await this._safeActivateTriggers(space, triggers, this.functions.getFunctions(space));
42
+ });
43
+ }
51
44
 
52
45
  async start() {
53
- this._client.spaces.subscribe(async (spaces) => {
54
- for (const space of spaces) {
55
- await space.waitUntilReady();
56
- for (const trigger of this._manifest.triggers ?? []) {
57
- await this.mount(new Context(), space, trigger);
58
- }
59
- }
60
- });
46
+ await this._ctx.dispose();
47
+ this._ctx = createContext();
48
+ await this.functions.open(this._ctx);
49
+ await this.triggers.open(this._ctx);
61
50
  }
62
51
 
63
52
  async stop() {
64
- for (const { id, spaceKey } of this._mounts.keys()) {
65
- await this.unmount(id, spaceKey);
66
- }
53
+ await this._ctx.dispose();
54
+ await this.functions.close();
55
+ await this.triggers.close();
67
56
  }
68
57
 
69
- private async mount(ctx: Context, space: Space, trigger: FunctionTrigger) {
70
- const key = { id: trigger.function, spaceKey: space.key };
71
- const def = this._manifest.functions.find((config) => config.id === trigger.function);
72
- invariant(def, `Function not found: ${trigger.function}`);
73
-
74
- // TODO(burdon): Currently supports only one trigger declaration per function.
75
- const exists = this._mounts.get(key);
76
- if (!exists) {
77
- this._mounts.set(key, { ctx, trigger });
78
- log('mount', { space: space.key, trigger });
79
- if (ctx.disposed) {
80
- return;
81
- }
82
-
83
- //
84
- // Triggers types.
85
- //
58
+ // TODO(burdon): Remove and update registries directly.
59
+ public async register(space: Space, manifest: FunctionManifest) {
60
+ await this.functions.register(space, manifest.functions);
61
+ await this.triggers.register(space, manifest);
62
+ }
86
63
 
87
- if (trigger.timer) {
88
- await this._createTimer(ctx, space, def, trigger.timer);
89
- }
64
+ private async _safeActivateTriggers(
65
+ space: Space,
66
+ triggers: FunctionTrigger[],
67
+ functions: FunctionDef[],
68
+ ): Promise<void> {
69
+ const mountTasks = triggers.map((trigger) => {
70
+ return this.activate(space, functions, trigger);
71
+ });
72
+ await Promise.all(mountTasks).catch(log.catch);
73
+ }
90
74
 
91
- if (trigger.webhook) {
92
- await this._createWebhook(ctx, space, def, trigger.webhook);
93
- }
75
+ private async activate(space: Space, functions: FunctionDef[], trigger: FunctionTrigger) {
76
+ const definition = functions.find((def) => def.uri === trigger.function);
77
+ if (!definition) {
78
+ log.info('function is not found for trigger', { trigger });
79
+ return;
80
+ }
94
81
 
95
- if (trigger.websocket) {
96
- await this._createWebsocket(ctx, space, def, trigger.websocket);
97
- }
82
+ await this.triggers.activate(space, trigger, async (args) => {
83
+ const mutex = this._functionUriToCallMutex.get(definition.uri) ?? new Mutex();
84
+ this._functionUriToCallMutex.set(definition.uri, mutex);
98
85
 
99
- if (trigger.subscription) {
100
- await this._createSubscription(ctx, space, def, trigger.subscription);
101
- }
102
- }
103
- }
86
+ log.info('function triggered, waiting for mutex', { uri: definition.uri });
87
+ return mutex.executeSynchronized(() => {
88
+ log.info('mutex acquired', { uri: definition.uri });
89
+ return this._execFunction(definition, trigger, {
90
+ meta: trigger.meta ?? {},
91
+ data: { ...args, spaceKey: space.key },
92
+ });
93
+ });
94
+ });
104
95
 
105
- private async unmount(id: string, spaceKey: PublicKey) {
106
- const key = { id, spaceKey };
107
- const { ctx } = this._mounts.get(key) ?? {};
108
- if (ctx) {
109
- this._mounts.delete(key);
110
- await ctx.dispose();
111
- }
96
+ log('activated trigger', { space: space.key, trigger });
112
97
  }
113
98
 
114
- // TODO(burdon): Pass in Space key (common context).
115
- private async _execFunction(def: FunctionDef, data: any) {
99
+ private async _execFunction<TData, TMeta>(
100
+ def: FunctionDef,
101
+ trigger: FunctionTrigger,
102
+ { data, meta }: { data: TData; meta?: TMeta },
103
+ ): Promise<number> {
104
+ let status = 0;
116
105
  try {
117
- log.info('exec', { function: def.id });
106
+ // TODO(burdon): Pass in Space key (common context)?
107
+ const payload = Object.assign({}, meta && ({ meta } satisfies FunctionEventMeta<TMeta>), data);
108
+
118
109
  const { endpoint, callback } = this._options;
119
110
  if (endpoint) {
120
111
  // TODO(burdon): Move out of scheduler (generalize as callback).
121
- await fetch(`${this._options.endpoint}/${def.name}`, {
112
+ const url = path.join(endpoint, def.route);
113
+ log.info('exec', { function: def.uri, url, triggerType: trigger.spec.type });
114
+ const response = await fetch(url, {
122
115
  method: 'POST',
123
116
  headers: {
124
117
  'Content-Type': 'application/json',
125
118
  },
126
- body: JSON.stringify(data),
119
+ body: JSON.stringify(payload),
127
120
  });
121
+
122
+ status = response.status;
128
123
  } else if (callback) {
129
- await callback(data);
124
+ log.info('exec', { function: def.uri });
125
+ status = (await callback(payload)) ?? 200;
126
+ }
127
+
128
+ // Check errors.
129
+ if (status && status >= 400) {
130
+ throw new Error(`Response: ${status}`);
130
131
  }
131
132
 
132
133
  // const result = await response.json();
133
- log.info('done', { function: def.id });
134
+ log.info('done', { function: def.uri, status });
134
135
  } catch (err: any) {
135
- log.error('error', { function: def.id, error: err.message });
136
+ log.error('error', { function: def.uri, error: err.message });
137
+ status = 500;
136
138
  }
137
- }
138
-
139
- //
140
- // Triggers
141
- //
142
-
143
- /**
144
- * Cron timer.
145
- */
146
- private async _createTimer(ctx: Context, space: Space, def: FunctionDef, trigger: TimerTrigger) {
147
- log.info('timer', { space: space.key, trigger });
148
- const { cron } = trigger;
149
-
150
- const task = new DeferredTask(ctx, async () => {
151
- await this._execFunction(def, { space: space.key });
152
- });
153
139
 
154
- let last = 0;
155
- let run = 0;
156
- // https://www.npmjs.com/package/cron#constructor
157
- const job = CronJob.from({
158
- cronTime: cron,
159
- runOnInit: false,
160
- onTick: () => {
161
- // TODO(burdon): Check greater than 30s (use cron-parser).
162
- const now = Date.now();
163
- const delta = last ? now - last : 0;
164
- last = now;
165
-
166
- run++;
167
- log.info('tick', { space: space.key.truncate(), count: run, delta });
168
- task.schedule();
169
- },
170
- });
171
-
172
- job.start();
173
- ctx.onDispose(() => job.stop());
174
- }
175
-
176
- /**
177
- * Webhook.
178
- */
179
- private async _createWebhook(ctx: Context, space: Space, def: FunctionDef, trigger: WebhookTrigger) {
180
- log.info('webhook', { space: space.key, trigger });
181
- const { port } = trigger;
182
-
183
- // TODO(burdon): POST JSON.
184
- const server = http.createServer(async (req, res) => {
185
- await this._execFunction(def, { space: space.key });
186
- });
187
-
188
- server.listen(port, () => {
189
- log.info('started webhook', { port });
190
- });
191
-
192
- ctx.onDispose(() => {
193
- server.close();
194
- });
195
- }
196
-
197
- /**
198
- * Websocket.
199
- */
200
- private async _createWebsocket(
201
- ctx: Context,
202
- space: Space,
203
- def: FunctionDef,
204
- trigger: WebsocketTrigger,
205
- options: {
206
- retryDelay: number;
207
- maxAttempts: number;
208
- } = {
209
- retryDelay: 2,
210
- maxAttempts: 5,
211
- },
212
- ) {
213
- log.info('websocket', { space: space.key, trigger });
214
- const { url } = trigger;
215
-
216
- let ws: WebSocket;
217
- for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
218
- const open = new Trigger<boolean>();
219
-
220
- ws = new WebSocket(url);
221
- Object.assign(ws, {
222
- onopen: () => {
223
- log.info('opened', { url });
224
- if (trigger.init) {
225
- ws.send(new TextEncoder().encode(JSON.stringify(trigger.init)));
226
- }
227
-
228
- open.wake(true);
229
- },
230
-
231
- onclose: () => {
232
- log.info('closed', { url });
233
- open.wake(false);
234
- },
235
-
236
- onerror: (event) => {
237
- log.catch(event.error, { url });
238
- },
239
-
240
- onmessage: async (event) => {
241
- try {
242
- const data = JSON.parse(new TextDecoder().decode(event.data as Uint8Array));
243
- await this._execFunction(def, { space: space.key, data });
244
- } catch (err) {
245
- log.catch(err, { url });
246
- }
247
- },
248
- } satisfies Partial<WebSocket>);
249
-
250
- const isOpen = await open.wait();
251
- if (isOpen) {
252
- break;
253
- } else {
254
- const wait = Math.pow(attempt, 2) * options.retryDelay;
255
- if (attempt < options.maxAttempts) {
256
- log.warn(`failed to connect; trying again in ${wait}s`, { attempt });
257
- await sleep(wait * 1_000);
258
- }
259
- }
260
- }
261
-
262
- ctx.onDispose(() => {
263
- ws?.close();
264
- });
265
- }
266
-
267
- /**
268
- * ECHO subscription.
269
- */
270
- private async _createSubscription(ctx: Context, space: Space, def: FunctionDef, trigger: SubscriptionTrigger) {
271
- log.info('subscription', { space: space.key, trigger });
272
- const objectIds = new Set<string>();
273
- const task = new DeferredTask(ctx, async () => {
274
- await this._execFunction(def, { space: space.key, objects: Array.from(objectIds) });
275
- });
276
-
277
- // TODO(burdon): Don't fire initially.
278
- // TODO(burdon): Subscription is called THREE times.
279
- const subscriptions: (() => void)[] = [];
280
- const subscription = createSubscription(({ added, updated }) => {
281
- log.info('updated', { added: added.length, updated: updated.length });
282
- for (const object of added) {
283
- objectIds.add(object.id);
284
- }
285
- for (const object of updated) {
286
- objectIds.add(object.id);
287
- }
288
-
289
- task.schedule();
290
- });
291
- subscriptions.push(() => subscription.unsubscribe());
292
-
293
- // TODO(burdon): Create queue. Only allow one invocation per trigger at a time?
294
- // TODO(burdon): Disable trigger if keeps failing.
295
- const { filter, options: { deep, delay } = {} } = trigger;
296
- const update = ({ objects }: Query) => {
297
- subscription.update(objects);
298
-
299
- // TODO(burdon): Hack to monitor changes to Document's text object.
300
- if (deep) {
301
- log.info('update', { objects: objects.length });
302
- for (const object of objects) {
303
- const content = object.content;
304
- if (content instanceof TextV0Type) {
305
- subscriptions.push(
306
- getAutomergeObjectCore(content).updates.on(debounce(() => subscription.update([object]), 1_000)),
307
- );
308
- }
309
- }
310
- }
311
- };
312
-
313
- // TODO(burdon): Is Filter.or implemented?
314
- // TODO(burdon): [Bug]: all callbacks are fired on the first mutation.
315
- // TODO(burdon): [Bug]: not updated when document is deleted (either top or hierarchically).
316
- const query = space.db.query(Filter.or(filter.map(({ type, props }) => Filter.typename(type, props))));
317
- subscriptions.push(query.subscribe(delay ? debounce(update, delay) : update));
318
-
319
- ctx.onDispose(() => {
320
- subscriptions.forEach((unsubscribe) => unsubscribe());
321
- });
140
+ return status;
322
141
  }
323
142
  }
143
+
144
+ const createContext = () => new Context({ name: 'FunctionScheduler' });