@angflow/angular 0.0.14 → 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 +91 -0
- package/dist/esm/lib/agent/agent-bridge.service.d.ts.map +1 -0
- package/dist/esm/lib/agent/agent-bridge.service.js +935 -0
- package/dist/esm/lib/agent/agent-bridge.service.js.map +1 -0
- 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 +8 -0
- package/dist/esm/lib/agent/index.d.ts.map +1 -0
- package/dist/esm/lib/agent/index.js +6 -0
- package/dist/esm/lib/agent/index.js.map +1 -0
- package/dist/esm/lib/agent/provide-agent-bridge.d.ts +47 -0
- package/dist/esm/lib/agent/provide-agent-bridge.d.ts.map +1 -0
- package/dist/esm/lib/agent/provide-agent-bridge.js +32 -0
- package/dist/esm/lib/agent/provide-agent-bridge.js.map +1 -0
- package/dist/esm/lib/agent/tool-schemas.d.ts +10 -0
- package/dist/esm/lib/agent/tool-schemas.d.ts.map +1 -0
- package/dist/esm/lib/agent/tool-schemas.js +635 -0
- package/dist/esm/lib/agent/tool-schemas.js.map +1 -0
- package/dist/esm/lib/agent/transports/websocket.d.ts +39 -0
- package/dist/esm/lib/agent/transports/websocket.d.ts.map +1 -0
- package/dist/esm/lib/agent/transports/websocket.js +95 -0
- package/dist/esm/lib/agent/transports/websocket.js.map +1 -0
- package/dist/esm/lib/agent/transports/window.d.ts +29 -0
- package/dist/esm/lib/agent/transports/window.d.ts.map +1 -0
- package/dist/esm/lib/agent/transports/window.js +73 -0
- package/dist/esm/lib/agent/transports/window.js.map +1 -0
- package/dist/esm/lib/agent/types.d.ts +66 -0
- package/dist/esm/lib/agent/types.d.ts.map +1 -0
- package/dist/esm/lib/agent/types.js +9 -0
- package/dist/esm/lib/agent/types.js.map +1 -0
- package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.d.ts +1 -1
- 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 +12 -12
- 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 +16 -16
- 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/public-api.d.ts +1 -0
- package/dist/esm/lib/public-api.d.ts.map +1 -1
- package/dist/esm/lib/public-api.js +2 -0
- package/dist/esm/lib/public-api.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 +70 -29
- package/dist/esm/lib/services/ng-flow.service.js.map +1 -1
- package/dist/esm/lib/types/edges.d.ts +18 -5
- 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 +2 -1
- package/dist/esm/lib/types/nodes.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
import { Inject, Injectable, InjectionToken, Injector, Optional, effect, inject, runInInjectionContext, signal, } from '@angular/core';
|
|
2
|
+
import { AgentHistory } from './history';
|
|
3
|
+
import { AGENT_TOOL_SCHEMAS } from './tool-schemas';
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
/** Provider token holding the user-supplied transport(s). */
|
|
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');
|
|
11
|
+
const ERROR_INVALID_PARAMS = -32602;
|
|
12
|
+
const ERROR_METHOD_NOT_FOUND = -32601;
|
|
13
|
+
const ERROR_FLOW_NOT_FOUND = -32000;
|
|
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
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Routes JSON-RPC requests from one or more transports to registered
|
|
30
|
+
* `NgFlowService` instances, and pushes change events back to the agent.
|
|
31
|
+
*
|
|
32
|
+
* Provide via {@link provideAgentBridge}; register a flow by calling
|
|
33
|
+
* `register(id, ngFlowService)` from `(init)` on `<ng-flow>`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* // appConfig.ts
|
|
38
|
+
* providers: [
|
|
39
|
+
* provideAgentBridge({
|
|
40
|
+
* transports: [new WindowTransport({ namespace: 'angflow' })],
|
|
41
|
+
* }),
|
|
42
|
+
* ]
|
|
43
|
+
*
|
|
44
|
+
* // some-component.ts
|
|
45
|
+
* bridge = inject(AngflowAgentBridge);
|
|
46
|
+
*
|
|
47
|
+
* onInit(flow: NgFlowService) {
|
|
48
|
+
* this.bridge.register('main', flow);
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export class AngflowAgentBridge {
|
|
53
|
+
constructor(transports, historyOptions, onError) {
|
|
54
|
+
/** Schemas for every exposed tool — feed straight to a Claude / OpenAI tools array. */
|
|
55
|
+
this.toolSchemas = AGENT_TOOL_SCHEMAS;
|
|
56
|
+
this.flows = new Map();
|
|
57
|
+
this.handlers = new Map();
|
|
58
|
+
this.injector = inject(Injector);
|
|
59
|
+
this.started = false;
|
|
60
|
+
this.nextInProcessId = 1;
|
|
61
|
+
this.warnedOnBeforeDeleteBypass = false;
|
|
62
|
+
/** Bumped every time a flow registers/unregisters. Useful for diagnostics. */
|
|
63
|
+
this.registeredFlows = signal([], ...(ngDevMode ? [{ debugName: "registeredFlows" }] : /* istanbul ignore next */ []));
|
|
64
|
+
this.transports = transports ?? [];
|
|
65
|
+
this.history =
|
|
66
|
+
historyOptions === false ? null : new AgentHistory(historyOptions ?? undefined);
|
|
67
|
+
this.onError = onError ?? null;
|
|
68
|
+
this.installHandlers();
|
|
69
|
+
this.start();
|
|
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
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Register a flow under `id`. The bridge subscribes to its `nodes`,
|
|
83
|
+
* `edges`, and `viewport` signals and emits change events to all
|
|
84
|
+
* transports.
|
|
85
|
+
*
|
|
86
|
+
* Returns a disposer; if not called manually, registration leaks until
|
|
87
|
+
* the bridge itself is torn down. In Angular components, call from
|
|
88
|
+
* `(init)` on `<ng-flow>` and pair with `OnDestroy`.
|
|
89
|
+
*/
|
|
90
|
+
register(id, flow) {
|
|
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);
|
|
103
|
+
}
|
|
104
|
+
const dispose = this.watchFlow(id, flow);
|
|
105
|
+
this.flows.set(id, { service: flow, dispose });
|
|
106
|
+
this.registeredFlows.set(Array.from(this.flows.keys()));
|
|
107
|
+
this.emit({ event: 'flow.registered', params: { flowId: id } });
|
|
108
|
+
return () => this.unregister(id);
|
|
109
|
+
}
|
|
110
|
+
unregister(id) {
|
|
111
|
+
const entry = this.flows.get(id);
|
|
112
|
+
if (!entry)
|
|
113
|
+
return;
|
|
114
|
+
entry.dispose();
|
|
115
|
+
this.flows.delete(id);
|
|
116
|
+
this.history?.dropFlow(id);
|
|
117
|
+
this.registeredFlows.set(Array.from(this.flows.keys()));
|
|
118
|
+
this.emit({ event: 'flow.unregistered', params: { flowId: id } });
|
|
119
|
+
}
|
|
120
|
+
/** Look up a registered flow. */
|
|
121
|
+
getFlow(id) {
|
|
122
|
+
return this.flows.get(id)?.service;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
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.
|
|
129
|
+
*/
|
|
130
|
+
async callTool(method, params = {}) {
|
|
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;
|
|
141
|
+
}
|
|
142
|
+
return response.result;
|
|
143
|
+
}
|
|
144
|
+
// ── Internals ────────────────────────────────────────────────────────
|
|
145
|
+
start() {
|
|
146
|
+
if (this.started)
|
|
147
|
+
return;
|
|
148
|
+
this.started = true;
|
|
149
|
+
for (const t of this.transports) {
|
|
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
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async dispatch(req) {
|
|
162
|
+
const handler = this.handlers.get(req.method);
|
|
163
|
+
if (!handler) {
|
|
164
|
+
return {
|
|
165
|
+
id: req.id,
|
|
166
|
+
error: { code: ERROR_METHOD_NOT_FOUND, message: `Unknown method: ${req.method}` },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const params = req.params ?? {};
|
|
171
|
+
if (req.method === 'list_flows') {
|
|
172
|
+
const result = await handler(null, params);
|
|
173
|
+
return { id: req.id, result };
|
|
174
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
208
|
+
return { id: req.id, result: result ?? null };
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
if (err instanceof FlowNotFoundError) {
|
|
212
|
+
return { id: req.id, error: { code: ERROR_FLOW_NOT_FOUND, message: err.message } };
|
|
213
|
+
}
|
|
214
|
+
if (err instanceof InvalidParamsError) {
|
|
215
|
+
return { id: req.id, error: { code: ERROR_INVALID_PARAMS, message: err.message } };
|
|
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 });
|
|
231
|
+
return {
|
|
232
|
+
id: req.id,
|
|
233
|
+
error: {
|
|
234
|
+
code: ERROR_INTERNAL,
|
|
235
|
+
message: err instanceof Error ? err.message : String(err),
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
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
|
+
}
|
|
253
|
+
emit(evt) {
|
|
254
|
+
for (const t of this.transports) {
|
|
255
|
+
try {
|
|
256
|
+
t.send(evt);
|
|
257
|
+
}
|
|
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 });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
resolveFlow(rawId) {
|
|
266
|
+
if (typeof rawId === 'string' && rawId.length > 0) {
|
|
267
|
+
const entry = this.flows.get(rawId);
|
|
268
|
+
if (!entry)
|
|
269
|
+
throw new FlowNotFoundError(`No flow registered with id "${rawId}"`);
|
|
270
|
+
return entry.service;
|
|
271
|
+
}
|
|
272
|
+
if (this.flows.size === 0) {
|
|
273
|
+
throw new FlowNotFoundError('No flows are registered with the agent bridge.');
|
|
274
|
+
}
|
|
275
|
+
if (this.flows.size > 1) {
|
|
276
|
+
throw new InvalidParamsError(`Multiple flows registered (${Array.from(this.flows.keys()).join(', ')}); pass flowId to disambiguate.`);
|
|
277
|
+
}
|
|
278
|
+
return this.flows.values().next().value.service;
|
|
279
|
+
}
|
|
280
|
+
watchFlow(id, flow) {
|
|
281
|
+
let pending = false;
|
|
282
|
+
let destroyed = false;
|
|
283
|
+
let lastSignature = '';
|
|
284
|
+
const ref = runInInjectionContext(this.injector, () => effect(() => {
|
|
285
|
+
// Touch every signal we want to broadcast so the effect re-runs on change.
|
|
286
|
+
flow.nodes();
|
|
287
|
+
flow.edges();
|
|
288
|
+
flow.viewport();
|
|
289
|
+
flow.selectedNodes();
|
|
290
|
+
flow.selectedEdges();
|
|
291
|
+
if (pending)
|
|
292
|
+
return;
|
|
293
|
+
pending = true;
|
|
294
|
+
queueMicrotask(() => {
|
|
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;
|
|
301
|
+
// Re-read on flush so coalesced bursts see the latest state.
|
|
302
|
+
const params = {
|
|
303
|
+
flowId: id,
|
|
304
|
+
nodes: flow.getNodes(),
|
|
305
|
+
edges: flow.getEdges(),
|
|
306
|
+
viewport: flow.getViewport(),
|
|
307
|
+
selection: {
|
|
308
|
+
nodeIds: flow.selectedNodes().map((n) => n.id),
|
|
309
|
+
edgeIds: flow.selectedEdges().map((e) => e.id),
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
// Suppress duplicate emissions when controlled-mode round-trips bounce
|
|
313
|
+
// identical state through the store twice. See signatureOf for the
|
|
314
|
+
// field set; it must cover anything a mutating tool can change.
|
|
315
|
+
const sig = signatureOf(params);
|
|
316
|
+
if (sig === lastSignature)
|
|
317
|
+
return;
|
|
318
|
+
lastSignature = sig;
|
|
319
|
+
this.emit({ event: 'flow.state', params });
|
|
320
|
+
});
|
|
321
|
+
}));
|
|
322
|
+
return () => {
|
|
323
|
+
destroyed = true;
|
|
324
|
+
ref.destroy();
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
installHandlers() {
|
|
328
|
+
this.handlers.set('list_flows', () => Array.from(this.flows.keys()));
|
|
329
|
+
this.handlers.set('get_state', (flow) => ({
|
|
330
|
+
nodes: flow.getNodes(),
|
|
331
|
+
edges: flow.getEdges(),
|
|
332
|
+
viewport: flow.getViewport(),
|
|
333
|
+
}));
|
|
334
|
+
this.handlers.set('get_nodes', (flow) => flow.getNodes());
|
|
335
|
+
this.handlers.set('get_edges', (flow) => flow.getEdges());
|
|
336
|
+
this.handlers.set('get_node', (flow, params) => {
|
|
337
|
+
const id = requireString(params, 'id');
|
|
338
|
+
return flow.getNode(id) ?? null;
|
|
339
|
+
});
|
|
340
|
+
this.handlers.set('get_edge', (flow, params) => {
|
|
341
|
+
const id = requireString(params, 'id');
|
|
342
|
+
return flow.getEdge(id) ?? null;
|
|
343
|
+
});
|
|
344
|
+
this.handlers.set('add_node', (flow, params) => {
|
|
345
|
+
const node = validateNodeShape(requireObject(params, 'node'), 'add_node');
|
|
346
|
+
flow.addNodes(node);
|
|
347
|
+
return flow.getNode(node.id) ?? null;
|
|
348
|
+
});
|
|
349
|
+
this.handlers.set('add_edge', (flow, params) => {
|
|
350
|
+
const edge = validateEdgeShape(requireObject(params, 'edge'), 'add_edge');
|
|
351
|
+
flow.addEdges(edge);
|
|
352
|
+
return flow.getEdge(edge.id) ?? null;
|
|
353
|
+
});
|
|
354
|
+
this.handlers.set('update_node', (flow, params) => {
|
|
355
|
+
const id = requireString(params, 'id');
|
|
356
|
+
const patch = requireObject(params, 'patch');
|
|
357
|
+
flow.updateNode(id, patch);
|
|
358
|
+
return flow.getNode(id) ?? null;
|
|
359
|
+
});
|
|
360
|
+
this.handlers.set('update_edge', (flow, params) => {
|
|
361
|
+
const id = requireString(params, 'id');
|
|
362
|
+
const patch = requireObject(params, 'patch');
|
|
363
|
+
flow.updateEdge(id, patch);
|
|
364
|
+
return flow.getEdge(id) ?? null;
|
|
365
|
+
});
|
|
366
|
+
this.handlers.set('delete_elements', async (flow, params) => {
|
|
367
|
+
const nodeIds = optionalStringArray(params, 'nodeIds');
|
|
368
|
+
const edgeIds = optionalStringArray(params, 'edgeIds');
|
|
369
|
+
const result = await flow.deleteElements({
|
|
370
|
+
nodes: nodeIds?.map((id) => ({ id })) ?? [],
|
|
371
|
+
edges: edgeIds?.map((id) => ({ id })) ?? [],
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
deletedNodeIds: result.deletedNodes.map((n) => n.id),
|
|
375
|
+
deletedEdgeIds: result.deletedEdges.map((e) => e.id),
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
this.handlers.set('set_nodes', (flow, params) => {
|
|
379
|
+
const nodes = requireArray(params, 'nodes').map((n, i) => validateNodeShape(n, `set_nodes[${i}]`));
|
|
380
|
+
flow.setNodes(nodes);
|
|
381
|
+
});
|
|
382
|
+
this.handlers.set('set_edges', (flow, params) => {
|
|
383
|
+
const edges = requireArray(params, 'edges').map((e, i) => validateEdgeShape(e, `set_edges[${i}]`));
|
|
384
|
+
flow.setEdges(edges);
|
|
385
|
+
});
|
|
386
|
+
this.handlers.set('fit_view', (flow, params) => {
|
|
387
|
+
const padding = typeof params['padding'] === 'number' ? params['padding'] : undefined;
|
|
388
|
+
const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
|
|
389
|
+
const nodeIds = optionalStringArray(params, 'nodeIds');
|
|
390
|
+
const nodes = nodeIds
|
|
391
|
+
? nodeIds
|
|
392
|
+
.map((id) => flow.getNode(id))
|
|
393
|
+
.filter((n) => !!n)
|
|
394
|
+
.map((n) => ({ id: n.id }))
|
|
395
|
+
: undefined;
|
|
396
|
+
return flow.fitView({ padding, duration, nodes });
|
|
397
|
+
});
|
|
398
|
+
this.handlers.set('set_viewport', (flow, params) => {
|
|
399
|
+
const viewport = requireObject(params, 'viewport');
|
|
400
|
+
const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
|
|
401
|
+
flow.setViewport(viewport, { duration });
|
|
402
|
+
});
|
|
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
|
+
});
|
|
672
|
+
}
|
|
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 }); }
|
|
674
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, providedIn: 'root' }); }
|
|
675
|
+
}
|
|
676
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, decorators: [{
|
|
677
|
+
type: Injectable,
|
|
678
|
+
args: [{ providedIn: 'root' }]
|
|
679
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
680
|
+
type: Optional
|
|
681
|
+
}, {
|
|
682
|
+
type: Inject,
|
|
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]
|
|
694
|
+
}] }] });
|
|
695
|
+
class FlowNotFoundError extends Error {
|
|
696
|
+
}
|
|
697
|
+
class InvalidParamsError extends Error {
|
|
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
|
+
*/
|
|
713
|
+
function signatureOf(params) {
|
|
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
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function requireString(params, key) {
|
|
869
|
+
const value = params[key];
|
|
870
|
+
if (typeof value !== 'string')
|
|
871
|
+
throw new InvalidParamsError(`Param "${key}" must be a string.`);
|
|
872
|
+
return value;
|
|
873
|
+
}
|
|
874
|
+
function requireObject(params, key) {
|
|
875
|
+
const value = params[key];
|
|
876
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
877
|
+
throw new InvalidParamsError(`Param "${key}" must be an object.`);
|
|
878
|
+
}
|
|
879
|
+
return value;
|
|
880
|
+
}
|
|
881
|
+
function requireArray(params, key) {
|
|
882
|
+
const value = params[key];
|
|
883
|
+
if (!Array.isArray(value))
|
|
884
|
+
throw new InvalidParamsError(`Param "${key}" must be an array.`);
|
|
885
|
+
return value;
|
|
886
|
+
}
|
|
887
|
+
function optionalStringArray(params, key) {
|
|
888
|
+
const value = params[key];
|
|
889
|
+
if (value == null)
|
|
890
|
+
return undefined;
|
|
891
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== 'string')) {
|
|
892
|
+
throw new InvalidParamsError(`Param "${key}" must be an array of strings.`);
|
|
893
|
+
}
|
|
894
|
+
return value;
|
|
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
|
+
}
|
|
935
|
+
//# sourceMappingURL=agent-bridge.service.js.map
|