@dxos/app-graph 0.8.3 → 0.8.4-main.1da679c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/app-graph",
3
- "version": "0.8.3",
3
+ "version": "0.8.4-main.1da679c",
4
4
  "description": "Constructs knowledge graphs for the purpose of building applications on top of",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -10,6 +10,7 @@
10
10
  "type": "module",
11
11
  "exports": {
12
12
  ".": {
13
+ "source": "./src/index.ts",
13
14
  "types": "./dist/types/src/index.d.ts",
14
15
  "browser": "./dist/lib/browser/index.mjs",
15
16
  "node": "./dist/lib/node-esm/index.mjs"
@@ -26,44 +27,42 @@
26
27
  "dependencies": {
27
28
  "@preact/signals-core": "^1.9.0",
28
29
  "main-thread-scheduling": "^14.1.1",
29
- "@dxos/async": "0.8.3",
30
- "@dxos/debug": "0.8.3",
31
- "@dxos/echo": "0.8.3",
32
- "@dxos/invariant": "0.8.3",
33
- "@dxos/echo-signals": "0.8.3",
34
- "@dxos/echo-schema": "0.8.3",
35
- "@dxos/live-object": "0.8.3",
36
- "@dxos/util": "0.8.3",
37
- "@dxos/log": "0.8.3"
30
+ "@dxos/async": "0.8.4-main.1da679c",
31
+ "@dxos/echo": "0.8.4-main.1da679c",
32
+ "@dxos/debug": "0.8.4-main.1da679c",
33
+ "@dxos/echo-schema": "0.8.4-main.1da679c",
34
+ "@dxos/echo-signals": "0.8.4-main.1da679c",
35
+ "@dxos/invariant": "0.8.4-main.1da679c",
36
+ "@dxos/live-object": "0.8.4-main.1da679c",
37
+ "@dxos/log": "0.8.4-main.1da679c",
38
+ "@dxos/util": "0.8.4-main.1da679c"
38
39
  },
39
40
  "devDependencies": {
40
- "@effect-rx/rx-react": "^0.34.1",
41
- "@effect/platform": "0.80.12",
42
- "@phosphor-icons/react": "^2.1.5",
41
+ "@effect-rx/rx-react": "0.38.0",
42
+ "@effect/platform": "0.90.2",
43
43
  "@types/react": "~18.2.0",
44
44
  "@types/react-dom": "~18.2.0",
45
- "effect": "3.14.21",
45
+ "effect": "3.17.7",
46
46
  "react": "~18.2.0",
47
47
  "react-dom": "~18.2.0",
48
- "vite": "5.4.7",
49
- "@dxos/random": "0.8.3",
50
- "@dxos/react-client": "0.8.3",
51
- "@dxos/react-ui": "0.8.3",
52
- "@dxos/react-ui-list": "0.8.3",
53
- "@dxos/echo-db": "0.8.3",
54
- "@dxos/react-ui-tabs": "0.8.3",
55
- "@dxos/react-ui-theme": "0.8.3",
56
- "@dxos/storybook-utils": "0.8.3"
48
+ "vite": "7.1.1",
49
+ "@dxos/random": "0.8.4-main.1da679c",
50
+ "@dxos/echo-db": "0.8.4-main.1da679c",
51
+ "@dxos/react-client": "0.8.4-main.1da679c",
52
+ "@dxos/react-ui": "0.8.4-main.1da679c",
53
+ "@dxos/react-ui-list": "0.8.4-main.1da679c",
54
+ "@dxos/react-ui-tabs": "0.8.4-main.1da679c",
55
+ "@dxos/react-ui-theme": "0.8.4-main.1da679c",
56
+ "@dxos/storybook-utils": "0.8.4-main.1da679c"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@effect-rx/rx-react": "^0.34.1",
60
- "@effect/platform": "0.80.12",
61
- "@phosphor-icons/react": "^2.1.5",
60
+ "@effect/platform": "^0.80.12",
62
61
  "effect": "3.14.21",
63
62
  "react": "~18.2.0",
64
63
  "react-dom": "~18.2.0",
65
- "@dxos/react-ui": "0.8.3",
66
- "@dxos/react-ui-theme": "0.8.3"
64
+ "@dxos/react-ui": "0.8.4-main.1da679c",
65
+ "@dxos/react-ui-theme": "0.8.4-main.1da679c"
67
66
  },
68
67
  "publishConfig": {
69
68
  "access": "public"
@@ -6,10 +6,10 @@ import { Registry, Rx } from '@effect-rx/rx-react';
6
6
  import { Option, pipe } from 'effect';
7
7
  import { describe, expect, onTestFinished, test } from 'vitest';
8
8
 
9
- import { sleep, Trigger } from '@dxos/async';
9
+ import { Trigger, sleep } from '@dxos/async';
10
10
 
11
11
  import { ROOT_ID } from './graph';
12
- import { createExtension, GraphBuilder } from './graph-builder';
12
+ import { GraphBuilder, createExtension } from './graph-builder';
13
13
  import { type Node } from './node';
14
14
 
15
15
  const exampleId = (id: number) => `dx:test:${id}`;
@@ -17,6 +17,63 @@ const EXAMPLE_ID = exampleId(1);
17
17
  const EXAMPLE_TYPE = 'dxos.org/type/example';
18
18
 
19
19
  describe('GraphBuilder', () => {
20
+ describe('resolver', () => {
21
+ test('works', async () => {
22
+ const registry = Registry.make();
23
+ const builder = new GraphBuilder({ registry });
24
+ const graph = builder.graph;
25
+
26
+ {
27
+ const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
28
+ expect(node).to.be.null;
29
+ }
30
+
31
+ builder.addExtension(
32
+ createExtension({
33
+ id: 'resolver',
34
+ resolver: () => {
35
+ console.log('resolver');
36
+ return Rx.make({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: 1 });
37
+ },
38
+ }),
39
+ );
40
+ await graph.initialize(EXAMPLE_ID);
41
+
42
+ {
43
+ const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
44
+ expect(node?.id).to.equal(EXAMPLE_ID);
45
+ expect(node?.type).to.equal(EXAMPLE_TYPE);
46
+ expect(node?.data).to.equal(1);
47
+ }
48
+ });
49
+
50
+ test('updates', async () => {
51
+ const registry = Registry.make();
52
+ const builder = new GraphBuilder({ registry });
53
+ const name = Rx.make('default');
54
+ builder.addExtension(
55
+ createExtension({
56
+ id: 'resolver',
57
+ resolver: () => Rx.make((get) => ({ id: EXAMPLE_ID, type: EXAMPLE_TYPE, data: get(name) })),
58
+ }),
59
+ );
60
+ const graph = builder.graph;
61
+ await graph.initialize(EXAMPLE_ID);
62
+
63
+ {
64
+ const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
65
+ expect(node?.data).to.equal('default');
66
+ }
67
+
68
+ registry.set(name, 'updated');
69
+
70
+ {
71
+ const node = graph.getNode(EXAMPLE_ID).pipe(Option.getOrNull);
72
+ expect(node?.data).to.equal('updated');
73
+ }
74
+ });
75
+ });
76
+
20
77
  describe('connector', () => {
21
78
  test('works', () => {
22
79
  const registry = Registry.make();
@@ -4,14 +4,19 @@
4
4
 
5
5
  import { Registry, Rx } from '@effect-rx/rx-react';
6
6
  import { effect } from '@preact/signals-core';
7
- import { Array, type Option, pipe, Record } from 'effect';
7
+ import { Array, Option, Record, pipe } from 'effect';
8
8
 
9
- import { type MulticastObservable, type CleanupFn } from '@dxos/async';
9
+ import { type CleanupFn, type MulticastObservable, type Trigger } from '@dxos/async';
10
10
  import { log } from '@dxos/log';
11
- import { byPosition, getDebugName, isNode, isNonNullable, type MaybePromise, type Position } from '@dxos/util';
11
+ import { type MaybePromise, type Position, byPosition, getDebugName, isNode, isNonNullable } from '@dxos/util';
12
12
 
13
- import { ACTION_GROUP_TYPE, ACTION_TYPE, Graph, ROOT_ID, type GraphParams, type ExpandableGraph } from './graph';
14
- import { actionGroupSymbol, type ActionData, type Node, type NodeArg, type Relation } from './node';
13
+ import { ACTION_GROUP_TYPE, ACTION_TYPE, type ExpandableGraph, Graph, type GraphParams, ROOT_ID } from './graph';
14
+ import { type ActionData, type Node, type NodeArg, type Relation, actionGroupSymbol } from './node';
15
+
16
+ /**
17
+ * Graph builder extension for adding nodes to the graph based on a node id.
18
+ */
19
+ export type ResolverExtension = (id: string) => Rx.Rx<NodeArg<any> | null>;
15
20
 
16
21
  /**
17
22
  * Graph builder extension for adding nodes to the graph based on a connection to an existing node.
@@ -49,8 +54,7 @@ export type CreateExtensionOptions = {
49
54
  id: string;
50
55
  relation?: Relation;
51
56
  position?: Position;
52
- // TODO(wittjosiah): On initialize to restore state from cache.
53
- // resolver?: ResolverExtension;
57
+ resolver?: ResolverExtension;
54
58
  connector?: ConnectorExtension;
55
59
  actions?: ActionsExtension;
56
60
  actionGroups?: ActionGroupsExtension;
@@ -64,12 +68,16 @@ export const createExtension = (extension: CreateExtensionOptions): BuilderExten
64
68
  id,
65
69
  position = 'static',
66
70
  relation = 'outbound',
71
+ resolver: _resolver,
67
72
  connector: _connector,
68
73
  actions: _actions,
69
74
  actionGroups: _actionGroups,
70
75
  } = extension;
71
76
  const getId = (key: string) => `${id}/${key}`;
72
77
 
78
+ const resolver =
79
+ _resolver && Rx.family((id: string) => _resolver(id).pipe(Rx.withLabel(`graph-builder:_resolver:${id}`)));
80
+
73
81
  const connector =
74
82
  _connector &&
75
83
  Rx.family((node: Rx.Rx<Option.Option<Node>>) =>
@@ -87,7 +95,7 @@ export const createExtension = (extension: CreateExtensionOptions): BuilderExten
87
95
  Rx.family((node: Rx.Rx<Option.Option<Node>>) => _actions(node).pipe(Rx.withLabel(`graph-builder:_actions:${id}`)));
88
96
 
89
97
  return [
90
- // resolver ? { id: getId('resolver'), position, resolver } : undefined,
98
+ resolver ? { id: getId('resolver'), position, resolver } : undefined,
91
99
  connector
92
100
  ? ({
93
101
  id: getId('connector'),
@@ -157,7 +165,7 @@ export type BuilderExtension = Readonly<{
157
165
  id: string;
158
166
  position: Position;
159
167
  relation?: Relation; // Only for connector.
160
- // resolver?: ResolverExtension;
168
+ resolver?: ResolverExtension;
161
169
  connector?: (node: Rx.Rx<Option.Option<Node>>) => Rx.Rx<NodeArg<any>[]>;
162
170
  }>;
163
171
 
@@ -179,12 +187,12 @@ export const flattenExtensions = (extension: BuilderExtensions, acc: BuilderExte
179
187
  // Should track LRU nodes that are not in the set/radius and remove them beyond a certain threshold.
180
188
  export class GraphBuilder {
181
189
  // TODO(wittjosiah): Use Context.
182
- private readonly _connectorSubscriptions = new Map<string, CleanupFn>();
190
+ private readonly _subscriptions = new Map<string, CleanupFn>();
183
191
  private readonly _extensions = Rx.make(Record.empty<string, BuilderExtension>()).pipe(
184
192
  Rx.keepAlive,
185
193
  Rx.withLabel('graph-builder:extensions'),
186
194
  );
187
-
195
+ private readonly _initialized: Record<string, Trigger> = {};
188
196
  private readonly _registry: Registry.Registry;
189
197
  private readonly _graph: Graph;
190
198
 
@@ -194,7 +202,7 @@ export class GraphBuilder {
194
202
  ...params,
195
203
  registry: this._registry,
196
204
  onExpand: (id, relation) => this._onExpand(id, relation),
197
- // onInitialize: (id) => this._onInitialize(id),
205
+ onInitialize: (id) => this._onInitialize(id),
198
206
  onRemoveNode: (id) => this._onRemoveNode(id),
199
207
  });
200
208
  }
@@ -275,10 +283,25 @@ export class GraphBuilder {
275
283
  }
276
284
 
277
285
  destroy(): void {
278
- this._connectorSubscriptions.forEach((unsubscribe) => unsubscribe());
279
- this._connectorSubscriptions.clear();
286
+ this._subscriptions.forEach((unsubscribe) => unsubscribe());
287
+ this._subscriptions.clear();
280
288
  }
281
289
 
290
+ private readonly _resolvers = Rx.family<string, Rx.Rx<Option.Option<NodeArg<any>>>>((id) => {
291
+ return Rx.make((get) => {
292
+ return pipe(
293
+ get(this._extensions),
294
+ Record.values,
295
+ Array.sortBy(byPosition),
296
+ Array.map(({ resolver }) => resolver),
297
+ Array.filter(isNonNullable),
298
+ Array.map((resolver) => get(resolver(id))),
299
+ Array.filter(isNonNullable),
300
+ Array.head,
301
+ );
302
+ });
303
+ });
304
+
282
305
  private readonly _connectors = Rx.family<string, Rx.Rx<NodeArg<any>[]>>((key) => {
283
306
  return Rx.make((get) => {
284
307
  const [id, relation] = key.split('+');
@@ -341,17 +364,38 @@ export class GraphBuilder {
341
364
  { immediate: true },
342
365
  );
343
366
 
344
- this._connectorSubscriptions.set(id, cancel);
367
+ this._subscriptions.set(id, cancel);
345
368
  }
346
369
 
347
- // TODO(wittjosiah): On initialize to restore state from cache.
348
- // private async _onInitialize(id: string) {
349
- // log('onInitialize', { id });
350
- // }
370
+ // TODO(wittjosiah): If the same node is added by a connector, the resolver should probably cancel itself?
371
+ private async _onInitialize(id: string) {
372
+ log('onInitialize', { id });
373
+ const resolver = this._resolvers(id);
374
+
375
+ const cancel = this._registry.subscribe(
376
+ resolver,
377
+ (node) => {
378
+ const trigger = this._initialized[id];
379
+ Option.match(node, {
380
+ onSome: (node) => {
381
+ this._graph.addNodes([node]);
382
+ trigger?.wake();
383
+ },
384
+ onNone: () => {
385
+ trigger?.wake();
386
+ this._graph.removeNodes([id]);
387
+ },
388
+ });
389
+ },
390
+ { immediate: true },
391
+ );
392
+
393
+ this._subscriptions.set(id, cancel);
394
+ }
351
395
 
352
396
  private _onRemoveNode(id: string): void {
353
- this._connectorSubscriptions.get(id)?.();
354
- this._connectorSubscriptions.delete(id);
397
+ this._subscriptions.get(id)?.();
398
+ this._subscriptions.delete(id);
355
399
  }
356
400
  }
357
401
 
package/src/graph.test.ts CHANGED
@@ -6,7 +6,7 @@ import { Registry, Rx } from '@effect-rx/rx-react';
6
6
  import { Option } from 'effect';
7
7
  import { assert, describe, expect, onTestFinished, test } from 'vitest';
8
8
 
9
- import { getGraph, Graph, ROOT_ID, ROOT_TYPE } from './graph';
9
+ import { Graph, ROOT_ID, ROOT_TYPE, getGraph } from './graph';
10
10
  import { type Node } from './node';
11
11
 
12
12
  const exampleId = (id: number) => `dx:test:${id}`;
package/src/graph.ts CHANGED
@@ -3,15 +3,15 @@
3
3
  //
4
4
 
5
5
  import { Registry, Rx } from '@effect-rx/rx-react';
6
- import { Option, pipe, Record } from 'effect';
6
+ import { Option, Record, pipe } from 'effect';
7
7
 
8
8
  import { Event, Trigger } from '@dxos/async';
9
9
  import { todo } from '@dxos/debug';
10
10
  import { invariant } from '@dxos/invariant';
11
11
  import { log } from '@dxos/log';
12
- import { isNonNullable, type MakeOptional } from '@dxos/util';
12
+ import { type MakeOptional, isNonNullable } from '@dxos/util';
13
13
 
14
- import { type NodeArg, type Node, type Relation, type Action, type ActionGroup } from './node';
14
+ import { type Action, type ActionGroup, type Node, type NodeArg, type Relation } from './node';
15
15
 
16
16
  const graphSymbol = Symbol('graph');
17
17
  type DeepWriteable<T> = { -readonly [K in keyof T]: T[K] extends object ? DeepWriteable<T[K]> : T[K] };
@@ -59,8 +59,7 @@ export type GraphParams = {
59
59
  nodes?: MakeOptional<Node, 'data' | 'cacheable'>[];
60
60
  edges?: Record<string, Edges>;
61
61
  onExpand?: Graph['_onExpand'];
62
- // TODO(wittjosiah): On initialize to restore state from cache.
63
- // onInitialize?: Graph['_onInitialize'];
62
+ onInitialize?: Graph['_onInitialize'];
64
63
  onRemoveNode?: Graph['_onRemoveNode'];
65
64
  };
66
65
 
@@ -166,7 +165,7 @@ export interface ExpandableGraph extends ReadableGraph {
166
165
  *
167
166
  * Fires the `onInitialize` callback to provide initial data for a node.
168
167
  */
169
- // initialize(id: string): Promise<void>;
168
+ initialize(id: string): Promise<void>;
170
169
 
171
170
  /**
172
171
  * Expand a node in the graph.
@@ -230,7 +229,7 @@ export class Graph implements WritableGraph {
230
229
  readonly onNodeChanged = new Event<{ id: string; node: Option.Option<Node> }>();
231
230
 
232
231
  private readonly _onExpand?: (id: string, relation: Relation) => void;
233
- // private readonly _onInitialize?: (id: string) => Promise<void>;
232
+ private readonly _onInitialize?: (id: string) => Promise<void>;
234
233
  private readonly _onRemoveNode?: (id: string) => void;
235
234
 
236
235
  private readonly _registry: Registry.Registry;
@@ -309,8 +308,9 @@ export class Graph implements WritableGraph {
309
308
  }).pipe(Rx.withLabel(`graph:json:${id}`));
310
309
  });
311
310
 
312
- constructor({ registry, nodes, edges, onExpand, onRemoveNode }: GraphParams = {}) {
311
+ constructor({ registry, nodes, edges, onInitialize, onExpand, onRemoveNode }: GraphParams = {}) {
313
312
  this._registry = registry ?? Registry.make();
313
+ this._onInitialize = onInitialize;
314
314
  this._onExpand = onExpand;
315
315
  this._onRemoveNode = onRemoveNode;
316
316
 
@@ -379,15 +379,14 @@ export class Graph implements WritableGraph {
379
379
  return this._registry.get(this.edges(id));
380
380
  }
381
381
 
382
- // TODO(wittjosiah): On initialize to restore state from cache.
383
- // async initialize(id: string) {
384
- // const initialized = Record.get(this._initialized, id).pipe(Option.getOrElse(() => false));
385
- // log('initialize', { id, initialized });
386
- // if (!initialized) {
387
- // await this._onInitialize?.(id);
388
- // Record.set(this._initialized, id, true);
389
- // }
390
- // }
382
+ async initialize(id: string) {
383
+ const initialized = Record.get(this._initialized, id).pipe(Option.getOrElse(() => false));
384
+ log('initialize', { id, initialized });
385
+ if (!initialized) {
386
+ await this._onInitialize?.(id);
387
+ Record.set(this._initialized, id, true);
388
+ }
389
+ }
391
390
 
392
391
  expand(id: string, relation: Relation = 'outbound'): void {
393
392
  const key = `${id}$${relation}`;
package/src/node.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type MaybePromise, type MakeOptional } from '@dxos/util';
5
+ import { type MakeOptional, type MaybePromise } from '@dxos/util';
6
+
7
+ import { ACTION_GROUP_TYPE, ACTION_TYPE } from './graph';
6
8
 
7
9
  /**
8
10
  * Represents a node in the graph.
@@ -84,7 +86,7 @@ export type Action<TProperties extends Record<string, any> = Record<string, any>
84
86
  >;
85
87
 
86
88
  export const isAction = (data: unknown): data is Action =>
87
- isGraphNode(data) ? typeof data.data === 'function' : false;
89
+ isGraphNode(data) ? typeof data.data === 'function' && data.type === ACTION_TYPE : false;
88
90
 
89
91
  export const actionGroupSymbol = Symbol('ActionGroup');
90
92
 
@@ -95,7 +97,7 @@ export type ActionGroup<TProperties extends Record<string, any> = Record<string,
95
97
  >;
96
98
 
97
99
  export const isActionGroup = (data: unknown): data is ActionGroup =>
98
- isGraphNode(data) ? data.data === actionGroupSymbol : false;
100
+ isGraphNode(data) ? data.data === actionGroupSymbol && data.type === ACTION_GROUP_TYPE : false;
99
101
 
100
102
  export type ActionLike = Action | ActionGroup;
101
103
 
@@ -14,7 +14,7 @@ import { registerSignalsRuntime } from '@dxos/echo-signals';
14
14
  import { live } from '@dxos/live-object';
15
15
 
16
16
  import { ROOT_ID } from './graph';
17
- import { createExtension, GraphBuilder, rxFromSignal } from './graph-builder';
17
+ import { GraphBuilder, createExtension, rxFromSignal } from './graph-builder';
18
18
  import { rxFromQuery } from './testing';
19
19
 
20
20
  registerSignalsRuntime();