@dxos/functions 0.5.3-main.bbd33a9 → 0.5.3-main.bfb5bca

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 (37) hide show
  1. package/dist/lib/browser/{chunk-P3HPDHNI.mjs → chunk-4D4I3YMJ.mjs} +4 -4
  2. package/dist/lib/browser/{chunk-P3HPDHNI.mjs.map → chunk-4D4I3YMJ.mjs.map} +3 -3
  3. package/dist/lib/browser/index.mjs +145 -108
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types.mjs +1 -1
  7. package/dist/lib/node/{chunk-KTLM3JNV.cjs → chunk-3UYUR5N5.cjs} +7 -7
  8. package/dist/lib/node/{chunk-KTLM3JNV.cjs.map → chunk-3UYUR5N5.cjs.map} +3 -3
  9. package/dist/lib/node/index.cjs +156 -119
  10. package/dist/lib/node/index.cjs.map +3 -3
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/types.cjs +5 -5
  13. package/dist/lib/node/types.cjs.map +1 -1
  14. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  15. package/dist/types/src/testing/setup.d.ts.map +1 -1
  16. package/dist/types/src/trigger/trigger-registry.d.ts +2 -5
  17. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -1
  18. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -1
  19. package/dist/types/src/trigger/type/timer-trigger.d.ts.map +1 -1
  20. package/dist/types/src/trigger/type/webhook-trigger.d.ts.map +1 -1
  21. package/dist/types/src/trigger/type/websocket-trigger.d.ts.map +1 -1
  22. package/dist/types/src/types.d.ts +15 -3
  23. package/dist/types/src/types.d.ts.map +1 -1
  24. package/package.json +14 -14
  25. package/schema/functions.json +5 -0
  26. package/src/runtime/dev-server.ts +3 -3
  27. package/src/runtime/scheduler.test.ts +14 -9
  28. package/src/runtime/scheduler.ts +7 -7
  29. package/src/testing/functions-integration.test.ts +1 -0
  30. package/src/testing/setup.ts +8 -10
  31. package/src/trigger/trigger-registry.test.ts +30 -13
  32. package/src/trigger/trigger-registry.ts +49 -39
  33. package/src/trigger/type/subscription-trigger.ts +13 -7
  34. package/src/trigger/type/timer-trigger.ts +4 -3
  35. package/src/trigger/type/webhook-trigger.ts +3 -2
  36. package/src/trigger/type/websocket-trigger.ts +4 -3
  37. package/src/types.ts +6 -3
@@ -51,6 +51,11 @@
51
51
  "description": "Function URI.",
52
52
  "title": "string"
53
53
  },
54
+ "enabled": {
55
+ "type": "boolean",
56
+ "description": "a boolean",
57
+ "title": "boolean"
58
+ },
54
59
  "meta": {
55
60
  "$ref": "#/$defs/object"
56
61
  },
@@ -46,6 +46,7 @@ export class DevServer {
46
46
  private readonly _functionsRegistry: FunctionRegistry,
47
47
  private readonly _options: DevServerOptions,
48
48
  ) {
49
+ // TODO(burdon): Add/remove listener in start/stop.
49
50
  this._functionsRegistry.registered.on(async ({ added }) => {
50
51
  added.forEach((def) => this._load(def));
51
52
  await this._safeUpdateRegistration();
@@ -188,8 +189,8 @@ export class DevServer {
188
189
  registrationId: this._functionServiceRegistration,
189
190
  functions: this.functions.map(({ def: { id, route } }) => ({ id, route })),
190
191
  });
191
- } catch (e) {
192
- log.catch(e);
192
+ } catch (err) {
193
+ log.catch(err);
193
194
  }
194
195
  }
195
196
 
