@dxos/functions 0.5.3-main.cb47aab → 0.5.3-main.d3c5e1f

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 (55) hide show
  1. package/dist/lib/browser/chunk-366QG6IX.mjs +81 -0
  2. package/dist/lib/browser/chunk-366QG6IX.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +280 -299
  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 +12 -0
  7. package/dist/lib/browser/types.mjs.map +7 -0
  8. package/dist/lib/node/chunk-3VSJ57ZZ.cjs +97 -0
  9. package/dist/lib/node/chunk-3VSJ57ZZ.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +286 -303
  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 +33 -0
  14. package/dist/lib/node/types.cjs.map +7 -0
  15. package/dist/types/src/{registry → function}/function-registry.d.ts +4 -4
  16. package/dist/types/src/function/function-registry.d.ts.map +1 -0
  17. package/dist/types/src/function/function-registry.test.d.ts.map +1 -0
  18. package/dist/types/src/function/index.d.ts.map +1 -0
  19. package/dist/types/src/index.d.ts +1 -1
  20. package/dist/types/src/index.d.ts.map +1 -1
  21. package/dist/types/src/runtime/dev-server.d.ts +1 -1
  22. package/dist/types/src/runtime/dev-server.d.ts.map +1 -1
  23. package/dist/types/src/runtime/scheduler.d.ts +2 -1
  24. package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
  25. package/dist/types/src/trigger/trigger-registry.d.ts.map +1 -1
  26. package/dist/types/src/trigger/type/subscription-trigger.d.ts.map +1 -1
  27. package/dist/types/src/types.d.ts +33 -19
  28. package/dist/types/src/types.d.ts.map +1 -1
  29. package/dist/types/src/util.d.ts +15 -0
  30. package/dist/types/src/util.d.ts.map +1 -0
  31. package/dist/types/src/util.test.d.ts +2 -0
  32. package/dist/types/src/util.test.d.ts.map +1 -0
  33. package/package.json +31 -18
  34. package/schema/functions.json +18 -9
  35. package/src/{registry → function}/function-registry.test.ts +10 -10
  36. package/src/{registry → function}/function-registry.ts +30 -24
  37. package/src/index.ts +1 -1
  38. package/src/runtime/dev-server.test.ts +2 -2
  39. package/src/runtime/dev-server.ts +5 -6
  40. package/src/runtime/scheduler.test.ts +1 -1
  41. package/src/runtime/scheduler.ts +16 -8
  42. package/src/testing/functions-integration.test.ts +1 -1
  43. package/src/testing/setup.ts +1 -1
  44. package/src/trigger/trigger-registry.test.ts +60 -34
  45. package/src/trigger/trigger-registry.ts +18 -5
  46. package/src/trigger/type/subscription-trigger.ts +17 -10
  47. package/src/types.ts +12 -10
  48. package/src/util.test.ts +43 -0
  49. package/src/util.ts +48 -0
  50. package/dist/types/src/registry/function-registry.d.ts.map +0 -1
  51. package/dist/types/src/registry/function-registry.test.d.ts.map +0 -1
  52. package/dist/types/src/registry/index.d.ts.map +0 -1
  53. /package/dist/types/src/{registry → function}/function-registry.test.d.ts +0 -0
  54. /package/dist/types/src/{registry → function}/index.d.ts +0 -0
  55. /package/src/{registry → function}/index.ts +0 -0
@@ -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,8 +4,9 @@
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';
@@ -13,6 +14,7 @@ import { ComplexMap } 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';
17
+ import { diff, intersection } from '../util';
16
18
 
17
19
  type ResponseCode = number;
18
20
 
@@ -100,10 +102,21 @@ export class TriggerRegistry extends Resource {
100
102
  space.db.graph.runtimeSchemaRegistry.registerSchema(FunctionTrigger);
101
103
  }
102
104
 
