@dxos/functions 0.5.3-main.59db342 → 0.5.3-main.61bbff4

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 (86) hide show
  1. package/dist/lib/browser/index.mjs +829 -265
  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 +817 -266
  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 +3 -1
  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 +15 -7
  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 +14 -15
  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 +188 -0
  54. package/dist/types/src/types.d.ts.map +1 -0
  55. package/dist/types/tools/schema.d.ts +2 -0
  56. package/dist/types/tools/schema.d.ts.map +1 -0
  57. package/package.json +23 -11
  58. package/schema/functions.json +197 -0
  59. package/src/handler.ts +56 -26
  60. package/src/index.ts +3 -1
  61. package/src/registry/function-registry.test.ts +105 -0
  62. package/src/registry/function-registry.ts +84 -0
  63. package/src/registry/index.ts +5 -0
  64. package/src/runtime/dev-server.test.ts +60 -0
  65. package/src/runtime/dev-server.ts +104 -52
  66. package/src/runtime/scheduler.test.ts +154 -21
  67. package/src/runtime/scheduler.ts +76 -152
  68. package/src/testing/functions-integration.test.ts +99 -0
  69. package/src/testing/index.ts +7 -0
  70. package/src/testing/setup.ts +45 -0
  71. package/src/testing/test/handler.ts +15 -0
  72. package/src/testing/test/index.ts +7 -0
  73. package/src/testing/types.ts +9 -0
  74. package/src/testing/util.ts +16 -0
  75. package/src/trigger/index.ts +5 -0
  76. package/src/trigger/trigger-registry.test.ts +229 -0
  77. package/src/trigger/trigger-registry.ts +176 -0
  78. package/src/trigger/type/index.ts +8 -0
  79. package/src/trigger/type/subscription-trigger.ts +73 -0
  80. package/src/trigger/type/timer-trigger.ts +44 -0
  81. package/src/trigger/type/webhook-trigger.ts +47 -0
  82. package/src/trigger/type/websocket-trigger.ts +91 -0
  83. package/src/types.ts +101 -0
  84. package/dist/types/src/manifest.d.ts +0 -26
  85. package/dist/types/src/manifest.d.ts.map +0 -1
  86. package/src/manifest.ts +0 -42
@@ -3,56 +3,189 @@
3
3
  //
4
4
 
5
5
  import { expect } from 'chai';
6
+ import WebSocket from 'ws';
6
7
 
7
8
  import { Trigger } from '@dxos/async';
8
- import { Client } from '@dxos/client';
9
+ import { type Client } from '@dxos/client';
9
10
  import { TestBuilder } from '@dxos/client/testing';
11
+ import { create } from '@dxos/echo-schema';
10
12
  import { describe, test } from '@dxos/test';
11
13
 
12
- import { Scheduler } from './scheduler';
13
- import { type FunctionManifest } from '../manifest';
14
+ import { Scheduler, type SchedulerOptions } from './scheduler';
15
+ import { FunctionRegistry } from '../registry';
16
+ import { createInitializedClients, TestType, triggerWebhook } from '../testing';
17
+ import { TriggerRegistry } from '../trigger';
18
+ import { type FunctionManifest } from '../types';
14
19
 
