@istar-ts/actions 0.0.0-stage → 0.15.0-beta.1

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/index.cjs ADDED
@@ -0,0 +1,1170 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _istar_ts_core = require("@istar-ts/core");
3
+ //#region src/actions.ts
4
+ const OPERATION_KIND_LIST = [
5
+ "createNode",
6
+ "createEdge",
7
+ "deleteElement",
8
+ "changeBounds",
9
+ "changeContainer",
10
+ "reconnectEdge",
11
+ "applyLabelEdit",
12
+ "compound",
13
+ "istar.addLeg",
14
+ "istar.removeLeg",
15
+ "istar.setCollapsed",
16
+ "istar.updateDiagram",
17
+ "istar.updateProperties",
18
+ "istar.updateDisplay",
19
+ "istar.setLinkValue",
20
+ "istar.updateLinkDisplay",
21
+ "istar.changeLinkKind",
22
+ "istar.setFileMetamodel"
23
+ ];
24
+ const REQUEST_KIND_LIST = [
25
+ "requestModel",
26
+ "requestTypeHints",
27
+ "requestCheckEdge",
28
+ "requestContextActions",
29
+ "requestMarkers"
30
+ ];
31
+ /**
32
+ * The other actions the dispatcher accepts: `glspUndo`/`glspRedo`/`saveModel` (acted on), then
33
+ * the notifications it only re-emits to subscribers (`IstarNotificationAction`'s kinds).
34
+ */
35
+ const NOTIFICATION_KIND_LIST = [
36
+ "glspUndo",
37
+ "glspRedo",
38
+ "saveModel",
39
+ "sourceModelChanged",
40
+ "setDirtyState",
41
+ "triggerNodeCreation",
42
+ "triggerEdgeCreation",
43
+ "deleteMarkers"
44
+ ];
45
+ const OPERATION_KINDS = new Set(OPERATION_KIND_LIST);
46
+ const REQUEST_KINDS = new Set(REQUEST_KIND_LIST);
47
+ /**
48
+ * The action kinds `createActionDispatcher` accepts, grouped the way it treats them: `operations`
49
+ * edit the model as one undo step, `requests` get a response action, `notifications` (plus
50
+ * `glspUndo`/`glspRedo`/`saveModel`) are acted on or re-emitted as is. Derived from the same
51
+ * lists `isOperation`/`isRequestAction` use, so it can't drift from the dispatcher.
52
+ */
53
+ const HANDLED_ACTION_KINDS = {
54
+ operations: OPERATION_KIND_LIST,
55
+ requests: REQUEST_KIND_LIST,
56
+ notifications: NOTIFICATION_KIND_LIST
57
+ };
58
+ /** True for the operations (actions that edit the model). */
59
+ function isOperation(action) {
60
+ return OPERATION_KINDS.has(action.kind);
61
+ }
62
+ /** True for the requests (actions answered with a response action). */
63
+ function isRequestAction(action) {
64
+ return REQUEST_KINDS.has(action.kind);
65
+ }
66
+ //#endregion
67
+ //#region src/hints.ts
68
+ /**
69
+ * What a client can know before it asks: type hints from the metamodel, and the palette as
70
+ * GLSP context actions. Both are derived from data (`Metamodel`, `PaletteDescription`), so they
71
+ * work for any root or extension.
72
+ */
73
+ function isHub(definition) {
74
+ return definition.role === "hub";
75
+ }
76
+ /** Element kinds a rule selector matches, as `canLink` matches elements against it. */
77
+ function matching(metamodel, selector) {
78
+ const kinds = [];
79
+ for (const definition of metamodel.elements.values()) if (isHub(definition) ? definition.kind === selector : selector === "*" || selector === definition.category || (0, _istar_ts_core.elementBehavesLike)(metamodel, definition.kind, selector)) kinds.push(definition.kind);
80
+ if (selector === _istar_ts_core.LINK_SELECTOR) kinds.push(...metamodel.links.keys());
81
+ else if (metamodel.links.has(selector)) {
82
+ for (const kind of metamodel.links.keys()) if ((0, _istar_ts_core.linkBehavesLike)(metamodel, kind, selector)) kinds.push(kind);
83
+ }
84
+ return kinds;
85
+ }
86
+ function union(metamodel, selectors) {
87
+ return [...new Set(selectors.flatMap((selector) => matching(metamodel, selector)))];
88
+ }
89
+ /** Kinds of a category, without hubs. */
90
+ function ofCategory(metamodel, category) {
91
+ return [...metamodel.elements.values()].filter((d) => !isHub(d) && (category === void 0 || d.category === category)).map((d) => d.kind);
92
+ }
93
+ /**
94
+ * GLSP type hints of a metamodel. Shapes: every element kind (hubs can't be resized or
95
+ * reparented; actors contain the node kinds that can be inner elements). Edges: every link kind,
96
+ * its sources and targets from its rules, or by category for kinds with built-in rules (iStar
97
+ * 2.0's), plus an n-ary kind's hub (`createEdge` onto a hub adds a leg to that link, #108). Edge
98
+ * hints are `repositionable` (their ends can be reconnected) and `dynamic`: `canLink` checks more
99
+ * than kinds.
100
+ */
101
+ function typeHintsFor(metamodel) {
102
+ const meta = metamodel;
103
+ const inner = [...meta.elements.values()].filter((d) => d.category === "node" && !isHub(d) && d.info?.canBeInnerElement !== false).map((d) => d.kind);
104
+ return {
105
+ shapeHints: [...meta.elements.values()].map((d) => {
106
+ const hub = isHub(d);
107
+ return {
108
+ elementTypeId: d.kind,
109
+ repositionable: true,
110
+ deletable: true,
111
+ resizable: !hub,
112
+ reparentable: !hub && d.category === "node" && d.info?.canBeInnerElement !== false,
113
+ ...d.category === "actor" ? { containableElementTypeIds: inner } : {}
114
+ };
115
+ }),
116
+ edgeHints: [...meta.links.values()].map((d) => {
117
+ const rules = meta.links.get((0, _istar_ts_core.effectiveLinkKind)(meta, d.kind))?.rules;
118
+ const builtIn = d.category === "dependency" ? ofCategory(meta) : ofCategory(meta, d.category);
119
+ const targets = rules ? union(meta, rules.targets) : builtIn;
120
+ const hub = d.parts?.hub;
121
+ return {
122
+ elementTypeId: d.kind,
123
+ repositionable: true,
124
+ deletable: true,
125
+ routable: true,
126
+ sourceElementTypeIds: rules ? union(meta, rules.sources) : builtIn,
127
+ targetElementTypeIds: hub === void 0 ? targets : [...targets, hub],
128
+ dynamic: true
129
+ };
130
+ })
131
+ };
132
+ }
133
+ function elementEntries(kind, value) {
134
+ return (Array.isArray(value) ? value : [value]).map((entry, i) => labeled(Array.isArray(value) ? `${kind}#${i}` : kind, entry, {
135
+ kind: "triggerNodeCreation",
136
+ elementTypeId: kind,
137
+ ...entry.properties ? { args: { customProperties: entry.properties } } : {}
138
+ }));
139
+ }
140
+ function linkEntries(kind, list) {
141
+ return list.map((entry, i) => {
142
+ const args = {
143
+ ...entry.value === void 0 ? {} : { value: entry.value },
144
+ ...entry.dependum === void 0 ? {} : { dependum: { kind: entry.dependum } }
145
+ };
146
+ return labeled(`${kind}#${i}`, entry, {
147
+ kind: "triggerEdgeCreation",
148
+ elementTypeId: kind,
149
+ ...Object.keys(args).length > 0 ? { args } : {}
150
+ });
151
+ });
152
+ }
153
+ function order(action) {
154
+ return action.order ?? Number.POSITIVE_INFINITY;
155
+ }
156
+ function labeled(id, entry, action) {
157
+ return {
158
+ id,
159
+ label: entry.label,
160
+ actions: [action],
161
+ ...entry.group === void 0 ? {} : { group: entry.group },
162
+ ...entry.section === void 0 ? {} : { section: entry.section },
163
+ ...entry.order === void 0 ? {} : { order: entry.order }
164
+ };
165
+ }
166
+ /**
167
+ * A palette as GLSP context actions: one `LabeledAction` per entry, triggering the client's
168
+ * creation tool with the entry's presets, sorted by `order`. Only the metamodel's kinds are
169
+ * offered, in the metamodel's order where orders tie; kinds the palette hides (`false`) or
170
+ * lacks are left out.
171
+ */
172
+ function contextActionsFor(palette, metamodel) {
173
+ const meta = metamodel;
174
+ const actions = [];
175
+ for (const kind of meta.elements.keys()) {
176
+ const value = palette.elements[kind];
177
+ if (value) actions.push(...elementEntries(kind, value));
178
+ }
179
+ for (const kind of meta.links.keys()) {
180
+ const list = palette.links[kind];
181
+ if (list) actions.push(...linkEntries(kind, list));
182
+ }
183
+ actions.sort((a, b) => order(a) === order(b) ? 0 : order(a) < order(b) ? -1 : 1);
184
+ return {
185
+ actions,
186
+ groups: palette.groups
187
+ };
188
+ }
189
+ //#endregion
190
+ //#region src/markers.ts
191
+ /** A `GoalDiagnostic` as a GLSP marker: `kind` is its severity, `label` its message. */
192
+ function diagnosticToMarker(diagnostic) {
193
+ const { elementId, key, severity, message, source, code } = diagnostic;
194
+ return {
195
+ label: message,
196
+ description: source === void 0 ? message : `${message} (${source})`,
197
+ elementId,
198
+ kind: severity,
199
+ ...key === void 0 ? {} : { key },
200
+ ...source === void 0 ? {} : { source },
201
+ ...code === void 0 ? {} : { code }
202
+ };
203
+ }
204
+ /**
205
+ * A GLSP marker as a `GoalDiagnostic`. A `kind` that is no `DiagnosticSeverity` reads as
206
+ * `info`; `range` and `data` are not carried by markers.
207
+ */
208
+ function markerToDiagnostic(marker) {
209
+ const { elementId, kind, label, key, source, code } = marker;
210
+ return {
211
+ elementId,
212
+ severity: _istar_ts_core.DIAGNOSTIC_SEVERITIES.includes(kind) ? kind : "info",
213
+ message: label,
214
+ ...key === void 0 ? {} : { key },
215
+ ...source === void 0 ? {} : { source },
216
+ ...code === void 0 ? {} : { code }
217
+ };
218
+ }
219
+ //#endregion
220
+ //#region src/dispatcher.ts
221
+ /**
222
+ * The dispatcher: the server side of the protocol over a `ModelStore`. Operations run as one
223
+ * store transaction each and are rejected, model untouched, with `GoalDiagnostic`s; requests
224
+ * are answered from the model, its metamodel and the palette; store changes reach subscribers
225
+ * as `ModelSourceChangedAction`s, whoever made them.
226
+ */
227
+ /** Thrown inside a transaction to roll it back and reject the action. */
228
+ var Rejection = class extends Error {
229
+ diagnostics;
230
+ constructor(diagnostics) {
231
+ super(diagnostics.map((d) => d.message).join("; "));
232
+ this.diagnostics = diagnostics;
233
+ }
234
+ };
235
+ function reject(message, elementId = "", code) {
236
+ throw new Rejection([{
237
+ elementId,
238
+ severity: "error",
239
+ message,
240
+ ...code === void 0 ? {} : { code }
241
+ }]);
242
+ }
243
+ function rejectCheck(check, endpoints) {
244
+ throw new Rejection([(0, _istar_ts_core.connectResultToDiagnostic)(check, endpoints)]);
245
+ }
246
+ function metaOf(model) {
247
+ return (0, _istar_ts_core.metamodelOf)(model);
248
+ }
249
+ /** A model with `links` changed, as the core operations derive one. */
250
+ function withLinks(model, links) {
251
+ return (0, _istar_ts_core.inheritMetamodel)(model, (0, _istar_ts_core.inheritSourceLayout)(model, {
252
+ ...model,
253
+ links
254
+ }));
255
+ }
256
+ function createdIds(before, after) {
257
+ const ids = [];
258
+ for (const id of after.elements.keys()) if (!before.elements.has(id)) ids.push(id);
259
+ for (const id of after.links.keys()) if (!before.links.has(id)) ids.push(id);
260
+ return ids;
261
+ }
262
+ function issueKey(d) {
263
+ return `${d.id}\u0000${d.key}\u0000${d.message}`;
264
+ }
265
+ function linkIssueDiagnostics(model) {
266
+ return (0, _istar_ts_core.validateModel)(model).map((issue) => ({
267
+ elementId: issue.linkId,
268
+ severity: "error",
269
+ message: issue.message,
270
+ code: issue.code
271
+ }));
272
+ }
273
+ /**
274
+ * A dispatcher over `store`, in-process. Hand it to a transport to serve remote clients.
275
+ */
276
+ function createActionDispatcher(store, options = {}) {
277
+ const s = store;
278
+ const guards = options.guards ?? [];
279
+ const postGuards = options.postGuards ?? [];
280
+ const palette = options.palette ?? _istar_ts_core.DEFAULT_PALETTE;
281
+ const sourceModelName = options.sourceModelName ?? "";
282
+ const listeners = /* @__PURE__ */ new Set();
283
+ let saved = s.getModel();
284
+ let dirty = false;
285
+ const emit = (action) => {
286
+ for (const listener of listeners) listener(action);
287
+ };
288
+ const updateDirty = (reason) => {
289
+ const next = s.getModel() !== saved;
290
+ if (next === dirty) return;
291
+ dirty = next;
292
+ emit({
293
+ kind: "setDirtyState",
294
+ isDirty: next,
295
+ reason
296
+ });
297
+ };
298
+ const REASONS = {
299
+ edit: "operation",
300
+ undo: "undo",
301
+ redo: "redo",
302
+ load: "external"
303
+ };
304
+ const unsubscribe = s.subscribe((event) => {
305
+ if (event.source === "load") saved = event.model;
306
+ emit({
307
+ kind: "sourceModelChanged",
308
+ sourceModelName,
309
+ source: event.source,
310
+ changes: event.changes,
311
+ ...options.sendModel ? { newRoot: (0, _istar_ts_core.toPistarObject)(event.model) } : {},
312
+ ...options.sendModel ? {
313
+ canUndo: s.canUndo(),
314
+ canRedo: s.canRedo()
315
+ } : {}
316
+ });
317
+ updateDirty(REASONS[event.source]);
318
+ });
319
+ const requireElement = (id) => s.getModel().elements.get(id) ?? reject(`unknown element "${id}"`, id, "unknown-element");
320
+ const runGuards = (operation, warnings) => {
321
+ const errors = [];
322
+ for (const guard of guards) for (const d of guard(operation, s.getModel()) ?? []) (d.severity === "error" ? errors : warnings).push(d);
323
+ if (errors.length > 0) throw new Rejection(errors);
324
+ };
325
+ const runPostGuards = (operation, before, warnings) => {
326
+ const errors = [];
327
+ for (const guard of postGuards) for (const d of guard(operation, s.getModel(), before) ?? []) (d.severity === "error" ? errors : warnings).push(d);
328
+ if (errors.length > 0) throw new Rejection(errors);
329
+ };
330
+ const apply = (operation, warnings) => {
331
+ runGuards(operation, warnings);
332
+ const before = s.getModel();
333
+ doApply(operation, warnings);
334
+ runPostGuards(operation, before, warnings);
335
+ };
336
+ const doApply = (operation, warnings) => {
337
+ const model = s.getModel();
338
+ const meta = metaOf(model);
339
+ switch (operation.kind) {
340
+ case "createNode": {
341
+ const { elementTypeId: kind, containerId, args = {} } = operation;
342
+ const definition = meta.elements.get(kind) ?? reject(`unknown element kind "${kind}" in ${meta.name}`, "", "unknown-kind");
343
+ if (definition.role === "hub") reject(`a ${definition.label} is created with its link (createEdge)`, "", "hub-kind");
344
+ const container = containerId === void 0 ? void 0 : requireElement(containerId);
345
+ if (definition.category === "node") {
346
+ if (container && definition.info?.canBeInnerElement === false) reject(`a ${definition.label} cannot be inside an actor`, containerId, "container");
347
+ if (!container && definition.info?.canBeOnPaper === false) reject(`a ${definition.label} must be created inside an actor`, "", "container");
348
+ }
349
+ const location = operation.location ?? container ?? {
350
+ x: 0,
351
+ y: 0
352
+ };
353
+ s.addElement({
354
+ kind,
355
+ x: location.x,
356
+ y: location.y,
357
+ ...containerId === void 0 ? {} : { parent: containerId },
358
+ ...args.id === void 0 ? {} : { id: args.id },
359
+ ...args.name === void 0 ? {} : { name: args.name },
360
+ ...args.customProperties ? { customProperties: args.customProperties } : {}
361
+ });
362
+ return;
363
+ }
364
+ case "createEdge": {
365
+ const { elementTypeId: kind, sourceElementId: source, targetElementId: target } = operation;
366
+ const args = operation.args ?? {};
367
+ const definition = meta.links.get(kind) ?? reject(`unknown link kind "${kind}" in ${meta.name}`, "", "unknown-kind");
368
+ if (definition.category === "dependency") {
369
+ const dependum = args.dependum ?? {};
370
+ const dependumKind = dependum.kind ?? [...meta.elements.values()].find((d) => d.category === "node" && d.role !== "hub" && d.info?.canBeDependum !== false)?.kind ?? reject(`${meta.name} has no kind that can be a dependum`, "", "unknown-kind");
371
+ const result = s.addDependency({
372
+ depender: source,
373
+ dependee: target,
374
+ linkKind: kind,
375
+ dependum: {
376
+ kind: dependumKind,
377
+ ...dependum.id === void 0 ? {} : { id: dependum.id },
378
+ ...dependum.name === void 0 ? {} : { name: dependum.name },
379
+ ...dependum.location ? {
380
+ x: dependum.location.x,
381
+ y: dependum.location.y
382
+ } : {},
383
+ ...dependum.customProperties ? { customProperties: dependum.customProperties } : {}
384
+ },
385
+ ...args.linkIds ? { linkIds: args.linkIds } : {}
386
+ });
387
+ if (!result.ok) rejectCheck(result, {
388
+ source,
389
+ target
390
+ });
391
+ return;
392
+ }
393
+ const values = definition.info.possibleLabels;
394
+ if (args.value !== void 0 && values && !values.includes(args.value)) reject(`"${args.value}" is not a value of a ${definition.label} link (${values.join(", ")})`, source, "invalid-value");
395
+ const result = s.connect({
396
+ kind,
397
+ source,
398
+ target,
399
+ ...args.id === void 0 ? {} : { id: args.id },
400
+ ...args.value === void 0 ? {} : { label: args.value },
401
+ ...args.name === void 0 ? {} : { name: args.name },
402
+ ...args.customProperties ? { customProperties: args.customProperties } : {},
403
+ ...args.legId === void 0 ? {} : { legId: args.legId },
404
+ ...args.endId === void 0 ? {} : { endId: args.endId }
405
+ });
406
+ if (!result.ok) rejectCheck(result, {
407
+ source,
408
+ target
409
+ });
410
+ return;
411
+ }
412
+ case "deleteElement": {
413
+ const elements = [];
414
+ const links = [];
415
+ for (const id of operation.elementIds) if (model.elements.has(id)) elements.push(id);
416
+ else if (model.links.has(id)) links.push(id);
417
+ else reject(`unknown element or link "${id}"`, id, "unknown-element");
418
+ if (elements.length > 0) s.removeElements(elements);
419
+ for (const id of links) if (s.getModel().links.has(id)) s.disconnect(id);
420
+ return;
421
+ }
422
+ case "changeBounds":
423
+ for (const { elementId, newPosition, newSize } of operation.newBounds) {
424
+ requireElement(elementId);
425
+ if (newPosition) s.moveElement(elementId, newPosition.x, newPosition.y);
426
+ if (newSize) s.updateElement(elementId, { display: {
427
+ width: newSize.width,
428
+ height: newSize.height
429
+ } });
430
+ }
431
+ return;
432
+ case "changeContainer": {
433
+ const { elementId, targetContainerId, location } = operation;
434
+ s.nestElement(elementId, targetContainerId);
435
+ if (location) s.moveElement(elementId, location.x, location.y);
436
+ return;
437
+ }
438
+ case "reconnectEdge":
439
+ reconnect(operation.edgeElementId, operation.sourceElementId, operation.targetElementId);
440
+ return;
441
+ case "applyLabelEdit": {
442
+ const hash = operation.labelId.indexOf("#");
443
+ const id = hash < 0 ? operation.labelId : operation.labelId.slice(0, hash);
444
+ const key = hash < 0 ? void 0 : operation.labelId.slice(hash + 1);
445
+ if (key === void 0) {
446
+ if (model.elements.has(id)) s.updateElement(id, { name: operation.text });
447
+ else if (model.links.has(id)) s.updateLink(id, { name: operation.text === "" ? null : operation.text });
448
+ else reject(`unknown element or link "${id}"`, id, "unknown-element");
449
+ } else setProperties(id, { [key]: operation.text });
450
+ return;
451
+ }
452
+ case "compound":
453
+ for (const child of operation.operationList) apply(child, warnings);
454
+ return;
455
+ case "istar.addLeg": {
456
+ const { hubId, sourceElementId: source } = operation;
457
+ const result = s.addLeg(hubId, source, operation.args?.id ? { id: operation.args.id } : {});
458
+ if (!result.ok) rejectCheck(result, {
459
+ source,
460
+ target: hubId
461
+ });
462
+ return;
463
+ }
464
+ case "istar.removeLeg":
465
+ s.removeLeg(operation.legId);
466
+ return;
467
+ case "istar.setCollapsed": {
468
+ const element = requireElement(operation.elementId);
469
+ if (meta.elements.get(element.kind)?.category !== "actor") reject(`"${element.id}" is not an actor`, element.id, "not-an-actor");
470
+ s.setCollapsed(element.id, operation.collapsed);
471
+ return;
472
+ }
473
+ case "istar.updateDiagram": {
474
+ const { name, width, height, customProperties } = operation.diagram;
475
+ s.updateDiagram({
476
+ ...name === void 0 ? {} : { name },
477
+ ...width === void 0 ? {} : { width },
478
+ ...height === void 0 ? {} : { height },
479
+ ...customProperties === void 0 ? {} : { customProperties }
480
+ });
481
+ return;
482
+ }
483
+ case "istar.updateProperties":
484
+ setProperties(operation.elementId, operation.properties);
485
+ return;
486
+ case "istar.updateDisplay":
487
+ requireElement(operation.elementId);
488
+ s.updateElement(operation.elementId, { display: operation.patch });
489
+ return;
490
+ case "istar.updateLinkDisplay":
491
+ if (!model.links.has(operation.linkId)) reject(`unknown link "${operation.linkId}"`, operation.linkId, "unknown-element");
492
+ s.updateLink(operation.linkId, { display: operation.patch });
493
+ return;
494
+ case "istar.setLinkValue": {
495
+ const { linkId, value } = operation;
496
+ const link = model.links.get(linkId) ?? reject(`unknown link "${linkId}"`, linkId, "unknown-element");
497
+ const definition = meta.links.get(link.kind);
498
+ const values = definition?.info.possibleLabels;
499
+ if (value !== null && values && !values.includes(value)) reject(`"${value}" is not a value of a ${definition.label} link (${values.join(", ")})`, linkId, "invalid-value");
500
+ s.updateLink(linkId, { label: value });
501
+ return;
502
+ }
503
+ case "istar.changeLinkKind": {
504
+ const { linkIds, elementTypeId: kind } = operation;
505
+ if (linkIds.length === 0) return;
506
+ const ids = new Set(linkIds);
507
+ const links = [...ids].map((id) => model.links.get(id) ?? reject(`unknown link "${id}"`, id, "unknown-element"));
508
+ const definition = meta.links.get(kind) ?? reject(`unknown link kind "${kind}" in ${meta.name}`, "", "unknown-kind");
509
+ const toChange = links.filter((link) => link.kind !== kind);
510
+ if (toChange.length === 0) return;
511
+ const remaining = new Map(model.links);
512
+ for (const id of ids) remaining.delete(id);
513
+ const without = withLinks(model, remaining);
514
+ const values = definition.info.possibleLabels;
515
+ for (const link of toChange) {
516
+ const check = (0, _istar_ts_core.canLink)(without, link.source, link.target, kind);
517
+ if (!check.ok) rejectCheck(check, {
518
+ source: link.source,
519
+ target: link.target
520
+ });
521
+ if (link.label != null && values && !values.includes(link.label)) reject(`"${link.label}" is not a value of a ${definition.label} link (${values.join(", ")})`, link.id, "invalid-value");
522
+ }
523
+ const next = new Map(model.links);
524
+ for (const link of toChange) next.set(link.id, {
525
+ ...link,
526
+ kind
527
+ });
528
+ s.replace(withLinks(model, next));
529
+ return;
530
+ }
531
+ case "istar.setFileMetamodel": {
532
+ let next;
533
+ try {
534
+ next = (0, _istar_ts_core.withFileMetamodel)(model, operation.block);
535
+ } catch (error) {
536
+ if (error instanceof _istar_ts_core.MetamodelError) reject(error.message, "", "invalid-metamodel");
537
+ throw error;
538
+ }
539
+ s.replace(next);
540
+ return;
541
+ }
542
+ default: reject(`unknown operation "${operation.kind}"`, "", "unknown-action");
543
+ }
544
+ };
545
+ /** Sets (or, with `null`, removes) custom properties of an element or link. */
546
+ const setProperties = (id, properties) => {
547
+ const model = s.getModel();
548
+ const owner = model.elements.get(id) ?? model.links.get(id);
549
+ if (!owner) reject(`unknown element or link "${id}"`, id, "unknown-element");
550
+ const next = { ...owner.customProperties };
551
+ for (const [key, value] of Object.entries(properties)) if (value === null) delete next[key];
552
+ else if (typeof value === "string") next[key] = value;
553
+ else reject(`the value of "${key}" must be a string or null`, id, "invalid-value");
554
+ const customProperties = Object.keys(next).length > 0 ? next : null;
555
+ if (model.elements.has(id)) s.updateElement(id, { customProperties });
556
+ else s.updateLink(id, { customProperties });
557
+ };
558
+ /**
559
+ * Gives a link new ends, checked with `canLink` against the model without it. The link keeps
560
+ * its place in the model (and the file), its id and everything else it carries.
561
+ */
562
+ const reconnect = (id, source, target) => {
563
+ const model = s.getModel();
564
+ const meta = metaOf(model);
565
+ const link = model.links.get(id) ?? reject(`unknown link "${id}"`, id, "unknown-element");
566
+ if (link.source === source && link.target === target) return;
567
+ const definition = meta.links.get(link.kind);
568
+ const others = new Map(model.links);
569
+ others.delete(id);
570
+ const without = withLinks(model, others);
571
+ let check;
572
+ if (definition?.category === "dependency") {
573
+ const dependumAtTarget = model.elements.get(link.target)?.isDependum === true;
574
+ const dependumId = dependumAtTarget ? link.target : link.source;
575
+ if ((dependumAtTarget ? target : source) !== dependumId) reject("a dependency link keeps its dependum", id, "dependum-endpoint");
576
+ const ends = (0, _istar_ts_core.dependencyEnds)(model, dependumId);
577
+ const depender = dependumAtTarget ? source : ends.depender;
578
+ const dependee = dependumAtTarget ? ends.dependee : target;
579
+ if (depender === void 0 || dependee === void 0) reject("the dependency is incomplete", id, "dependum-endpoint");
580
+ check = (0, _istar_ts_core.canLink)(without, depender, dependee, link.kind);
581
+ if (!check.ok) rejectCheck(check, {
582
+ source: depender,
583
+ target: dependee
584
+ });
585
+ } else {
586
+ if (definition?.role === "end" && source !== link.source) reject("the end of an n-ary link stays on its hub", id, "hub-target");
587
+ if (definition?.role === "leg" && target !== link.target) {
588
+ if ([...model.links.values()].filter((l) => l.target === link.target && l.kind === link.kind).length === 1) reject("the last leg of an n-ary link cannot leave its hub", id, "hub-target");
589
+ }
590
+ check = (0, _istar_ts_core.canLink)(without, source, target, link.kind);
591
+ if (!check.ok) rejectCheck(check, {
592
+ source,
593
+ target
594
+ });
595
+ }
596
+ s.replace(withLinks(model, new Map(model.links).set(id, {
597
+ ...link,
598
+ source,
599
+ target
600
+ })));
601
+ };
602
+ const runOperation = (operation) => {
603
+ const before = s.getModel();
604
+ const warnings = [];
605
+ s.transaction(() => {
606
+ apply(operation, warnings);
607
+ const schemas = options.schemas;
608
+ if (!schemas || s.getModel() === before) return;
609
+ const known = new Set((0, _istar_ts_core.validateModelProperties)(before, schemas).map(issueKey));
610
+ const added = (0, _istar_ts_core.validateModelProperties)(s.getModel(), schemas).filter((issue) => !known.has(issueKey(issue)));
611
+ if (added.length > 0) throw new Rejection(added.map((issue) => ({
612
+ elementId: issue.id,
613
+ key: issue.key,
614
+ severity: "error",
615
+ message: issue.message,
616
+ code: "invalid-property"
617
+ })));
618
+ });
619
+ return {
620
+ ok: true,
621
+ createdIds: createdIds(before, s.getModel()),
622
+ ...warnings.length > 0 ? { diagnostics: warnings } : {}
623
+ };
624
+ };
625
+ const checkEdge = (action) => {
626
+ const { edgeType, sourceElementId: source, targetElementId: target } = action;
627
+ const model = s.getModel();
628
+ const meta = metaOf(model);
629
+ const answer = (check) => ({
630
+ kind: "checkEdgeTargetResult",
631
+ responseId: action.requestId,
632
+ isValid: check.ok,
633
+ edgeType,
634
+ sourceElementId: source,
635
+ ...target === void 0 ? {} : { targetElementId: target },
636
+ ...check.ok ? {} : {
637
+ reason: check.reason,
638
+ code: check.code
639
+ }
640
+ });
641
+ if (target !== void 0) {
642
+ const check = (0, _istar_ts_core.canLink)(model, source, target, edgeType);
643
+ if (!check.ok && meta.links.get(edgeType)?.info.tryReversedWhenAdding) {
644
+ const reversed = (0, _istar_ts_core.canLink)(model, target, source, edgeType);
645
+ if (reversed.ok) return answer(reversed);
646
+ }
647
+ return answer(check);
648
+ }
649
+ const element = model.elements.get(source);
650
+ if (!element) return answer({
651
+ ok: false,
652
+ code: "unknown-element",
653
+ reason: `unknown source element "${source}"`
654
+ });
655
+ const hint = typeHintsFor(meta).edgeHints.find((h) => h.elementTypeId === edgeType);
656
+ if (!hint) return answer({
657
+ ok: false,
658
+ code: "unknown-link-kind",
659
+ reason: `unknown link kind "${edgeType}"`
660
+ });
661
+ if (hint.sourceElementTypeIds && !hint.sourceElementTypeIds.includes(element.kind)) return answer({
662
+ ok: false,
663
+ code: "invalid-source",
664
+ reason: `a ${meta.elements.get(element.kind)?.label ?? element.kind} cannot be the source of a ${meta.links.get(edgeType)?.label ?? edgeType} link`
665
+ });
666
+ return answer({ ok: true });
667
+ };
668
+ const markers = (action) => {
669
+ const model = s.getModel();
670
+ const diagnostics = linkIssueDiagnostics(model);
671
+ if (options.schemas) for (const issue of (0, _istar_ts_core.validateModelProperties)(model, options.schemas)) diagnostics.push({
672
+ elementId: issue.id,
673
+ key: issue.key,
674
+ severity: "error",
675
+ message: issue.message,
676
+ code: "invalid-property"
677
+ });
678
+ diagnostics.push(...options.diagnostics?.() ?? []);
679
+ const ids = action.elementsIDs.filter((id) => model.elements.has(id) || model.links.has(id));
680
+ const wanted = new Set(ids);
681
+ return {
682
+ kind: "setMarkers",
683
+ responseId: action.requestId,
684
+ markers: diagnostics.filter((d) => wanted.size === 0 || wanted.has(d.elementId)).map(diagnosticToMarker),
685
+ ...action.reason === void 0 ? {} : { reason: action.reason }
686
+ };
687
+ };
688
+ const respond = (action) => {
689
+ const model = s.getModel();
690
+ switch (action.kind) {
691
+ case "requestModel": return {
692
+ kind: "setModel",
693
+ responseId: action.requestId,
694
+ newRoot: (0, _istar_ts_core.toPistarObject)(model)
695
+ };
696
+ case "requestTypeHints": return {
697
+ kind: "setTypeHints",
698
+ responseId: action.requestId,
699
+ ...typeHintsFor(metaOf(model))
700
+ };
701
+ case "requestCheckEdge": return checkEdge(action);
702
+ case "requestContextActions": return {
703
+ kind: "setContextActions",
704
+ responseId: action.requestId,
705
+ ...contextActionsFor(palette, metaOf(model))
706
+ };
707
+ case "requestMarkers": return markers(action);
708
+ default: return;
709
+ }
710
+ };
711
+ const run = async (action) => {
712
+ if (isOperation(action)) return runOperation(action);
713
+ if (isRequestAction(action)) return {
714
+ ok: true,
715
+ createdIds: [],
716
+ response: respond(action)
717
+ };
718
+ switch (action.kind) {
719
+ case "glspUndo":
720
+ s.undo();
721
+ return {
722
+ ok: true,
723
+ createdIds: []
724
+ };
725
+ case "glspRedo":
726
+ s.redo();
727
+ return {
728
+ ok: true,
729
+ createdIds: []
730
+ };
731
+ case "saveModel": {
732
+ if (!options.save) reject("this host does not save models", "", "no-save-handler");
733
+ const model = s.getModel();
734
+ await options.save((0, _istar_ts_core.toPistarObject)(model, { saveDate: /* @__PURE__ */ new Date() }), action);
735
+ saved = model;
736
+ updateDirty("save");
737
+ return {
738
+ ok: true,
739
+ createdIds: []
740
+ };
741
+ }
742
+ case "sourceModelChanged":
743
+ case "setDirtyState":
744
+ case "triggerNodeCreation":
745
+ case "triggerEdgeCreation":
746
+ case "deleteMarkers":
747
+ emit(action);
748
+ return {
749
+ ok: true,
750
+ createdIds: []
751
+ };
752
+ default: return reject(`"${action.kind}" is not an action this dispatcher handles`, "", "unknown-action");
753
+ }
754
+ };
755
+ return {
756
+ async dispatch(action) {
757
+ try {
758
+ return await run(action);
759
+ } catch (error) {
760
+ if (error instanceof Rejection) return {
761
+ ok: false,
762
+ diagnostics: error.diagnostics
763
+ };
764
+ if (error instanceof _istar_ts_core.ModelOperationError) return {
765
+ ok: false,
766
+ diagnostics: [error.toDiagnostic()]
767
+ };
768
+ throw error;
769
+ }
770
+ },
771
+ subscribe(listener) {
772
+ const l = listener;
773
+ listeners.add(l);
774
+ return () => listeners.delete(l);
775
+ },
776
+ isDirty: () => s.getModel() !== saved,
777
+ dispose: unsubscribe
778
+ };
779
+ }
780
+ //#endregion
781
+ //#region src/host.ts
782
+ /** `dispatcher`, refusing anything but requests: what a read-only host tool is handed. */
783
+ function readOnlyDispatcher(dispatcher, tool) {
784
+ return {
785
+ dispatch: (action) => isRequestAction(action) ? dispatcher.dispatch(action) : Promise.resolve({
786
+ ok: false,
787
+ diagnostics: [{
788
+ elementId: "",
789
+ severity: "error",
790
+ code: "read-only-tool",
791
+ message: `"${tool}" is read-only: it cannot dispatch ${action.kind}`
792
+ }]
793
+ }),
794
+ subscribe: (listener) => dispatcher.subscribe(listener)
795
+ };
796
+ }
797
+ /** The context a call of `tool` gets: `dispatcher` as is, or read-only for a read-only tool. */
798
+ function hostToolContext(tool, dispatcher, getModel, signal) {
799
+ return {
800
+ getModel,
801
+ dispatcher: tool.readOnly ? readOnlyDispatcher(dispatcher, tool.name) : dispatcher,
802
+ ...signal ? { signal } : {}
803
+ };
804
+ }
805
+ /** A provider's text for `model`. */
806
+ function contextText(provider, model) {
807
+ const { text } = provider;
808
+ return Promise.resolve(typeof text === "string" ? text : text(model));
809
+ }
810
+ //#endregion
811
+ //#region src/mirror.ts
812
+ /**
813
+ * A mirror store: a read-only `ModelStore` in this process whose model follows a dispatcher that
814
+ * lives elsewhere (a worker, a server, the other end of a `createRemoteDispatcher` port), so a
815
+ * canvas and an inspector written against `ModelStore` can render it. Edits never touch the
816
+ * mirror: they are dispatched to the remote, and come back as the remote's `sourceModelChanged`.
817
+ *
818
+ * How the model travels: an initial `requestModel`, then one update per `sourceModelChanged`.
819
+ * A notification that carries the new model (`newRoot`, a dispatcher created with `sendModel`)
820
+ * is applied as it arrives; one without it is answered with a `requestModel`, in order. Either
821
+ * way the update is a whole piStar file, re-parsed (2 ms for the largest fixture, 237 ids), and
822
+ * then shared with the previous model: an element, link or diagram equal to the one it replaces
823
+ * keeps the old object, so whatever is memoised on identity only redoes what changed.
824
+ */
825
+ /** Deep equality of JSON values (what parsed models are made of). */
826
+ function equal(a, b) {
827
+ if (a === b) return true;
828
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
829
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
830
+ const ka = Object.keys(a);
831
+ const kb = Object.keys(b);
832
+ if (ka.length !== kb.length) return false;
833
+ for (const key of ka) if (!equal(a[key], b[key])) return false;
834
+ return true;
835
+ }
836
+ /**
837
+ * `next`, with every element, link and the diagram that equals `previous`'s kept as
838
+ * `previous`'s object; `previous` itself when nothing differs (in content, order or metamodel).
839
+ */
840
+ function share(previous, next) {
841
+ let same = previous.elements.size === next.elements.size && previous.links.size === next.links.size && (0, _istar_ts_core.metamodelOf)(previous) === (0, _istar_ts_core.metamodelOf)(next);
842
+ const shareMap = (from, to) => {
843
+ const order = from.keys();
844
+ const out = /* @__PURE__ */ new Map();
845
+ for (const [id, value] of to) {
846
+ const old = from.get(id);
847
+ const keep = old !== void 0 && equal(old, value);
848
+ if (!keep || order.next().value !== id) same = false;
849
+ out.set(id, keep ? old : value);
850
+ }
851
+ return out;
852
+ };
853
+ const elements = shareMap(previous.elements, next.elements);
854
+ const links = shareMap(previous.links, next.links);
855
+ const diagram = equal(previous.diagram, next.diagram) ? previous.diagram : next.diagram;
856
+ const { elements: _e, links: _l, diagram: _d, ...rest } = next;
857
+ const { elements: _pe, links: _pl, diagram: _pd, ...previousRest } = previous;
858
+ if (same && equal(rest, previousRest)) return previous;
859
+ const shared = {
860
+ ...rest,
861
+ elements,
862
+ links,
863
+ ...diagram === void 0 ? {} : { diagram }
864
+ };
865
+ return (0, _istar_ts_core.inheritMetamodel)(next, (0, _istar_ts_core.inheritSourceLayout)(next, shared));
866
+ }
867
+ function readOnly() {
868
+ throw new _istar_ts_core.ModelOperationError("a mirror store is read-only: dispatch the edit through its remote dispatcher");
869
+ }
870
+ const parseWith = _istar_ts_core.parsePistar;
871
+ /**
872
+ * A mirror of `remote`'s model (see `MirrorStore`). Shows an empty model until `ready`.
873
+ *
874
+ * Order: changes are shown in the order the remote sends them, whoever made them, and a
875
+ * `dispatch` through `mirror.dispatcher` resolves after its change is shown, so code that
876
+ * selects a created id or rolls back a rejected one sees the model it expects. A rejected
877
+ * action sends no notification: the mirror stays as it was.
878
+ */
879
+ function createMirrorStore(remote, options = {}) {
880
+ const r = remote;
881
+ const metamodel = options.metamodel;
882
+ const limit = options.historyLimit ?? 100;
883
+ const onError = options.onError ?? (() => {});
884
+ const listeners = /* @__PURE__ */ new Set();
885
+ let model = metamodel ? (0, _istar_ts_core.createEmptyModel)(void 0, { metamodel }) : (0, _istar_ts_core.createEmptyModel)();
886
+ /** The last file's `"metamodel"` block, as JSON; `undefined` before the first file. */
887
+ let block;
888
+ let undoDepth = 0;
889
+ let redoDepth = 0;
890
+ /** How many pushed models (`newRoot`) were applied: a response asked for before one is stale. */
891
+ let pushed = 0;
892
+ let requests = 0;
893
+ let tail = Promise.resolve();
894
+ let disposed = false;
895
+ const parse = (file) => {
896
+ const nextBlock = JSON.stringify(file[_istar_ts_core.FILE_METAMODEL_KEY] ?? null);
897
+ if (nextBlock === block) return (0, _istar_ts_core.inheritMetamodel)(model, parseWith(file, { metamodel: (0, _istar_ts_core.metamodelOf)(model) }));
898
+ const parsed = parseWith(file, {
899
+ ...metamodel ? { metamodel } : {},
900
+ ...options.fileMetamodel ? { fileMetamodel: true } : {}
901
+ });
902
+ block = nextBlock;
903
+ return parsed;
904
+ };
905
+ const apply = (file, source, changes) => {
906
+ const previous = model;
907
+ const next = share(previous, parse(file));
908
+ if (next === previous) return;
909
+ model = next;
910
+ const event = {
911
+ model,
912
+ previous,
913
+ changes,
914
+ source
915
+ };
916
+ for (const listener of listeners) listener(event);
917
+ };
918
+ /** Counts the remote's undo stack from what it reports (see `MirrorStore`). */
919
+ const track = (source, exact) => {
920
+ if (exact) {
921
+ undoDepth = exact.canUndo ? 1 : 0;
922
+ redoDepth = exact.canRedo ? 1 : 0;
923
+ } else if (source === "edit") {
924
+ undoDepth = Math.min(undoDepth + 1, limit);
925
+ redoDepth = 0;
926
+ } else if (source === "undo") {
927
+ undoDepth = Math.max(undoDepth - 1, 0);
928
+ redoDepth++;
929
+ } else if (source === "redo") {
930
+ undoDepth = Math.min(undoDepth + 1, limit);
931
+ redoDepth = Math.max(redoDepth - 1, 0);
932
+ } else {
933
+ undoDepth = 0;
934
+ redoDepth = 0;
935
+ }
936
+ };
937
+ /**
938
+ * Asks for the model now (so the remote answers in order) and shows it once every earlier
939
+ * update is shown. Dropped if a pushed model arrived meanwhile: that one is at least as new.
940
+ */
941
+ const refresh = (source, changes) => {
942
+ const seen = pushed;
943
+ const sent = r.dispatch({
944
+ kind: "requestModel",
945
+ requestId: `mirror-${++requests}`
946
+ }).then((result) => ({ result })).catch((error) => ({ error }));
947
+ const step = tail.then(async () => {
948
+ const answer = await sent;
949
+ if (disposed || pushed !== seen) return;
950
+ if ("error" in answer) throw answer.error;
951
+ const { result } = answer;
952
+ if (!result.ok) throw new Error(result.diagnostics[0]?.message ?? "requestModel was rejected");
953
+ if (!result.response) throw new Error("requestModel was answered without a model");
954
+ apply(result.response.newRoot, source, changes);
955
+ });
956
+ tail = step.catch(onError);
957
+ return step;
958
+ };
959
+ const unsubscribe = r.subscribe((action) => {
960
+ if (disposed || action.kind !== "sourceModelChanged") return;
961
+ track(action.source, action.canUndo !== void 0 && action.canRedo !== void 0 ? {
962
+ canUndo: action.canUndo,
963
+ canRedo: action.canRedo
964
+ } : void 0);
965
+ if (action.newRoot) {
966
+ pushed++;
967
+ try {
968
+ apply(action.newRoot, action.source, action.changes);
969
+ } catch (error) {
970
+ onError(error);
971
+ }
972
+ } else refresh(action.source, action.changes).catch(() => {});
973
+ });
974
+ const ready = refresh("load", []);
975
+ ready.catch(() => {});
976
+ const send = (kind) => {
977
+ r.dispatch({ kind }).catch(onError);
978
+ };
979
+ return {
980
+ ready,
981
+ dispatcher: {
982
+ async dispatch(action) {
983
+ const result = await r.dispatch(action);
984
+ await tail;
985
+ return result;
986
+ },
987
+ subscribe: (listener) => r.subscribe(listener)
988
+ },
989
+ synced: () => tail,
990
+ dispose() {
991
+ disposed = true;
992
+ unsubscribe();
993
+ },
994
+ getModel: () => model,
995
+ subscribe(listener) {
996
+ listeners.add(listener);
997
+ return () => listeners.delete(listener);
998
+ },
999
+ canLink: (source, target, kind) => (0, _istar_ts_core.canLink)(model, source, target, kind),
1000
+ addElement: readOnly,
1001
+ updateElement: readOnly,
1002
+ moveElement: readOnly,
1003
+ nestElement: readOnly,
1004
+ removeElement: readOnly,
1005
+ removeElements: readOnly,
1006
+ setCollapsed: readOnly,
1007
+ connect: readOnly,
1008
+ addDependency: readOnly,
1009
+ addLeg: readOnly,
1010
+ removeLeg: readOnly,
1011
+ updateLink: readOnly,
1012
+ disconnect: readOnly,
1013
+ updateDiagram: readOnly,
1014
+ replace: readOnly,
1015
+ load: readOnly,
1016
+ transaction: readOnly,
1017
+ clearHistory: readOnly,
1018
+ undo() {
1019
+ if (undoDepth === 0) return false;
1020
+ send("glspUndo");
1021
+ return true;
1022
+ },
1023
+ redo() {
1024
+ if (redoDepth === 0) return false;
1025
+ send("glspRedo");
1026
+ return true;
1027
+ },
1028
+ canUndo: () => undoDepth > 0,
1029
+ canRedo: () => redoDepth > 0
1030
+ };
1031
+ }
1032
+ //#endregion
1033
+ //#region src/transport.ts
1034
+ const JSONRPC_VERSION = "2.0";
1035
+ function isRecord(value) {
1036
+ return typeof value === "object" && value !== null;
1037
+ }
1038
+ function isJsonRpcId(value) {
1039
+ return typeof value === "string" || typeof value === "number";
1040
+ }
1041
+ function errorMessage(id, code, message) {
1042
+ return {
1043
+ jsonrpc: JSONRPC_VERSION,
1044
+ id,
1045
+ error: {
1046
+ code,
1047
+ message
1048
+ }
1049
+ };
1050
+ }
1051
+ /**
1052
+ * Serves `dispatcher` on `port`: every well-formed request is answered with the
1053
+ * `ActionResult` as `result`; `dispatcher.subscribe` events are pushed as notifications.
1054
+ * A malformed request answers `-32600` (invalid request) or `-32601` (no usable `method`); an
1055
+ * exception `dispatch` itself doesn't catch answers `-32603` (internal error). Returns a
1056
+ * disposer that stops forwarding subscriber events and detaches the port's `onmessage`.
1057
+ */
1058
+ function connectDispatcher(port, dispatcher) {
1059
+ const unsubscribe = dispatcher.subscribe((action) => {
1060
+ port.postMessage({
1061
+ jsonrpc: JSONRPC_VERSION,
1062
+ method: action.kind,
1063
+ params: action
1064
+ });
1065
+ });
1066
+ port.onmessage = (ev) => {
1067
+ const message = ev.data;
1068
+ if (!isRecord(message) || message.jsonrpc !== JSONRPC_VERSION) return;
1069
+ const { id, method, params } = message;
1070
+ const hasId = isJsonRpcId(id);
1071
+ if (typeof method !== "string" || method.length === 0) {
1072
+ if (hasId) port.postMessage(errorMessage(id, -32601, "method not found"));
1073
+ return;
1074
+ }
1075
+ if (!isRecord(params) || params.kind !== method) {
1076
+ if (hasId) port.postMessage(errorMessage(id, -32600, "invalid request"));
1077
+ return;
1078
+ }
1079
+ const action = params;
1080
+ if (!hasId) {
1081
+ dispatcher.dispatch(action);
1082
+ return;
1083
+ }
1084
+ dispatcher.dispatch(action).then((result) => {
1085
+ port.postMessage({
1086
+ jsonrpc: JSONRPC_VERSION,
1087
+ id,
1088
+ result
1089
+ });
1090
+ }, (error) => {
1091
+ const text = error instanceof Error ? error.message : "internal error";
1092
+ port.postMessage(errorMessage(id, -32603, text));
1093
+ });
1094
+ };
1095
+ return () => {
1096
+ unsubscribe();
1097
+ port.onmessage = null;
1098
+ };
1099
+ }
1100
+ /**
1101
+ * The `ActionDispatcher` for the other end of a `connectDispatcher` port: `dispatch` sends a
1102
+ * request and resolves with the `ActionResult` it is answered with (a JSON-RPC `error` rejects
1103
+ * the promise instead — a transport fault, not an `ActionResult`); `subscribe` hears the
1104
+ * notifications the server pushes.
1105
+ */
1106
+ function createRemoteDispatcher(port) {
1107
+ let nextId = 0;
1108
+ const pending = /* @__PURE__ */ new Map();
1109
+ const listeners = /* @__PURE__ */ new Set();
1110
+ port.onmessage = (ev) => {
1111
+ const message = ev.data;
1112
+ if (!isRecord(message) || message.jsonrpc !== JSONRPC_VERSION) return;
1113
+ const { id, method, params, result, error } = message;
1114
+ if (isJsonRpcId(id)) {
1115
+ const waiting = pending.get(id);
1116
+ if (!waiting) return;
1117
+ pending.delete(id);
1118
+ if (isRecord(error)) waiting.reject(/* @__PURE__ */ new Error(`JSON-RPC error ${String(error.code)}: ${String(error.message)}`));
1119
+ else waiting.resolve(result);
1120
+ return;
1121
+ }
1122
+ if (typeof method === "string" && isRecord(params)) {
1123
+ const action = params;
1124
+ for (const listener of listeners) listener(action);
1125
+ }
1126
+ };
1127
+ return {
1128
+ dispatch(action) {
1129
+ const id = nextId++;
1130
+ return new Promise((resolve, reject) => {
1131
+ pending.set(id, {
1132
+ resolve,
1133
+ reject
1134
+ });
1135
+ port.postMessage({
1136
+ jsonrpc: JSONRPC_VERSION,
1137
+ id,
1138
+ method: action.kind,
1139
+ params: action
1140
+ });
1141
+ });
1142
+ },
1143
+ subscribe(listener) {
1144
+ listeners.add(listener);
1145
+ return () => listeners.delete(listener);
1146
+ }
1147
+ };
1148
+ }
1149
+ //#endregion
1150
+ //#region src/index.ts
1151
+ /** Version of the action protocol this package speaks. */
1152
+ const ACTIONS_PROTOCOL_VERSION = "0.0.0";
1153
+ //#endregion
1154
+ exports.ACTIONS_PROTOCOL_VERSION = ACTIONS_PROTOCOL_VERSION;
1155
+ exports.HANDLED_ACTION_KINDS = HANDLED_ACTION_KINDS;
1156
+ exports.connectDispatcher = connectDispatcher;
1157
+ exports.contextActionsFor = contextActionsFor;
1158
+ exports.contextText = contextText;
1159
+ exports.createActionDispatcher = createActionDispatcher;
1160
+ exports.createMirrorStore = createMirrorStore;
1161
+ exports.createRemoteDispatcher = createRemoteDispatcher;
1162
+ exports.diagnosticToMarker = diagnosticToMarker;
1163
+ exports.hostToolContext = hostToolContext;
1164
+ exports.isOperation = isOperation;
1165
+ exports.isRequestAction = isRequestAction;
1166
+ exports.markerToDiagnostic = markerToDiagnostic;
1167
+ exports.readOnlyDispatcher = readOnlyDispatcher;
1168
+ exports.typeHintsFor = typeHintsFor;
1169
+
1170
+ //# sourceMappingURL=index.cjs.map