@dxos/functions 0.5.3-main.d7fe7b5 → 0.5.3-main.e38fceb

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