@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.
Files changed (53) hide show
  1. package/dist/lib/browser/chunk-P3HPDHNI.mjs +86 -0
  2. package/dist/lib/browser/chunk-P3HPDHNI.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +327 -310
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types.mjs +14 -0
  7. package/dist/lib/browser/types.mjs.map +7 -0
  8. package/dist/lib/node/chunk-KTLM3JNV.cjs +103 -0
  9. package/dist/lib/node/chunk-KTLM3JNV.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +332 -309
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/types.cjs +35 -0
  14. package/dist/lib/node/types.cjs.map +7 -0
  15. package/dist/types/src/browser/index.d.ts +2 -0
  16. package/dist/types/src/browser/index.d.ts.map +1 -0
  17. package/dist/types/src/{registry → function}/function-registry.d.ts +4 -4
  18. package/dist/types/src/function/function-registry.d.ts.map +1 -0
  19. package/dist/types/src/function/function-registry.test.d.ts.map +1 -0
  20. package/dist/types/src/function/index.d.ts.map +1 -0
  21. package/dist/types/src/index.d.ts +1 -1
  22. package/dist/types/src/index.d.ts.map +1 -1
  23. package/dist/types/src/runtime/dev-server.d.ts +1 -1
  24. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  25. package/dist/types/src/runtime/scheduler.d.ts +2 -1
  26. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  27. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -1
  28. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -1
  29. package/dist/types/src/types.d.ts +64 -49
  30. package/dist/types/src/types.d.ts.map +1 -1
  31. package/package.json +31 -18
  32. package/schema/functions.json +18 -9
  33. package/src/browser/index.ts +5 -0
  34. package/src/{registry → function}/function-registry.test.ts +10 -10
  35. package/src/function/function-registry.ts +90 -0
  36. package/src/index.ts +1 -1
  37. package/src/runtime/dev-server.test.ts +2 -2
  38. package/src/runtime/dev-server.ts +5 -6
  39. package/src/runtime/scheduler.test.ts +1 -1
  40. package/src/runtime/scheduler.ts +21 -8
  41. package/src/testing/functions-integration.test.ts +1 -1
  42. package/src/testing/setup.ts +1 -1
  43. package/src/trigger/trigger-registry.test.ts +60 -34
  44. package/src/trigger/trigger-registry.ts +38 -13
  45. package/src/trigger/type/subscription-trigger.ts +17 -10
  46. package/src/types.ts +47 -36
  47. package/dist/types/src/registry/function-registry.d.ts.map +0 -1
  48. package/dist/types/src/registry/function-registry.test.d.ts.map +0 -1
  49. package/dist/types/src/registry/index.d.ts.map +0 -1
  50. package/src/registry/function-registry.ts +0 -84
  51. /package/dist/types/src/{registry → function}/function-registry.test.d.ts +0 -0
  52. /package/dist/types/src/{registry → function}/index.d.ts +0 -0
  53. /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 '../registry';
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';
@@ -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 testManifest: FunctionManifest = {
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: [{ type: TestType.typename }],
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, testManifest);
80
+ await registry.register(space, manifest);
61
81
  const { objects } = await space.db.query(Filter.schema(FunctionTrigger)).run();
62
- expect(objects.length).to.eq(testManifest.triggers?.length);
63
- const expected = testManifest.triggers?.map((t) => t.function).sort();
64
- expect(objects.map((o: FunctionTrigger) => o.function).sort()).to.deep.eq(expected);
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, testManifest);
94
+ await registry.register(space, manifest);
74
95
  await registry.open(ctx);
75
- await waitHasInactiveTriggers(registry, space);
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((t: FunctionTrigger) => t.spec.type === 'webhook')!;
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, testManifest);
114
+ await registry.register(space, manifest);
94
115
  await registry.open(ctx);
95
- await waitHasInactiveTriggers(registry, space);
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((t: FunctionTrigger) => t.function === inactiveTrigger.function)).to.be.undefined;
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, testManifest);
132
+ await registry.register(space, manifest);
111
133
  await registry.open(ctx);
112
- await waitHasInactiveTriggers(registry, space);
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((t: FunctionTrigger) => t.spec.type === 'subscription')!;
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, testManifest);
158
+ await registry.register(space, manifest);
138
159
  await registry.open(ctx);
139
- await waitHasInactiveTriggers(registry, space);
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((t: FunctionTrigger) => t.spec.type === 'subscription')!;
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 = createTriggersInSpace(client.spaces.default, 3);
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((obj) => obj.id).sort();
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: testManifest?.triggers?.slice(0, 1) });
207
+ await registry.register(space, { triggers: manifest?.triggers?.slice(0, 1) });
186
208
  const registered = await triggerRegistered.wait();
187
- expect(registered.function).to.eq(testManifest.triggers![0].function);
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 = createTriggersInSpace(space, 3);
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, testManifest);
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 createTriggersInSpace = (space: Space, count: number) => {
225
- const triggers = range(count, () => create(FunctionTrigger, { ...testManifest.triggers![0] }));
226
- triggers.forEach((def) => space.db.add(def));
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.isSchemaRegistered(FunctionTrigger)) {
100
+ if (!space.db.graph.runtimeSchemaRegistry.hasSchema(FunctionTrigger)) {
100
101
  space.db.graph.runtimeSchemaRegistry.registerSchema(FunctionTrigger);
101
102
  }
102
103
 
103
- const reactiveObjects = manifest.triggers.map((template: Omit<FunctionTrigger, 'id'>) =>
104
- create(FunctionTrigger, { ...template }),
105
- );
106
- reactiveObjects.forEach((obj) => space.db.add(obj));
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
- this._ctx.onDispose(functionsSubscription);
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('registered new triggers', () => ({ spaceKey: space.key, functions: newTriggers.map((t) => t.function) }));
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, DeferredTask } from '@dxos/async';
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 DeferredTask(ctx, async () => {
22
- if (objectIds.size > 0) {
23
- await callback({ objects: Array.from(objectIds) });
24
- objectIds.clear();
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
- log.info('updated', { added: added.length, updated: updated.length });
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
- task.schedule();
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 { AST, S, TypedObject } from '@dxos/echo-schema';
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.struct({
19
- type: S.literal('subscription'),
20
- // TODO(burdon): Define query DSL.
21
- filter: S.array(
22
- S.struct({
23
- type: S.string,
24
- props: S.optional(S.record(S.string, S.any)),
25
- }),
26
- ),
27
- options: S.optional(
28
- S.struct({
29
- // Watch changes to object (not just creation).
30
- deep: S.optional(S.boolean),
31
- // Debounce changes (delay in ms).
32
- delay: S.optional(S.number),
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.struct({
39
- type: S.literal('timer'),
40
- cron: S.string,
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.struct({
55
- type: S.literal('websocket'),
56
- url: S.string,
57
- init: S.optional(S.record(S.string, S.any)),
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 ID/URI.')),
88
- // Context passed to a function.
89
- meta: S.optional(S.record(S.string, S.any)),
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(omitEchoId(FunctionDef)))),
98
- triggers: S.optional(S.mutable(S.array(omitEchoId(FunctionTrigger)))),
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