20
+ // TODO(burdon): Test we can add and remove triggers.
15
21
  describe('scheduler', () => {
16
- test.only('callback', async () => {
17
- const testBuilder = new TestBuilder();
18
- const client = new Client({ services: testBuilder.createLocal() });
19
- await client.initialize();
20
- await client.halo.createIdentity();
22
+ let testBuilder: TestBuilder;
23
+ let client: Client;
24
+ before(async () => {
25
+ testBuilder = new TestBuilder();
26
+ client = (await createInitializedClients(testBuilder, 1))[0];
27
+ });
28
+ after(async () => {
29
+ await testBuilder.destroy();
30
+ });
21
31
 
32
+ test('timer', async () => {
22
33
  const manifest: FunctionManifest = {
23
34
  functions: [
24
35
  {
25
- id: 'example.com/function/test',
26
- name: 'test',
36
+ uri: 'example.com/function/test',
37
+ route: '/test',
27
38
  handler: 'test',
28
39
  },
29
40
  ],
30
41
  triggers: [
31
42
  {
32
43
  function: 'example.com/function/test',
33
- schedule: '0/1 * * * * *', // Every 1s.
44
+ spec: {
45
+ type: 'timer',
46
+ cron: '0/1 * * * * *', // Every 1s.
47
+ },
34
48
  },
35
49
  ],
36
50
  };
37
51
 
38
52
  let count = 0;
39
53
  const done = new Trigger();
40
- const scheduler = new Scheduler(client, manifest, {
41
- callback: async () => {
42
- if (++count === 3) {
43
- done.wake();
44
- }
54
+ const scheduler = createScheduler(async () => {
55
+ if (++count === 3) {
56
+ done.wake();
57
+ }
58
+ });
59
+ await scheduler.register(client.spaces.default, manifest);
60
+ await scheduler.start();
61
+
62
+ await done.wait({ timeout: 5_000 });
63
+ expect(count).to.equal(3);
64
+ });
65
+
66
+ test('webhook', async () => {
67
+ const manifest: FunctionManifest = {
68
+ functions: [
69
+ {
70
+ uri: 'example.com/function/test',
71
+ route: '/test',
72
+ handler: 'test',
73
+ },
74
+ ],
75
+ triggers: [
76
+ {
77
+ function: 'example.com/function/test',
78
+ spec: {
79
+ type: 'webhook',
80
+ method: 'GET',
81
+ },
82
+ },
83
+ ],
84
+ };
45
85
 
46
- return 200;
47
- },
86
+ const done = new Trigger();
87
+ const scheduler = createScheduler(async () => {
88
+ done.wake();
48
89
  });
90
+ const space = await client.spaces.create();
91
+ await scheduler.register(space, manifest);
92
+ await scheduler.start();
93
+
94
+ setTimeout(async () => triggerWebhook(space, manifest.functions![0].uri));
95
+
96
+ await done.wait();
97
+ });
98
+
99
+ test('websocket', async () => {
100
+ const manifest: FunctionManifest = {
101
+ functions: [
102
+ {
103
+ uri: 'example.com/function/test',
104
+ route: '/test',
105
+ handler: 'test',
106
+ },
107
+ ],
108
+ triggers: [
109
+ {
110
+ function: 'example.com/function/test',
111
+ spec: {
112
+ type: 'websocket',
113
+ // url: 'https://hub.dxos.network/api/mailbox/test',
114
+ url: 'http://localhost:8081',
115
+ init: {
116
+ type: 'sync',
117
+ },
118
+ },
119
+ },
120
+ ],
121
+ };
49
122
 
123
+ const done = new Trigger();
124
+ const scheduler = createScheduler(async () => {
125
+ done.wake();
126
+ });
127
+ await scheduler.register(client.spaces.default, manifest);
50
128
  await scheduler.start();
51
129
 
130
+ // Test server.
131
+ setTimeout(() => {
132
+ const wss = new WebSocket.Server({ port: 8081 });
133
+ wss.on('connection', (ws: WebSocket) => {
134
+ ws.on('message', (data) => {
135
+ const info = JSON.parse(new TextDecoder().decode(data as ArrayBuffer));
136
+ expect(info.type).to.equal('sync');
137
+ done.wake();
138
+ });
139
+ });
140
+ }, 500);
141
+
52
142
  await done.wait();
53
- expect(count).to.equal(3);
143
+ });
144
+
145
+ test('subscription', async () => {
146
+ const manifest: FunctionManifest = {
147
+ functions: [
148
+ {
149
+ uri: 'example.com/function/test',
150
+ route: '/test',
151
+ handler: 'test',
152
+ },
153
+ ],
154
+ triggers: [
155
+ {
156
+ function: 'example.com/function/test',
157
+ spec: {
158
+ type: 'subscription',
159
+ filter: [{ type: TestType.typename }],
160
+ },
161
+ },
162
+ ],
163
+ };
54
164
 
55
- await scheduler.stop();
56
- await client.destroy();
165
+ let count = 0;
166
+ const done = new Trigger();
167
+ const scheduler = createScheduler(async () => {
168
+ if (++count === 2) {
169
+ done.wake();
170
+ }
171
+ });
172
+ await scheduler.register(client.spaces.default, manifest);
173
+ await scheduler.start();
174
+
175
+ setTimeout(() => {
176
+ const space = client.spaces.default;
177
+ const object = create(TestType, { title: 'Hello world!' });
178
+ space.db.add(object);
179
+ }, 100);
180
+
181
+ await done.wait();
57
182
  });
183
+
184
+ const createScheduler = (callback: SchedulerOptions['callback']) => {
185
+ const scheduler = new Scheduler(new FunctionRegistry(client), new TriggerRegistry(client), { callback });
186
+ after(async () => {
187
+ await scheduler.stop();
188
+ });
189
+ return scheduler;
190
+ };
58
191
  });
@@ -2,206 +2,130 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { CronJob } from 'cron';
5
+ import path from 'node:path';
6
6
 
7
- import { TextV0Type } from '@braneframe/types';
8
- import { debounce, DeferredTask } from '@dxos/async';
9
- import { type Client, type PublicKey } from '@dxos/client';
10
- import { type Space, Filter, createSubscription, type Query, getAutomergeObjectCore } from '@dxos/client/echo';
7
+ import { type Space } from '@dxos/client/echo';
11
8
  import { Context } from '@dxos/context';
12
- import { invariant } from '@dxos/invariant';
13
9
  import { log } from '@dxos/log';
14
- import { ComplexMap } from '@dxos/util';
15
10
 
16
- import { type FunctionSubscriptionEvent } from '../handler';
17
- import { type FunctionDef, type FunctionManifest, type FunctionTrigger, type TriggerSubscription } from '../manifest';
11
+ import { type FunctionEventMeta } from '../handler';
12
+ import { type FunctionRegistry } from '../registry';
13
+ import { type TriggerRegistry } from '../trigger';
14
+ import { type FunctionDef, type FunctionManifest, type FunctionTrigger } from '../types';
18
15
 
19
- type Callback = (data: FunctionSubscriptionEvent) => Promise<number>;
16
+ export type Callback = (data: any) => Promise<void | number>;
20
17
 
21
- type SchedulerOptions = {
18
+ export type SchedulerOptions = {
22
19
  endpoint?: string;
23
20
  callback?: Callback;
24
21
  };
25
22
 
26
23
  /**
27
- * Functions scheduler.
24
+ * The scheduler triggers function execution based on various triggers.
28
25
  */
29
- // TODO(burdon): Create tests.
30
26
  export class Scheduler {
31
- // Map of mounted functions.
32
- private readonly _mounts = new ComplexMap<
33
- { id: string; spaceKey: PublicKey },
34
- { ctx: Context; trigger: FunctionTrigger }
35
- >(({ id, spaceKey }) => `${spaceKey.toHex()}:${id}`);
27
+ private _ctx = createContext();
36
28
 
37
29
  constructor(
38
- private readonly _client: Client,
39
- private readonly _manifest: FunctionManifest,
30
+ public readonly functions: FunctionRegistry,
31
+ public readonly triggers: TriggerRegistry,
40
32
  private readonly _options: SchedulerOptions = {},
41
- ) {}
42
-
43
- async start() {
44
- this._client.spaces.subscribe(async (spaces) => {
45
- for (const space of spaces) {
46
- await space.waitUntilReady();
47
- for (const trigger of this._manifest.triggers ?? []) {
48
- await this.mount(new Context(), space, trigger);
49
- }
50
- }
33
+ ) {
34
+ this.functions.onFunctionsRegistered.on(async ({ space, newFunctions }) => {
35
+ await this._safeActivateTriggers(space, this.triggers.getInactiveTriggers(space), newFunctions);
36
+ });
37
+ this.triggers.registered.on(async ({ space, triggers }) => {
38
+ await this._safeActivateTriggers(space, triggers, this.functions.getFunctions(space));
51
39
  });
52
40
  }
53
41
 
54
- async stop() {
55
- for (const { id, spaceKey } of this._mounts.keys()) {
56
- await this.unmount(id, spaceKey);
57
- }
42
+ async start() {
43
+ await this._ctx.dispose();
44
+ this._ctx = createContext();
45
+ await this.functions.open(this._ctx);
46
+ await this.triggers.open(this._ctx);
58
47
  }
59
48
 
60
- private async mount(ctx: Context, space: Space, trigger: FunctionTrigger) {
61
- const key = { id: trigger.function, spaceKey: space.key };
62
- const def = this._manifest.functions.find((config) => config.id === trigger.function);
63
- invariant(def, `Function not found: ${trigger.function}`);
64
-
65
- // Currently supports only one trigger declaration per function.
66
- const exists = this._mounts.get(key);
67
- if (!exists) {
68
- this._mounts.set(key, { ctx, trigger });
69
- log('mount', { space: space.key, trigger });
70
- if (ctx.disposed) {
71
- return;
72
- }
73
-
74
- // Timer.
75
- if (trigger.schedule) {
76
- this._createTimer(ctx, space, def, trigger);
77
- }
78
-
79
- // Subscription.
80
- for (const triggerSubscription of trigger.subscriptions ?? []) {
81
- this._createSubscription(ctx, space, def, triggerSubscription);
82
- }
83
- }
49
+ async stop() {
50
+ await this._ctx.dispose();
51
+ await this.functions.close();
52
+ await this.triggers.close();
84
53
  }
85
54
 
86
- private async unmount(id: string, spaceKey: PublicKey) {
87
- const key = { id, spaceKey };
88
- const { ctx } = this._mounts.get(key) ?? {};
89
- if (ctx) {
90
- this._mounts.delete(key);
91
- await ctx.dispose();
92
- }
55
+ public async register(space: Space, manifest: FunctionManifest) {
56
+ await this.functions.register(space, manifest);
57
+ await this.triggers.register(space, manifest);
93
58
  }
94
59
 
95
- private _createTimer(ctx: Context, space: Space, def: FunctionDef, trigger: FunctionTrigger) {
96
- const task = new DeferredTask(ctx, async () => {
97
- await this._execFunction(def, {
98
- space: space.key,
99
- });
100
- });
101
-
102
- invariant(trigger.schedule);
103
- let last = 0;
104
- let run = 0;
105
- // https://www.npmjs.com/package/cron#constructor
106
- const job = CronJob.from({
107
- cronTime: trigger.schedule,
108
- runOnInit: false,
109
- onTick: () => {
110
- // TODO(burdon): Check greater than 30s (use cron-parser).
111
- const now = Date.now();
112
- const delta = last ? now - last : 0;
113
- last = now;
114
-
115
- run++;
116
- log.info('tick', { space: space.key.truncate(), count: run, delta });
117
- task.schedule();
118
- },
60
+ private async _safeActivateTriggers(
61
+ space: Space,
62
+ triggers: FunctionTrigger[],
63
+ functions: FunctionDef[],
64
+ ): Promise<void> {
65
+ const mountTasks = triggers.map((trigger) => {
66
+ return this.activate(space, functions, trigger);
119
67
  });
120
-
121
- job.start();
122
- ctx.onDispose(() => job.stop());
68
+ await Promise.all(mountTasks).catch(log.catch);
123
69
  }
124
70
 
125
- private _createSubscription(ctx: Context, space: Space, def: FunctionDef, triggerSubscription: TriggerSubscription) {
126
- log.info('subscription', { space: space.key, triggerSubscription });
127
- const objectIds = new Set<string>();
128
- const task = new DeferredTask(ctx, async () => {
129
- await this._execFunction(def, {
130
- space: space.key,
131
- objects: Array.from(objectIds),
132
- });
133
- });
134
-
135
- // TODO(burdon): Don't fire initially.
136
- // TODO(burdon): Standardize subscription handles.
137
- const subscriptions: (() => void)[] = [];
138
- const subscription = createSubscription(({ added, updated }) => {
139
- log.info('updated', { added: added.length, updated: updated.length });
140
- for (const object of added) {
141
- objectIds.add(object.id);
142
- }
143
- for (const object of updated) {
144
- objectIds.add(object.id);
145
- }
146
-
147
- task.schedule();
148
- });
149
- subscriptions.push(() => subscription.unsubscribe());
150
-
151
- // TODO(burdon): Create queue. Only allow one invocation per trigger at a time?
152
- // TODO(burdon): Disable trigger if keeps failing.
153
- const { type, props, deep, delay } = triggerSubscription;
154
- const update = ({ objects }: Query) => {
155
- subscription.update(objects);
156
-
157
- // TODO(burdon): Hack to monitor changes to Document's text object.
158
- if (deep) {
159
- log.info('update', { type, deep, objects: objects.length });
160
- for (const object of objects) {
161
- const content = object.content;
162
- if (content instanceof TextV0Type) {
163
- subscriptions.push(
164
- getAutomergeObjectCore(content).updates.on(debounce(() => subscription.update([object]), 1_000)),
165
- );
166
- }
167
- }
168
- }
169
- };
170
-
171
- // TODO(burdon): [Bug]: all callbacks are fired on the first mutation.
172
- // TODO(burdon): [Bug]: not updated when document is deleted (either top or hierarchically).
173
- const query = space.db.query(Filter.typename(type, props));
174
- subscriptions.push(query.subscribe(delay ? debounce(update, delay * 1_000) : update));
71
+ private async activate(space: Space, functions: FunctionDef[], fnTrigger: FunctionTrigger) {
72
+ const definition = functions.find((def) => def.uri === fnTrigger.function);
73
+ if (!definition) {
74
+ log.info('function is not found for trigger', { fnTrigger });
75
+ return;
76
+ }
175
77
 
176
- ctx.onDispose(() => {
177
- subscriptions.forEach((unsubscribe) => unsubscribe());
78
+ await this.triggers.activate({ space }, fnTrigger, async (args) => {
79
+ return this._execFunction(definition, {
80
+ meta: fnTrigger.meta,
81
+ data: { ...args, spaceKey: space.key },
82
+ });
178
83
  });
84
+ log('activated trigger', { space: space.key, trigger: fnTrigger });
179
85
  }
180
86
 
181
- private async _execFunction(def: FunctionDef, data: any) {
87
+ private async _execFunction<TData, TMeta>(
88
+ def: FunctionDef,
89
+ { data, meta }: { data: TData; meta?: TMeta },
90
+ ): Promise<number> {
91
+ let status = 0;
182
92
  try {
183
- log('request', { function: def.id });
93
+ // TODO(burdon): Pass in Space key (common context)?
94
+ const payload = Object.assign({}, meta && ({ meta } satisfies FunctionEventMeta<TMeta>), data);
95
+
184
96
  const { endpoint, callback } = this._options;
185
- let status = 0;
186
97
  if (endpoint) {
187
98
  // TODO(burdon): Move out of scheduler (generalize as callback).
188
- const response = await fetch(`${this._options.endpoint}/${def.name}`, {
99
+ const url = path.join(endpoint, def.route);
100
+ log.info('exec', { function: def.uri, url });
101
+ const response = await fetch(url, {
189
102
  method: 'POST',
190
103
  headers: {
191
104
  'Content-Type': 'application/json',
192
105
  },
193
- body: JSON.stringify(data),
106
+ body: JSON.stringify(payload),
194
107
  });
195
108
 
196
109
  status = response.status;
197
110
  } else if (callback) {
198
- status = await callback(data);
111
+ log.info('exec', { function: def.uri });
112
+ status = (await callback(payload)) ?? 200;
113
+ }
114
+
115
+ // Check errors.
116
+ if (status && status >= 400) {
117
+ throw new Error(`Response: ${status}`);
199
118
  }
200
119
 
201
120
  // const result = await response.json();
202
- log('result', { function: def.id, result: status });
121
+ log.info('done', { function: def.uri, status });
203
122
  } catch (err: any) {
204
- log.error('error', { function: def.id, error: err.message });
123
+ log.error('error', { function: def.uri, error: err.message });
124
+ status = 500;
205
125
  }
126
+
127
+ return status;
206
128
  }
207
129
  }
130
+
131
+ const createContext = () => new Context({ name: 'FunctionScheduler' });
@@ -0,0 +1,99 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+ import path from 'path';
7
+
8
+ import { Trigger, waitForCondition } from '@dxos/async';
9
+ import { type Client } from '@dxos/client';
10
+ import { create, type Space } from '@dxos/client/echo';
11
+ import { performInvitation, TestBuilder } from '@dxos/client/testing';
12
+ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
13
+ import { describe, test } from '@dxos/test';
14
+
15
+ import { setTestCallHandler } from './test/handler';
16
+ import { FunctionRegistry } from '../registry';
17
+ import { DevServer, Scheduler } from '../runtime';
18
+ import { createFunctionRuntime, createInitializedClients, TestType } from '../testing';
19
+ import { TriggerRegistry } from '../trigger';
20
+ import { FunctionDef, FunctionTrigger } from '../types';
21
+
22
+ describe('functions e2e', () => {
23
+ let testBuilder: TestBuilder;
24
+ before(async () => {
25
+ testBuilder = new TestBuilder();
26
+ });
27
+ after(async () => {
28
+ await testBuilder.destroy();
29
+ });
30
+
31
+ test('a function gets triggered in response to another peer object creations', async () => {
32
+ // TODO(burdon): Create builder pattern.
33
+ const functionRuntime = await createFunctionRuntime(testBuilder);
34
+ const devServer = await startDevServer(functionRuntime);
35
+ const scheduler = await startScheduler(functionRuntime, devServer);
36
+
37
+ const app = (await createInitializedClients(testBuilder, 1))[0];
38
+ const space = await app.spaces.create();
39
+ await inviteMember(space, functionRuntime);
40
+
41
+ const uri = 'example.com/function/test';
42
+ space.db.add(create(FunctionDef, { uri, route: '/test', handler: 'test' }));
43
+ const triggerMeta: FunctionTrigger['meta'] = { name: 'DXOS' };
44
+ space.db.add(
45
+ create(FunctionTrigger, {
46
+ function: uri,
47
+ meta: triggerMeta,
48
+ spec: {
49
+ type: 'subscription',
50
+ filter: [{ type: TestType.typename }],
51
+ },
52
+ }),
53
+ );
54
+
55
+ const called = new Trigger<any>();
56
+ setTestCallHandler(async (args) => {
57
+ called.wake(args.event.data);
58
+ return args.response.status(200);
59
+ });
60
+
61
+ await waitTriggersReplicated(space, scheduler);
62
+ const addedObject = space.db.add(create(TestType, { title: '42' }));
63
+
64
+ const callArgs = await called.wait();
65
+ expect(callArgs.meta).to.deep.eq(triggerMeta);
66
+ expect(callArgs.objects).to.deep.eq([addedObject.id]);
67
+ expect(callArgs.spaceKey).to.eq(space.key.toHex());
68
+ });
69
+
70
+ const waitTriggersReplicated = async (space: Space, scheduler: Scheduler) => {
71
+ await waitForCondition({ condition: () => scheduler.triggers.getActiveTriggers(space).length > 0 });
72
+ };
73
+
74
+ // TODO(burdon): Factor out utils to builder pattern.
75
+
76
+ const startScheduler = async (client: Client, devServer: DevServer) => {
77
+ const functionRegistry = new FunctionRegistry(client);
78
+ const triggerRegistry = new TriggerRegistry(client);
79
+ const scheduler = new Scheduler(functionRegistry, triggerRegistry, { endpoint: devServer.endpoint });
80
+ await scheduler.start();
81
+ testBuilder.ctx.onDispose(() => scheduler.stop());
82
+ return scheduler;
83
+ };
84
+
85
+ const startDevServer = async (client: Client) => {
86
+ const functionRegistry = new FunctionRegistry(client);
87
+ const server = new DevServer(client, functionRegistry, {
88
+ baseDir: path.join(__dirname, '../testing'),
89
+ });
90
+ await server.start();
91
+ testBuilder.ctx.onDispose(() => server.stop());
92
+ return server;
93
+ };
94
+
95
+ const inviteMember = async (host: Space, guest: Client) => {
96
+ const [{ invitation: hostInvitation }] = await Promise.all(performInvitation({ host, guest: guest.spaces }));
97
+ expect(hostInvitation?.state).to.eq(Invitation.State.SUCCESS);
98
+ };
99
+ });
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './setup';
6
+ export * from './types';
7
+ export * from './util';
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { FunctionsPlugin } from '@dxos/agent';
6
+ import { Client, Config } from '@dxos/client';
7
+ import { type TestBuilder } from '@dxos/client/testing';
8
+ import { range } from '@dxos/util';
9
+
10
+ import { TestType } from './types';
11
+ import { FunctionDef, FunctionTrigger } from '../types';
12
+
13
+ // TODO(burdon): Create TestBuilder.
14
+
15
+ export const createInitializedClients = async (testBuilder: TestBuilder, count: number = 1, config?: Config) => {
16
+ const clients = range(count).map(() => new Client({ config, services: testBuilder.createLocalClientServices() }));
17
+ testBuilder.ctx.onDispose(() => Promise.all(clients.map((c) => c.destroy())));
18
+ return Promise.all(
19
+ clients.map(async (client, index) => {
20
+ await client.initialize();
21
+ await client.halo.createIdentity({ displayName: `Peer ${index}` });
22
+ client.addSchema(TestType, FunctionDef, FunctionTrigger);
23
+ return client;
24
+ }),
25
+ );
26
+ };
27
+
28
+ export const createFunctionRuntime = async (testBuilder: TestBuilder): Promise<Client> => {
29
+ const config = new Config({
30
+ runtime: {
31
+ agent: {
32
+ plugins: [{ id: 'dxos.org/agent/plugin/functions', config: { port: 8080 } }],
33
+ },
34
+ },
35
+ });
36
+
37
+ const client = (await createInitializedClients(testBuilder, 1, config))[0];
38
+
39
+ // TODO(burdon): Better way to configure plugin? (Rationalize chess.test).
40
+ const functionsPlugin = new FunctionsPlugin();
41
+ await functionsPlugin.initialize({ client, clientServices: client.services });
42
+ await functionsPlugin.open();
43
+ testBuilder.ctx.onDispose(() => functionsPlugin.close());
44
+ return client;
45
+ };
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type FunctionHandler } from '../../handler';
6
+
7
+ let callHandler: FunctionHandler<any> = async ({ response }) => response.status(200);
8
+
9
+ export const setTestCallHandler = (handler: FunctionHandler<any>) => {
10
+ callHandler = handler;
11
+ };
12
+
13
+ export const handler: FunctionHandler<any> = async (args) => {
14
+ return callHandler(args);
15
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { handler } from './handler';
6
+
7
+ export default handler;
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { S, TypedObject } from '@dxos/echo-schema';
6
+
7
+ export class TestType extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
8
+ title: S.string,
9
+ }) {}