103
- const reactiveObjects = manifest.triggers.map((template: Omit<FunctionTrigger, 'id'>) =>
104
- create(FunctionTrigger, { ...template }),
105
- );
106
- reactiveObjects.forEach((obj) => space.db.add(obj));
105
+ // Sync triggers.
106
+ const { objects: existing } = await space.db.query(Filter.schema(FunctionTrigger)).run();
107
+ const { added, removed } = diff(existing, manifest.triggers, (a, b) => {
108
+ // Create FK to enable syncing if none are set.
109
+ // TODO(burdon): Warn if not unique.
110
+ const keys = b[ECHO_ATTR_META]?.keys ?? [foreignKey('manifest', [b.function, b.spec.type].join('-'))];
111
+ return intersection(getMeta(a)?.keys ?? [], keys, foreignKeyEquals).length > 0;
112
+ });
113
+
114
+ added.forEach((trigger) => {
115
+ const { meta, object } = splitMeta(trigger);
116
+ space.db.add(create(FunctionTrigger, object, meta));
117
+ });
118
+ // TODO(burdon): Update existing triggers.
119
+ removed.forEach((trigger) => space.db.remove(trigger));
107
120
  }
108
121
 
109
122
  protected override async _open(): Promise<void> {
@@ -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.
@@ -17,7 +14,7 @@ export type FunctionTriggerType = 'subscription' | 'timer' | 'webhook' | 'websoc
17
14
 
18
15
  const SubscriptionTriggerSchema = S.struct({
19
16
  type: S.literal('subscription'),
20
- // TODO(burdon): Define query DSL.
17
+ // TODO(burdon): Define query DSL (from ECHO).
21
18
  filter: S.array(
22
19
  S.struct({
23
20
  type: S.string,
@@ -33,12 +30,14 @@ const SubscriptionTriggerSchema = S.struct({
33
30
  }),
34
31
  ),
35
32
  });
33
+
36
34
  export type SubscriptionTrigger = S.Schema.Type<typeof SubscriptionTriggerSchema>;
37
35
 
38
36
  const TimerTriggerSchema = S.struct({
39
37
  type: S.literal('timer'),
40
38
  cron: S.string,
41
39
  });
40
+
42
41
  export type TimerTrigger = S.Schema.Type<typeof TimerTriggerSchema>;
43
42
 
44
43
  const WebhookTriggerSchema = S.mutable(
@@ -49,6 +48,7 @@ const WebhookTriggerSchema = S.mutable(
49
48
  port: S.optional(S.number),
50
49
  }),
51
50
  );
51
+
52
52
  export type WebhookTrigger = S.Schema.Type<typeof WebhookTriggerSchema>;
53
53
 
54
54
  const WebsocketTriggerSchema = S.struct({
@@ -56,6 +56,7 @@ const WebsocketTriggerSchema = S.struct({
56
56
  url: S.string,
57
57
  init: S.optional(S.record(S.string, S.any)),
58
58
  });
59
+
59
60
  export type WebsocketTrigger = S.Schema.Type<typeof WebsocketTriggerSchema>;
60
61
 
61
62
  const TriggerSpecSchema = S.union(
@@ -64,6 +65,7 @@ const TriggerSpecSchema = S.union(
64
65
  WebsocketTriggerSchema,
65
66
  SubscriptionTriggerSchema,
66
67
  );
68
+
67
69
  export type TriggerSpec = TimerTrigger | WebhookTrigger | WebsocketTrigger | SubscriptionTrigger;
68
70
 
69
71
  /**
@@ -84,9 +86,9 @@ export class FunctionTrigger extends TypedObject({
84
86
  typename: 'dxos.org/type/FunctionTrigger',
85
87
  version: '0.1.0',
86
88
  })({
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)),
89
+ function: S.string.pipe(S.description('Function URI.')),
90
+ // Context is merged into the event data passed to the function.
91
+ meta: S.optional(S.object),
90
92
  spec: TriggerSpecSchema,
91
93
  }) {}
92
94
 
@@ -94,8 +96,8 @@ export class FunctionTrigger extends TypedObject({
94
96
  * Function manifest file.
95
97
  */
96
98
  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)))),
99
+ functions: S.optional(S.mutable(S.array(RawObject(FunctionDef)))),
100
+ triggers: S.optional(S.mutable(S.array(RawObject(FunctionTrigger)))),
99
101
  });
