@dxos/functions 0.5.3-main.d7fe7b5 → 0.5.3-main.eb56347
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-P3HPDHNI.mjs +86 -0
- package/dist/lib/browser/chunk-P3HPDHNI.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +327 -310
- 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-KTLM3JNV.cjs +103 -0
- package/dist/lib/node/chunk-KTLM3JNV.cjs.map +7 -0
- package/dist/lib/node/index.cjs +332 -309
- 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/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/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/types.d.ts +64 -49
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +31 -18
- package/schema/functions.json +18 -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 +5 -6
- package/src/runtime/scheduler.test.ts +1 -1
- package/src/runtime/scheduler.ts +21 -8
- package/src/testing/functions-integration.test.ts +1 -1
- package/src/testing/setup.ts +1 -1
- package/src/trigger/trigger-registry.test.ts +60 -34
- package/src/trigger/trigger-registry.ts +38 -13
- package/src/trigger/type/subscription-trigger.ts +17 -10
- package/src/types.ts +47 -36
- 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
|
@@ -13,7 +13,7 @@ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
|
13
13
|
import { describe, test } from '@dxos/test';
|
|
14
14
|
|
|
15
15
|
import { setTestCallHandler } from './test/handler';
|
|
16
|
-
import { FunctionRegistry } from '../
|
|
16
|
+
import { FunctionRegistry } from '../function';
|
|
17
17
|
import { DevServer, Scheduler } from '../runtime';
|
|
18
18
|
import { createFunctionRuntime, createInitializedClients, TestType } from '../testing';
|
|
19
19
|
import { TriggerRegistry } from '../trigger';
|
package/src/testing/setup.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { range } from '@dxos/util';
|
|
|
10
10
|
import { TestType } from './types';
|
|
11
11
|
import { FunctionDef, FunctionTrigger } from '../types';
|
|
12
12
|
|
|
13
|
-
// TODO(burdon): Create TestBuilder.
|
|
13
|
+
// TODO(burdon): Create new or extend existing TestBuilder.
|
|
14
14
|
|
|
15
15
|
export const createInitializedClients = async (testBuilder: TestBuilder, count: number = 1, config?: Config) => {
|
|
16
16
|
const clients = range(count).map(() => new Client({ config, services: testBuilder.createLocalClientServices() }));
|
|
@@ -11,7 +11,7 @@ import { type Space } from '@dxos/client/echo';
|
|
|
11
11
|
import { TestBuilder } from '@dxos/client/testing';
|
|
12
12
|
import { Context } from '@dxos/context';
|
|
13
13
|
import { Filter } from '@dxos/echo-db';
|
|
14
|
-
import { create } from '@dxos/echo-schema';
|
|
14
|
+
import { create, splitMeta } from '@dxos/echo-schema';
|
|
15
15
|
import { describe, test } from '@dxos/test';
|
|
16
16
|
import { range } from '@dxos/util';
|
|
17
17
|
|
|
@@ -19,9 +19,17 @@ import { TriggerRegistry } from './trigger-registry';
|
|
|
19
19
|
import { createInitializedClients, TestType, triggerWebhook } from '../testing';
|
|
20
20
|
import { type FunctionManifest, FunctionTrigger } from '../types';
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const manifest: FunctionManifest = {
|
|
23
23
|
triggers: [
|
|
24
24
|
{
|
|
25
|
+
'@meta': {
|
|
26
|
+
keys: [
|
|
27
|
+
{
|
|
28
|
+
source: 'example.com',
|
|
29
|
+
id: 'trigger-1',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
25
33
|
function: 'example.com/function/webhook-test',
|
|
26
34
|
spec: {
|
|
27
35
|
type: 'webhook',
|
|
@@ -29,10 +37,22 @@ const testManifest: FunctionManifest = {
|
|
|
29
37
|
},
|
|
30
38
|
},
|
|
31
39
|
{
|
|
40
|
+
'@meta': {
|
|
41
|
+
keys: [
|
|
42
|
+
{
|
|
43
|
+
source: 'example.com',
|
|
44
|
+
id: 'trigger-2',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
32
48
|
function: 'example.com/function/subscription-test',
|
|
33
49
|
spec: {
|
|
34
50
|
type: 'subscription',
|
|
35
|
-
filter: [
|
|
51
|
+
filter: [
|
|
52
|
+
{
|
|
53
|
+
type: TestType.typename,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
36
56
|
},
|
|
37
57
|
},
|
|
38
58
|
],
|
|
@@ -57,11 +77,12 @@ describe('trigger registry', () => {
|
|
|
57
77
|
const client = (await createInitializedClients(testBuilder))[0];
|
|
58
78
|
const registry = createRegistry(client);
|
|
59
79
|
const space = await client.spaces.create();
|
|
60
|
-
await registry.register(space,
|
|
80
|
+
await registry.register(space, manifest);
|
|
61
81
|
const { objects } = await space.db.query(Filter.schema(FunctionTrigger)).run();
|
|
62
|
-
expect(objects.length).to.eq(
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
expect(objects.length).to.eq(manifest.triggers?.length);
|
|
83
|
+
|
|
84
|
+
const expected = manifest.triggers?.map((trigger) => trigger.function).sort();
|
|
85
|
+
expect(objects.map((object: FunctionTrigger) => object.function).sort()).to.deep.eq(expected);
|
|
65
86
|
});
|
|
66
87
|
});
|
|
67
88
|
|
|
@@ -70,13 +91,13 @@ describe('trigger registry', () => {
|
|
|
70
91
|
const client = (await createInitializedClients(testBuilder))[0];
|
|
71
92
|
const space = await client.spaces.create();
|
|
72
93
|
const registry = createRegistry(client);
|
|
73
|
-
await registry.register(space,
|
|
94
|
+
await registry.register(space, manifest);
|
|
74
95
|
await registry.open(ctx);
|
|
75
|
-
await
|
|
96
|
+
await waitForInactiveTriggers(registry, space);
|
|
76
97
|
|
|
77
98
|
const callbackInvoked = new Trigger();
|
|
78
99
|
const { objects: allTriggers } = await space.db.query(Filter.schema(FunctionTrigger)).run();
|
|
79
|
-
const webhookTrigger = allTriggers.find((
|
|
100
|
+
const webhookTrigger = allTriggers.find((trigger: FunctionTrigger) => trigger.spec.type === 'webhook')!;
|
|
80
101
|
await registry.activate({ space }, webhookTrigger, async () => {
|
|
81
102
|
callbackInvoked.wake();
|
|
82
103
|
return 200;
|
|
@@ -90,15 +111,16 @@ describe('trigger registry', () => {
|
|
|
90
111
|
const client = (await createInitializedClients(testBuilder))[0];
|
|
91
112
|
const space = await client.spaces.create();
|
|
92
113
|
const registry = createRegistry(client);
|
|
93
|
-
await registry.register(space,
|
|
114
|
+
await registry.register(space, manifest);
|
|
94
115
|
await registry.open(ctx);
|
|
95
|
-
await
|
|
116
|
+
await waitForInactiveTriggers(registry, space);
|
|
96
117
|
|
|
97
118
|
const inactiveTrigger = registry.getInactiveTriggers(space)[0];
|
|
98
119
|
await registry.activate({ space }, inactiveTrigger, async () => 200);
|
|
99
120
|
|
|
100
121
|
const updatedInactiveList = registry.getInactiveTriggers(space);
|
|
101
|
-
expect(updatedInactiveList.find((
|
|
122
|
+
expect(updatedInactiveList.find((trigger: FunctionTrigger) => trigger.function === inactiveTrigger.function)).to
|
|
123
|
+
.be.undefined;
|
|
102
124
|
});
|
|
103
125
|
});
|
|
104
126
|
|
|
@@ -107,12 +129,12 @@ describe('trigger registry', () => {
|
|
|
107
129
|
const client = (await createInitializedClients(testBuilder))[0];
|
|
108
130
|
const space = await client.spaces.create();
|
|
109
131
|
const registry = createRegistry(client);
|
|
110
|
-
await registry.register(space,
|
|
132
|
+
await registry.register(space, manifest);
|
|
111
133
|
await registry.open(ctx);
|
|
112
|
-
await
|
|
134
|
+
await waitForInactiveTriggers(registry, space);
|
|
113
135
|
|
|
114
136
|
const { objects: allTriggers } = await space.db.query(Filter.schema(FunctionTrigger)).run();
|
|
115
|
-
const echoTrigger = allTriggers.find((
|
|
137
|
+
const echoTrigger = allTriggers.find((trigger: FunctionTrigger) => trigger.spec.type === 'subscription')!;
|
|
116
138
|
let count = 0;
|
|
117
139
|
await registry.activate({ space }, echoTrigger, async () => {
|
|
118
140
|
count++;
|
|
@@ -124,7 +146,6 @@ describe('trigger registry', () => {
|
|
|
124
146
|
expect(count).to.eq(1);
|
|
125
147
|
|
|
126
148
|
space.db.remove(echoTrigger);
|
|
127
|
-
|
|
128
149
|
space.db.add(create(TestType, { title: '2' }));
|
|
129
150
|
await sleep(20);
|
|
130
151
|
expect(count).to.eq(1);
|
|
@@ -134,12 +155,12 @@ describe('trigger registry', () => {
|
|
|
134
155
|
const client = (await createInitializedClients(testBuilder))[0];
|
|
135
156
|
const space = await client.spaces.create();
|
|
136
157
|
const registry = createRegistry(client);
|
|
137
|
-
await registry.register(space,
|
|
158
|
+
await registry.register(space, manifest);
|
|
138
159
|
await registry.open(ctx);
|
|
139
|
-
await
|
|
160
|
+
await waitForInactiveTriggers(registry, space);
|
|
140
161
|
|
|
141
162
|
const { objects: allTriggers } = await space.db.query(Filter.schema(FunctionTrigger)).run();
|
|
142
|
-
const echoTrigger = allTriggers.find((
|
|
163
|
+
const echoTrigger = allTriggers.find((trigger: FunctionTrigger) => trigger.spec.type === 'subscription')!;
|
|
143
164
|
let count = 0;
|
|
144
165
|
await registry.activate({ space }, echoTrigger, async () => {
|
|
145
166
|
count++;
|
|
@@ -155,19 +176,20 @@ describe('trigger registry', () => {
|
|
|
155
176
|
});
|
|
156
177
|
|
|
157
178
|
describe('trigger events', () => {
|
|
158
|
-
test('event fired when all registered when opened', async () => {
|
|
179
|
+
test.only('event fired when all registered when opened', async () => {
|
|
159
180
|
const client = (await createInitializedClients(testBuilder))[0];
|
|
160
181
|
const registry = createRegistry(client);
|
|
161
|
-
const triggers =
|
|
182
|
+
const triggers = createTriggers(client.spaces.default, 3);
|
|
162
183
|
|
|
163
184
|
const triggersRegistered = new Trigger<FunctionTrigger[]>();
|
|
164
185
|
registry.registered.on((fn) => {
|
|
165
186
|
expect(fn.space.key.toHex()).to.eq(client.spaces.default.key.toHex());
|
|
166
187
|
triggersRegistered.wake(fn.triggers);
|
|
167
188
|
});
|
|
189
|
+
|
|
168
190
|
void registry.open(ctx);
|
|
169
191
|
const functions = await triggersRegistered.wait();
|
|
170
|
-
const expected = triggers.map((
|
|
192
|
+
const expected = triggers.map((object) => object.id).sort();
|
|
171
193
|
expect(functions.map((fn) => fn.id).sort()).to.deep.eq(expected);
|
|
172
194
|
});
|
|
173
195
|
|
|
@@ -182,16 +204,16 @@ describe('trigger registry', () => {
|
|
|
182
204
|
triggerRegistered.wake(fn.triggers[0]);
|
|
183
205
|
});
|
|
184
206
|
await registry.open(ctx);
|
|
185
|
-
await registry.register(space, { triggers:
|
|
207
|
+
await registry.register(space, { triggers: manifest?.triggers?.slice(0, 1) });
|
|
186
208
|
const registered = await triggerRegistered.wait();
|
|
187
|
-
expect(registered.function).to.eq(
|
|
209
|
+
expect(registered.function).to.eq(manifest.triggers![0].function);
|
|
188
210
|
});
|
|
189
211
|
|
|
190
212
|
test('event fired when a new trigger is removed', async () => {
|
|
191
213
|
const client = (await createInitializedClients(testBuilder))[0];
|
|
192
214
|
const registry = createRegistry(client);
|
|
193
215
|
const space = await client.spaces.create();
|
|
194
|
-
const triggers =
|
|
216
|
+
const triggers = createTriggers(space, 3);
|
|
195
217
|
|
|
196
218
|
const triggerLoaded = new Trigger();
|
|
197
219
|
registry.registered.on((fn) => triggerLoaded.wake());
|
|
@@ -201,7 +223,7 @@ describe('trigger registry', () => {
|
|
|
201
223
|
expect(fn.triggers.length).to.eq(1);
|
|
202
224
|
triggerRemoved.wake(fn.triggers[0]);
|
|
203
225
|
});
|
|
204
|
-
await registry.register(space,
|
|
226
|
+
await registry.register(space, manifest);
|
|
205
227
|
await registry.open(ctx);
|
|
206
228
|
await triggerLoaded.wait();
|
|
207
229
|
|
|
@@ -211,19 +233,23 @@ describe('trigger registry', () => {
|
|
|
211
233
|
});
|
|
212
234
|
});
|
|
213
235
|
|
|
214
|
-
const waitHasInactiveTriggers = async (registry: TriggerRegistry, space: Space) => {
|
|
215
|
-
await waitForCondition({ condition: () => registry.getInactiveTriggers(space).length > 0 });
|
|
216
|
-
};
|
|
217
|
-
|
|
218
236
|
const createRegistry = (client: Client) => {
|
|
219
237
|
const registry = new TriggerRegistry(client);
|
|
220
238
|
ctx.onDispose(() => registry.close());
|
|
221
239
|
return registry;
|
|
222
240
|
};
|
|
223
241
|
|
|
224
|
-
const
|
|
225
|
-
const triggers = range(count, () =>
|
|
226
|
-
|
|
242
|
+
const createTriggers = (space: Space, count: number) => {
|
|
243
|
+
const triggers = range(count, () => {
|
|
244
|
+
const { meta, object } = splitMeta(manifest.triggers![0]);
|
|
245
|
+
return create(FunctionTrigger, object, meta);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
triggers.forEach((trigger) => space.db.add(trigger));
|
|
227
249
|
return triggers;
|
|
228
250
|
};
|
|
251
|
+
|
|
252
|
+
const waitForInactiveTriggers = async (registry: TriggerRegistry, space: Space) => {
|
|
253
|
+
await waitForCondition({ condition: () => registry.getInactiveTriggers(space).length > 0 });
|
|
254
|
+
};
|
|
229
255
|
});
|
|
@@ -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 { ECHO_ATTR_META, foreignKey, foreignKeyEquals, splitMeta } 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, intersection } 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';
|
|
@@ -96,17 +97,28 @@ export class TriggerRegistry extends Resource {
|
|
|
96
97
|
if (!manifest.triggers?.length) {
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
|
-
if (!space.db.graph.runtimeSchemaRegistry.
|
|
100
|
+
if (!space.db.graph.runtimeSchemaRegistry.hasSchema(FunctionTrigger)) {
|
|
100
101
|
space.db.graph.runtimeSchemaRegistry.registerSchema(FunctionTrigger);
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
|
|
104
|
+
// Sync triggers.
|
|
105
|
+
const { objects: existing } = await space.db.query(Filter.schema(FunctionTrigger)).run();
|
|
106
|
+
const { added } = diff(existing, manifest.triggers, (a, b) => {
|
|
107
|
+
// Create FK to enable syncing if none are set.
|
|
108
|
+
// TODO(burdon): Warn if not unique.
|
|
109
|
+
const keys = b[ECHO_ATTR_META]?.keys ?? [foreignKey('manifest', [b.function, b.spec.type].join('-'))];
|
|
110
|
+
return intersection(getMeta(a)?.keys ?? [], keys, foreignKeyEquals).length > 0;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// TODO(burdon): Update existing.
|
|
114
|
+
added.forEach((trigger) => {
|
|
115
|
+
const { meta, object } = splitMeta(trigger);
|
|
116
|
+
space.db.add(create(FunctionTrigger, object, meta));
|
|
117
|
+
});
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
protected override async _open(): Promise<void> {
|
|
121
|
+
log.info('open...');
|
|
110
122
|
const spaceListSubscription = this._client.spaces.subscribe(async (spaces) => {
|
|
111
123
|
for (const space of spaces) {
|
|
112
124
|
if (this._triggersBySpaceKey.has(space.key)) {
|
|
@@ -119,12 +131,15 @@ export class TriggerRegistry extends Resource {
|
|
|
119
131
|
if (this._ctx.disposed) {
|
|
120
132
|
break;
|
|
121
133
|
}
|
|
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
134
|
|
|
127
|
-
|
|
135
|
+
// Subscribe to updates.
|
|
136
|
+
this._ctx.onDispose(
|
|
137
|
+
space.db.query(Filter.schema(FunctionTrigger)).subscribe(async (triggers) => {
|
|
138
|
+
log.info('update', { space: space.key, triggers: triggers.objects.length });
|
|
139
|
+
await this._handleRemovedTriggers(space, triggers.objects, registered);
|
|
140
|
+
this._handleNewTriggers(space, triggers.objects, registered);
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
128
143
|
}
|
|
129
144
|
});
|
|
130
145
|
|
|
@@ -132,6 +147,7 @@ export class TriggerRegistry extends Resource {
|
|
|
132
147
|
}
|
|
133
148
|
|
|
134
149
|
protected override async _close(_: Context): Promise<void> {
|
|
150
|
+
log.info('close...');
|
|
135
151
|
this._triggersBySpaceKey.clear();
|
|
136
152
|
}
|
|
137
153
|
|
|
@@ -143,7 +159,10 @@ export class TriggerRegistry extends Resource {
|
|
|
143
159
|
if (newTriggers.length > 0) {
|
|
144
160
|
const newRegisteredTriggers: RegisteredTrigger[] = newTriggers.map((trigger) => ({ trigger }));
|
|
145
161
|
registered.push(...newRegisteredTriggers);
|
|
146
|
-
log('
|
|
162
|
+
log.info('added', () => ({
|
|
163
|
+
spaceKey: space.key,
|
|
164
|
+
triggers: newTriggers.map((trigger) => trigger.function),
|
|
165
|
+
}));
|
|
147
166
|
this.registered.emit({ space, triggers: newTriggers });
|
|
148
167
|
}
|
|
149
168
|
}
|
|
@@ -158,6 +177,12 @@ export class TriggerRegistry extends Resource {
|
|
|
158
177
|
const wasRemoved =
|
|
159
178
|
allTriggers.find((trigger: FunctionTrigger) => trigger.id === registered[i].trigger.id) == null;
|
|
160
179
|
if (wasRemoved) {
|
|
180
|
+
if (removed.length) {
|
|
181
|
+
log.info('removed', () => ({
|
|
182
|
+
spaceKey: space.key,
|
|
183
|
+
triggers: removed.map((trigger) => trigger.function),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
161
186
|
const unregistered = registered.splice(i, 1)[0];
|
|
162
187
|
await unregistered.activationCtx?.dispose();
|
|
163
188
|
removed.push(unregistered.trigger);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { TextV0Type } from '@braneframe/types';
|
|
6
|
-
import { debounce,
|
|
6
|
+
import { debounce, UpdateScheduler } from '@dxos/async';
|
|
7
7
|
import { type Context } from '@dxos/context';
|
|
8
8
|
import { createSubscription, Filter, getAutomergeObjectCore, type Query } from '@dxos/echo-db';
|
|
9
9
|
import { log } from '@dxos/log';
|
|
@@ -18,26 +18,33 @@ export const createSubscriptionTrigger: TriggerFactory<SubscriptionTrigger> = as
|
|
|
18
18
|
callback: TriggerCallback,
|
|
19
19
|
) => {
|
|
20
20
|
const objectIds = new Set<string>();
|
|
21
|
-
const task = new
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
objectIds.
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
const task = new UpdateScheduler(
|
|
22
|
+
ctx,
|
|
23
|
+
async () => {
|
|
24
|
+
if (objectIds.size > 0) {
|
|
25
|
+
const objects = Array.from(objectIds);
|
|
26
|
+
objectIds.clear();
|
|
27
|
+
await callback({ objects });
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{ maxFrequency: 4 },
|
|
31
|
+
);
|
|
27
32
|
|
|
28
33
|
// TODO(burdon): Don't fire initially?
|
|
29
34
|
// TODO(burdon): Create queue. Only allow one invocation per trigger at a time?
|
|
30
35
|
const subscriptions: (() => void)[] = [];
|
|
31
36
|
const subscription = createSubscription(({ added, updated }) => {
|
|
32
|
-
|
|
37
|
+
const sizeBefore = objectIds.size;
|
|
33
38
|
for (const object of added) {
|
|
34
39
|
objectIds.add(object.id);
|
|
35
40
|
}
|
|
36
41
|
for (const object of updated) {
|
|
37
42
|
objectIds.add(object.id);
|
|
38
43
|
}
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
if (objectIds.size > sizeBefore) {
|
|
45
|
+
log.info('updated', { added: added.length, updated: updated.length });
|
|
46
|
+
task.trigger();
|
|
47
|
+
}
|
|
41
48
|
});
|
|
42
49
|
|
|
43
50
|
subscriptions.push(() => subscription.unsubscribe());
|
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
|
/**
|
|
@@ -84,9 +92,9 @@ export class FunctionTrigger extends TypedObject({
|
|
|
84
92
|
typename: 'dxos.org/type/FunctionTrigger',
|
|
85
93
|
version: '0.1.0',
|
|
86
94
|
})({
|
|
87
|
-
function: S.string.pipe(S.description('Function
|
|
88
|
-
// Context passed to
|
|
89
|
-
meta: S.optional(S.
|
|
95
|
+
function: S.string.pipe(S.description('Function URI.')),
|
|
96
|
+
// Context is merged into the event data passed to the function.
|
|
97
|
+
meta: S.optional(S.object),
|
|
90
98
|
spec: TriggerSpecSchema,
|
|
91
99
|
}) {}
|
|
92
100
|
|
|
@@ -94,8 +102,11 @@ export class FunctionTrigger extends TypedObject({
|
|
|
94
102
|
* Function manifest file.
|
|
95
103
|
*/
|
|
96
104
|
export const FunctionManifestSchema = S.struct({
|
|
97
|
-
functions: S.optional(S.mutable(S.array(
|
|
98
|
-
triggers: S.optional(S.mutable(S.array(
|
|
105
|
+
functions: S.optional(S.mutable(S.array(RawObject(FunctionDef)))),
|
|
106
|
+
triggers: S.optional(S.mutable(S.array(RawObject(FunctionTrigger)))),
|
|
99
107
|
});
|
|
100
108
|
|
|
101
109
|
export type FunctionManifest = S.Schema.Type<typeof FunctionManifestSchema>;
|
|
110
|
+
|
|
111
|
+
// TODO(burdon): Standards?
|
|
112
|
+
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
|