@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.
- package/README.md +1 -1
- package/dist/lib/browser/{chunk-4D4I3YMJ.mjs → chunk-ERWZ4JUZ.mjs} +31 -31
- package/dist/lib/browser/{chunk-4D4I3YMJ.mjs.map → chunk-ERWZ4JUZ.mjs.map} +2 -2
- package/dist/lib/browser/chunk-SXZ5DYJG.mjs +1089 -0
- package/dist/lib/browser/chunk-SXZ5DYJG.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +18 -1076
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +151 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/browser/types.mjs +1 -1
- package/dist/lib/node/{chunk-3UYUR5N5.cjs → chunk-BLLSDTKZ.cjs} +34 -34
- package/dist/lib/node/{chunk-3UYUR5N5.cjs.map → chunk-BLLSDTKZ.cjs.map} +2 -2
- package/dist/lib/node/chunk-RPHL3ORN.cjs +1102 -0
- package/dist/lib/node/chunk-RPHL3ORN.cjs.map +7 -0
- package/dist/lib/node/index.cjs +20 -1074
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +172 -0
- package/dist/lib/node/testing/index.cjs.map +7 -0
- package/dist/lib/node/types.cjs +5 -5
- package/dist/lib/node/types.cjs.map +1 -1
- package/dist/types/src/function/function-registry.d.ts +1 -0
- package/dist/types/src/function/function-registry.d.ts.map +1 -1
- package/dist/types/src/runtime/dev-server.d.ts +1 -0
- package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/manifest.d.ts +3 -0
- package/dist/types/src/testing/manifest.d.ts.map +1 -0
- package/dist/types/src/testing/plugin-init.d.ts +6 -0
- package/dist/types/src/testing/plugin-init.d.ts.map +1 -0
- package/dist/types/src/testing/setup.d.ts +11 -1
- package/dist/types/src/testing/setup.d.ts.map +1 -1
- package/dist/types/src/testing/util.d.ts +2 -0
- package/dist/types/src/testing/util.d.ts.map +1 -1
- package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +27 -38
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +24 -15
- package/src/function/function-registry.test.ts +14 -1
- package/src/function/function-registry.ts +10 -0
- package/src/runtime/dev-server.test.ts +42 -24
- package/src/runtime/dev-server.ts +17 -13
- package/src/runtime/scheduler.test.ts +4 -2
- package/src/testing/functions-integration.test.ts +8 -45
- package/src/testing/index.ts +1 -0
- package/src/testing/manifest.ts +15 -0
- package/src/testing/plugin-init.ts +20 -0
- package/src/testing/setup.ts +65 -6
- package/src/testing/types.ts +1 -1
- package/src/testing/util.ts +10 -0
- package/src/trigger/type/websocket-trigger.ts +12 -8
- package/src/types.ts +31 -31
package/src/testing/setup.ts
CHANGED
|
@@ -2,14 +2,23 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
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 (
|
|
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:
|
|
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 =
|
|
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
|
+
};
|
package/src/testing/types.ts
CHANGED
package/src/testing/util.ts
CHANGED
|
@@ -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
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
17
|
-
type: S.
|
|
16
|
+
S.Struct({
|
|
17
|
+
type: S.Literal('subscription'),
|
|
18
18
|
// TODO(burdon): Define query DSL (from ECHO).
|
|
19
|
-
filter: S.
|
|
20
|
-
S.
|
|
21
|
-
type: S.
|
|
22
|
-
props: S.optional(S.
|
|
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.
|
|
26
|
+
S.Struct({
|
|
27
27
|
// Watch changes to object (not just creation).
|
|
28
|
-
deep: S.optional(S.
|
|
28
|
+
deep: S.optional(S.Boolean),
|
|
29
29
|
// Debounce changes (delay in ms).
|
|
30
|
-
delay: S.optional(S.
|
|
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.
|
|
40
|
-
type: S.
|
|
41
|
-
cron: S.
|
|
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.
|
|
49
|
-
type: S.
|
|
50
|
-
method: S.
|
|
48
|
+
S.Struct({
|
|
49
|
+
type: S.Literal('webhook'),
|
|
50
|
+
method: S.String,
|
|
51
51
|
// Assigned port.
|
|
52
|
-
port: S.optional(S.
|
|
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.
|
|
60
|
-
type: S.
|
|
61
|
-
url: S.
|
|
62
|
-
init: S.optional(S.
|
|
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.
|
|
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.
|
|
85
|
-
description: S.optional(S.
|
|
86
|
-
route: S.
|
|
87
|
-
handler: S.
|
|
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.
|
|
98
|
-
function: S.
|
|
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.
|
|
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.
|
|
108
|
-
functions: S.optional(S.mutable(S.
|
|
109
|
-
triggers: S.optional(S.mutable(S.
|
|
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>;
|