@angflow/angular 0.0.15 → 0.0.17
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/esm/lib/agent/agent-bridge.service.d.ts +26 -5
- package/dist/esm/lib/agent/agent-bridge.service.d.ts.map +1 -1
- package/dist/esm/lib/agent/agent-bridge.service.js +638 -34
- package/dist/esm/lib/agent/agent-bridge.service.js.map +1 -1
- package/dist/esm/lib/agent/history.d.ts +45 -0
- package/dist/esm/lib/agent/history.d.ts.map +1 -0
- package/dist/esm/lib/agent/history.js +89 -0
- package/dist/esm/lib/agent/history.js.map +1 -0
- package/dist/esm/lib/agent/index.d.ts +2 -1
- package/dist/esm/lib/agent/index.d.ts.map +1 -1
- package/dist/esm/lib/agent/index.js.map +1 -1
- package/dist/esm/lib/agent/provide-agent-bridge.d.ts +20 -0
- package/dist/esm/lib/agent/provide-agent-bridge.d.ts.map +1 -1
- package/dist/esm/lib/agent/provide-agent-bridge.js +6 -1
- package/dist/esm/lib/agent/provide-agent-bridge.js.map +1 -1
- package/dist/esm/lib/agent/tool-schemas.d.ts.map +1 -1
- package/dist/esm/lib/agent/tool-schemas.js +401 -0
- package/dist/esm/lib/agent/tool-schemas.js.map +1 -1
- package/dist/esm/lib/agent/transports/window.d.ts.map +1 -1
- package/dist/esm/lib/agent/transports/window.js +9 -1
- package/dist/esm/lib/agent/transports/window.js.map +1 -1
- package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.d.ts +2 -2
- package/dist/esm/lib/components/handle/handle.component.d.ts.map +1 -1
- package/dist/esm/lib/components/handle/handle.component.js +25 -11
- package/dist/esm/lib/components/handle/handle.component.js.map +1 -1
- package/dist/esm/lib/components/nodes/default-node.component.js +4 -4
- package/dist/esm/lib/components/nodes/input-node.component.js +2 -2
- package/dist/esm/lib/components/nodes/output-node.component.js +2 -2
- package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts +14 -0
- package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts.map +1 -1
- package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js +66 -19
- package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js.map +1 -1
- package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts +1 -1
- package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts.map +1 -1
- package/dist/esm/lib/container/ng-flow/ng-flow.component.js +2 -2
- package/dist/esm/lib/container/ng-flow/ng-flow.component.js.map +1 -1
- package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts +2 -0
- package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts.map +1 -1
- package/dist/esm/lib/container/node-renderer/node-renderer.component.js +35 -10
- package/dist/esm/lib/container/node-renderer/node-renderer.component.js.map +1 -1
- package/dist/esm/lib/directives/drag.directive.d.ts.map +1 -1
- package/dist/esm/lib/directives/drag.directive.js +9 -1
- package/dist/esm/lib/directives/drag.directive.js.map +1 -1
- package/dist/esm/lib/services/flow-store.service.d.ts.map +1 -1
- package/dist/esm/lib/services/flow-store.service.js +5 -0
- package/dist/esm/lib/services/flow-store.service.js.map +1 -1
- package/dist/esm/lib/services/ng-flow.service.d.ts +24 -0
- package/dist/esm/lib/services/ng-flow.service.d.ts.map +1 -1
- package/dist/esm/lib/services/ng-flow.service.js +39 -0
- package/dist/esm/lib/services/ng-flow.service.js.map +1 -1
- package/dist/esm/lib/types/edges.d.ts +14 -2
- package/dist/esm/lib/types/edges.d.ts.map +1 -1
- package/dist/esm/lib/types/general.d.ts +10 -0
- package/dist/esm/lib/types/general.d.ts.map +1 -1
- package/dist/esm/lib/types/nodes.d.ts +1 -9
- package/dist/esm/lib/types/nodes.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { Inject, Injectable, InjectionToken, Injector, Optional, effect, inject, runInInjectionContext, signal, } from '@angular/core';
|
|
2
|
+
import { AgentHistory } from './history';
|
|
2
3
|
import { AGENT_TOOL_SCHEMAS } from './tool-schemas';
|
|
3
4
|
import * as i0 from "@angular/core";
|
|
4
5
|
/** Provider token holding the user-supplied transport(s). */
|
|
5
6
|
export const AGENT_TRANSPORTS = new InjectionToken('AngflowAgentTransports');
|
|
7
|
+
/** Provider token for history config. `false` disables history entirely. */
|
|
8
|
+
export const AGENT_HISTORY_OPTIONS = new InjectionToken('AngflowAgentHistoryOptions');
|
|
9
|
+
/** Optional error sink. Receives transport/dispatch failures that the bridge swallows. */
|
|
10
|
+
export const AGENT_ON_ERROR = new InjectionToken('AngflowAgentOnError');
|
|
6
11
|
const ERROR_INVALID_PARAMS = -32602;
|
|
7
12
|
const ERROR_METHOD_NOT_FOUND = -32601;
|
|
8
13
|
const ERROR_FLOW_NOT_FOUND = -32000;
|
|
9
14
|
const ERROR_INTERNAL = -32603;
|
|
15
|
+
const MUTATING_TOOLS = new Set([
|
|
16
|
+
'add_node',
|
|
17
|
+
'add_nodes',
|
|
18
|
+
'add_edge',
|
|
19
|
+
'add_edges',
|
|
20
|
+
'update_node',
|
|
21
|
+
'update_node_data',
|
|
22
|
+
'update_edge',
|
|
23
|
+
'update_edge_data',
|
|
24
|
+
'delete_elements',
|
|
25
|
+
'set_nodes',
|
|
26
|
+
'set_edges',
|
|
27
|
+
]);
|
|
10
28
|
/**
|
|
11
29
|
* Routes JSON-RPC requests from one or more transports to registered
|
|
12
30
|
* `NgFlowService` instances, and pushes change events back to the agent.
|
|
@@ -32,19 +50,34 @@ const ERROR_INTERNAL = -32603;
|
|
|
32
50
|
* ```
|
|
33
51
|
*/
|
|
34
52
|
export class AngflowAgentBridge {
|
|
35
|
-
constructor(transports) {
|
|
53
|
+
constructor(transports, historyOptions, onError) {
|
|
36
54
|
/** Schemas for every exposed tool — feed straight to a Claude / OpenAI tools array. */
|
|
37
55
|
this.toolSchemas = AGENT_TOOL_SCHEMAS;
|
|
38
56
|
this.flows = new Map();
|
|
39
57
|
this.handlers = new Map();
|
|
40
58
|
this.injector = inject(Injector);
|
|
41
59
|
this.started = false;
|
|
60
|
+
this.nextInProcessId = 1;
|
|
61
|
+
this.warnedOnBeforeDeleteBypass = false;
|
|
42
62
|
/** Bumped every time a flow registers/unregisters. Useful for diagnostics. */
|
|
43
63
|
this.registeredFlows = signal([], ...(ngDevMode ? [{ debugName: "registeredFlows" }] : /* istanbul ignore next */ []));
|
|
44
64
|
this.transports = transports ?? [];
|
|
65
|
+
this.history =
|
|
66
|
+
historyOptions === false ? null : new AgentHistory(historyOptions ?? undefined);
|
|
67
|
+
this.onError = onError ?? null;
|
|
45
68
|
this.installHandlers();
|
|
46
69
|
this.start();
|
|
47
70
|
}
|
|
71
|
+
reportError(err, ctx) {
|
|
72
|
+
if (!this.onError)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
this.onError(err, ctx);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// An onError handler must not crash the bridge.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
48
81
|
/**
|
|
49
82
|
* Register a flow under `id`. The bridge subscribes to its `nodes`,
|
|
50
83
|
* `edges`, and `viewport` signals and emits change events to all
|
|
@@ -55,11 +88,21 @@ export class AngflowAgentBridge {
|
|
|
55
88
|
* `(init)` on `<ng-flow>` and pair with `OnDestroy`.
|
|
56
89
|
*/
|
|
57
90
|
register(id, flow) {
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
const existing = this.flows.get(id);
|
|
92
|
+
if (existing) {
|
|
93
|
+
// Idempotent re-register: same id + same service is a no-op so callers
|
|
94
|
+
// can safely call from (init) without tracking lifecycle.
|
|
95
|
+
if (existing.service === flow) {
|
|
96
|
+
return () => this.unregister(id);
|
|
97
|
+
}
|
|
98
|
+
// Different service under the same id: tear down the old watcher and
|
|
99
|
+
// drop history (the previous snapshots refer to a different graph and
|
|
100
|
+
// restoring them into this service would corrupt it).
|
|
101
|
+
existing.dispose();
|
|
102
|
+
this.history?.dropFlow(id);
|
|
60
103
|
}
|
|
61
|
-
const
|
|
62
|
-
this.flows.set(id, { service: flow,
|
|
104
|
+
const dispose = this.watchFlow(id, flow);
|
|
105
|
+
this.flows.set(id, { service: flow, dispose });
|
|
63
106
|
this.registeredFlows.set(Array.from(this.flows.keys()));
|
|
64
107
|
this.emit({ event: 'flow.registered', params: { flowId: id } });
|
|
65
108
|
return () => this.unregister(id);
|
|
@@ -68,8 +111,9 @@ export class AngflowAgentBridge {
|
|
|
68
111
|
const entry = this.flows.get(id);
|
|
69
112
|
if (!entry)
|
|
70
113
|
return;
|
|
71
|
-
entry.
|
|
114
|
+
entry.dispose();
|
|
72
115
|
this.flows.delete(id);
|
|
116
|
+
this.history?.dropFlow(id);
|
|
73
117
|
this.registeredFlows.set(Array.from(this.flows.keys()));
|
|
74
118
|
this.emit({ event: 'flow.unregistered', params: { flowId: id } });
|
|
75
119
|
}
|
|
@@ -78,20 +122,24 @@ export class AngflowAgentBridge {
|
|
|
78
122
|
return this.flows.get(id)?.service;
|
|
79
123
|
}
|
|
80
124
|
/**
|
|
81
|
-
* Invoke a tool directly without going through a transport.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
125
|
+
* Invoke a tool directly without going through a transport. Behaves
|
|
126
|
+
* identically to a JSON-RPC request: captures a history snapshot, emits
|
|
127
|
+
* `flow.history` / `flow.state` events, and throws a structured error
|
|
128
|
+
* (with `code` and `data` attached) on failure.
|
|
84
129
|
*/
|
|
85
130
|
async callTool(method, params = {}) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
131
|
+
const response = await this.dispatch({
|
|
132
|
+
id: `in-process:${this.nextInProcessId++}`,
|
|
133
|
+
method,
|
|
134
|
+
params,
|
|
135
|
+
});
|
|
136
|
+
if ('error' in response) {
|
|
137
|
+
const err = new Error(response.error.message);
|
|
138
|
+
err.code = response.error.code;
|
|
139
|
+
err.data = response.error.data;
|
|
140
|
+
throw err;
|
|
92
141
|
}
|
|
93
|
-
|
|
94
|
-
return handler(flow, params);
|
|
142
|
+
return response.result;
|
|
95
143
|
}
|
|
96
144
|
// ── Internals ────────────────────────────────────────────────────────
|
|
97
145
|
start() {
|
|
@@ -99,7 +147,15 @@ export class AngflowAgentBridge {
|
|
|
99
147
|
return;
|
|
100
148
|
this.started = true;
|
|
101
149
|
for (const t of this.transports) {
|
|
102
|
-
|
|
150
|
+
try {
|
|
151
|
+
const maybePromise = t.start((req) => this.dispatch(req));
|
|
152
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
153
|
+
maybePromise.catch((err) => this.reportError(err, { kind: 'transport-start', transport: t }));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.reportError(err, { kind: 'transport-start', transport: t });
|
|
158
|
+
}
|
|
103
159
|
}
|
|
104
160
|
}
|
|
105
161
|
async dispatch(req) {
|
|
@@ -117,7 +173,38 @@ export class AngflowAgentBridge {
|
|
|
117
173
|
return { id: req.id, result };
|
|
118
174
|
}
|
|
119
175
|
const flow = this.resolveFlow(params['flowId']);
|
|
176
|
+
const flowId = this.findFlowId(flow);
|
|
177
|
+
const isApplyChanges = req.method === 'apply_changes';
|
|
178
|
+
// Pre-mutation snapshot for history capture. Skipped for non-mutating tools.
|
|
179
|
+
// Shallow-clone each element so subsequent in-place mutations (notably
|
|
180
|
+
// the drag fast-path in FlowStore) can't retroactively corrupt the
|
|
181
|
+
// snapshot we already captured.
|
|
182
|
+
let snapshot = null;
|
|
183
|
+
if (this.history && (MUTATING_TOOLS.has(req.method) || isApplyChanges)) {
|
|
184
|
+
snapshot = {
|
|
185
|
+
nodes: flow.getNodes().map((n) => ({ ...n })),
|
|
186
|
+
edges: flow.getEdges().map((e) => ({ ...e })),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
120
189
|
const result = await handler(flow, params);
|
|
190
|
+
// Commit the captured snapshot to history. For apply_changes, the handler
|
|
191
|
+
// either succeeded entirely or threw and was rolled back already.
|
|
192
|
+
if (snapshot && flowId && this.history) {
|
|
193
|
+
if (isApplyChanges) {
|
|
194
|
+
const ops = params['ops'] ?? [];
|
|
195
|
+
const hasNonSelection = ops.some((o) => o['op'] !== 'select_nodes' &&
|
|
196
|
+
o['op'] !== 'select_edges' &&
|
|
197
|
+
o['op'] !== 'deselect_all');
|
|
198
|
+
if (hasNonSelection) {
|
|
199
|
+
this.history.capture(flowId, snapshot);
|
|
200
|
+
this.emitHistory(flowId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this.history.capture(flowId, snapshot);
|
|
205
|
+
this.emitHistory(flowId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
121
208
|
return { id: req.id, result: result ?? null };
|
|
122
209
|
}
|
|
123
210
|
catch (err) {
|
|
@@ -127,6 +214,20 @@ export class AngflowAgentBridge {
|
|
|
127
214
|
if (err instanceof InvalidParamsError) {
|
|
128
215
|
return { id: req.id, error: { code: ERROR_INVALID_PARAMS, message: err.message } };
|
|
129
216
|
}
|
|
217
|
+
if (err instanceof ApplyChangesError) {
|
|
218
|
+
return {
|
|
219
|
+
id: req.id,
|
|
220
|
+
error: {
|
|
221
|
+
code: ERROR_INTERNAL,
|
|
222
|
+
message: err.message,
|
|
223
|
+
data: { failedIndex: err.failedIndex },
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// Anything that lands here is an unexpected throw inside a handler — a
|
|
228
|
+
// bug or an underlying service failure. Surface it via onError so the
|
|
229
|
+
// host can log / report it; the wire response is still -32603.
|
|
230
|
+
this.reportError(err, { kind: 'dispatch', method: req.method });
|
|
130
231
|
return {
|
|
131
232
|
id: req.id,
|
|
132
233
|
error: {
|
|
@@ -136,13 +237,28 @@ export class AngflowAgentBridge {
|
|
|
136
237
|
};
|
|
137
238
|
}
|
|
138
239
|
}
|
|
240
|
+
findFlowId(flow) {
|
|
241
|
+
for (const [id, entry] of this.flows.entries()) {
|
|
242
|
+
if (entry.service === flow)
|
|
243
|
+
return id;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
emitHistory(flowId) {
|
|
248
|
+
if (!this.history)
|
|
249
|
+
return;
|
|
250
|
+
const status = this.history.status(flowId);
|
|
251
|
+
this.emit({ event: 'flow.history', params: { flowId, ...status } });
|
|
252
|
+
}
|
|
139
253
|
emit(evt) {
|
|
140
254
|
for (const t of this.transports) {
|
|
141
255
|
try {
|
|
142
256
|
t.send(evt);
|
|
143
257
|
}
|
|
144
|
-
catch {
|
|
145
|
-
// Transport errors are isolated per-transport; never let one break the
|
|
258
|
+
catch (err) {
|
|
259
|
+
// Transport errors are isolated per-transport; never let one break the
|
|
260
|
+
// bridge. Forward to onError if the host wants to observe them.
|
|
261
|
+
this.reportError(err, { kind: 'transport-send', transport: t });
|
|
146
262
|
}
|
|
147
263
|
}
|
|
148
264
|
}
|
|
@@ -163,8 +279,9 @@ export class AngflowAgentBridge {
|
|
|
163
279
|
}
|
|
164
280
|
watchFlow(id, flow) {
|
|
165
281
|
let pending = false;
|
|
282
|
+
let destroyed = false;
|
|
166
283
|
let lastSignature = '';
|
|
167
|
-
|
|
284
|
+
const ref = runInInjectionContext(this.injector, () => effect(() => {
|
|
168
285
|
// Touch every signal we want to broadcast so the effect re-runs on change.
|
|
169
286
|
flow.nodes();
|
|
170
287
|
flow.edges();
|
|
@@ -176,6 +293,11 @@ export class AngflowAgentBridge {
|
|
|
176
293
|
pending = true;
|
|
177
294
|
queueMicrotask(() => {
|
|
178
295
|
pending = false;
|
|
296
|
+
// A queued microtask may outlive the effect (unregister between
|
|
297
|
+
// effect run and microtask drain). Drop late emissions so we never
|
|
298
|
+
// push state for a flowId that's no longer registered.
|
|
299
|
+
if (destroyed)
|
|
300
|
+
return;
|
|
179
301
|
// Re-read on flush so coalesced bursts see the latest state.
|
|
180
302
|
const params = {
|
|
181
303
|
flowId: id,
|
|
@@ -188,7 +310,8 @@ export class AngflowAgentBridge {
|
|
|
188
310
|
},
|
|
189
311
|
};
|
|
190
312
|
// Suppress duplicate emissions when controlled-mode round-trips bounce
|
|
191
|
-
//
|
|
313
|
+
// identical state through the store twice. See signatureOf for the
|
|
314
|
+
// field set; it must cover anything a mutating tool can change.
|
|
192
315
|
const sig = signatureOf(params);
|
|
193
316
|
if (sig === lastSignature)
|
|
194
317
|
return;
|
|
@@ -196,6 +319,10 @@ export class AngflowAgentBridge {
|
|
|
196
319
|
this.emit({ event: 'flow.state', params });
|
|
197
320
|
});
|
|
198
321
|
}));
|
|
322
|
+
return () => {
|
|
323
|
+
destroyed = true;
|
|
324
|
+
ref.destroy();
|
|
325
|
+
};
|
|
199
326
|
}
|
|
200
327
|
installHandlers() {
|
|
201
328
|
this.handlers.set('list_flows', () => Array.from(this.flows.keys()));
|
|
@@ -215,12 +342,12 @@ export class AngflowAgentBridge {
|
|
|
215
342
|
return flow.getEdge(id) ?? null;
|
|
216
343
|
});
|
|
217
344
|
this.handlers.set('add_node', (flow, params) => {
|
|
218
|
-
const node = requireObject(params, 'node');
|
|
345
|
+
const node = validateNodeShape(requireObject(params, 'node'), 'add_node');
|
|
219
346
|
flow.addNodes(node);
|
|
220
347
|
return flow.getNode(node.id) ?? null;
|
|
221
348
|
});
|
|
222
349
|
this.handlers.set('add_edge', (flow, params) => {
|
|
223
|
-
const edge = requireObject(params, 'edge');
|
|
350
|
+
const edge = validateEdgeShape(requireObject(params, 'edge'), 'add_edge');
|
|
224
351
|
flow.addEdges(edge);
|
|
225
352
|
return flow.getEdge(edge.id) ?? null;
|
|
226
353
|
});
|
|
@@ -249,11 +376,11 @@ export class AngflowAgentBridge {
|
|
|
249
376
|
};
|
|
250
377
|
});
|
|
251
378
|
this.handlers.set('set_nodes', (flow, params) => {
|
|
252
|
-
const nodes = requireArray(params, 'nodes');
|
|
379
|
+
const nodes = requireArray(params, 'nodes').map((n, i) => validateNodeShape(n, `set_nodes[${i}]`));
|
|
253
380
|
flow.setNodes(nodes);
|
|
254
381
|
});
|
|
255
382
|
this.handlers.set('set_edges', (flow, params) => {
|
|
256
|
-
const edges = requireArray(params, 'edges');
|
|
383
|
+
const edges = requireArray(params, 'edges').map((e, i) => validateEdgeShape(e, `set_edges[${i}]`));
|
|
257
384
|
flow.setEdges(edges);
|
|
258
385
|
});
|
|
259
386
|
this.handlers.set('fit_view', (flow, params) => {
|
|
@@ -274,8 +401,276 @@ export class AngflowAgentBridge {
|
|
|
274
401
|
flow.setViewport(viewport, { duration });
|
|
275
402
|
});
|
|
276
403
|
this.handlers.set('get_viewport', (flow) => flow.getViewport());
|
|
404
|
+
this.handlers.set('get_internal_node', (flow, params) => {
|
|
405
|
+
const id = requireString(params, 'id');
|
|
406
|
+
const internal = flow.getInternalNode(id);
|
|
407
|
+
if (!internal)
|
|
408
|
+
return null;
|
|
409
|
+
return {
|
|
410
|
+
id: internal.id,
|
|
411
|
+
positionAbsolute: internal.internals?.positionAbsolute ?? internal.position,
|
|
412
|
+
measured: internal.measured
|
|
413
|
+
? { width: internal.measured.width, height: internal.measured.height }
|
|
414
|
+
: null,
|
|
415
|
+
handleBounds: internal.internals?.handleBounds ?? null,
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
this.handlers.set('get_nodes_bounds', (flow, params) => {
|
|
419
|
+
const nodeIds = optionalStringArray(params, 'nodeIds');
|
|
420
|
+
const nodes = nodeIds ? flow.getNodes(nodeIds) : flow.getNodes();
|
|
421
|
+
return flow.getNodesBounds(nodes);
|
|
422
|
+
});
|
|
423
|
+
this.handlers.set('get_intersecting_nodes', (flow, params) => {
|
|
424
|
+
const id = requireString(params, 'id');
|
|
425
|
+
const partially = typeof params['partially'] === 'boolean' ? params['partially'] : true;
|
|
426
|
+
const node = flow.getNode(id);
|
|
427
|
+
if (!node)
|
|
428
|
+
return [];
|
|
429
|
+
return flow.getIntersectingNodes(node, partially);
|
|
430
|
+
});
|
|
431
|
+
this.handlers.set('is_node_in_area', (flow, params) => {
|
|
432
|
+
const id = requireString(params, 'id');
|
|
433
|
+
const area = requireObject(params, 'area');
|
|
434
|
+
const partially = typeof params['partially'] === 'boolean' ? params['partially'] : true;
|
|
435
|
+
const node = flow.getNode(id);
|
|
436
|
+
if (!node)
|
|
437
|
+
return false;
|
|
438
|
+
return flow.isNodeIntersecting(node, area, partially);
|
|
439
|
+
});
|
|
440
|
+
this.handlers.set('get_outgoers', (flow, params) => {
|
|
441
|
+
const id = requireString(params, 'id');
|
|
442
|
+
// Use the signal-based selector then read its current value to stay non-reactive at the JSON boundary.
|
|
443
|
+
return flow.selectOutgoers(id)();
|
|
444
|
+
});
|
|
445
|
+
this.handlers.set('get_incomers', (flow, params) => {
|
|
446
|
+
const id = requireString(params, 'id');
|
|
447
|
+
return flow.selectIncomers(id)();
|
|
448
|
+
});
|
|
449
|
+
this.handlers.set('get_connected_edges', (flow, params) => {
|
|
450
|
+
const nodeIds = optionalStringArray(params, 'nodeIds');
|
|
451
|
+
if (!nodeIds)
|
|
452
|
+
throw new InvalidParamsError('Param "nodeIds" must be an array of strings.');
|
|
453
|
+
return flow.getConnectedEdges(nodeIds);
|
|
454
|
+
});
|
|
455
|
+
this.handlers.set('get_node_connections', (flow, params) => {
|
|
456
|
+
const nodeId = requireString(params, 'nodeId');
|
|
457
|
+
return flow.getNodeConnections(nodeId);
|
|
458
|
+
});
|
|
459
|
+
this.handlers.set('get_handle_connections', (flow, params) => {
|
|
460
|
+
const nodeId = requireString(params, 'nodeId');
|
|
461
|
+
const type = requireString(params, 'type');
|
|
462
|
+
if (type !== 'source' && type !== 'target') {
|
|
463
|
+
throw new InvalidParamsError('Param "type" must be "source" or "target".');
|
|
464
|
+
}
|
|
465
|
+
const handleId = typeof params['handleId'] === 'string' ? params['handleId'] : undefined;
|
|
466
|
+
return flow.getHandleConnections({ nodeId, type, id: handleId });
|
|
467
|
+
});
|
|
468
|
+
this.handlers.set('get_handle_data', (flow, params) => {
|
|
469
|
+
const nodeId = requireString(params, 'nodeId');
|
|
470
|
+
const type = requireString(params, 'type');
|
|
471
|
+
if (type !== 'source' && type !== 'target') {
|
|
472
|
+
throw new InvalidParamsError('Param "type" must be "source" or "target".');
|
|
473
|
+
}
|
|
474
|
+
const rawHandleId = params['handleId'];
|
|
475
|
+
if (rawHandleId !== null && typeof rawHandleId !== 'string') {
|
|
476
|
+
throw new InvalidParamsError('Param "handleId" must be a string or null.');
|
|
477
|
+
}
|
|
478
|
+
return flow.getHandleData(nodeId, rawHandleId, type) ?? null;
|
|
479
|
+
});
|
|
480
|
+
this.handlers.set('screen_to_flow_position', (flow, params) => {
|
|
481
|
+
const position = requireObject(params, 'position');
|
|
482
|
+
const snapToGrid = typeof params['snapToGrid'] === 'boolean' ? params['snapToGrid'] : undefined;
|
|
483
|
+
return flow.screenToFlowPosition(position, snapToGrid !== undefined ? { snapToGrid } : undefined);
|
|
484
|
+
});
|
|
485
|
+
this.handlers.set('flow_to_screen_position', (flow, params) => {
|
|
486
|
+
const position = requireObject(params, 'position');
|
|
487
|
+
return flow.flowToScreenPosition(position);
|
|
488
|
+
});
|
|
489
|
+
this.handlers.set('zoom_in', (flow, params) => {
|
|
490
|
+
const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
|
|
491
|
+
return flow.zoomIn({ duration });
|
|
492
|
+
});
|
|
493
|
+
this.handlers.set('zoom_out', (flow, params) => {
|
|
494
|
+
const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
|
|
495
|
+
return flow.zoomOut({ duration });
|
|
496
|
+
});
|
|
497
|
+
this.handlers.set('zoom_to', (flow, params) => {
|
|
498
|
+
const level = params['level'];
|
|
499
|
+
if (typeof level !== 'number')
|
|
500
|
+
throw new InvalidParamsError('Param "level" must be a number.');
|
|
501
|
+
const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
|
|
502
|
+
return flow.zoomTo(level, { duration });
|
|
503
|
+
});
|
|
504
|
+
this.handlers.set('set_center', (flow, params) => {
|
|
505
|
+
const x = params['x'];
|
|
506
|
+
const y = params['y'];
|
|
507
|
+
if (typeof x !== 'number' || typeof y !== 'number') {
|
|
508
|
+
throw new InvalidParamsError('Params "x" and "y" must be numbers.');
|
|
509
|
+
}
|
|
510
|
+
const zoom = typeof params['zoom'] === 'number' ? params['zoom'] : undefined;
|
|
511
|
+
const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
|
|
512
|
+
return flow.setCenter(x, y, { zoom, duration });
|
|
513
|
+
});
|
|
514
|
+
this.handlers.set('fit_bounds', (flow, params) => {
|
|
515
|
+
const bounds = requireObject(params, 'bounds');
|
|
516
|
+
const padding = typeof params['padding'] === 'number' ? params['padding'] : undefined;
|
|
517
|
+
const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
|
|
518
|
+
return flow.fitBounds(bounds, { padding, duration });
|
|
519
|
+
});
|
|
520
|
+
this.handlers.set('add_nodes', (flow, params) => {
|
|
521
|
+
const nodes = requireArray(params, 'nodes').map((n, i) => validateNodeShape(n, `add_nodes[${i}]`));
|
|
522
|
+
flow.addNodes(nodes);
|
|
523
|
+
return nodes.map((n) => flow.getNode(n.id)).filter((n) => !!n);
|
|
524
|
+
});
|
|
525
|
+
this.handlers.set('add_edges', (flow, params) => {
|
|
526
|
+
const edges = requireArray(params, 'edges').map((e, i) => validateEdgeShape(e, `add_edges[${i}]`));
|
|
527
|
+
flow.addEdges(edges);
|
|
528
|
+
return edges.map((e) => flow.getEdge(e.id)).filter((e) => !!e);
|
|
529
|
+
});
|
|
530
|
+
this.handlers.set('update_node_data', (flow, params) => {
|
|
531
|
+
const id = requireString(params, 'id');
|
|
532
|
+
const dataPatch = requireObject(params, 'dataPatch');
|
|
533
|
+
flow.updateNodeData(id, dataPatch);
|
|
534
|
+
return flow.getNode(id) ?? null;
|
|
535
|
+
});
|
|
536
|
+
this.handlers.set('update_edge_data', (flow, params) => {
|
|
537
|
+
const id = requireString(params, 'id');
|
|
538
|
+
const dataPatch = requireObject(params, 'dataPatch');
|
|
539
|
+
flow.updateEdgeData(id, dataPatch);
|
|
540
|
+
return flow.getEdge(id) ?? null;
|
|
541
|
+
});
|
|
542
|
+
this.handlers.set('select_nodes', (flow, params) => {
|
|
543
|
+
const nodeIds = optionalStringArray(params, 'nodeIds');
|
|
544
|
+
if (!nodeIds)
|
|
545
|
+
throw new InvalidParamsError('Param "nodeIds" must be an array of strings.');
|
|
546
|
+
const additive = typeof params['additive'] === 'boolean' ? params['additive'] : false;
|
|
547
|
+
flow.setSelection({ nodeIds, additive });
|
|
548
|
+
return { selectedNodeIds: flow.selectedNodes().map((n) => n.id) };
|
|
549
|
+
});
|
|
550
|
+
this.handlers.set('select_edges', (flow, params) => {
|
|
551
|
+
const edgeIds = optionalStringArray(params, 'edgeIds');
|
|
552
|
+
if (!edgeIds)
|
|
553
|
+
throw new InvalidParamsError('Param "edgeIds" must be an array of strings.');
|
|
554
|
+
const additive = typeof params['additive'] === 'boolean' ? params['additive'] : false;
|
|
555
|
+
flow.setSelection({ edgeIds, additive });
|
|
556
|
+
return { selectedEdgeIds: flow.selectedEdges().map((e) => e.id) };
|
|
557
|
+
});
|
|
558
|
+
this.handlers.set('deselect_all', (flow) => {
|
|
559
|
+
flow.setSelection({ nodeIds: [], edgeIds: [], additive: false });
|
|
560
|
+
});
|
|
561
|
+
this.handlers.set('apply_changes', (flow, params) => {
|
|
562
|
+
const ops = requireArray(params, 'ops');
|
|
563
|
+
// apply_changes' delete_elements op takes the synchronous setNodes/
|
|
564
|
+
// setEdges path so the whole batch can roll back, which means
|
|
565
|
+
// onBeforeDelete is bypassed (it can't be awaited inside a rollback-
|
|
566
|
+
// capable batch). Warn once per bridge lifetime if the host actually
|
|
567
|
+
// has the veto hook registered — otherwise the silent veto-loss is
|
|
568
|
+
// very hard to discover.
|
|
569
|
+
if (!this.warnedOnBeforeDeleteBypass && flow.hasOnBeforeDeleteHook()) {
|
|
570
|
+
const hasDelete = ops.some((o) => o['op'] === 'delete_elements');
|
|
571
|
+
if (hasDelete) {
|
|
572
|
+
this.warnedOnBeforeDeleteBypass = true;
|
|
573
|
+
// eslint-disable-next-line no-console
|
|
574
|
+
console.warn('[angflow] apply_changes/delete_elements bypasses onBeforeDelete. ' +
|
|
575
|
+
'Call the standalone `delete_elements` tool if you need the veto hook.');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Capture snapshot for rollback. Viewport is intentionally excluded.
|
|
579
|
+
// Shallow-clone each element so in-place field assignments (e.g.
|
|
580
|
+
// FlowStore's drag fast-path writing `node.position` / `node.dragging`)
|
|
581
|
+
// cannot corrupt the snapshot mid-batch. We do not deep-clone `data`
|
|
582
|
+
// because mutating `data` directly violates the documented contract.
|
|
583
|
+
const snapshot = {
|
|
584
|
+
nodes: flow.getNodes().map((n) => ({ ...n })),
|
|
585
|
+
edges: flow.getEdges().map((e) => ({ ...e })),
|
|
586
|
+
};
|
|
587
|
+
const results = [];
|
|
588
|
+
let failure = null;
|
|
589
|
+
flow.batch(() => {
|
|
590
|
+
for (let i = 0; i < ops.length; i++) {
|
|
591
|
+
try {
|
|
592
|
+
results.push({ ok: true, value: executeOp(flow, ops[i]) });
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
failure = { failedIndex: i, cause: err };
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
if (failure) {
|
|
601
|
+
flow.batch(() => {
|
|
602
|
+
flow.setNodes(snapshot.nodes);
|
|
603
|
+
flow.setEdges(snapshot.edges);
|
|
604
|
+
});
|
|
605
|
+
const f = failure;
|
|
606
|
+
const message = f.cause instanceof Error ? f.cause.message : String(f.cause);
|
|
607
|
+
throw new ApplyChangesError(f.failedIndex, message);
|
|
608
|
+
}
|
|
609
|
+
return { results };
|
|
610
|
+
});
|
|
611
|
+
this.handlers.set('undo', (flow, params) => {
|
|
612
|
+
if (!this.history)
|
|
613
|
+
return { undone: 0, canUndo: false, canRedo: false };
|
|
614
|
+
const flowId = this.findFlowId(flow);
|
|
615
|
+
if (!flowId)
|
|
616
|
+
return { undone: 0, canUndo: false, canRedo: false };
|
|
617
|
+
const steps = typeof params['steps'] === 'number' ? params['steps'] : 1;
|
|
618
|
+
const current = {
|
|
619
|
+
nodes: flow.getNodes().map((n) => ({ ...n })),
|
|
620
|
+
edges: flow.getEdges().map((e) => ({ ...e })),
|
|
621
|
+
};
|
|
622
|
+
const result = this.history.undo(flowId, steps, current);
|
|
623
|
+
if (result) {
|
|
624
|
+
flow.batch(() => {
|
|
625
|
+
flow.setNodes(result.snapshot.nodes);
|
|
626
|
+
flow.setEdges(result.snapshot.edges);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
this.emitHistory(flowId);
|
|
630
|
+
const status = this.history.status(flowId);
|
|
631
|
+
return { undone: result?.consumed ?? 0, canUndo: status.canUndo, canRedo: status.canRedo };
|
|
632
|
+
});
|
|
633
|
+
this.handlers.set('redo', (flow, params) => {
|
|
634
|
+
if (!this.history)
|
|
635
|
+
return { redone: 0, canUndo: false, canRedo: false };
|
|
636
|
+
const flowId = this.findFlowId(flow);
|
|
637
|
+
if (!flowId)
|
|
638
|
+
return { redone: 0, canUndo: false, canRedo: false };
|
|
639
|
+
const steps = typeof params['steps'] === 'number' ? params['steps'] : 1;
|
|
640
|
+
const current = {
|
|
641
|
+
nodes: flow.getNodes().map((n) => ({ ...n })),
|
|
642
|
+
edges: flow.getEdges().map((e) => ({ ...e })),
|
|
643
|
+
};
|
|
644
|
+
const result = this.history.redo(flowId, steps, current);
|
|
645
|
+
if (result) {
|
|
646
|
+
flow.batch(() => {
|
|
647
|
+
flow.setNodes(result.snapshot.nodes);
|
|
648
|
+
flow.setEdges(result.snapshot.edges);
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
this.emitHistory(flowId);
|
|
652
|
+
const status = this.history.status(flowId);
|
|
653
|
+
return { redone: result?.consumed ?? 0, canUndo: status.canUndo, canRedo: status.canRedo };
|
|
654
|
+
});
|
|
655
|
+
this.handlers.set('history_status', (flow) => {
|
|
656
|
+
if (!this.history)
|
|
657
|
+
return { canUndo: false, canRedo: false, pastDepth: 0, futureDepth: 0 };
|
|
658
|
+
const flowId = this.findFlowId(flow);
|
|
659
|
+
if (!flowId)
|
|
660
|
+
return { canUndo: false, canRedo: false, pastDepth: 0, futureDepth: 0 };
|
|
661
|
+
return this.history.status(flowId);
|
|
662
|
+
});
|
|
663
|
+
this.handlers.set('clear_history', (flow) => {
|
|
664
|
+
if (!this.history)
|
|
665
|
+
return;
|
|
666
|
+
const flowId = this.findFlowId(flow);
|
|
667
|
+
if (!flowId)
|
|
668
|
+
return;
|
|
669
|
+
this.history.clear(flowId);
|
|
670
|
+
this.emitHistory(flowId);
|
|
671
|
+
});
|
|
277
672
|
}
|
|
278
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, deps: [{ token: AGENT_TRANSPORTS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
673
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, deps: [{ token: AGENT_TRANSPORTS, optional: true }, { token: AGENT_HISTORY_OPTIONS, optional: true }, { token: AGENT_ON_ERROR, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
279
674
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, providedIn: 'root' }); }
|
|
280
675
|
}
|
|
281
676
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, decorators: [{
|
|
@@ -286,19 +681,189 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
|
|
|
286
681
|
}, {
|
|
287
682
|
type: Inject,
|
|
288
683
|
args: [AGENT_TRANSPORTS]
|
|
684
|
+
}] }, { type: undefined, decorators: [{
|
|
685
|
+
type: Optional
|
|
686
|
+
}, {
|
|
687
|
+
type: Inject,
|
|
688
|
+
args: [AGENT_HISTORY_OPTIONS]
|
|
689
|
+
}] }, { type: undefined, decorators: [{
|
|
690
|
+
type: Optional
|
|
691
|
+
}, {
|
|
692
|
+
type: Inject,
|
|
693
|
+
args: [AGENT_ON_ERROR]
|
|
289
694
|
}] }] });
|
|
290
695
|
class FlowNotFoundError extends Error {
|
|
291
696
|
}
|
|
292
697
|
class InvalidParamsError extends Error {
|
|
293
698
|
}
|
|
699
|
+
class ApplyChangesError extends Error {
|
|
700
|
+
constructor(failedIndex, message) {
|
|
701
|
+
super(message);
|
|
702
|
+
this.failedIndex = failedIndex;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Build a stable signature of the emit payload, used to suppress duplicate
|
|
707
|
+
* emissions when controlled-mode round-trips bounce identical state through
|
|
708
|
+
* the store twice. Must hash every field that can change via a mutating
|
|
709
|
+
* tool — including `data`, `style`, `type`, `hidden`, `animated`, `label` —
|
|
710
|
+
* otherwise `update_node_data` / `update_edge_data` updates would be silently
|
|
711
|
+
* dropped.
|
|
712
|
+
*/
|
|
294
713
|
function signatureOf(params) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
714
|
+
// Curated subset of node/edge fields that surface in `flow.state` consumers
|
|
715
|
+
// (renderers, history, agents). Order is fixed by the literal so JSON.stringify
|
|
716
|
+
// produces a deterministic string.
|
|
717
|
+
const n = params.nodes.map((node) => ({
|
|
718
|
+
id: node.id,
|
|
719
|
+
p: [node.position.x, node.position.y],
|
|
720
|
+
m: node.measured ? [node.measured.width ?? null, node.measured.height ?? null] : null,
|
|
721
|
+
t: node.type ?? null,
|
|
722
|
+
h: node.hidden === true,
|
|
723
|
+
d: node.data ?? null,
|
|
724
|
+
s: node.style ?? null,
|
|
725
|
+
}));
|
|
726
|
+
const e = params.edges.map((edge) => ({
|
|
727
|
+
id: edge.id,
|
|
728
|
+
src: edge.source,
|
|
729
|
+
tgt: edge.target,
|
|
730
|
+
sh: edge.sourceHandle ?? null,
|
|
731
|
+
th: edge.targetHandle ?? null,
|
|
732
|
+
t: edge.type ?? null,
|
|
733
|
+
h: edge.hidden === true,
|
|
734
|
+
a: edge.animated === true,
|
|
735
|
+
l: edge.label ?? null,
|
|
736
|
+
d: edge.data ?? null,
|
|
737
|
+
s: edge.style ?? null,
|
|
738
|
+
}));
|
|
739
|
+
try {
|
|
740
|
+
return JSON.stringify({ n, e, v: params.viewport, sel: params.selection });
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
// Defensive: if any field is non-serializable (e.g. cyclic data), fall back
|
|
744
|
+
// to a coarser signature so dedup never silently swallows updates.
|
|
745
|
+
return `__nonserializable__:${Date.now()}:${Math.random()}`;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function executeOp(flow, op) {
|
|
749
|
+
const kind = op['op'];
|
|
750
|
+
switch (kind) {
|
|
751
|
+
case 'add_node': {
|
|
752
|
+
const node = validateNodeShape(op['node'], 'apply_changes/add_node');
|
|
753
|
+
flow.addNodes(node);
|
|
754
|
+
return flow.getNode(node.id) ?? null;
|
|
755
|
+
}
|
|
756
|
+
case 'add_nodes': {
|
|
757
|
+
const nodes = op['nodes'];
|
|
758
|
+
if (!Array.isArray(nodes))
|
|
759
|
+
throw new InvalidParamsError('add_nodes: "nodes" must be an array.');
|
|
760
|
+
const validated = nodes.map((n, i) => validateNodeShape(n, `apply_changes/add_nodes[${i}]`));
|
|
761
|
+
flow.addNodes(validated);
|
|
762
|
+
return validated.map((n) => flow.getNode(n.id)).filter((n) => !!n);
|
|
763
|
+
}
|
|
764
|
+
case 'add_edge': {
|
|
765
|
+
const edge = validateEdgeShape(op['edge'], 'apply_changes/add_edge');
|
|
766
|
+
flow.addEdges(edge);
|
|
767
|
+
return flow.getEdge(edge.id) ?? null;
|
|
768
|
+
}
|
|
769
|
+
case 'add_edges': {
|
|
770
|
+
const edges = op['edges'];
|
|
771
|
+
if (!Array.isArray(edges))
|
|
772
|
+
throw new InvalidParamsError('add_edges: "edges" must be an array.');
|
|
773
|
+
const validated = edges.map((e, i) => validateEdgeShape(e, `apply_changes/add_edges[${i}]`));
|
|
774
|
+
flow.addEdges(validated);
|
|
775
|
+
return validated.map((e) => flow.getEdge(e.id)).filter((e) => !!e);
|
|
776
|
+
}
|
|
777
|
+
case 'update_node': {
|
|
778
|
+
const id = op['id'];
|
|
779
|
+
const patch = op['patch'];
|
|
780
|
+
if (typeof id !== 'string')
|
|
781
|
+
throw new InvalidParamsError('update_node: "id" must be a string.');
|
|
782
|
+
if (!patch || typeof patch !== 'object')
|
|
783
|
+
throw new InvalidParamsError('update_node: "patch" must be an object.');
|
|
784
|
+
if (!flow.getNode(id))
|
|
785
|
+
throw new InvalidParamsError(`update_node: node "${id}" not found.`);
|
|
786
|
+
flow.updateNode(id, patch);
|
|
787
|
+
return flow.getNode(id) ?? null;
|
|
788
|
+
}
|
|
789
|
+
case 'update_node_data': {
|
|
790
|
+
const id = op['id'];
|
|
791
|
+
const dataPatch = op['dataPatch'];
|
|
792
|
+
if (typeof id !== 'string')
|
|
793
|
+
throw new InvalidParamsError('update_node_data: "id" must be a string.');
|
|
794
|
+
if (!dataPatch || typeof dataPatch !== 'object')
|
|
795
|
+
throw new InvalidParamsError('update_node_data: "dataPatch" must be an object.');
|
|
796
|
+
if (!flow.getNode(id))
|
|
797
|
+
throw new InvalidParamsError(`update_node_data: node "${id}" not found.`);
|
|
798
|
+
flow.updateNodeData(id, dataPatch);
|
|
799
|
+
return flow.getNode(id) ?? null;
|
|
800
|
+
}
|
|
801
|
+
case 'update_edge': {
|
|
802
|
+
const id = op['id'];
|
|
803
|
+
const patch = op['patch'];
|
|
804
|
+
if (typeof id !== 'string')
|
|
805
|
+
throw new InvalidParamsError('update_edge: "id" must be a string.');
|
|
806
|
+
if (!patch || typeof patch !== 'object')
|
|
807
|
+
throw new InvalidParamsError('update_edge: "patch" must be an object.');
|
|
808
|
+
if (!flow.getEdge(id))
|
|
809
|
+
throw new InvalidParamsError(`update_edge: edge "${id}" not found.`);
|
|
810
|
+
flow.updateEdge(id, patch);
|
|
811
|
+
return flow.getEdge(id) ?? null;
|
|
812
|
+
}
|
|
813
|
+
case 'update_edge_data': {
|
|
814
|
+
const id = op['id'];
|
|
815
|
+
const dataPatch = op['dataPatch'];
|
|
816
|
+
if (typeof id !== 'string')
|
|
817
|
+
throw new InvalidParamsError('update_edge_data: "id" must be a string.');
|
|
818
|
+
if (!dataPatch || typeof dataPatch !== 'object')
|
|
819
|
+
throw new InvalidParamsError('update_edge_data: "dataPatch" must be an object.');
|
|
820
|
+
if (!flow.getEdge(id))
|
|
821
|
+
throw new InvalidParamsError(`update_edge_data: edge "${id}" not found.`);
|
|
822
|
+
flow.updateEdgeData(id, dataPatch);
|
|
823
|
+
return flow.getEdge(id) ?? null;
|
|
824
|
+
}
|
|
825
|
+
case 'delete_elements': {
|
|
826
|
+
const nodeIds = Array.isArray(op['nodeIds']) ? op['nodeIds'] : [];
|
|
827
|
+
const edgeIds = Array.isArray(op['edgeIds']) ? op['edgeIds'] : [];
|
|
828
|
+
// deleteElements is async because of onBeforeDelete; inside apply_changes we
|
|
829
|
+
// intentionally do not await — the synchronous setNodes/setEdges paths are
|
|
830
|
+
// what we need for rollback semantics. Skip onBeforeDelete hooks inside batches.
|
|
831
|
+
const allEdgeIds = new Set(edgeIds);
|
|
832
|
+
for (const e of flow.getEdges()) {
|
|
833
|
+
if (nodeIds.includes(e.source) || nodeIds.includes(e.target))
|
|
834
|
+
allEdgeIds.add(e.id);
|
|
835
|
+
}
|
|
836
|
+
if (nodeIds.length > 0) {
|
|
837
|
+
flow.setNodes(flow.getNodes().filter((n) => !nodeIds.includes(n.id)));
|
|
838
|
+
}
|
|
839
|
+
if (allEdgeIds.size > 0) {
|
|
840
|
+
flow.setEdges(flow.getEdges().filter((e) => !allEdgeIds.has(e.id)));
|
|
841
|
+
}
|
|
842
|
+
return { deletedNodeIds: nodeIds, deletedEdgeIds: Array.from(allEdgeIds) };
|
|
843
|
+
}
|
|
844
|
+
case 'select_nodes': {
|
|
845
|
+
const nodeIds = op['nodeIds'];
|
|
846
|
+
if (!Array.isArray(nodeIds))
|
|
847
|
+
throw new InvalidParamsError('select_nodes: "nodeIds" must be an array.');
|
|
848
|
+
const additive = typeof op['additive'] === 'boolean' ? op['additive'] : false;
|
|
849
|
+
flow.setSelection({ nodeIds: nodeIds, additive });
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
case 'select_edges': {
|
|
853
|
+
const edgeIds = op['edgeIds'];
|
|
854
|
+
if (!Array.isArray(edgeIds))
|
|
855
|
+
throw new InvalidParamsError('select_edges: "edgeIds" must be an array.');
|
|
856
|
+
const additive = typeof op['additive'] === 'boolean' ? op['additive'] : false;
|
|
857
|
+
flow.setSelection({ edgeIds: edgeIds, additive });
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
case 'deselect_all': {
|
|
861
|
+
flow.setSelection({ nodeIds: [], edgeIds: [], additive: false });
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
default:
|
|
865
|
+
throw new InvalidParamsError(`Unknown op kind: ${String(kind)}`);
|
|
866
|
+
}
|
|
302
867
|
}
|
|
303
868
|
function requireString(params, key) {
|
|
304
869
|
const value = params[key];
|
|
@@ -328,4 +893,43 @@ function optionalStringArray(params, key) {
|
|
|
328
893
|
}
|
|
329
894
|
return value;
|
|
330
895
|
}
|
|
896
|
+
/** Validate that `value` is a structurally valid Node payload for add_*. */
|
|
897
|
+
function validateNodeShape(value, ctx) {
|
|
898
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
899
|
+
throw new InvalidParamsError(`${ctx}: node must be an object.`);
|
|
900
|
+
}
|
|
901
|
+
const n = value;
|
|
902
|
+
if (typeof n['id'] !== 'string' || n['id'].length === 0) {
|
|
903
|
+
throw new InvalidParamsError(`${ctx}: node.id must be a non-empty string.`);
|
|
904
|
+
}
|
|
905
|
+
const pos = n['position'];
|
|
906
|
+
if (!pos || typeof pos !== 'object' || Array.isArray(pos)) {
|
|
907
|
+
throw new InvalidParamsError(`${ctx}: node.position must be { x, y }.`);
|
|
908
|
+
}
|
|
909
|
+
const p = pos;
|
|
910
|
+
if (typeof p['x'] !== 'number' || typeof p['y'] !== 'number') {
|
|
911
|
+
throw new InvalidParamsError(`${ctx}: node.position.{x,y} must be numbers.`);
|
|
912
|
+
}
|
|
913
|
+
if (!Number.isFinite(p['x']) || !Number.isFinite(p['y'])) {
|
|
914
|
+
throw new InvalidParamsError(`${ctx}: node.position.{x,y} must be finite (no NaN/Infinity).`);
|
|
915
|
+
}
|
|
916
|
+
return value;
|
|
917
|
+
}
|
|
918
|
+
/** Validate that `value` is a structurally valid Edge payload for add_*. */
|
|
919
|
+
function validateEdgeShape(value, ctx) {
|
|
920
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
921
|
+
throw new InvalidParamsError(`${ctx}: edge must be an object.`);
|
|
922
|
+
}
|
|
923
|
+
const e = value;
|
|
924
|
+
if (typeof e['id'] !== 'string' || e['id'].length === 0) {
|
|
925
|
+
throw new InvalidParamsError(`${ctx}: edge.id must be a non-empty string.`);
|
|
926
|
+
}
|
|
927
|
+
if (typeof e['source'] !== 'string' || e['source'].length === 0) {
|
|
928
|
+
throw new InvalidParamsError(`${ctx}: edge.source must be a non-empty string.`);
|
|
929
|
+
}
|
|
930
|
+
if (typeof e['target'] !== 'string' || e['target'].length === 0) {
|
|
931
|
+
throw new InvalidParamsError(`${ctx}: edge.target must be a non-empty string.`);
|
|
932
|
+
}
|
|
933
|
+
return value;
|
|
934
|
+
}
|
|
331
935
|
//# sourceMappingURL=agent-bridge.service.js.map
|