@@ -211,7 +212,6 @@ export class DevServer {
211
212
  private async _invoke(path: string, event: FunctionEvent) {
212
213
  const { handler } = this._handlers[path] ?? {};
213
214
  invariant(handler, `invalid path: ${path}`);
214
-
215
215
  const context: FunctionContext = {
216
216
  client: this._client,
217
217
  dataDir: this._options.dataDir,
@@ -29,6 +29,15 @@ describe('scheduler', () => {
29
29
  await testBuilder.destroy();
30
30
  });
31
31
 
32
+ const createScheduler = (callback: SchedulerOptions['callback']) => {
33
+ const scheduler = new Scheduler(new FunctionRegistry(client), new TriggerRegistry(client), { callback });
34
+ after(async () => {
35
+ await scheduler.stop();
36
+ });
37
+
38
+ return scheduler;
39
+ };
40
+
32
41
  test('timer', async () => {
33
42
  const manifest: FunctionManifest = {
34
43
  functions: [
@@ -41,6 +50,7 @@ describe('scheduler', () => {
41
50
  triggers: [
42
51
  {
43
52
  function: 'example.com/function/test',
53
+ enabled: true,
44
54
  spec: {
45
55
  type: 'timer',
46
56
  cron: '0/1 * * * * *', // Every 1s.
@@ -75,6 +85,7 @@ describe('scheduler', () => {
75
85
  triggers: [
76
86
  {
77
87
  function: 'example.com/function/test',
88
+ enabled: true,
78
89
  spec: {
79
90
  type: 'webhook',
80
91
  method: 'GET',
@@ -108,6 +119,7 @@ describe('scheduler', () => {
108
119
  triggers: [
109
120
  {
110
121
  function: 'example.com/function/test',
122
+ enabled: true,
111
123
  spec: {
112
124
  type: 'websocket',
113
125
  // url: 'https://hub.dxos.network/api/mailbox/test',
@@ -154,6 +166,7 @@ describe('scheduler', () => {
154
166
  triggers: [
155
167
  {
156
168
  function: 'example.com/function/test',
169
+ enabled: true,
157
170
  spec: {
158
171
  type: 'subscription',
159
172
  filter: [{ type: TestType.typename }],
@@ -165,7 +178,7 @@ describe('scheduler', () => {
165
178
  let count = 0;
166
179
  const done = new Trigger();
167
180
  const scheduler = createScheduler(async () => {
168
- if (++count === 2) {
181
+ if (++count === 1) {
169
182
  done.wake();
170
183
  }
171
184
  });
@@ -180,12 +193,4 @@ describe('scheduler', () => {
180
193
 
181
194
  await done.wait();
182
195
  });
183
-
184
- const createScheduler = (callback: SchedulerOptions['callback']) => {
185
- const scheduler = new Scheduler(new FunctionRegistry(client), new TriggerRegistry(client), { callback });
186
- after(async () => {
187
- await scheduler.stop();
188
- });
189
- return scheduler;
190
- };
191
196
  });
@@ -72,28 +72,28 @@ export class Scheduler {
72
72
  await Promise.all(mountTasks).catch(log.catch);
73
73
  }
74
74
 
75
- private async activate(space: Space, functions: FunctionDef[], fnTrigger: FunctionTrigger) {
76
- const definition = functions.find((def) => def.uri === fnTrigger.function);
75
+ private async activate(space: Space, functions: FunctionDef[], trigger: FunctionTrigger) {
76
+ const definition = functions.find((def) => def.uri === trigger.function);
77
77
  if (!definition) {
78
- log.info('function is not found for trigger', { fnTrigger });
78
+ log.info('function is not found for trigger', { trigger });
79
79
  return;
80
80
  }
81
81
 
82
- await this.triggers.activate({ space }, fnTrigger, async (args) => {
82
+ await this.triggers.activate(space, trigger, async (args) => {
83
83
  const mutex = this._functionUriToCallMutex.get(definition.uri) ?? new Mutex();
84
84
  this._functionUriToCallMutex.set(definition.uri, mutex);
85
85
 
86
86
  log.info('function triggered, waiting for mutex', { uri: definition.uri });
87
87
  return mutex.executeSynchronized(() => {
88
88
  log.info('mutex acquired', { uri: definition.uri });
89
- return this._execFunction(definition, fnTrigger, {
90
- meta: fnTrigger.meta,
89
+ return this._execFunction(definition, trigger, {
90
+ meta: trigger.meta ?? {},
91
91
  data: { ...args, spaceKey: space.key },
92
92
  });
93
93
  });
94
94
  });
95
95
 
96
- log('activated trigger', { space: space.key, trigger: fnTrigger });
96
+ log('activated trigger', { space: space.key, trigger });
97
97
  }
98
98
 
99
99
  private async _execFunction<TData, TMeta>(
@@ -44,6 +44,7 @@ describe('functions e2e', () => {
44
44
  space.db.add(
45
45
  create(FunctionTrigger, {
46
46
  function: uri,
47
+ enabled: true,
47
48
  meta: triggerMeta,
48
49
  spec: {
49
50
  type: 'subscription',
@@ -10,16 +10,16 @@ import { range } from '@dxos/util';
10
10
  import { TestType } from './types';
11
11
  import { FunctionDef, FunctionTrigger } from '../types';
12
12
 
13
- // TODO(burdon): Create new or extend existing TestBuilder.
13
+ // TODO(burdon): Extend/wrap 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() }));
17
- testBuilder.ctx.onDispose(() => Promise.all(clients.map((c) => c.destroy())));
17
+ testBuilder.ctx.onDispose(() => Promise.all(clients.map((client) => client.destroy())));
18
18
  return Promise.all(
19
19
  clients.map(async (client, index) => {
20
20
  await client.initialize();
21
21
  await client.halo.createIdentity({ displayName: `Peer ${index}` });
22
- client.addSchema(TestType, FunctionDef, FunctionTrigger);
22
+ client.addSchema(FunctionDef, FunctionTrigger, TestType);
23
23
  return client;
24
24
  }),
25
25
  );
@@ -34,12 +34,10 @@ export const createFunctionRuntime = async (testBuilder: TestBuilder): Promise<C
34
34
  },
35
35
  });
36
36
 
37
- const client = (await createInitializedClients(testBuilder, 1, config))[0];
38
-
39
- // TODO(burdon): Better way to configure plugin? (Rationalize chess.test).
40
- const functionsPlugin = new FunctionsPlugin();
41
- await functionsPlugin.initialize({ client, clientServices: client.services });
42
- await functionsPlugin.open();
43
- testBuilder.ctx.onDispose(() => functionsPlugin.close());
37
+ const [client] = await createInitializedClients(testBuilder, 1, config);
38
+ const plugin = new FunctionsPlugin();
39
+ await plugin.initialize({ client, clientServices: client.services });
40
+ await plugin.open();
41
+ testBuilder.ctx.onDispose(() => plugin.close());
44
42
  return client;
45
43
  };
@@ -31,6 +31,7 @@ const manifest: FunctionManifest = {
31
31
  ],
32
32
  },
33
33
  function: 'example.com/function/webhook-test',
34
+ enabled: true,
34
35
  spec: {
35
36
  type: 'webhook',
36
37
  method: 'GET',
@@ -46,6 +47,7 @@ const manifest: FunctionManifest = {
46
47
  ],
47
48
  },
48
49
  function: 'example.com/function/subscription-test',
50
+ enabled: true,
49
51
  spec: {
50
52
  type: 'subscription',
51
53
  filter: [
@@ -74,7 +76,7 @@ describe('trigger registry', () => {
74
76
 
75
77
  describe('register', () => {
76
78
  test('creates new triggers', async () => {
77
- const client = (await createInitializedClients(testBuilder))[0];
79
+ const [client] = await createInitializedClients(testBuilder);
78
80
  const registry = createRegistry(client);
79
81
  const space = await client.spaces.create();
80
82
  await registry.register(space, manifest);
@@ -84,11 +86,24 @@ describe('trigger registry', () => {
84
86
  const expected = manifest.triggers?.map((trigger) => trigger.function).sort();
85
87
  expect(objects.map((object: FunctionTrigger) => object.function).sort()).to.deep.eq(expected);
86
88
  });
89
+
90
+ test('set meta', () => {
91
+ const trigger = create(FunctionTrigger, {
92
+ function: 'example.com/function/webhook-test',
93
+ spec: {
94
+ type: 'webhook',
95
+ method: 'GET',
96
+ },
97
+ });
98
+
99
+ (trigger.meta ??= {}).test = 100;
100
+ expect(trigger.meta.test).to.eq(100);
101
+ });
87
102
  });
88
103
 
89
104
  describe('activate', () => {
90
105
  test('invokes the provided callback', async () => {
91
- const client = (await createInitializedClients(testBuilder))[0];
106
+ const [client] = await createInitializedClients(testBuilder);
92
107
  const space = await client.spaces.create();
93
108
  const registry = createRegistry(client);
94
109
  await registry.register(space, manifest);
@@ -98,7 +113,7 @@ describe('trigger registry', () => {
98
113
  const callbackInvoked = new Trigger();
99
114
  const { objects: allTriggers } = await space.db.query(Filter.schema(FunctionTrigger)).run();
100
115
  const webhookTrigger = allTriggers.find((trigger: FunctionTrigger) => trigger.spec.type === 'webhook')!;
101
- await registry.activate({ space }, webhookTrigger, async () => {
116
+ await registry.activate(space, webhookTrigger, async () => {
102
117
  callbackInvoked.wake();
103
118
  return 200;
104
119
  });
@@ -108,7 +123,7 @@ describe('trigger registry', () => {
108
123
  });
109
124
 
110
125
  test('removes from inactive list', async () => {
111
- const client = (await createInitializedClients(testBuilder))[0];
126
+ const [client] = await createInitializedClients(testBuilder);
112
127
  const space = await client.spaces.create();
113
128
  const registry = createRegistry(client);
114
129
  await registry.register(space, manifest);
@@ -116,17 +131,19 @@ describe('trigger registry', () => {
116
131
  await waitForInactiveTriggers(registry, space);
117
132
 
118
133
  const inactiveTrigger = registry.getInactiveTriggers(space)[0];
119
- await registry.activate({ space }, inactiveTrigger, async () => 200);
134
+ await registry.activate(space, inactiveTrigger, async () => 200);
120
135
 
121
136
  const updatedInactiveList = registry.getInactiveTriggers(space);
122
137
  expect(updatedInactiveList.find((trigger: FunctionTrigger) => trigger.function === inactiveTrigger.function)).to
123
138
  .be.undefined;
124
139
  });
140
+
141
+ // TODO(burdon): Test enable/disable trigger.
125
142
  });
126
143
 
127
144
  describe('deactivate', () => {
128
145
  test('trigger object deletion deactivates a trigger', async () => {
129
- const client = (await createInitializedClients(testBuilder))[0];
146
+ const [client] = await createInitializedClients(testBuilder);
130
147
  const space = await client.spaces.create();
131
148
  const registry = createRegistry(client);
132
149
  await registry.register(space, manifest);
@@ -136,7 +153,7 @@ describe('trigger registry', () => {
136
153
  const { objects: allTriggers } = await space.db.query(Filter.schema(FunctionTrigger)).run();
137
154
  const echoTrigger = allTriggers.find((trigger: FunctionTrigger) => trigger.spec.type === 'subscription')!;
138
155
  let count = 0;
139
- await registry.activate({ space }, echoTrigger, async () => {
156
+ await registry.activate(space, echoTrigger, async () => {
140
157
  count++;
141
158
  return 200;
142
159
  });
@@ -152,7 +169,7 @@ describe('trigger registry', () => {
152
169
  });
153
170
 
154
171
  test('registry closing deactivates a trigger', async () => {
155
- const client = (await createInitializedClients(testBuilder))[0];
172
+ const [client] = await createInitializedClients(testBuilder);
156
173
  const space = await client.spaces.create();
157
174
  const registry = createRegistry(client);
158
175
  await registry.register(space, manifest);
@@ -162,7 +179,7 @@ describe('trigger registry', () => {
162
179
  const { objects: allTriggers } = await space.db.query(Filter.schema(FunctionTrigger)).run();
163
180
  const echoTrigger = allTriggers.find((trigger: FunctionTrigger) => trigger.spec.type === 'subscription')!;
164
181
  let count = 0;
165
- await registry.activate({ space }, echoTrigger, async () => {
182
+ await registry.activate(space, echoTrigger, async () => {
166
183
  count++;
167
184
  return 200;
168
185
  });
@@ -176,8 +193,8 @@ describe('trigger registry', () => {
176
193
  });
177
194
 
178
195
  describe('trigger events', () => {
179
- test.only('event fired when all registered when opened', async () => {
180
- const client = (await createInitializedClients(testBuilder))[0];
196
+ test('event fired when all registered when opened', async () => {
197
+ const [client] = await createInitializedClients(testBuilder);
181
198
  const registry = createRegistry(client);
182
199
  const triggers = createTriggers(client.spaces.default, 3);
183
200
 
@@ -194,7 +211,7 @@ describe('trigger registry', () => {
194
211
  });
195
212
 
196
213
  test('event fired when a new trigger is added', async () => {
197
- const client = (await createInitializedClients(testBuilder))[0];
214
+ const [client] = await createInitializedClients(testBuilder);
198
215
  const registry = createRegistry(client);
199
216
  const space = await client.spaces.create();
200
217
 
@@ -210,7 +227,7 @@ describe('trigger registry', () => {
210
227
  });
211
228
 
212
229
  test('event fired when a new trigger is removed', async () => {
213
- const client = (await createInitializedClients(testBuilder))[0];
230
+ const [client] = await createInitializedClients(testBuilder);
214
231
  const registry = createRegistry(client);
215
232
  const space = await client.spaces.create();
216
233
  const triggers = createTriggers(space, 3);
@@ -6,11 +6,11 @@ import { Event } from '@dxos/async';
6
6
  import { type Client } from '@dxos/client';
7
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
+ import { compareForeignKeys, ECHO_ATTR_META, foreignKey } from '@dxos/echo-schema';
10
10
  import { invariant } from '@dxos/invariant';
11
11
  import { PublicKey } from '@dxos/keys';
12
12
  import { log } from '@dxos/log';
13
- import { ComplexMap, diff, intersection } from '@dxos/util';
13
+ import { ComplexMap, diff } from '@dxos/util';
14
14
 
15
15
  import { createSubscriptionTrigger, createTimerTrigger, createWebhookTrigger, createWebsocketTrigger } from './type';
16
16
  import { type FunctionManifest, FunctionTrigger, type FunctionTriggerType, type TriggerSpec } from '../types';
@@ -19,12 +19,10 @@ type ResponseCode = number;
19
19
 
20
20
  export type TriggerCallback = (args: object) => Promise<ResponseCode>;
21
21
 
22
- export type TriggerContext = { space: Space };
23
-
24
22
  // TODO(burdon): Make object?
25
23
  export type TriggerFactory<Spec extends TriggerSpec, Options = any> = (
26
24
  ctx: Context,
27
- context: TriggerContext,
25
+ space: Space,
28
26
  spec: Spec,
29
27
  callback: TriggerCallback,
30
28
  options?: Options,
@@ -70,19 +68,18 @@ export class TriggerRegistry extends Resource {
70
68
  return this._getTriggers(space, (t) => t.activationCtx == null);
71
69
  }
72
70
 
73
- async activate(triggerCtx: TriggerContext, trigger: FunctionTrigger, callback: TriggerCallback): Promise<void> {
74
- log('activate', { space: triggerCtx.space.key, trigger });
75
- const activationCtx = new Context({ name: `trigger_${trigger.function}` });
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}` });
76
75
  this._ctx.onDispose(() => activationCtx.dispose());
77
- const registeredTrigger = this._triggersBySpaceKey
78
- .get(triggerCtx.space.key)
79
- ?.find((reg) => reg.trigger.id === trigger.id);
76
+ const registeredTrigger = this._triggersBySpaceKey.get(space.key)?.find((reg) => reg.trigger.id === trigger.id);
80
77
  invariant(registeredTrigger, `Trigger is not registered: ${trigger.function}`);
81
78
  registeredTrigger.activationCtx = activationCtx;
82
79
 
83
80
  try {
84
81
  const options = this._options?.[trigger.spec.type];
85
- await triggerHandlers[trigger.spec.type](activationCtx, triggerCtx, trigger.spec, callback, options);
82
+ await triggerHandlers[trigger.spec.type](activationCtx, space, trigger.spec, callback, options);
86
83
  } catch (err) {
87
84
  delete registeredTrigger.activationCtx;
88
85
  throw err;
@@ -97,24 +94,35 @@ export class TriggerRegistry extends Resource {
97
94
  if (!manifest.triggers?.length) {
98
95
  return;
99
96
  }
97
+
100
98
  if (!space.db.graph.runtimeSchemaRegistry.hasSchema(FunctionTrigger)) {
101
99
  space.db.graph.runtimeSchemaRegistry.registerSchema(FunctionTrigger);
102
100
  }
103
101
 
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
+
104
113
  // Sync triggers.
105
114
  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
- });
115
+ const { added } = diff(existing, manifestTriggers, compareForeignKeys);
112
116
 
113
117
  // TODO(burdon): Update existing.
114
118
  added.forEach((trigger) => {
115
- const { meta, object } = splitMeta(trigger);
116
- space.db.add(create(FunctionTrigger, object, meta));
119
+ space.db.add(trigger);
120
+ log.info('added', { meta: getMeta(trigger) });
117
121
  });
122
+
123
+ if (added.length > 0) {
124
+ await space.db.flush();
125
+ }
118
126
  }
119
127
 
120
128
  protected override async _open(): Promise<void> {
@@ -134,55 +142,52 @@ export class TriggerRegistry extends Resource {
134
142
 
135
143
  // Subscribe to updates.
136
144
  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);
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);
141
149
  }),
142
150
  );
143
151
  }
144
152
  });
145
153
 
146
154
  this._ctx.onDispose(() => spaceListSubscription.unsubscribe());
155
+ log.info('opened');
147
156
  }
148
157
 
149
158
  protected override async _close(_: Context): Promise<void> {
150
159
  log.info('close...');
151
160
  this._triggersBySpaceKey.clear();
161
+ log.info('closed');
152
162
  }
153
163
 
154
- private _handleNewTriggers(space: Space, allTriggers: FunctionTrigger[], registered: RegisteredTrigger[]) {
155
- const newTriggers = allTriggers.filter((candidate) => {
156
- 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;
157
167
  });
158
168
 
159
- if (newTriggers.length > 0) {
160
- const newRegisteredTriggers: RegisteredTrigger[] = newTriggers.map((trigger) => ({ trigger }));
169
+ if (added.length > 0) {
170
+ const newRegisteredTriggers: RegisteredTrigger[] = added.map((trigger) => ({ trigger }));
161
171
  registered.push(...newRegisteredTriggers);
162
172
  log.info('added', () => ({
163
173
  spaceKey: space.key,
164
- triggers: newTriggers.map((trigger) => trigger.function),
174
+ triggers: added.map((trigger) => trigger.function),
165
175
  }));
166
- this.registered.emit({ space, triggers: newTriggers });
176
+
177
+ this.registered.emit({ space, triggers: added });
167
178
  }
168
179
  }
169
180
 
170
181
  private async _handleRemovedTriggers(
171
182
  space: Space,
172
- allTriggers: FunctionTrigger[],
183
+ current: FunctionTrigger[],
173
184
  registered: RegisteredTrigger[],
174
185
  ): Promise<void> {
175
186
  const removed: FunctionTrigger[] = [];
176
187
  for (let i = registered.length - 1; i >= 0; i--) {
177
188
  const wasRemoved =
178
- allTriggers.find((trigger: FunctionTrigger) => trigger.id === registered[i].trigger.id) == null;
189
+ current.filter((trigger) => trigger.enabled).find((trigger) => trigger.id === registered[i].trigger.id) == null;
179
190
  if (wasRemoved) {
180
- if (removed.length) {
181
- log.info('removed', () => ({
182
- spaceKey: space.key,
183
- triggers: removed.map((trigger) => trigger.function),
184
- }));
185
- }
186
191
  const unregistered = registered.splice(i, 1)[0];
187
192
  await unregistered.activationCtx?.dispose();
188
193
  removed.push(unregistered.trigger);
@@ -190,6 +195,11 @@ export class TriggerRegistry extends Resource {
190
195
  }
191
196
 
192
197
  if (removed.length > 0) {
198
+ log.info('removed', () => ({
199
+ spaceKey: space.key,
200
+ triggers: removed.map((trigger) => trigger.function),
201
+ }));
202
+
193
203
  this.removed.emit({ space, triggers: removed });
194
204
  }
195
205
  }
@@ -4,16 +4,17 @@
4
4
 
5
5
  import { TextV0Type } from '@braneframe/types';
6
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, Filter, getAutomergeObjectCore, type Query } from '@dxos/echo-db';
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 TriggerContext, type TriggerFactory } from '../trigger-registry';
13
+ import { type TriggerCallback, type TriggerFactory } from '../trigger-registry';
13
14
 
14
15
  export const createSubscriptionTrigger: TriggerFactory<SubscriptionTrigger> = async (
15
16
  ctx: Context,
16
- triggerCtx: TriggerContext,
17
+ space: Space,
17
18
  spec: SubscriptionTrigger,
18
19
  callback: TriggerCallback,
19
20
  ) => {
@@ -30,6 +31,7 @@ export const createSubscriptionTrigger: TriggerFactory<SubscriptionTrigger> = as
30
31
  { maxFrequency: 4 },
31
32
  );
32
33
 
34
+ // TODO(burdon): Factor out diff.
33
35
  // TODO(burdon): Don't fire initially?
34
36
  // TODO(burdon): Create queue. Only allow one invocation per trigger at a time?
35
37
  const subscriptions: (() => void)[] = [];
@@ -52,11 +54,11 @@ export const createSubscriptionTrigger: TriggerFactory<SubscriptionTrigger> = as
52
54
  // TODO(burdon): Disable trigger if keeps failing.
53
55
  const { filter, options: { deep, delay } = {} } = spec;
54
56
  const update = ({ objects }: Query) => {
57
+ log.info('update', { objects: objects.length });
55
58
  subscription.update(objects);
56
59
 
57
60
  // TODO(burdon): Hack to monitor changes to Document's text object.
58
61
  if (deep) {
59
- log.info('update', { objects: objects.length });
60
62
  for (const object of objects) {
61
63
  const content = object.content;
62
64
  if (content instanceof TextV0Type) {
@@ -68,11 +70,15 @@ export const createSubscriptionTrigger: TriggerFactory<SubscriptionTrigger> = as
68
70
  }
69
71
  };
70
72
 
71
- // TODO(burdon): Is Filter.or implemented?
73
+ // TODO(burdon): OR not working.
72
74
  // TODO(burdon): [Bug]: all callbacks are fired on the first mutation.
73
75
  // TODO(burdon): [Bug]: not updated when document is deleted (either top or hierarchically).
74
- const query = triggerCtx.space.db.query(Filter.or(filter.map(({ type, props }) => Filter.typename(type, props))));
75
- subscriptions.push(query.subscribe(delay ? debounce(update, delay) : update));
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
+ }
76
82
 
77
83
  ctx.onDispose(() => {
78
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 TriggerContext, type TriggerFactory } from '../trigger-registry';
13
+ import { type TriggerCallback, type TriggerFactory } from '../trigger-registry';
13
14
 
14
15
  export const createTimerTrigger: TriggerFactory<TimerTrigger> = async (
15
16
  ctx: Context,
16
- triggerContext: TriggerContext,
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: triggerContext.space.key.truncate(), count: run, delta });
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 TriggerContext, type TriggerFactory } from '../trigger-registry';
13
+ import { type TriggerCallback, type TriggerFactory } from '../trigger-registry';
13
14
 
14
15
  export const createWebhookTrigger: TriggerFactory<WebhookTrigger> = async (
15
16
  ctx: Context,
16
- _: TriggerContext,
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 TriggerContext, type TriggerFactory } from '../trigger-registry';
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
- triggerCtx: TriggerContext,
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, triggerCtx, spec, callback, options);
55
+ await createWebsocketTrigger(ctx, space, spec, callback, options);
55
56
  }, options.retryDelay * 1_000);
56
57
  }
57
58