@dxos/functions 0.5.3-main.f9b873d → 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.
- package/dist/lib/browser/chunk-4D4I3YMJ.mjs +86 -0
- package/dist/lib/browser/chunk-4D4I3YMJ.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +390 -336
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types.mjs +14 -0
- package/dist/lib/browser/types.mjs.map +7 -0
- package/dist/lib/node/chunk-3UYUR5N5.cjs +103 -0
- package/dist/lib/node/chunk-3UYUR5N5.cjs.map +7 -0
- package/dist/lib/node/index.cjs +393 -333
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types.cjs +35 -0
- package/dist/lib/node/types.cjs.map +7 -0
- package/dist/types/src/browser/index.d.ts +2 -0
- package/dist/types/src/browser/index.d.ts.map +1 -0
- package/dist/types/src/{registry → function}/function-registry.d.ts +4 -4
- package/dist/types/src/function/function-registry.d.ts.map +1 -0
- package/dist/types/src/function/function-registry.test.d.ts.map +1 -0
- package/dist/types/src/function/index.d.ts.map +1 -0
- package/dist/types/src/handler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/runtime/dev-server.d.ts +1 -1
- package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
- package/dist/types/src/runtime/scheduler.d.ts +2 -1
- package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
- package/dist/types/src/testing/setup.d.ts.map +1 -1
- package/dist/types/src/trigger/trigger-registry.d.ts +2 -5
- package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -1
- package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -1
- package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -1
- package/dist/types/src/trigger/type/webhook-trigger.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 +70 -43
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +31 -18
- package/schema/functions.json +23 -9
- package/src/browser/index.ts +5 -0
- package/src/{registry → function}/function-registry.test.ts +10 -10
- package/src/function/function-registry.ts +90 -0
- package/src/index.ts +1 -1
- package/src/runtime/dev-server.test.ts +2 -2
- package/src/runtime/dev-server.ts +8 -9
- package/src/runtime/scheduler.test.ts +15 -10
- package/src/runtime/scheduler.ts +26 -13
- package/src/testing/functions-integration.test.ts +2 -1
- package/src/testing/setup.ts +8 -10
- package/src/trigger/trigger-registry.test.ts +88 -45
- package/src/trigger/trigger-registry.ts +66 -31
- package/src/trigger/type/subscription-trigger.ts +30 -17
- package/src/trigger/type/timer-trigger.ts +4 -3
- package/src/trigger/type/webhook-trigger.ts +3 -2
- package/src/trigger/type/websocket-trigger.ts +4 -3
- package/src/types.ts +51 -37
- package/dist/types/src/registry/function-registry.d.ts.map +0 -1
- package/dist/types/src/registry/function-registry.test.d.ts.map +0 -1
- package/dist/types/src/registry/index.d.ts.map +0 -1
- package/src/registry/function-registry.ts +0 -84
- /package/dist/types/src/{registry → function}/function-registry.test.d.ts +0 -0
- /package/dist/types/src/{registry → function}/index.d.ts +0 -0
- /package/src/{registry → function}/index.ts +0 -0
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { Event } from '@dxos/async';
|
|
6
6
|
import { type Client } from '@dxos/client';
|
|
7
|
-
import { create, Filter, type Space } from '@dxos/client/echo';
|
|
7
|
+
import { create, Filter, getMeta, type Space } from '@dxos/client/echo';
|
|
8
8
|
import { Context, Resource } from '@dxos/context';
|
|
9
|
+
import { compareForeignKeys, ECHO_ATTR_META, foreignKey } from '@dxos/echo-schema';
|
|
9
10
|
import { invariant } from '@dxos/invariant';
|
|
10
11
|
import { PublicKey } from '@dxos/keys';
|
|
11
12
|
import { log } from '@dxos/log';
|
|
12
|
-
import { ComplexMap } from '@dxos/util';
|
|
13
|
+
import { ComplexMap, diff } from '@dxos/util';
|
|
13
14
|
|
|
14
15
|
import { createSubscriptionTrigger, createTimerTrigger, createWebhookTrigger, createWebsocketTrigger } from './type';
|
|
15
16
|
import { type FunctionManifest, FunctionTrigger, type FunctionTriggerType, type TriggerSpec } from '../types';
|
|
@@ -18,12 +19,10 @@ type ResponseCode = number;
|
|
|
18
19
|
|
|
19
20
|
export type TriggerCallback = (args: object) => Promise<ResponseCode>;
|
|
20
21
|
|
|
21
|
-
export type TriggerContext = { space: Space };
|
|
22
|
-
|
|
23
22
|
// TODO(burdon): Make object?
|
|
24
23
|
export type TriggerFactory<Spec extends TriggerSpec, Options = any> = (
|
|
25
24
|
ctx: Context,
|
|
26
|
-
|
|
25
|
+
space: Space,
|
|
27
26
|
spec: Spec,
|
|
28
27
|
callback: TriggerCallback,
|
|
29
28
|
options?: Options,
|
|
@@ -69,19 +68,18 @@ export class TriggerRegistry extends Resource {
|
|
|
69
68
|
return this._getTriggers(space, (t) => t.activationCtx == null);
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
async activate(
|
|
73
|
-
log('activate', { space:
|
|
74
|
-
|
|
71
|
+
async activate(space: Space, trigger: FunctionTrigger, callback: TriggerCallback): Promise<void> {
|
|
72
|
+
log('activate', { space: space.key, trigger });
|
|
73
|
+
|
|
74
|
+
const activationCtx = new Context({ name: `FunctionTrigger-${trigger.function}` });
|
|
75
75
|
this._ctx.onDispose(() => activationCtx.dispose());
|
|
76
|
-
const registeredTrigger = this._triggersBySpaceKey
|
|
77
|
-
.get(triggerCtx.space.key)
|
|
78
|
-
?.find((reg) => reg.trigger.id === trigger.id);
|
|
76
|
+
const registeredTrigger = this._triggersBySpaceKey.get(space.key)?.find((reg) => reg.trigger.id === trigger.id);
|
|
79
77
|
invariant(registeredTrigger, `Trigger is not registered: ${trigger.function}`);
|
|
80
78
|
registeredTrigger.activationCtx = activationCtx;
|
|
81
79
|
|
|
82
80
|
try {
|
|
83
81
|
const options = this._options?.[trigger.spec.type];
|
|
84
|
-
await triggerHandlers[trigger.spec.type](activationCtx,
|
|
82
|
+
await triggerHandlers[trigger.spec.type](activationCtx, space, trigger.spec, callback, options);
|
|
85
83
|
} catch (err) {
|
|
86
84
|
delete registeredTrigger.activationCtx;
|
|
87
85
|
throw err;
|
|
@@ -96,17 +94,39 @@ export class TriggerRegistry extends Resource {
|
|
|
96
94
|
if (!manifest.triggers?.length) {
|
|
97
95
|
return;
|
|
98
96
|
}
|
|
99
|
-
|
|
97
|
+
|
|
98
|
+
if (!space.db.graph.runtimeSchemaRegistry.hasSchema(FunctionTrigger)) {
|
|
100
99
|
space.db.graph.runtimeSchemaRegistry.registerSchema(FunctionTrigger);
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
// Create FK to enable syncing if none are set (NOTE: Possible collision).
|
|
103
|
+
const manifestTriggers = manifest.triggers.map((trigger) => {
|
|
104
|
+
let keys = trigger[ECHO_ATTR_META]?.keys;
|
|
105
|
+
delete trigger[ECHO_ATTR_META];
|
|
106
|
+
if (!keys?.length) {
|
|
107
|
+
keys = [foreignKey('manifest', [trigger.function, trigger.spec.type].join(':'))];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return create(FunctionTrigger, trigger, { keys });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Sync triggers.
|
|
114
|
+
const { objects: existing } = await space.db.query(Filter.schema(FunctionTrigger)).run();
|
|
115
|
+
const { added } = diff(existing, manifestTriggers, compareForeignKeys);
|
|
116
|
+
|
|
117
|
+
// TODO(burdon): Update existing.
|
|
118
|
+
added.forEach((trigger) => {
|
|
119
|
+
space.db.add(trigger);
|
|
120
|
+
log.info('added', { meta: getMeta(trigger) });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (added.length > 0) {
|
|
124
|
+
await space.db.flush();
|
|
125
|
+
}
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
protected override async _open(): Promise<void> {
|
|
129
|
+
log.info('open...');
|
|
110
130
|
const spaceListSubscription = this._client.spaces.subscribe(async (spaces) => {
|
|
111
131
|
for (const space of spaces) {
|
|
112
132
|
if (this._triggersBySpaceKey.has(space.key)) {
|
|
@@ -119,44 +139,54 @@ export class TriggerRegistry extends Resource {
|
|
|
119
139
|
if (this._ctx.disposed) {
|
|
120
140
|
break;
|
|
121
141
|
}
|
|
122
|
-
const functionsSubscription = space.db.query(Filter.schema(FunctionTrigger)).subscribe(async (triggers) => {
|
|
123
|
-
await this._handleRemovedTriggers(space, triggers.objects, registered);
|
|
124
|
-
this._handleNewTriggers(space, triggers.objects, registered);
|
|
125
|
-
});
|
|
126
142
|
|
|
127
|
-
|
|
143
|
+
// Subscribe to updates.
|
|
144
|
+
this._ctx.onDispose(
|
|
145
|
+
space.db.query(Filter.schema(FunctionTrigger)).subscribe(async ({ objects: current }) => {
|
|
146
|
+
log.info('update', { space: space.key, registered: registered.length, current: current.length });
|
|
147
|
+
await this._handleRemovedTriggers(space, current, registered);
|
|
148
|
+
this._handleNewTriggers(space, current, registered);
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
128
151
|
}
|
|
129
152
|
});
|
|
130
153
|
|
|
131
154
|
this._ctx.onDispose(() => spaceListSubscription.unsubscribe());
|
|
155
|
+
log.info('opened');
|
|
132
156
|
}
|
|
133
157
|
|
|
134
158
|
protected override async _close(_: Context): Promise<void> {
|
|
159
|
+
log.info('close...');
|
|
135
160
|
this._triggersBySpaceKey.clear();
|
|
161
|
+
log.info('closed');
|
|
136
162
|
}
|
|
137
163
|
|
|
138
|
-
private _handleNewTriggers(space: Space,
|
|
139
|
-
const
|
|
140
|
-
return registered.find((reg) => reg.trigger.id === candidate.id) == null;
|
|
164
|
+
private _handleNewTriggers(space: Space, current: FunctionTrigger[], registered: RegisteredTrigger[]) {
|
|
165
|
+
const added = current.filter((candidate) => {
|
|
166
|
+
return candidate.enabled && registered.find((reg) => reg.trigger.id === candidate.id) == null;
|
|
141
167
|
});
|
|
142
168
|
|
|
143
|
-
if (
|
|
144
|
-
const newRegisteredTriggers: RegisteredTrigger[] =
|
|
169
|
+
if (added.length > 0) {
|
|
170
|
+
const newRegisteredTriggers: RegisteredTrigger[] = added.map((trigger) => ({ trigger }));
|
|
145
171
|
registered.push(...newRegisteredTriggers);
|
|
146
|
-
log('
|
|
147
|
-
|
|
172
|
+
log.info('added', () => ({
|
|
173
|
+
spaceKey: space.key,
|
|
174
|
+
triggers: added.map((trigger) => trigger.function),
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
this.registered.emit({ space, triggers: added });
|
|
148
178
|
}
|
|
149
179
|
}
|
|
150
180
|
|
|
151
181
|
private async _handleRemovedTriggers(
|
|
152
182
|
space: Space,
|
|
153
|
-
|
|
183
|
+
current: FunctionTrigger[],
|
|
154
184
|
registered: RegisteredTrigger[],
|
|
155
185
|
): Promise<void> {
|
|
156
186
|
const removed: FunctionTrigger[] = [];
|
|
157
187
|
for (let i = registered.length - 1; i >= 0; i--) {
|
|
158
188
|
const wasRemoved =
|
|
159
|
-
|
|
189
|
+
current.filter((trigger) => trigger.enabled).find((trigger) => trigger.id === registered[i].trigger.id) == null;
|
|
160
190
|
if (wasRemoved) {
|
|
161
191
|
const unregistered = registered.splice(i, 1)[0];
|
|
162
192
|
await unregistered.activationCtx?.dispose();
|
|
@@ -165,6 +195,11 @@ export class TriggerRegistry extends Resource {
|
|
|
165
195
|
}
|
|
166
196
|
|
|
167
197
|
if (removed.length > 0) {
|
|
198
|
+
log.info('removed', () => ({
|
|
199
|
+
spaceKey: space.key,
|
|
200
|
+
triggers: removed.map((trigger) => trigger.function),
|
|
201
|
+
}));
|
|
202
|
+
|
|
168
203
|
this.removed.emit({ space, triggers: removed });
|
|
169
204
|
}
|
|
170
205
|
}
|
|
@@ -3,41 +3,50 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { TextV0Type } from '@braneframe/types';
|
|
6
|
-
import { debounce,
|
|
6
|
+
import { debounce, UpdateScheduler } from '@dxos/async';
|
|
7
|
+
import { Filter, type Space } from '@dxos/client/echo';
|
|
7
8
|
import { type Context } from '@dxos/context';
|
|
8
|
-
import { createSubscription,
|
|
9
|
+
import { createSubscription, getAutomergeObjectCore, type Query } from '@dxos/echo-db';
|
|
9
10
|
import { log } from '@dxos/log';
|
|
10
11
|
|
|
11
12
|
import type { SubscriptionTrigger } from '../../types';
|
|
12
|
-
import { type TriggerCallback, type
|
|
13
|
+
import { type TriggerCallback, type TriggerFactory } from '../trigger-registry';
|
|
13
14
|
|
|
14
15
|
export const createSubscriptionTrigger: TriggerFactory<SubscriptionTrigger> = async (
|
|
15
16
|
ctx: Context,
|
|
16
|
-
|
|
17
|
+
space: Space,
|
|
17
18
|
spec: SubscriptionTrigger,
|
|
18
19
|
callback: TriggerCallback,
|
|
19
20
|
) => {
|
|
20
21
|
const objectIds = new Set<string>();
|
|
21
|
-
const task = new
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
objectIds.
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const task = new UpdateScheduler(
|
|
23
|
+
ctx,
|
|
24
|
+
async () => {
|
|
25
|
+
if (objectIds.size > 0) {
|
|
26
|
+
const objects = Array.from(objectIds);
|
|
27
|
+
objectIds.clear();
|
|
28
|
+
await callback({ objects });
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{ maxFrequency: 4 },
|
|
32
|
+
);
|
|
27
33
|
|
|
34
|
+
// TODO(burdon): Factor out diff.
|
|
28
35
|
// TODO(burdon): Don't fire initially?
|
|
29
36
|
// TODO(burdon): Create queue. Only allow one invocation per trigger at a time?
|
|
30
37
|
const subscriptions: (() => void)[] = [];
|
|
31
38
|
const subscription = createSubscription(({ added, updated }) => {
|
|
32
|
-
|
|
39
|
+
const sizeBefore = objectIds.size;
|
|
33
40
|
for (const object of added) {
|
|
34
41
|
objectIds.add(object.id);
|
|
35
42
|
}
|
|
36
43
|
for (const object of updated) {
|
|
37
44
|
objectIds.add(object.id);
|
|
38
45
|
}
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
if (objectIds.size > sizeBefore) {
|
|
47
|
+
log.info('updated', { added: added.length, updated: updated.length });
|
|
48
|
+
task.trigger();
|
|
49
|
+
}
|
|
41
50
|
});
|
|
42
51
|
|
|
43
52
|
subscriptions.push(() => subscription.unsubscribe());
|
|
@@ -45,11 +54,11 @@ export const createSubscriptionTrigger: TriggerFactory<SubscriptionTrigger> = as
|
|
|
45
54
|
// TODO(burdon): Disable trigger if keeps failing.
|
|
46
55
|
const { filter, options: { deep, delay } = {} } = spec;
|
|
47
56
|
const update = ({ objects }: Query) => {
|
|
57
|
+
log.info('update', { objects: objects.length });
|
|
48
58
|
subscription.update(objects);
|
|
49
59
|
|
|
50
60
|
// TODO(burdon): Hack to monitor changes to Document's text object.
|
|
51
61
|
if (deep) {
|
|
52
|
-
log.info('update', { objects: objects.length });
|
|
53
62
|
for (const object of objects) {
|
|
54
63
|
const content = object.content;
|
|
55
64
|
if (content instanceof TextV0Type) {
|
|
@@ -61,11 +70,15 @@ export const createSubscriptionTrigger: TriggerFactory<SubscriptionTrigger> = as
|
|
|
61
70
|
}
|
|
62
71
|
};
|
|
63
72
|
|
|
64
|
-
// TODO(burdon):
|
|
73
|
+
// TODO(burdon): OR not working.
|
|
65
74
|
// TODO(burdon): [Bug]: all callbacks are fired on the first mutation.
|
|
66
75
|
// TODO(burdon): [Bug]: not updated when document is deleted (either top or hierarchically).
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
log.info('subscription', { filter });
|
|
77
|
+
// const query = triggerCtx.space.db.query(Filter.or(filter.map(({ type, props }) => Filter.typename(type, props))));
|
|
78
|
+
if (filter) {
|
|
79
|
+
const query = space.db.query(Filter.typename(filter[0].type, filter[0].props));
|
|
80
|
+
subscriptions.push(query.subscribe(delay ? debounce(update, delay) : update));
|
|
81
|
+
}
|
|
69
82
|
|
|
70
83
|
ctx.onDispose(() => {
|
|
71
84
|
subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
import { CronJob } from 'cron';
|
|
6
6
|
|
|
7
7
|
import { DeferredTask } from '@dxos/async';
|
|
8
|
+
import { type Space } from '@dxos/client/echo';
|
|
8
9
|
import { type Context } from '@dxos/context';
|
|
9
10
|
import { log } from '@dxos/log';
|
|
10
11
|
|
|
11
12
|
import type { TimerTrigger } from '../../types';
|
|
12
|
-
import { type TriggerCallback, type
|
|
13
|
+
import { type TriggerCallback, type TriggerFactory } from '../trigger-registry';
|
|
13
14
|
|
|
14
15
|
export const createTimerTrigger: TriggerFactory<TimerTrigger> = async (
|
|
15
16
|
ctx: Context,
|
|
16
|
-
|
|
17
|
+
space: Space,
|
|
17
18
|
spec: TimerTrigger,
|
|
18
19
|
callback: TriggerCallback,
|
|
19
20
|
) => {
|
|
@@ -34,7 +35,7 @@ export const createTimerTrigger: TriggerFactory<TimerTrigger> = async (
|
|
|
34
35
|
last = now;
|
|
35
36
|
|
|
36
37
|
run++;
|
|
37
|
-
log.info('tick', { space:
|
|
38
|
+
log.info('tick', { space: space.key.truncate(), count: run, delta });
|
|
38
39
|
task.schedule();
|
|
39
40
|
},
|
|
40
41
|
});
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
import { getPort } from 'get-port-please';
|
|
6
6
|
import http from 'node:http';
|
|
7
7
|
|
|
8
|
+
import { type Space } from '@dxos/client/echo';
|
|
8
9
|
import { type Context } from '@dxos/context';
|
|
9
10
|
import { log } from '@dxos/log';
|
|
10
11
|
|
|
11
12
|
import type { WebhookTrigger } from '../../types';
|
|
12
|
-
import { type TriggerCallback, type
|
|
13
|
+
import { type TriggerCallback, type TriggerFactory } from '../trigger-registry';
|
|
13
14
|
|
|
14
15
|
export const createWebhookTrigger: TriggerFactory<WebhookTrigger> = async (
|
|
15
16
|
ctx: Context,
|
|
16
|
-
|
|
17
|
+
space: Space,
|
|
17
18
|
spec: WebhookTrigger,
|
|
18
19
|
callback: TriggerCallback,
|
|
19
20
|
) => {
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
import WebSocket from 'ws';
|
|
6
6
|
|
|
7
7
|
import { sleep, Trigger } from '@dxos/async';
|
|
8
|
+
import { type Space } from '@dxos/client/echo';
|
|
8
9
|
import { type Context } from '@dxos/context';
|
|
9
10
|
import { log } from '@dxos/log';
|
|
10
11
|
|
|
11
12
|
import { type WebsocketTrigger } from '../../types';
|
|
12
|
-
import { type TriggerCallback, type
|
|
13
|
+
import { type TriggerCallback, type TriggerFactory } from '../trigger-registry';
|
|
13
14
|
|
|
14
15
|
interface WebsocketTriggerOptions {
|
|
15
16
|
retryDelay: number;
|
|
@@ -22,7 +23,7 @@ interface WebsocketTriggerOptions {
|
|
|
22
23
|
*/
|
|
23
24
|
export const createWebsocketTrigger: TriggerFactory<WebsocketTrigger, WebsocketTriggerOptions> = async (
|
|
24
25
|
ctx: Context,
|
|
25
|
-
|
|
26
|
+
space: Space,
|
|
26
27
|
spec: WebsocketTrigger,
|
|
27
28
|
callback: TriggerCallback,
|
|
28
29
|
options: WebsocketTriggerOptions = { retryDelay: 2, maxAttempts: 5 },
|
|
@@ -51,7 +52,7 @@ export const createWebsocketTrigger: TriggerFactory<WebsocketTrigger, WebsocketT
|
|
|
51
52
|
if (event.code === 1006) {
|
|
52
53
|
setTimeout(async () => {
|
|
53
54
|
log.info(`reconnecting in ${options.retryDelay}s...`, { url });
|
|
54
|
-
await createWebsocketTrigger(ctx,
|
|
55
|
+
await createWebsocketTrigger(ctx, space, spec, callback, options);
|
|
55
56
|
}, options.retryDelay * 1_000);
|
|
56
57
|
}
|
|
57
58
|
|
package/src/types.ts
CHANGED
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
// TODO(burdon): Factor out.
|
|
8
|
-
const omitEchoId = <T>(schema: S.Schema<T>): S.Schema<Omit<T, 'id'>> => S.make(AST.omit(schema.ast, ['id']));
|
|
5
|
+
import { RawObject, S, TypedObject } from '@dxos/echo-schema';
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
8
|
* Type discriminator for TriggerSpec.
|
|
@@ -15,30 +12,36 @@ const omitEchoId = <T>(schema: S.Schema<T>): S.Schema<Omit<T, 'id'>> => S.make(A
|
|
|
15
12
|
*/
|
|
16
13
|
export type FunctionTriggerType = 'subscription' | 'timer' | 'webhook' | 'websocket';
|
|
17
14
|
|
|
18
|
-
const SubscriptionTriggerSchema = S.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
S.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
S.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
15
|
+
const SubscriptionTriggerSchema = S.mutable(
|
|
16
|
+
S.struct({
|
|
17
|
+
type: S.literal('subscription'),
|
|
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)),
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
options: S.optional(
|
|
26
|
+
S.struct({
|
|
27
|
+
// Watch changes to object (not just creation).
|
|
28
|
+
deep: S.optional(S.boolean),
|
|
29
|
+
// Debounce changes (delay in ms).
|
|
30
|
+
delay: S.optional(S.number),
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
36
|
export type SubscriptionTrigger = S.Schema.Type<typeof SubscriptionTriggerSchema>;
|
|
37
37
|
|
|
38
|
-
const TimerTriggerSchema = S.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const TimerTriggerSchema = S.mutable(
|
|
39
|
+
S.struct({
|
|
40
|
+
type: S.literal('timer'),
|
|
41
|
+
cron: S.string,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
42
45
|
export type TimerTrigger = S.Schema.Type<typeof TimerTriggerSchema>;
|
|
43
46
|
|
|
44
47
|
const WebhookTriggerSchema = S.mutable(
|
|
@@ -49,13 +52,17 @@ const WebhookTriggerSchema = S.mutable(
|
|
|
49
52
|
port: S.optional(S.number),
|
|
50
53
|
}),
|
|
51
54
|
);
|
|
55
|
+
|
|
52
56
|
export type WebhookTrigger = S.Schema.Type<typeof WebhookTriggerSchema>;
|
|
53
57
|
|
|
54
|
-
const WebsocketTriggerSchema = S.
|
|
55
|
-
|
|
56
|
-
|
|
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)),
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
59
66
|
export type WebsocketTrigger = S.Schema.Type<typeof WebsocketTriggerSchema>;
|
|
60
67
|
|
|
61
68
|
const TriggerSpecSchema = S.union(
|
|
@@ -64,6 +71,7 @@ const TriggerSpecSchema = S.union(
|
|
|
64
71
|
WebsocketTriggerSchema,
|
|
65
72
|
SubscriptionTriggerSchema,
|
|
66
73
|
);
|
|
74
|
+
|
|
67
75
|
export type TriggerSpec = TimerTrigger | WebhookTrigger | WebsocketTrigger | SubscriptionTrigger;
|
|
68
76
|
|
|
69
77
|
/**
|
|
@@ -76,17 +84,20 @@ export class FunctionDef extends TypedObject({
|
|
|
76
84
|
uri: S.string,
|
|
77
85
|
description: S.optional(S.string),
|
|
78
86
|
route: S.string,
|
|
79
|
-
// TODO(burdon): NPM/GitHub/Docker/CF URL?
|
|
80
87
|
handler: S.string,
|
|
81
88
|
}) {}
|
|
82
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Function trigger.
|
|
92
|
+
*/
|
|
83
93
|
export class FunctionTrigger extends TypedObject({
|
|
84
94
|
typename: 'dxos.org/type/FunctionTrigger',
|
|
85
95
|
version: '0.1.0',
|
|
86
96
|
})({
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
meta
|
|
97
|
+
enabled: S.optional(S.boolean),
|
|
98
|
+
function: S.string.pipe(S.description('Function URI.')),
|
|
99
|
+
// The `meta` property is merged into the event data passed to the function.
|
|
100
|
+
meta: S.optional(S.mutable(S.any)),
|
|
90
101
|
spec: TriggerSpecSchema,
|
|
91
102
|
}) {}
|
|
92
103
|
|
|
@@ -94,8 +105,11 @@ export class FunctionTrigger extends TypedObject({
|
|
|
94
105
|
* Function manifest file.
|
|
95
106
|
*/
|
|
96
107
|
export const FunctionManifestSchema = S.struct({
|
|
97
|
-
functions: S.optional(S.mutable(S.array(
|
|
98
|
-
triggers: S.optional(S.mutable(S.array(
|
|
108
|
+
functions: S.optional(S.mutable(S.array(RawObject(FunctionDef)))),
|
|
109
|
+
triggers: S.optional(S.mutable(S.array(RawObject(FunctionTrigger)))),
|
|
99
110
|
});
|
|
100
111
|
|
|
101
112
|
export type FunctionManifest = S.Schema.Type<typeof FunctionManifestSchema>;
|
|
113
|
+
|
|
114
|
+
// TODO(burdon): Standards?
|
|
115
|
+
export const FUNCTION_SCHEMA = [FunctionDef, FunctionTrigger];
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"function-registry.d.ts","sourceRoot":"","sources":["../../../../src/registry/function-registry.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAkB,KAAK,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,KAAK,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAIvD,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE9D,MAAM,MAAM,wBAAwB,GAAG;IACrC,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,QAAQ;IAKhC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA4D;IAEhG,SAAgB,qBAAqB,kCAAyC;gBAEjD,OAAO,EAAE,MAAM;IAIrC,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW,EAAE;IAIhD;;;OAGG;IAEU,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;cAcrD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;cA0BtB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAG3D"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"function-registry.test.d.ts","sourceRoot":"","sources":["../../../../src/registry/function-registry.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/registry/index.ts"],"names":[],"mappings":"AAIA,cAAc,qBAAqB,CAAC"}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { Event } from '@dxos/async';
|
|
6
|
-
import { type Client } from '@dxos/client';
|
|
7
|
-
import { create, Filter, type Space } from '@dxos/client/echo';
|
|
8
|
-
import { type Context, Resource } from '@dxos/context';
|
|
9
|
-
import { PublicKey } from '@dxos/keys';
|
|
10
|
-
import { ComplexMap } from '@dxos/util';
|
|
11
|
-
|
|
12
|
-
import { FunctionDef, type FunctionManifest } from '../types';
|
|
13
|
-
|
|
14
|
-
export type FunctionsRegisteredEvent = {
|
|
15
|
-
space: Space;
|
|
16
|
-
newFunctions: FunctionDef[];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export class FunctionRegistry extends Resource {
|
|
20
|
-
private readonly _functionBySpaceKey = new ComplexMap<PublicKey, FunctionDef[]>(PublicKey.hash);
|
|
21
|
-
|
|
22
|
-
public readonly onFunctionsRegistered = new Event<FunctionsRegisteredEvent>();
|
|
23
|
-
|
|
24
|
-
constructor(private readonly _client: Client) {
|
|
25
|
-
super();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
public getFunctions(space: Space): FunctionDef[] {
|
|
29
|
-
return this._functionBySpaceKey.get(space.key) ?? [];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The method loads function definitions from the manifest into the space.
|
|
34
|
-
* We first load all the definitions from the space to deduplicate by functionId.
|
|
35
|
-
*/
|
|
36
|
-
// TODO(burdon): This should not be space specific (they are static for the agent).
|
|
37
|
-
public async register(space: Space, manifest: FunctionManifest): Promise<void> {
|
|
38
|
-
if (!manifest.functions?.length) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (!space.db.graph.runtimeSchemaRegistry.isSchemaRegistered(FunctionDef)) {
|
|
42
|
-
space.db.graph.runtimeSchemaRegistry.registerSchema(FunctionDef);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const { objects: existingDefinitions } = await space.db.query(Filter.schema(FunctionDef)).run();
|
|
46
|
-
const newDefinitions = getNewDefinitions(manifest.functions, existingDefinitions);
|
|
47
|
-
const reactiveObjects = newDefinitions.map((template) => create(FunctionDef, { ...template }));
|
|
48
|
-
reactiveObjects.forEach((obj) => space.db.add(obj));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
protected override async _open(): Promise<void> {
|
|
52
|
-
const spaceListSubscription = this._client.spaces.subscribe(async (spaces) => {
|
|
53
|
-
for (const space of spaces) {
|
|
54
|
-
if (this._functionBySpaceKey.has(space.key)) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const registered: FunctionDef[] = [];
|
|
58
|
-
this._functionBySpaceKey.set(space.key, registered);
|
|
59
|
-
await space.waitUntilReady();
|
|
60
|
-
if (this._ctx.disposed) {
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const functionsSubscription = space.db.query(Filter.schema(FunctionDef)).subscribe((definitions) => {
|
|
65
|
-
const newFunctions = getNewDefinitions(definitions.objects, registered);
|
|
66
|
-
if (newFunctions.length > 0) {
|
|
67
|
-
registered.push(...newFunctions);
|
|
68
|
-
this.onFunctionsRegistered.emit({ space, newFunctions });
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
this._ctx.onDispose(functionsSubscription);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
this._ctx.onDispose(() => spaceListSubscription.unsubscribe());
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
protected override async _close(_: Context): Promise<void> {
|
|
78
|
-
this._functionBySpaceKey.clear();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const getNewDefinitions = <T extends { uri: string }>(candidateList: T[], existing: FunctionDef[]): T[] => {
|
|
83
|
-
return candidateList.filter((candidate) => existing.find((def) => def.uri === candidate.uri) == null);
|
|
84
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|