@dxos/functions 0.5.4 → 0.5.5-main.1aa8b60

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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/browser/{chunk-4D4I3YMJ.mjs → chunk-ERWZ4JUZ.mjs} +31 -31
  3. package/dist/lib/browser/{chunk-4D4I3YMJ.mjs.map → chunk-ERWZ4JUZ.mjs.map} +2 -2
  4. package/dist/lib/browser/chunk-SXZ5DYJG.mjs +1089 -0
  5. package/dist/lib/browser/chunk-SXZ5DYJG.mjs.map +7 -0
  6. package/dist/lib/browser/index.mjs +18 -1076
  7. package/dist/lib/browser/index.mjs.map +4 -4
  8. package/dist/lib/browser/meta.json +1 -1
  9. package/dist/lib/browser/testing/index.mjs +151 -0
  10. package/dist/lib/browser/testing/index.mjs.map +7 -0
  11. package/dist/lib/browser/types.mjs +1 -1
  12. package/dist/lib/node/{chunk-3UYUR5N5.cjs → chunk-BLLSDTKZ.cjs} +34 -34
  13. package/dist/lib/node/{chunk-3UYUR5N5.cjs.map → chunk-BLLSDTKZ.cjs.map} +2 -2
  14. package/dist/lib/node/chunk-RPHL3ORN.cjs +1102 -0
  15. package/dist/lib/node/chunk-RPHL3ORN.cjs.map +7 -0
  16. package/dist/lib/node/index.cjs +20 -1074
  17. package/dist/lib/node/index.cjs.map +4 -4
  18. package/dist/lib/node/meta.json +1 -1
  19. package/dist/lib/node/testing/index.cjs +172 -0
  20. package/dist/lib/node/testing/index.cjs.map +7 -0
  21. package/dist/lib/node/types.cjs +5 -5
  22. package/dist/lib/node/types.cjs.map +1 -1
  23. package/dist/types/src/function/function-registry.d.ts +1 -0
  24. package/dist/types/src/function/function-registry.d.ts.map +1 -1
  25. package/dist/types/src/runtime/dev-server.d.ts +1 -0
  26. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  27. package/dist/types/src/testing/index.d.ts +1 -0
  28. package/dist/types/src/testing/index.d.ts.map +1 -1
  29. package/dist/types/src/testing/manifest.d.ts +3 -0
  30. package/dist/types/src/testing/manifest.d.ts.map +1 -0
  31. package/dist/types/src/testing/plugin-init.d.ts +6 -0
  32. package/dist/types/src/testing/plugin-init.d.ts.map +1 -0
  33. package/dist/types/src/testing/setup.d.ts +11 -1
  34. package/dist/types/src/testing/setup.d.ts.map +1 -1
  35. package/dist/types/src/testing/util.d.ts +2 -0
  36. package/dist/types/src/testing/util.d.ts.map +1 -1
  37. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -1
  38. package/dist/types/src/types.d.ts +27 -38
  39. package/dist/types/src/types.d.ts.map +1 -1
  40. package/package.json +24 -15
  41. package/src/function/function-registry.test.ts +14 -1
  42. package/src/function/function-registry.ts +10 -0
  43. package/src/runtime/dev-server.test.ts +42 -24
  44. package/src/runtime/dev-server.ts +17 -13
  45. package/src/runtime/scheduler.test.ts +4 -2
  46. package/src/testing/functions-integration.test.ts +8 -45
  47. package/src/testing/index.ts +1 -0
  48. package/src/testing/manifest.ts +15 -0
  49. package/src/testing/plugin-init.ts +20 -0
  50. package/src/testing/setup.ts +65 -6
  51. package/src/testing/types.ts +1 -1
  52. package/src/testing/util.ts +10 -0
  53. package/src/trigger/type/websocket-trigger.ts +12 -8
  54. package/src/types.ts +31 -31
@@ -2,14 +2,23 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { FunctionsPlugin } from '@dxos/agent';
5
+ import { getRandomPort } from 'get-port-please';
6
+ import path from 'node:path';
7
+
8
+ import { waitForCondition } from '@dxos/async';
6
9
  import { Client, Config } from '@dxos/client';
10
+ import { type Space } from '@dxos/client/echo';
7
11
  import { type TestBuilder } from '@dxos/client/testing';
8
12
  import { range } from '@dxos/util';
9
13
 
10
14
  import { TestType } from './types';