100
102
 
101
103
  export type FunctionManifest = S.Schema.Type<typeof FunctionManifestSchema>;
@@ -0,0 +1,43 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { describe, test } from '@dxos/test';
8
+
9
+ import { diff, intersection } from './util';
10
+
11
+ describe('diff', () => {
12
+ test('returns the difference between two sets', () => {
13
+ {
14
+ const { added, updated, removed } = diff<number>([], [], (a, b) => a === b);
15
+ expect(added).to.deep.eq([]);
16
+ expect(updated).to.deep.eq([]);
17
+ expect(removed).to.deep.eq([]);
18
+ }
19
+ {
20
+ const previous = [1, 2, 3];
21
+ const next = [2, 3, 4];
22
+ const { added, updated, removed } = diff(previous, next, (a, b) => a === b);
23
+ expect(added).to.deep.eq([4]);
24
+ expect(updated).to.deep.eq([2, 3]);
25
+ expect(removed).to.deep.eq([1]);
26
+ }
27
+ {
28
+ const previous = [{ x: 1 }, { x: 2 }, { x: 3 }];
29
+ const next = [{ x: 2 }, { x: 3 }, { x: 4 }];
30
+ const { added, updated, removed } = diff(previous, next, (a, b) => a.x === b.x);
31
+ expect(added).to.deep.eq([{ x: 4 }]);
32
+ expect(updated).to.deep.eq([{ x: 2 }, { x: 3 }]);
33
+ expect(removed).to.deep.eq([{ x: 1 }]);
34
+ }
35
+ });
36
+
37
+ test('intersection', () => {
38
+ expect(intersection([1, 2, 3], [2, 3, 4], (a, b) => a === b)).to.deep.eq([2, 3]);
39
+ expect(
40
+ intersection([{ x: 1 }, { x: 2 }, { x: 3 }], [{ x: 2 }, { x: 3 }, { x: 4 }], (a, b) => a.x === b.x),
41
+ ).to.deep.eq([{ x: 2 }, { x: 3 }]);
42
+ });
43
+ });
package/src/util.ts ADDED
@@ -0,0 +1,48 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export type Comparator<A, B = A> = (a: A, b: B) => boolean;
6
+
7
+ export type DiffResult<A, B = A> = {
8
+ added: B[];
9
+ updated: A[];
10
+ removed: A[];
11
+ };
12
+
13
+ /**
14
+ *
15
+ * @param previous
16
+ * @param next
17
+ * @param comparator
18
+ */
19
+ // TODO(burdon): Factor out.
20
+ export const diff = <A, B = A>(
21
+ previous: readonly A[],
22
+ next: readonly B[],
23
+ comparator: Comparator<A, B>,
24
+ ): DiffResult<A, B> => {
25
+ const remaining = [...previous];
26
+ const result: DiffResult<A, B> = {
27
+ added: [],
28
+ updated: [],
29
+ removed: remaining,
30
+ };
31
+
32
+ // TODO(burdon): Mark and sweep.
33
+ for (const object of next) {
34
+ const index = remaining.findIndex((item) => comparator(item, object));
35
+ if (index === -1) {
36
+ result.added.push(object);
37
+ } else {
38
+ result.updated.push(remaining[index]);
39
+ remaining.splice(index, 1);
40
+ }
41
+ }
42
+
43
+ return result;
44
+ };
45
+
46
+ // TODO(burdon): Factor out.
47
+ export const intersection = <A, B = A>(a: A[], b: B[], comparator: Comparator<A, B>): A[] =>
48
+ a.filter((a) => b.find((b) => comparator(a, b)) !== undefined);
@@ -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"}
File without changes