@dxos/app-graph 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6
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/dist/lib/browser/chunk-AKBGYELG.mjs +1603 -0
- package/dist/lib/browser/chunk-AKBGYELG.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +17 -1276
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +39 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs +1604 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +17 -1276
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +40 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/graph-builder.d.ts +11 -7
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +13 -17
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +43 -17
- package/dist/types/src/node-matcher.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +21 -5
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +39 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -26
- package/src/graph-builder.test.ts +569 -102
- package/src/graph-builder.ts +202 -74
- package/src/graph.test.ts +187 -52
- package/src/graph.ts +174 -98
- package/src/index.ts +1 -0
- package/src/node-matcher.ts +58 -28
- package/src/node.ts +46 -5
- package/src/stories/EchoGraph.stories.tsx +90 -61
- package/src/stories/Tree.tsx +1 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/setup-graph-builder.ts +41 -0
- package/src/util.ts +95 -0
package/src/graph-builder.ts
CHANGED
|
@@ -11,15 +11,25 @@ import * as Option from 'effect/Option';
|
|
|
11
11
|
import * as Pipeable from 'effect/Pipeable';
|
|
12
12
|
import * as Record from 'effect/Record';
|
|
13
13
|
import type * as Schema from 'effect/Schema';
|
|
14
|
+
import { scheduleTask, yieldOrContinue } from 'main-thread-scheduling';
|
|
14
15
|
|
|
15
16
|
import { type CleanupFn, type Trigger } from '@dxos/async';
|
|
16
17
|
import { type Entity, type Type } from '@dxos/echo';
|
|
17
18
|
import { log } from '@dxos/log';
|
|
18
|
-
import { type MaybePromise, type Position, byPosition, getDebugName,
|
|
19
|
+
import { type MaybePromise, type Position, byPosition, getDebugName, isNonNullable } from '@dxos/util';
|
|
19
20
|
|
|
20
21
|
import * as Graph from './graph';
|
|
21
22
|
import * as Node from './node';
|
|
22
23
|
import * as NodeMatcher from './node-matcher';
|
|
24
|
+
import {
|
|
25
|
+
getParentId,
|
|
26
|
+
nodeArgsUnchanged,
|
|
27
|
+
normalizeRelation,
|
|
28
|
+
primaryKey,
|
|
29
|
+
primaryParts,
|
|
30
|
+
qualifyId,
|
|
31
|
+
validateSegmentId,
|
|
32
|
+
} from './util';
|
|
23
33
|
|
|
24
34
|
//
|
|
25
35
|
// Extension Types
|
|
@@ -54,7 +64,7 @@ export type ActionGroupsExtension = (
|
|
|
54
64
|
export type BuilderExtension = Readonly<{
|
|
55
65
|
id: string;
|
|
56
66
|
position: Position;
|
|
57
|
-
relation?: Node.
|
|
67
|
+
relation?: Node.RelationInput;
|
|
58
68
|
resolver?: ResolverExtension;
|
|
59
69
|
connector?: (node: Atom.Atom<Option.Option<Node.Node>>) => Atom.Atom<Node.NodeArg<any>[]>;
|
|
60
70
|
}>;
|
|
@@ -69,7 +79,7 @@ export type GraphBuilderTraverseOptions = {
|
|
|
69
79
|
visitor: (node: Node.Node, path: string[]) => MaybePromise<boolean | void>;
|
|
70
80
|
registry?: Registry.Registry;
|
|
71
81
|
source?: string;
|
|
72
|
-
relation
|
|
82
|
+
relation: Node.RelationInput | Node.RelationInput[];
|
|
73
83
|
};
|
|
74
84
|
|
|
75
85
|
/**
|
|
@@ -103,13 +113,34 @@ class GraphBuilderImpl implements GraphBuilder {
|
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
// TODO(wittjosiah): Use Context.
|
|
116
|
+
/** Active subscriptions keyed by composite ID, cleaned up on node removal. */
|
|
106
117
|
readonly _subscriptions = new Map<string, CleanupFn>();
|
|
118
|
+
/** Connector updates pending flush, keyed by connector key. */
|
|
119
|
+
readonly _dirtyConnectors = new Map<
|
|
120
|
+
string,
|
|
121
|
+
{
|
|
122
|
+
nodes: Node.NodeArg<any>[];
|
|
123
|
+
previous: string[];
|
|
124
|
+
}
|
|
125
|
+
>();
|
|
126
|
+
/** Last-flushed node IDs per connector key, used for edge removal on update. */
|
|
127
|
+
readonly _connectorPrevious = new Map<string, string[]>();
|
|
128
|
+
/** Last-flushed node args per connector key, used for change detection. */
|
|
129
|
+
readonly _connectorPreviousArgs = new Map<string, Node.NodeArg<any>[]>();
|
|
130
|
+
/** Whether a dirty-flush task is already scheduled. */
|
|
131
|
+
_flushScheduled = false;
|
|
132
|
+
/** Resolves when the current flush completes. */
|
|
133
|
+
_flushPromise: Promise<void> = Promise.resolve();
|
|
134
|
+
/** Registered builder extensions keyed by extension ID. */
|
|
107
135
|
readonly _extensions = Atom.make(Record.empty<string, BuilderExtension>()).pipe(
|
|
108
136
|
Atom.keepAlive,
|
|
109
137
|
Atom.withLabel('graph-builder:extensions'),
|
|
110
138
|
);
|
|
139
|
+
/** Triggers signalling that a node's resolver has fired at least once. */
|
|
111
140
|
readonly _initialized: Record<string, Trigger> = {};
|
|
141
|
+
/** Shared atom registry for reactive subscriptions. */
|
|
112
142
|
readonly _registry: Registry.Registry;
|
|
143
|
+
/** Backing graph with internal accessors for node atoms and construction. */
|
|
113
144
|
readonly _graph: Graph.Graph & {
|
|
114
145
|
_node: (id: string) => Atom.Writable<Option.Option<Node.Node>>;
|
|
115
146
|
_constructNode: (node: Node.NodeArg<any>) => Option.Option<Node.Node>;
|
|
@@ -139,6 +170,56 @@ class GraphBuilderImpl implements GraphBuilder {
|
|
|
139
170
|
return this._extensions;
|
|
140
171
|
}
|
|
141
172
|
|
|
173
|
+
/** Apply a set of node changes for a single connector key. */
|
|
174
|
+
private _applyConnectorUpdate(key: string, nodes: Node.NodeArg<any>[], previous: string[]): void {
|
|
175
|
+
const { id, relation } = relationFromConnectorKey(key);
|
|
176
|
+
const ids = nodes.map((node) => node.id);
|
|
177
|
+
const removed = previous.filter((pid) => !ids.includes(pid));
|
|
178
|
+
this._connectorPrevious.set(key, ids);
|
|
179
|
+
this._connectorPreviousArgs.set(key, nodes);
|
|
180
|
+
|
|
181
|
+
Graph.removeEdges(
|
|
182
|
+
this._graph,
|
|
183
|
+
removed.map((target) => ({ source: id, target, relation })),
|
|
184
|
+
true,
|
|
185
|
+
);
|
|
186
|
+
Graph.addNodes(this._graph, nodes);
|
|
187
|
+
Graph.addEdges(
|
|
188
|
+
this._graph,
|
|
189
|
+
nodes.map((node) => ({ source: id, target: node.id, relation })),
|
|
190
|
+
);
|
|
191
|
+
if (ids.length > 0) {
|
|
192
|
+
const sortedIds = [...nodes]
|
|
193
|
+
.sort((a, b) =>
|
|
194
|
+
byPosition(a.properties ?? ({} as { position?: Position }), b.properties ?? ({} as { position?: Position })),
|
|
195
|
+
)
|
|
196
|
+
.map((n) => n.id);
|
|
197
|
+
Graph.sortEdges(this._graph, id, relation, sortedIds);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private _scheduleDirtyFlush(): void {
|
|
202
|
+
if (!this._flushScheduled) {
|
|
203
|
+
this._flushScheduled = true;
|
|
204
|
+
this._flushPromise = scheduleTask(
|
|
205
|
+
() => {
|
|
206
|
+
this._flushScheduled = false;
|
|
207
|
+
while (this._dirtyConnectors.size > 0) {
|
|
208
|
+
const entries = [...this._dirtyConnectors.entries()];
|
|
209
|
+
this._dirtyConnectors.clear();
|
|
210
|
+
|
|
211
|
+
Atom.batch(() => {
|
|
212
|
+
for (const [key, { nodes, previous }] of entries) {
|
|
213
|
+
this._applyConnectorUpdate(key, nodes, previous);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{ strategy: 'smooth' },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
142
223
|
private readonly _resolvers = Atom.family<string, Atom.Atom<Option.Option<Node.NodeArg<any>>>>((id) => {
|
|
143
224
|
return Atom.make((get) => {
|
|
144
225
|
return Function.pipe(
|
|
@@ -156,73 +237,72 @@ class GraphBuilderImpl implements GraphBuilder {
|
|
|
156
237
|
|
|
157
238
|
private readonly _connectors = Atom.family<string, Atom.Atom<Node.NodeArg<any>[]>>((key) => {
|
|
158
239
|
return Atom.make((get) => {
|
|
159
|
-
const
|
|
240
|
+
const { id, relation } = relationFromConnectorKey(key);
|
|
160
241
|
const node = this._graph.node(id);
|
|
161
242
|
|
|
162
|
-
|
|
243
|
+
const sourceNode = Option.getOrElse(get(node), () => undefined);
|
|
244
|
+
if (!sourceNode) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const extensions = Function.pipe(
|
|
163
249
|
get(this._extensions),
|
|
164
250
|
Record.values,
|
|
165
|
-
// TODO(wittjosiah): Sort on write rather than read.
|
|
166
251
|
Array.sortBy(byPosition),
|
|
167
|
-
Array.filter(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
252
|
+
Array.filter(
|
|
253
|
+
(ext): ext is BuilderExtension & { connector: NonNullable<BuilderExtension['connector']> } =>
|
|
254
|
+
Graph.relationKey(ext.relation ?? 'child') === Graph.relationKey(relation) && ext.connector != null,
|
|
255
|
+
),
|
|
171
256
|
);
|
|
257
|
+
|
|
258
|
+
const nodes: Node.NodeArg<any>[] = [];
|
|
259
|
+
for (const ext of extensions) {
|
|
260
|
+
const result = get(ext.connector(node));
|
|
261
|
+
nodes.push(...result);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return nodes;
|
|
172
265
|
}).pipe(Atom.withLabel(`graph-builder:connectors:${key}`));
|
|
173
266
|
});
|
|
174
267
|
|
|
175
268
|
private _onExpand(id: string, relation: Node.Relation): void {
|
|
176
269
|
log('onExpand', { id, relation, registry: getDebugName(this._registry) });
|
|
177
|
-
|
|
270
|
+
this._expandRelation(id, relation);
|
|
271
|
+
|
|
272
|
+
// TODO(wittjosiah): Remove. This is for backwards compatibility.
|
|
273
|
+
if (relation.kind === 'child' && relation.direction === 'outbound') {
|
|
274
|
+
Graph.expand(this._graph, id, 'action');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private _expandRelation(id: string, relation: Node.RelationInput): void {
|
|
279
|
+
const key = connectorKey(id, relation);
|
|
280
|
+
const connectors = this._connectors(key);
|
|
178
281
|
|
|
179
|
-
let previous: string[] = [];
|
|
180
282
|
const cancel = this._registry.subscribe(
|
|
181
283
|
connectors,
|
|
182
|
-
(
|
|
284
|
+
(rawNodes) => {
|
|
285
|
+
const nodes = qualifyNodeArgs(id)(rawNodes);
|
|
286
|
+
const previous = this._connectorPrevious.get(key) ?? [];
|
|
183
287
|
const ids = nodes.map((n) => n.id);
|
|
184
|
-
|
|
185
|
-
previous
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
Graph.removeEdges(
|
|
191
|
-
this._graph,
|
|
192
|
-
removed.map((target) => ({ source: id, target })),
|
|
193
|
-
true,
|
|
194
|
-
);
|
|
195
|
-
Graph.addNodes(this._graph, nodes);
|
|
196
|
-
Graph.addEdges(
|
|
197
|
-
this._graph,
|
|
198
|
-
nodes.map((node) =>
|
|
199
|
-
relation === 'outbound' ? { source: id, target: node.id } : { source: node.id, target: id },
|
|
200
|
-
),
|
|
201
|
-
);
|
|
202
|
-
Graph.sortEdges(
|
|
203
|
-
this._graph,
|
|
204
|
-
id,
|
|
205
|
-
relation,
|
|
206
|
-
nodes.map(({ id }) => id),
|
|
207
|
-
);
|
|
208
|
-
});
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// TODO(wittjosiah): Remove `requestAnimationFrame` once we have a better solution.
|
|
212
|
-
// This is a workaround to avoid a race condition where the graph is updated during React render.
|
|
213
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
214
|
-
requestAnimationFrame(update);
|
|
215
|
-
} else {
|
|
216
|
-
update();
|
|
288
|
+
|
|
289
|
+
if (ids.length === previous.length && ids.every((nodeId, idx) => nodeId === previous[idx])) {
|
|
290
|
+
const prevArgs = this._connectorPreviousArgs.get(key);
|
|
291
|
+
if (prevArgs && nodeArgsUnchanged(prevArgs, nodes)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
217
294
|
}
|
|
295
|
+
|
|
296
|
+
log('update', { id, relation, ids });
|
|
297
|
+
this._dirtyConnectors.set(key, { nodes, previous });
|
|
298
|
+
this._scheduleDirtyFlush();
|
|
218
299
|
},
|
|
219
300
|
{ immediate: true },
|
|
220
301
|
);
|
|
221
302
|
|
|
222
|
-
this._subscriptions.set(id, cancel);
|
|
303
|
+
this._subscriptions.set(subscriptionKey(id, 'expand', key), cancel);
|
|
223
304
|
}
|
|
224
305
|
|
|
225
|
-
// TODO(wittjosiah): If the same node is added by a connector, the resolver should probably cancel itself?
|
|
226
306
|
private async _onInitialize(id: string) {
|
|
227
307
|
log('onInitialize', { id });
|
|
228
308
|
const resolver = this._resolvers(id);
|
|
@@ -231,26 +311,40 @@ class GraphBuilderImpl implements GraphBuilder {
|
|
|
231
311
|
resolver,
|
|
232
312
|
(node) => {
|
|
233
313
|
const trigger = this._initialized[id];
|
|
314
|
+
const connectorOwned = [...this._connectorPrevious.values()].some((ids) => ids.includes(id));
|
|
234
315
|
Option.match(node, {
|
|
235
316
|
onSome: (node) => {
|
|
236
|
-
|
|
317
|
+
if (!connectorOwned) {
|
|
318
|
+
Graph.addNodes(this._graph, [node]);
|
|
319
|
+
// Connect resolved node to its parent via a child edge.
|
|
320
|
+
const parentId = getParentId(id);
|
|
321
|
+
if (parentId) {
|
|
322
|
+
Graph.addEdges(this._graph, [{ source: parentId, target: id, relation: 'child' }]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
237
325
|
trigger?.wake();
|
|
238
326
|
},
|
|
239
327
|
onNone: () => {
|
|
240
328
|
trigger?.wake();
|
|
241
|
-
|
|
329
|
+
if (!connectorOwned) {
|
|
330
|
+
Graph.removeNodes(this._graph, [id]);
|
|
331
|
+
}
|
|
242
332
|
},
|
|
243
333
|
});
|
|
244
334
|
},
|
|
245
335
|
{ immediate: true },
|
|
246
336
|
);
|
|
247
337
|
|
|
248
|
-
this._subscriptions.set(id, cancel);
|
|
338
|
+
this._subscriptions.set(subscriptionKey(id, 'init'), cancel);
|
|
249
339
|
}
|
|
250
340
|
|
|
251
341
|
private _onRemoveNode(id: string): void {
|
|
252
|
-
this._subscriptions
|
|
253
|
-
|
|
342
|
+
for (const [key, cleanup] of this._subscriptions) {
|
|
343
|
+
if (primaryParts(key)[0] === id) {
|
|
344
|
+
cleanup();
|
|
345
|
+
this._subscriptions.delete(key);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
254
348
|
}
|
|
255
349
|
}
|
|
256
350
|
|
|
@@ -344,18 +438,13 @@ const exploreImpl = async (
|
|
|
344
438
|
path: string[] = [],
|
|
345
439
|
): Promise<void> => {
|
|
346
440
|
const internal = builder as GraphBuilderImpl;
|
|
347
|
-
const { registry = Registry.make(), source = Node.RootId, relation
|
|
441
|
+
const { registry = Registry.make(), source = Node.RootId, relation, visitor } = options;
|
|
348
442
|
// Break cycles.
|
|
349
443
|
if (path.includes(source)) {
|
|
350
444
|
return;
|
|
351
445
|
}
|
|
352
446
|
|
|
353
|
-
|
|
354
|
-
// Switching to vitest is blocked by having node esm versions of echo-schema & echo-signals.
|
|
355
|
-
if (!isNode()) {
|
|
356
|
-
const { yieldOrContinue } = await import('main-thread-scheduling');
|
|
357
|
-
await yieldOrContinue('idle');
|
|
358
|
-
}
|
|
447
|
+
await yieldOrContinue('idle');
|
|
359
448
|
|
|
360
449
|
const node = registry.get(internal._graph.nodeOrThrow(source));
|
|
361
450
|
const shouldContinue = await visitor(node, [...path, node.id]);
|
|
@@ -363,11 +452,14 @@ const exploreImpl = async (
|
|
|
363
452
|
return;
|
|
364
453
|
}
|
|
365
454
|
|
|
366
|
-
const nodes =
|
|
367
|
-
.
|
|
368
|
-
.
|
|
369
|
-
.
|
|
370
|
-
.
|
|
455
|
+
const nodes = Function.pipe(
|
|
456
|
+
internal._registry.get(internal._extensions),
|
|
457
|
+
Record.values,
|
|
458
|
+
Array.map((extension) => extension.connector),
|
|
459
|
+
Array.filter(isNonNullable),
|
|
460
|
+
Array.flatMap((connector) => registry.get(connector(internal._graph.node(source)))),
|
|
461
|
+
qualifyNodeArgs(source),
|
|
462
|
+
);
|
|
371
463
|
|
|
372
464
|
await Promise.all(
|
|
373
465
|
nodes.map((nodeArg) => {
|
|
@@ -433,6 +525,13 @@ export function destroy(builder?: GraphBuilder): void | ((builder: GraphBuilder)
|
|
|
433
525
|
}
|
|
434
526
|
}
|
|
435
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Wait for all pending connector updates to be flushed.
|
|
530
|
+
*/
|
|
531
|
+
export const flush = (builder: GraphBuilder): Promise<void> => {
|
|
532
|
+
return (builder as GraphBuilderImpl)._flushPromise;
|
|
533
|
+
};
|
|
534
|
+
|
|
436
535
|
//
|
|
437
536
|
// Extension Creation
|
|
438
537
|
//
|
|
@@ -450,7 +549,7 @@ export function destroy(builder?: GraphBuilder): void | ((builder: GraphBuilder)
|
|
|
450
549
|
*/
|
|
451
550
|
export type CreateExtensionRawOptions = {
|
|
452
551
|
id: string;
|
|
453
|
-
relation?: Node.
|
|
552
|
+
relation?: Node.RelationInput;
|
|
454
553
|
position?: Position;
|
|
455
554
|
resolver?: ResolverExtension;
|
|
456
555
|
connector?: ConnectorExtension;
|
|
@@ -465,12 +564,13 @@ export const createExtensionRaw = (extension: CreateExtensionRawOptions): Builde
|
|
|
465
564
|
const {
|
|
466
565
|
id,
|
|
467
566
|
position = 'static',
|
|
468
|
-
relation = '
|
|
567
|
+
relation = 'child',
|
|
469
568
|
resolver: _resolver,
|
|
470
569
|
connector: _connector,
|
|
471
570
|
actions: _actions,
|
|
472
571
|
actionGroups: _actionGroups,
|
|
473
572
|
} = extension;
|
|
573
|
+
const normalizedRelation = normalizeRelation(relation);
|
|
474
574
|
const getId = (key: string) => `${id}/${key}`;
|
|
475
575
|
|
|
476
576
|
const resolver =
|
|
@@ -500,7 +600,7 @@ export const createExtensionRaw = (extension: CreateExtensionRawOptions): Builde
|
|
|
500
600
|
? ({
|
|
501
601
|
id: getId('connector'),
|
|
502
602
|
position,
|
|
503
|
-
relation,
|
|
603
|
+
relation: normalizedRelation,
|
|
504
604
|
connector: Atom.family((node) =>
|
|
505
605
|
Atom.make((get) => {
|
|
506
606
|
try {
|
|
@@ -517,7 +617,7 @@ export const createExtensionRaw = (extension: CreateExtensionRawOptions): Builde
|
|
|
517
617
|
? ({
|
|
518
618
|
id: getId('actionGroups'),
|
|
519
619
|
position,
|
|
520
|
-
relation:
|
|
620
|
+
relation: Node.actionRelation(),
|
|
521
621
|
connector: Atom.family((node) =>
|
|
522
622
|
Atom.make((get) => {
|
|
523
623
|
try {
|
|
@@ -538,7 +638,7 @@ export const createExtensionRaw = (extension: CreateExtensionRawOptions): Builde
|
|
|
538
638
|
? ({
|
|
539
639
|
id: getId('actions'),
|
|
540
640
|
position,
|
|
541
|
-
relation:
|
|
641
|
+
relation: Node.actionRelation(),
|
|
542
642
|
connector: Atom.family((node) =>
|
|
543
643
|
Atom.make((get) => {
|
|
544
644
|
try {
|
|
@@ -568,7 +668,7 @@ export type CreateExtensionOptions<TMatched = Node.Node, R = never> = {
|
|
|
568
668
|
) => Effect.Effect<Omit<Node.NodeArg<Node.ActionData<any>, any>, 'type'>[], Error, R>;
|
|
569
669
|
connector?: (matched: TMatched, get: Atom.Context) => Effect.Effect<Node.NodeArg<any, any>[], Error, R>;
|
|
570
670
|
resolver?: (id: string, get: Atom.Context) => Effect.Effect<Node.NodeArg<any, any> | null, Error, R>;
|
|
571
|
-
relation?: Node.
|
|
671
|
+
relation?: Node.RelationInput;
|
|
572
672
|
position?: Position;
|
|
573
673
|
};
|
|
574
674
|
|
|
@@ -685,7 +785,7 @@ const createConnectorWithRuntime = <TData, R>(
|
|
|
685
785
|
* All callbacks must return Effects for dependency injection.
|
|
686
786
|
* Effects may fail - errors are caught, logged, and the extension returns empty results.
|
|
687
787
|
*/
|
|
688
|
-
export type CreateTypeExtensionOptions<T extends Type.
|
|
788
|
+
export type CreateTypeExtensionOptions<T extends Type.AnyEntity = Type.AnyEntity, R = never> = {
|
|
689
789
|
id: string;
|
|
690
790
|
type: T;
|
|
691
791
|
actions?: (
|
|
@@ -696,7 +796,7 @@ export type CreateTypeExtensionOptions<T extends Type.Entity.Any = Type.Entity.A
|
|
|
696
796
|
object: Entity.Entity<Schema.Schema.Type<T>>,
|
|
697
797
|
get: Atom.Context,
|
|
698
798
|
) => Effect.Effect<Node.NodeArg<any>[], Error, R>;
|
|
699
|
-
relation?: Node.
|
|
799
|
+
relation?: Node.RelationInput;
|
|
700
800
|
position?: Position;
|
|
701
801
|
};
|
|
702
802
|
|
|
@@ -705,7 +805,7 @@ export type CreateTypeExtensionOptions<T extends Type.Entity.Any = Type.Entity.A
|
|
|
705
805
|
* The entity type is inferred from the schema type and works for both object and relation schemas.
|
|
706
806
|
* Returns an Effect to allow callbacks to access services via dependency injection.
|
|
707
807
|
*/
|
|
708
|
-
export const createTypeExtension = <T extends Type.
|
|
808
|
+
export const createTypeExtension = <T extends Type.AnyEntity, R = never>(
|
|
709
809
|
options: CreateTypeExtensionOptions<T, R>,
|
|
710
810
|
): Effect.Effect<BuilderExtension[], never, R> => {
|
|
711
811
|
const { id, type, actions, connector, relation, position } = options;
|
|
@@ -723,6 +823,34 @@ export const createTypeExtension = <T extends Type.Entity.Any, R = never>(
|
|
|
723
823
|
// Extension Utilities
|
|
724
824
|
//
|
|
725
825
|
|
|
826
|
+
/**
|
|
827
|
+
* Qualify node IDs by prefixing with the parent path.
|
|
828
|
+
* Validates that segment IDs do not contain the path separator.
|
|
829
|
+
* Recursively qualifies inline child nodes.
|
|
830
|
+
*/
|
|
831
|
+
const qualifyNodeArgs =
|
|
832
|
+
(parentId: string) =>
|
|
833
|
+
(nodes: Node.NodeArg<any>[]): Node.NodeArg<any>[] =>
|
|
834
|
+
nodes.map((node) => {
|
|
835
|
+
validateSegmentId(node.id);
|
|
836
|
+
const qualified = qualifyId(parentId, node.id);
|
|
837
|
+
return {
|
|
838
|
+
...node,
|
|
839
|
+
id: qualified,
|
|
840
|
+
nodes: node.nodes ? qualifyNodeArgs(qualified)(node.nodes) : undefined,
|
|
841
|
+
};
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
const connectorKey = (id: string, relation: Node.RelationInput): string => primaryKey(id, Graph.relationKey(relation));
|
|
845
|
+
|
|
846
|
+
const relationFromConnectorKey = (key: string): { id: string; relation: Node.Relation } => {
|
|
847
|
+
const [id, encodedRelation] = primaryParts(key);
|
|
848
|
+
return { id, relation: Graph.relationFromKey(encodedRelation) };
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const subscriptionKey = (id: string, kind: string, detail?: string): string =>
|
|
852
|
+
detail != null ? primaryKey(id, kind, detail) : primaryKey(id, kind);
|
|
853
|
+
|
|
726
854
|
export const flattenExtensions = (extension: BuilderExtensions, acc: BuilderExtension[] = []): BuilderExtension[] => {
|
|
727
855
|
if (Array.isArray(extension)) {
|
|
728
856
|
return [...acc, ...extension.flatMap((ext) => flattenExtensions(ext, acc))];
|