15
+ import { FunctionRegistry } from '../function';
16
+ import { DevServer, type DevServerOptions, Scheduler } from '../runtime';
17
+ import { TriggerRegistry } from '../trigger';
11
18
  import { FunctionDef, FunctionTrigger } from '../types';
12
19
 
20
+ export type FunctionsPluginInitializer = (client: Client) => Promise<{ close: () => Promise<void> }>;
21
+
13
22
  // TODO(burdon): Extend/wrap TestBuilder.
14
23
 
15
24
  export const createInitializedClients = async (testBuilder: TestBuilder, count: number = 1, config?: Config) => {
@@ -19,25 +28,75 @@ export const createInitializedClients = async (testBuilder: TestBuilder, count:
19
28
  clients.map(async (client, index) => {
20
29
  await client.initialize();
21
30
  await client.halo.createIdentity({ displayName: `Peer ${index}` });
31
+ await client.spaces.isReady;
22
32
  client.addSchema(FunctionDef, FunctionTrigger, TestType);
23
33
  return client;
24
34
  }),
25
35
  );
26
36
  };
27
37
 
28
- export const createFunctionRuntime = async (testBuilder: TestBuilder): Promise<Client> => {
38
+ export const createFunctionRuntime = async (
39
+ testBuilder: TestBuilder,
40
+ pluginInitializer: FunctionsPluginInitializer,
41
+ ): Promise<Client> => {
42
+ const functionsPort = await getRandomPort('127.0.0.1');
29
43
  const config = new Config({
30
44
  runtime: {
31
45
  agent: {
32
- plugins: [{ id: 'dxos.org/agent/plugin/functions', config: { port: 8080 } }],
46
+ plugins: [{ id: 'dxos.org/agent/plugin/functions', config: { port: functionsPort } }],
33
47
  },
34
48
  },
35
49
  });
36
50
 
37
51
  const [client] = await createInitializedClients(testBuilder, 1, config);
38
- const plugin = new FunctionsPlugin();
39
- await plugin.initialize({ client, clientServices: client.services });
40
- await plugin.open();
52
+ const plugin = await pluginInitializer(client);
41
53
  testBuilder.ctx.onDispose(() => plugin.close());
42
54
  return client;
43
55
  };
56
+
57
+ export const startFunctionsHost = async (
58
+ testBuilder: TestBuilder,
59
+ pluginInitializer: FunctionsPluginInitializer,
60
+ options?: DevServerOptions,
61
+ ) => {
62
+ const functionRuntime = await createFunctionRuntime(testBuilder, pluginInitializer);
63
+ const functionsRegistry = new FunctionRegistry(functionRuntime);
64
+ const devServer = await startDevServer(testBuilder, functionRuntime, functionsRegistry, options);
65
+ const scheduler = await startScheduler(testBuilder, functionRuntime, devServer, functionsRegistry);
66
+ return {
67
+ scheduler,
68
+ client: functionRuntime,
69
+ waitHasActiveTriggers: async (space: Space) => {
70
+ await waitForCondition({ condition: () => scheduler.triggers.getActiveTriggers(space).length > 0 });
71
+ },
72
+ };
73
+ };
74
+
75
+ const startScheduler = async (
76
+ testBuilder: TestBuilder,
77
+ client: Client,
78
+ devServer: DevServer,
79
+ functionRegistry: FunctionRegistry,
80
+ ) => {
81
+ const triggerRegistry = new TriggerRegistry(client);
82
+ const scheduler = new Scheduler(functionRegistry, triggerRegistry, { endpoint: devServer.endpoint });
83
+ await scheduler.start();
84
+ testBuilder.ctx.onDispose(() => scheduler.stop());
85
+ return scheduler;
86
+ };
87
+
88
+ const startDevServer = async (
89
+ testBuilder: TestBuilder,
90
+ client: Client,
91
+ functionRegistry: FunctionRegistry,
92
+ options?: { baseDir?: string },
93
+ ) => {
94
+ const server = new DevServer(client, functionRegistry, {
95
+ baseDir: path.join(__dirname, '../testing'),
96
+ port: await getRandomPort('127.0.0.1'),
97
+ ...options,
98
+ });
99
+ await server.start();
100
+ testBuilder.ctx.onDispose(() => server.stop());
101
+ return server;
102
+ };
@@ -5,5 +5,5 @@
5
5
  import { S, TypedObject } from '@dxos/echo-schema';
6
6
 
7
7
  export class TestType extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
8
- title: S.string,
8
+ title: S.String,
9
9
  }) {}
@@ -2,8 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import type { Client } from '@dxos/client';
5
6
  import { Filter, type Space } from '@dxos/client/echo';
7
+ import { performInvitation } from '@dxos/client/testing';
6
8
  import { invariant } from '@dxos/invariant';
9
+ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
7
10
 
8
11
  import { FunctionTrigger } from '../types';
9
12
 
@@ -14,3 +17,10 @@ export const triggerWebhook = async (space: Space, uri: string) => {
14
17
  invariant(trigger.spec.type === 'webhook');
15
18
  void fetch(`http://localhost:${trigger.spec.port}`);
16
19
  };
20
+
21
+ export const inviteMember = async (host: Space, guest: Client) => {
22
+ const [{ invitation: hostInvitation }] = await Promise.all(performInvitation({ host, guest: guest.spaces }));
23
+ if (hostInvitation?.state !== Invitation.State.SUCCESS) {
24
+ throw new Error(`Expected ${hostInvitation?.state} to be ${Invitation.State.SUCCESS}.`);
25
+ }
26
+ };
@@ -30,6 +30,7 @@ export const createWebsocketTrigger: TriggerFactory<WebsocketTrigger, WebsocketT
30
30
  ) => {
31
31
  const { url, init } = spec;
32
32
 
33
+ let wasOpen = false;
33
34
  let ws: WebSocket;
34
35
  for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
35
36
  const open = new Trigger<boolean>();
@@ -49,18 +50,18 @@ export const createWebsocketTrigger: TriggerFactory<WebsocketTrigger, WebsocketT
49
50
  log.info('closed', { url, code: event.code });
50
51
  // Reconnect if server closes (e.g., CF restart).
51
52
  // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
52
- if (event.code === 1006) {
53
+ if (event.code === 1006 && wasOpen && !ctx.disposed) {
53
54
  setTimeout(async () => {
54
55
  log.info(`reconnecting in ${options.retryDelay}s...`, { url });
55
56
  await createWebsocketTrigger(ctx, space, spec, callback, options);
56
57
  }, options.retryDelay * 1_000);
57
58
  }
58
-
59
59
  open.wake(false);
60
60
  },
61
61
 
62
62
  onerror: (event) => {
63
63
  log.catch(event.error, { url });
64
+ open.wake(false);
64
65
  },
65
66
 
66
67
  onmessage: async (event) => {
@@ -75,14 +76,17 @@ export const createWebsocketTrigger: TriggerFactory<WebsocketTrigger, WebsocketT
75
76
  } satisfies Partial<WebSocket>);
76
77
 
77
78
  const isOpen = await open.wait();
79
+ if (ctx.disposed) {
80
+ break;
81
+ }
78
82
  if (isOpen) {
83
+ wasOpen = true;
79
84
  break;
80
- } else {
81
- const wait = Math.pow(attempt, 2) * options.retryDelay;
82
- if (attempt < options.maxAttempts) {
83
- log.warn(`failed to connect; trying again in ${wait}s`, { attempt });
84
- await sleep(wait * 1_000);
85
- }
85
+ }
86
+ const wait = Math.pow(attempt, 2) * options.retryDelay;
87
+ if (attempt < options.maxAttempts) {
88
+ log.warn(`failed to connect; trying again in ${wait}s`, { attempt });
89
+ await sleep(wait * 1_000);
86
90
  }
87
91
  }
88
92
 
package/src/types.ts CHANGED
@@ -13,21 +13,21 @@ import { RawObject, S, TypedObject } from '@dxos/echo-schema';
13
13
  export type FunctionTriggerType = 'subscription' | 'timer' | 'webhook' | 'websocket';
14
14
 
15
15
  const SubscriptionTriggerSchema = S.mutable(
16
- S.struct({
17
- type: S.literal('subscription'),
16
+ S.Struct({
17
+ type: S.Literal('subscription'),
18
18
  // TODO(burdon): Define query DSL (from ECHO).
19
- filter: S.array(
20
- S.struct({
21
- type: S.string,
22
- props: S.optional(S.record(S.string, S.any)),
19
+ filter: S.Array(
20
+ S.Struct({
21
+ type: S.String,
22
+ props: S.optional(S.Record(S.String, S.Any)),
23
23
  }),
24
24
  ),
25
25
  options: S.optional(
26
- S.struct({
26
+ S.Struct({
27
27
  // Watch changes to object (not just creation).
28
- deep: S.optional(S.boolean),
28
+ deep: S.optional(S.Boolean),
29
29
  // Debounce changes (delay in ms).
30
- delay: S.optional(S.number),
30
+ delay: S.optional(S.Number),
31
31
  }),
32
32
  ),
33
33
  }),
@@ -36,36 +36,36 @@ const SubscriptionTriggerSchema = S.mutable(
36
36
  export type SubscriptionTrigger = S.Schema.Type<typeof SubscriptionTriggerSchema>;
37
37
 
38
38
  const TimerTriggerSchema = S.mutable(
39
- S.struct({
40
- type: S.literal('timer'),
41
- cron: S.string,
39
+ S.Struct({
40
+ type: S.Literal('timer'),
41
+ cron: S.String,
42
42
  }),
43
43
  );
44
44
 
45
45
  export type TimerTrigger = S.Schema.Type<typeof TimerTriggerSchema>;
46
46
 
47
47
  const WebhookTriggerSchema = S.mutable(
48
- S.struct({
49
- type: S.literal('webhook'),
50
- method: S.string,
48
+ S.Struct({
49
+ type: S.Literal('webhook'),
50
+ method: S.String,
51
51
  // Assigned port.
52
- port: S.optional(S.number),
52
+ port: S.optional(S.Number),
53
53
  }),
54
54
  );
55
55
 
56
56
  export type WebhookTrigger = S.Schema.Type<typeof WebhookTriggerSchema>;
57
57
 
58
58
  const WebsocketTriggerSchema = S.mutable(
59
- S.struct({
60
- type: S.literal('websocket'),
61
- url: S.string,
62
- init: S.optional(S.record(S.string, S.any)),
59
+ S.Struct({
60
+ type: S.Literal('websocket'),
61
+ url: S.String,
62
+ init: S.optional(S.Record(S.String, S.Any)),
63
63
  }),
64
64
  );
65
65
 
66
66
  export type WebsocketTrigger = S.Schema.Type<typeof WebsocketTriggerSchema>;
67
67
 
68
- const TriggerSpecSchema = S.union(
68
+ const TriggerSpecSchema = S.Union(
69
69
  TimerTriggerSchema,
70
70
  WebhookTriggerSchema,
71
71
  WebsocketTriggerSchema,
@@ -81,10 +81,10 @@ export class FunctionDef extends TypedObject({
81
81
  typename: 'dxos.org/type/FunctionDef',
82
82
  version: '0.1.0',
83
83
  })({
84
- uri: S.string,
85
- description: S.optional(S.string),
86
- route: S.string,
87
- handler: S.string,
84
+ uri: S.String,
85
+ description: S.optional(S.String),
86
+ route: S.String,
87
+ handler: S.String,
88
88
  }) {}
89
89
 
90
90
  /**
@@ -94,19 +94,19 @@ export class FunctionTrigger extends TypedObject({
94
94
  typename: 'dxos.org/type/FunctionTrigger',
95
95
  version: '0.1.0',
96
96
  })({
97
- enabled: S.optional(S.boolean),
98
- function: S.string.pipe(S.description('Function URI.')),
97
+ enabled: S.optional(S.Boolean),
98
+ function: S.String.pipe(S.description('Function URI.')),
99
99
  // The `meta` property is merged into the event data passed to the function.
100
- meta: S.optional(S.mutable(S.any)),
100
+ meta: S.optional(S.mutable(S.Any)),
101
101
  spec: TriggerSpecSchema,
102
102
  }) {}
103
103
 
104
104
  /**
105
105
  * Function manifest file.
106
106
  */
107
- export const FunctionManifestSchema = S.struct({
108
- functions: S.optional(S.mutable(S.array(RawObject(FunctionDef)))),
109
- triggers: S.optional(S.mutable(S.array(RawObject(FunctionTrigger)))),
107
+ export const FunctionManifestSchema = S.Struct({
108
+ functions: S.optional(S.mutable(S.Array(RawObject(FunctionDef)))),
109
+ triggers: S.optional(S.mutable(S.Array(RawObject(FunctionTrigger)))),
110
110
  });
111
111
 
112
112
  export type FunctionManifest = S.Schema.Type<typeof FunctionManifestSchema>;