@hydranium/glsp-server 1.0.0-next.31 → 1.0.0-next.315
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/README.md +80 -88
- package/lib/browser/start-glsp-server-in-worker.d.ts +43 -42
- package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -1
- package/lib/browser/start-glsp-server-in-worker.js +62 -13
- package/lib/browser/start-glsp-server-in-worker.js.map +1 -1
- package/lib/command/diagram-edit-dropped-error.d.ts +31 -0
- package/lib/command/diagram-edit-dropped-error.d.ts.map +1 -0
- package/lib/command/diagram-edit-dropped-error.js +48 -0
- package/lib/command/diagram-edit-dropped-error.js.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts +33 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.js +63 -0
- package/lib/command/hydranium-glsp-command-stack.js.map +1 -0
- package/lib/command/hydranium-glsp-operation-action-handler.d.ts +64 -0
- package/lib/command/hydranium-glsp-operation-action-handler.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-operation-action-handler.js +142 -0
- package/lib/command/hydranium-glsp-operation-action-handler.js.map +1 -0
- package/lib/command/hydranium-glsp-operation-command.d.ts +250 -0
- package/lib/command/hydranium-glsp-operation-command.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-operation-command.js +491 -0
- package/lib/command/hydranium-glsp-operation-command.js.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts +26 -87
- package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
- package/lib/command/hydranium-glsp-recording-command.js +40 -117
- package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
- package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts +23 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.js +41 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.js.map +1 -0
- package/lib/command/index.d.ts +5 -0
- package/lib/command/index.d.ts.map +1 -1
- package/lib/command/index.js +5 -0
- package/lib/command/index.js.map +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +2 -2
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -1
- package/lib/dispatcher/server-action-dispatcher.d.ts +25 -24
- package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -1
- package/lib/dispatcher/server-action-dispatcher.js +38 -26
- package/lib/dispatcher/server-action-dispatcher.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +35 -59
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +60 -71
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -1
- package/lib/launcher/drop-notifications-to-gone-client.d.ts +23 -0
- package/lib/launcher/drop-notifications-to-gone-client.d.ts.map +1 -0
- package/lib/launcher/drop-notifications-to-gone-client.js +43 -0
- package/lib/launcher/drop-notifications-to-gone-client.js.map +1 -0
- package/lib/launcher/glsp-app-module.d.ts +1 -2
- package/lib/launcher/glsp-app-module.d.ts.map +1 -1
- package/lib/launcher/glsp-app-module.js.map +1 -1
- package/lib/launcher/glsp-connection-logger.d.ts +19 -0
- package/lib/launcher/glsp-connection-logger.d.ts.map +1 -0
- package/lib/launcher/glsp-connection-logger.js +24 -0
- package/lib/launcher/glsp-connection-logger.js.map +1 -0
- package/lib/launcher/glsp-server-overrides.d.ts +10 -8
- package/lib/launcher/glsp-server-overrides.d.ts.map +1 -1
- package/lib/launcher/glsp-server-overrides.js +25 -11
- package/lib/launcher/glsp-server-overrides.js.map +1 -1
- package/lib/launcher/hydranium-glsp-server.d.ts +60 -15
- package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
- package/lib/launcher/hydranium-glsp-server.js +145 -22
- package/lib/launcher/hydranium-glsp-server.js.map +1 -1
- package/lib/launcher/index.d.ts +2 -0
- package/lib/launcher/index.d.ts.map +1 -1
- package/lib/launcher/index.js +5 -4
- package/lib/launcher/index.js.map +1 -1
- package/lib/logging/glsp-client-logger.d.ts +10 -6
- package/lib/logging/glsp-client-logger.d.ts.map +1 -1
- package/lib/logging/glsp-client-logger.js +14 -7
- package/lib/logging/glsp-client-logger.js.map +1 -1
- package/lib/messages/index.d.ts +11 -6
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +11 -6
- package/lib/messages/index.js.map +1 -1
- package/lib/node/start-glsp-server.d.ts +50 -15
- package/lib/node/start-glsp-server.d.ts.map +1 -1
- package/lib/node/start-glsp-server.js +97 -30
- package/lib/node/start-glsp-server.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts +326 -102
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +529 -139
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
- package/lib/state/diagram-status.d.ts +38 -0
- package/lib/state/diagram-status.d.ts.map +1 -0
- package/lib/state/diagram-status.js +27 -0
- package/lib/state/diagram-status.js.map +1 -0
- package/lib/state/full-text-hydranium-glsp-state.d.ts +22 -9
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.js +45 -18
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
- package/lib/state/hydranium-glsp-index.d.ts +40 -29
- package/lib/state/hydranium-glsp-index.d.ts.map +1 -1
- package/lib/state/hydranium-glsp-index.js +82 -45
- package/lib/state/hydranium-glsp-index.js.map +1 -1
- package/lib/state/hydranium-shared-core-services.d.ts +16 -29
- package/lib/state/hydranium-shared-core-services.d.ts.map +1 -1
- package/lib/state/hydranium-shared-core-services.js +15 -28
- package/lib/state/hydranium-shared-core-services.js.map +1 -1
- package/lib/state/index.d.ts +1 -0
- package/lib/state/index.d.ts.map +1 -1
- package/lib/state/index.js +1 -0
- package/lib/state/index.js.map +1 -1
- package/lib/state/model-ready-timeout-error.d.ts.map +1 -1
- package/lib/state/model-ready-timeout-error.js.map +1 -1
- package/lib/state/reconcile-source-model-write.d.ts +13 -27
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
- package/lib/state/reconcile-source-model-write.js +44 -32
- package/lib/state/reconcile-source-model-write.js.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +125 -69
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.js +242 -100
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +36 -28
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +59 -42
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
- package/lib/status/diagram-status-reporter.d.ts +44 -0
- package/lib/status/diagram-status-reporter.d.ts.map +1 -0
- package/lib/status/diagram-status-reporter.js +88 -0
- package/lib/status/diagram-status-reporter.js.map +1 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.d.ts +27 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.d.ts.map +1 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.js +52 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.js.map +1 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts +27 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts.map +1 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js +53 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js.map +1 -0
- package/lib/status/index.d.ts +12 -0
- package/lib/status/index.d.ts.map +1 -0
- package/lib/status/index.js +12 -0
- package/lib/status/index.js.map +1 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts +30 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts.map +1 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.js +47 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.js.map +1 -0
- package/lib/storage/hydranium-glsp-storage.d.ts +324 -143
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +569 -213
- package/lib/storage/hydranium-glsp-storage.js.map +1 -1
- package/lib/storage/index.d.ts +2 -1
- package/lib/storage/index.d.ts.map +1 -1
- package/lib/storage/index.js +2 -1
- package/lib/storage/index.js.map +1 -1
- package/lib/storage/save-delivery-policy.d.ts +51 -0
- package/lib/storage/save-delivery-policy.d.ts.map +1 -0
- package/lib/storage/{save-conflict-policy.js → save-delivery-policy.js} +7 -8
- package/lib/storage/save-delivery-policy.js.map +1 -0
- package/lib/submission/hydranium-glsp-submission-handler.d.ts +44 -4
- package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -1
- package/lib/submission/hydranium-glsp-submission-handler.js +82 -6
- package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -1
- package/lib/testing/glsp-harness.d.ts +34 -7
- package/lib/testing/glsp-harness.d.ts.map +1 -1
- package/lib/testing/glsp-harness.js +47 -19
- package/lib/testing/glsp-harness.js.map +1 -1
- package/lib/validation/diagnostic-markers.d.ts +3 -3
- package/lib/validation/diagnostic-markers.d.ts.map +1 -1
- package/lib/validation/diagnostic-markers.js +1 -1
- package/lib/validation/diagnostic-markers.js.map +1 -1
- package/lib/validation/hydranium-glsp-model-validator.d.ts +1 -2
- package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -1
- package/lib/validation/hydranium-glsp-model-validator.js +1 -1
- package/lib/validation/hydranium-glsp-model-validator.js.map +1 -1
- package/package.json +31 -52
- package/src/browser/start-glsp-server-in-worker.ts +79 -51
- package/src/command/diagram-edit-dropped-error.ts +51 -0
- package/src/command/hydranium-glsp-command-stack.ts +47 -0
- package/src/command/hydranium-glsp-operation-action-handler.ts +141 -0
- package/src/command/hydranium-glsp-operation-command.ts +602 -0
- package/src/command/hydranium-glsp-recording-command.ts +40 -121
- package/src/command/hydranium-glsp-undo-redo-action-handler.ts +29 -0
- package/src/command/index.ts +10 -0
- package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +2 -2
- package/src/dispatcher/server-action-dispatcher.ts +39 -31
- package/src/index.ts +1 -0
- package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +79 -91
- package/src/launcher/drop-notifications-to-gone-client.ts +47 -0
- package/src/launcher/glsp-app-module.ts +1 -2
- package/src/launcher/glsp-connection-logger.ts +27 -0
- package/src/launcher/glsp-server-overrides.ts +27 -11
- package/src/launcher/hydranium-glsp-server.ts +153 -23
- package/src/launcher/index.ts +5 -4
- package/src/logging/glsp-client-logger.ts +16 -9
- package/src/messages/index.ts +16 -6
- package/src/node/start-glsp-server.ts +133 -40
- package/src/state/abstract-hydranium-glsp-state.ts +609 -154
- package/src/state/diagram-status.ts +41 -0
- package/src/state/full-text-hydranium-glsp-state.ts +46 -18
- package/src/state/hydranium-glsp-index.ts +85 -41
- package/src/state/hydranium-shared-core-services.ts +16 -29
- package/src/state/index.ts +1 -0
- package/src/state/reconcile-source-model-write.ts +51 -53
- package/src/state/reconciling-multi-document-glsp-state.ts +276 -102
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +67 -45
- package/src/status/diagram-status-reporter.ts +106 -0
- package/src/status/hydranium-glsp-request-model-action-handler.ts +43 -0
- package/src/status/hydranium-glsp-set-edit-mode-action-handler.ts +38 -0
- package/src/status/index.ts +12 -0
- package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
- package/src/storage/hydranium-glsp-storage.ts +647 -223
- package/src/storage/index.ts +2 -1
- package/src/storage/save-delivery-policy.ts +49 -0
- package/src/submission/hydranium-glsp-submission-handler.ts +91 -5
- package/src/testing/glsp-harness.ts +67 -25
- package/src/validation/diagnostic-markers.ts +3 -3
- package/src/validation/hydranium-glsp-model-validator.ts +4 -5
- package/lib/storage/save-conflict-policy.d.ts +0 -56
- package/lib/storage/save-conflict-policy.d.ts.map +0 -1
- package/lib/storage/save-conflict-policy.js.map +0 -1
- package/src/storage/save-conflict-policy.ts +0 -52
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { type AnyObject, Command, CompoundCommand, type MaybePromise } from '@eclipse-glsp/server';
|
|
11
|
+
import {
|
|
12
|
+
type AstNode,
|
|
13
|
+
type AstNodeDescription,
|
|
14
|
+
AstUtils,
|
|
15
|
+
type GenericAstNode,
|
|
16
|
+
isMultiReference,
|
|
17
|
+
type LinkingError,
|
|
18
|
+
type MultiReference,
|
|
19
|
+
type MultiReferenceItem,
|
|
20
|
+
type Reference
|
|
21
|
+
} from '@hydranium/langium';
|
|
22
|
+
import { type ModelVersion, describeErrorWithStack } from '@hydranium/protocol';
|
|
23
|
+
import { DiagramEditDroppedError } from './diagram-edit-dropped-error.js';
|
|
24
|
+
import { type HydraniumGlspRecordingState } from './hydranium-glsp-recording-command.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* What becomes of an operation {@link HydraniumGlspOperationCommand} ran: its
|
|
28
|
+
* command written, its write dropped, or no command.
|
|
29
|
+
*/
|
|
30
|
+
export type OperationOutcome = 'executed' | 'dropped' | 'none';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One executed side effect the operation undoes and redoes: a recording
|
|
34
|
+
* command's bridge, or a command that does not record.
|
|
35
|
+
*/
|
|
36
|
+
export interface OperationSideEffect {
|
|
37
|
+
undo(): MaybePromise<void>;
|
|
38
|
+
redo(): MaybePromise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The source model an operation changed, before and after it; a redo applies `from → to`, an undo the reverse. */
|
|
42
|
+
export interface OperationTransition<TSourceModel> {
|
|
43
|
+
readonly from: TSourceModel;
|
|
44
|
+
readonly to: TSourceModel;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whether an undo or a redo of an operation runs. */
|
|
48
|
+
export type UndoRedoDirection = 'undo' | 'redo';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The members of an operation the framework's state, recording command and
|
|
52
|
+
* operation action handler call; reached through {@link openOperationOf} and
|
|
53
|
+
* {@link runOperation}, not through the operation itself.
|
|
54
|
+
*/
|
|
55
|
+
export interface OperationInternals {
|
|
56
|
+
run(create: () => MaybePromise<Command | undefined>): Promise<OperationOutcome>;
|
|
57
|
+
workingRootOf(key: string, built: AstNode): AstNode;
|
|
58
|
+
existingWorkingRootOf(key: string): AstNode | undefined;
|
|
59
|
+
workingUriOf(node: AstNode): string | undefined;
|
|
60
|
+
builtNodeOf<T extends AstNode>(node: T): T;
|
|
61
|
+
recordWrittenRoot(root: AstNode): void;
|
|
62
|
+
recordDropped(current: AstNode | undefined): void;
|
|
63
|
+
recordExecuted(step: OperationSideEffect): void;
|
|
64
|
+
assertNotDuringUndoRedo(label: string): void;
|
|
65
|
+
amendBefore(update: (before: AnyObject) => AnyObject): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const internals = new WeakMap<object, OperationInternals>();
|
|
69
|
+
/** The canonical URI of every working root an operation made, by root. */
|
|
70
|
+
const workingRootUris = new WeakMap<AstNode, string>();
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The canonical URI of the document whose working copy `node` belongs to;
|
|
74
|
+
* `undefined` for a node of no copy. A copy has no `$document`, so a lookup
|
|
75
|
+
* that routes by a node's document routes a copy node through this instead.
|
|
76
|
+
*/
|
|
77
|
+
export function workingUriOfCopy(node: AstNode): string | undefined {
|
|
78
|
+
return workingRootUris.get(AstUtils.findRootNode(node));
|
|
79
|
+
}
|
|
80
|
+
const openOperations = new WeakMap<object, OperationInternals>();
|
|
81
|
+
|
|
82
|
+
/** The operation open on `state`, executing, undoing or redoing its side effects; `undefined` between operations. */
|
|
83
|
+
export function openOperationOf(state: object): OperationInternals | undefined {
|
|
84
|
+
return openOperations.get(state);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Run `operation` as {@link HydraniumGlspOperationCommand} describes. */
|
|
88
|
+
export function runOperation<TSourceModel extends AnyObject>(
|
|
89
|
+
operation: HydraniumGlspOperationCommand<TSourceModel>,
|
|
90
|
+
create: () => MaybePromise<Command | undefined>
|
|
91
|
+
): Promise<OperationOutcome> {
|
|
92
|
+
return membersOf(operation).run(create);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function membersOf(operation: object): OperationInternals {
|
|
96
|
+
const members = internals.get(operation);
|
|
97
|
+
if (!members) {
|
|
98
|
+
throw new Error('Not a HydraniumGlspOperationCommand');
|
|
99
|
+
}
|
|
100
|
+
return members;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One GLSP operation as a transaction, and the command-stack entry that undoes
|
|
105
|
+
* and redoes it.
|
|
106
|
+
*
|
|
107
|
+
* {@link runOperation} executes the operation's command against copies of the
|
|
108
|
+
* built roots and writes their projection once; a reader of a built root never
|
|
109
|
+
* sees an edit its write has not committed, and an edit the write drops stays
|
|
110
|
+
* out of every built root. Call it inside `runExclusive`: the copies, the write
|
|
111
|
+
* and the capture that follows assume no other operation, undo or storage
|
|
112
|
+
* capture runs meanwhile.
|
|
113
|
+
*
|
|
114
|
+
* The entry holds one model transition, from the copies' projection before the
|
|
115
|
+
* command to their projection after it, both as the documents hold them after
|
|
116
|
+
* a write (see `normalizeTransition`), and the side effects that executed, in
|
|
117
|
+
* order: each command that does not record, a `CompoundCommand`'s children
|
|
118
|
+
* counted one by one, and the bridge of each recording command at any depth.
|
|
119
|
+
* Rollback and undo run them in reverse, redo in order. A recording command
|
|
120
|
+
* executed inside an open operation writes nothing, and its own undo and redo
|
|
121
|
+
* do nothing there.
|
|
122
|
+
*/
|
|
123
|
+
export class HydraniumGlspOperationCommand<TSourceModel extends AnyObject = AnyObject> implements Command {
|
|
124
|
+
/** Working copies by canonical URI. */
|
|
125
|
+
protected readonly workingRoots = new Map<string, AstNode>();
|
|
126
|
+
/** Built node to copy and copy to built node, across every working root, as `AstUtils.copyAstNode` records it. */
|
|
127
|
+
protected readonly trace = new Map<AstNode, AstNode>();
|
|
128
|
+
/** The source root captured when the operation opened. */
|
|
129
|
+
protected sourceRootAtOpen?: AstNode;
|
|
130
|
+
/** The version of {@link sourceRootAtOpen}, which the write is gated on. */
|
|
131
|
+
protected baseVersionAtOpen?: ModelVersion;
|
|
132
|
+
/**
|
|
133
|
+
* The primary root to capture when the operation closes: the one its write
|
|
134
|
+
* produced, or the document's built one after a dropped write or one the
|
|
135
|
+
* document already held.
|
|
136
|
+
*/
|
|
137
|
+
protected writtenRoot?: AstNode;
|
|
138
|
+
/** Whether the write's reconcile dropped the edit. */
|
|
139
|
+
protected dropped = false;
|
|
140
|
+
/** The command the operation executed. */
|
|
141
|
+
protected command?: Command;
|
|
142
|
+
/** The side effects that executed, in execution order. */
|
|
143
|
+
protected readonly steps: OperationSideEffect[] = [];
|
|
144
|
+
/**
|
|
145
|
+
* The copies' projection when the operation opened, amended as documents
|
|
146
|
+
* join or leave the write set during it.
|
|
147
|
+
*/
|
|
148
|
+
protected before?: TSourceModel;
|
|
149
|
+
protected transition?: OperationTransition<TSourceModel>;
|
|
150
|
+
/** Whether an undo or redo is running the side effects, on copies it discards. */
|
|
151
|
+
protected undoingOrRedoing = false;
|
|
152
|
+
|
|
153
|
+
constructor(protected readonly modelState: HydraniumGlspRecordingState<TSourceModel>) {
|
|
154
|
+
const members: OperationInternals = {
|
|
155
|
+
run: create => this.run(create),
|
|
156
|
+
workingRootOf: (key, built) => this.workingRootOf(key, built),
|
|
157
|
+
existingWorkingRootOf: key => this.workingRoots.get(key),
|
|
158
|
+
workingUriOf: node => this.workingUriOf(node),
|
|
159
|
+
builtNodeOf: node => this.builtNodeOf(node),
|
|
160
|
+
recordWrittenRoot: root => {
|
|
161
|
+
this.writtenRoot = root;
|
|
162
|
+
},
|
|
163
|
+
recordDropped: current => {
|
|
164
|
+
this.dropped = true;
|
|
165
|
+
this.writtenRoot = current;
|
|
166
|
+
},
|
|
167
|
+
recordExecuted: step => {
|
|
168
|
+
this.steps.push(step);
|
|
169
|
+
},
|
|
170
|
+
amendBefore: update => {
|
|
171
|
+
if (this.before !== undefined) {
|
|
172
|
+
this.before = update(this.before) as TSourceModel;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
assertNotDuringUndoRedo: label => {
|
|
176
|
+
if (this.undoingOrRedoing) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Recording command '${label}' executed during an undo or redo, which writes only the recorded transition, ` +
|
|
179
|
+
'so its edit would be discarded. Execute it in an operation of its own.'
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
internals.set(this, members);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Open the operation, execute the command `create` makes, write the result
|
|
189
|
+
* once, and close the operation.
|
|
190
|
+
*
|
|
191
|
+
* `create` runs inside the operation, because handlers resolve their
|
|
192
|
+
* targets there. On any failure, `create`, the command or the write, the
|
|
193
|
+
* executed commands' side effects are undone, nothing is written, and the
|
|
194
|
+
* error is rethrown; after a write whose reconcile dropped the edit, the
|
|
195
|
+
* side effects are undone the same way and this answers `'dropped'`. The
|
|
196
|
+
* operation closes either way: the copies are dropped and the source root
|
|
197
|
+
* is captured again, with the base version, every tracked version and the
|
|
198
|
+
* index.
|
|
199
|
+
*/
|
|
200
|
+
protected async run(create: () => MaybePromise<Command | undefined>): Promise<'executed' | 'dropped' | 'none'> {
|
|
201
|
+
this.open();
|
|
202
|
+
let recorded: OperationTransition<TSourceModel> | undefined;
|
|
203
|
+
try {
|
|
204
|
+
this.before = await this.project();
|
|
205
|
+
await this.checkpoint('create');
|
|
206
|
+
const command = await create();
|
|
207
|
+
if (!command) {
|
|
208
|
+
return 'none';
|
|
209
|
+
}
|
|
210
|
+
await this.tracked(command).execute();
|
|
211
|
+
this.command = command;
|
|
212
|
+
await this.checkpoint('project');
|
|
213
|
+
const written = await this.project();
|
|
214
|
+
if (written !== undefined && !(await this.write(written))) {
|
|
215
|
+
await this.rollback();
|
|
216
|
+
return 'dropped';
|
|
217
|
+
}
|
|
218
|
+
if (this.before !== undefined && written !== undefined) {
|
|
219
|
+
recorded = { from: this.before, to: written };
|
|
220
|
+
}
|
|
221
|
+
} catch (error: unknown) {
|
|
222
|
+
await this.rollback();
|
|
223
|
+
throw error;
|
|
224
|
+
} finally {
|
|
225
|
+
this.close();
|
|
226
|
+
}
|
|
227
|
+
// After the write landed, so a failure here leaves it in place: the
|
|
228
|
+
// transition is then kept as recorded, and an undo may conflict.
|
|
229
|
+
try {
|
|
230
|
+
this.transition = recorded && (await this.modelState.normalizeTransition(recorded));
|
|
231
|
+
} catch (error: unknown) {
|
|
232
|
+
this.modelState.logger.warn(`Normalizing an operation's transition failed: ${describeErrorWithStack(error)}`);
|
|
233
|
+
this.transition = recorded;
|
|
234
|
+
}
|
|
235
|
+
return 'executed';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Awaited where the operation, or an undo or redo of it, yields to code it
|
|
240
|
+
* does not control: before creating the command, before each side effect
|
|
241
|
+
* executes, is undone or redone, rolls back or is compensated, before
|
|
242
|
+
* projecting the copies, before resolving an undo or redo. Does nothing; a
|
|
243
|
+
* test overrides it to land a foreign edit or a failure there.
|
|
244
|
+
*/
|
|
245
|
+
protected async checkpoint(
|
|
246
|
+
_point: 'create' | 'execute' | 'project' | 'resolve' | UndoRedoDirection | 'rollback' | 'compensate'
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
// A seam for tests.
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Does nothing: {@link runOperation} executed the command; the command stack
|
|
253
|
+
* calls this when it pushes the entry.
|
|
254
|
+
*/
|
|
255
|
+
async execute(): Promise<void> {
|
|
256
|
+
// Executed by runOperation().
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async undo(): Promise<void> {
|
|
260
|
+
await this.undoOrRedo('undo');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async redo(): Promise<void> {
|
|
264
|
+
await this.undoOrRedo('redo');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
canUndo(): boolean {
|
|
268
|
+
return this.command?.canUndo?.() ?? true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The working copy of `built`, the root of the document whose canonical URI is `key`, made on the first request. */
|
|
272
|
+
protected workingRootOf(key: string, built: AstNode): AstNode {
|
|
273
|
+
let working = this.workingRoots.get(key);
|
|
274
|
+
if (!working) {
|
|
275
|
+
working = AstUtils.copyAstNode(
|
|
276
|
+
built,
|
|
277
|
+
(_node, _property, _refNode, _refText, original) => this.buildWorkingReference(original),
|
|
278
|
+
this.trace
|
|
279
|
+
);
|
|
280
|
+
this.rebuildWorkingMultiReferences(working);
|
|
281
|
+
this.workingRoots.set(key, working);
|
|
282
|
+
workingRootUris.set(working, key);
|
|
283
|
+
this.modelState.index.remapSemanticAliases(node =>
|
|
284
|
+
AstUtils.findRootNode(node) === built ? (this.trace.get(node) ?? node) : node
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return working;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The canonical URI of the working root `node` belongs to; `undefined` for a node of no copy. */
|
|
291
|
+
protected workingUriOf(node: AstNode): string | undefined {
|
|
292
|
+
const root = AstUtils.findRootNode(node);
|
|
293
|
+
for (const [key, working] of this.workingRoots) {
|
|
294
|
+
if (working === root) {
|
|
295
|
+
return key;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The built node `node` was copied from; `node` itself for a node of no copy
|
|
303
|
+
* and for a node the operation created.
|
|
304
|
+
*/
|
|
305
|
+
protected builtNodeOf<T extends AstNode>(node: T): T {
|
|
306
|
+
if (this.workingUriOf(node) === undefined) {
|
|
307
|
+
return node;
|
|
308
|
+
}
|
|
309
|
+
const built = this.trace.get(node);
|
|
310
|
+
return built === undefined ? node : (built as T);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Copy the source root, and point the index at the copy, so the nodes a
|
|
315
|
+
* handler resolves by id are the nodes it edits.
|
|
316
|
+
*/
|
|
317
|
+
protected open(): void {
|
|
318
|
+
this.sourceRootAtOpen = this.modelState.sourceRoot;
|
|
319
|
+
this.baseVersionAtOpen = this.modelState.baseVersion;
|
|
320
|
+
openOperations.set(this.modelState, membersOf(this));
|
|
321
|
+
if (this.sourceRootAtOpen !== undefined) {
|
|
322
|
+
this.modelState.index.reindexSemanticElements(this.modelState.sourceRoot, this.modelState.sourceUri);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Drop the copies, move every id added through `indexSemanticElement` from
|
|
328
|
+
* a copy back to the built node it was copied from, and capture the source
|
|
329
|
+
* root again, whether or not the write changed it.
|
|
330
|
+
*/
|
|
331
|
+
protected close(): void {
|
|
332
|
+
const copies = new Set(this.workingRoots.values());
|
|
333
|
+
this.modelState.index.remapSemanticAliases(node => (copies.has(AstUtils.findRootNode(node)) ? this.trace.get(node) : node));
|
|
334
|
+
openOperations.delete(this.modelState);
|
|
335
|
+
const root = this.writtenRoot ?? this.sourceRootAtOpen;
|
|
336
|
+
if (root !== undefined) {
|
|
337
|
+
this.modelState.setSourceRoot(this.modelState.sourceUri, root);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Undo the side effects of the executed commands, newest first, each step
|
|
343
|
+
* on its own: a step that fails is logged and the rest are still undone.
|
|
344
|
+
* The operation's own error is the one thrown.
|
|
345
|
+
*/
|
|
346
|
+
protected async rollback(): Promise<void> {
|
|
347
|
+
await this.runEach(
|
|
348
|
+
[...this.steps].reverse().map(step => () => step.undo()),
|
|
349
|
+
'Rolling back a failed operation',
|
|
350
|
+
'rollback'
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Run each of `actions`, each reverting a side effect of its own: a failure
|
|
356
|
+
* is logged as `what` failing and the rest still run, so one step that
|
|
357
|
+
* cannot be reverted leaves no other step applied. The caller reports its
|
|
358
|
+
* own failure, not these.
|
|
359
|
+
*/
|
|
360
|
+
protected async runEach(
|
|
361
|
+
actions: ReadonlyArray<() => MaybePromise<void>>,
|
|
362
|
+
what: string,
|
|
363
|
+
point: 'rollback' | 'compensate'
|
|
364
|
+
): Promise<void> {
|
|
365
|
+
for (const action of actions) {
|
|
366
|
+
try {
|
|
367
|
+
await this.checkpoint(point);
|
|
368
|
+
await action();
|
|
369
|
+
} catch (error: unknown) {
|
|
370
|
+
this.modelState.logger.error(`${what} failed at one step: ${describeErrorWithStack(error)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* `command` with every side effect it executes recorded as a step: the
|
|
377
|
+
* command itself, or for a `CompoundCommand` each child, at any depth. A
|
|
378
|
+
* tracked command's own undo and redo do nothing, so a `CompoundCommand`
|
|
379
|
+
* reverting its children when a later one throws leaves that to the steps.
|
|
380
|
+
*
|
|
381
|
+
* The children are read from GLSP's protected `CompoundCommand.commands`;
|
|
382
|
+
* should that field stop being readable, a compound becomes one step and
|
|
383
|
+
* its children's side effects no longer interleave with the bridges.
|
|
384
|
+
*/
|
|
385
|
+
protected tracked(command: Command): Command {
|
|
386
|
+
const children: unknown = command instanceof CompoundCommand ? Reflect.get(command, 'commands') : undefined;
|
|
387
|
+
if (isCommandArray(children)) {
|
|
388
|
+
children.forEach((child, i) => {
|
|
389
|
+
children[i] = this.tracked(child);
|
|
390
|
+
});
|
|
391
|
+
return command;
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
execute: async () => {
|
|
395
|
+
await this.checkpoint('execute');
|
|
396
|
+
await command.execute();
|
|
397
|
+
this.steps.push({ undo: () => command.undo(), redo: () => command.redo() });
|
|
398
|
+
},
|
|
399
|
+
undo: () => undefined,
|
|
400
|
+
redo: () => undefined,
|
|
401
|
+
canUndo: () => Command.canUndo(command)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** The model projected from the working copies, cloned; `undefined` for a state that projects none. */
|
|
406
|
+
protected async project(): Promise<TSourceModel | undefined> {
|
|
407
|
+
const model: TSourceModel | undefined = await this.modelState.sourceModel;
|
|
408
|
+
return model === undefined ? undefined : structuredClone(model);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Resolve the transition onto the current model, run the commands' side
|
|
413
|
+
* effects, then write once, gated on the versions the model was read at. A
|
|
414
|
+
* collision does nothing but warn; a transition with nothing left to apply,
|
|
415
|
+
* or one the model already holds, still runs the side effects.
|
|
416
|
+
*
|
|
417
|
+
* The caller holds `runExclusive`, which nothing here takes again.
|
|
418
|
+
*/
|
|
419
|
+
protected async undoOrRedo(direction: UndoRedoDirection): Promise<void> {
|
|
420
|
+
// Captured again so the model the transition is resolved onto, the base
|
|
421
|
+
// a conflict reconciles from and the versions the write is gated on are
|
|
422
|
+
// read together.
|
|
423
|
+
if (this.modelState.sourceRoot !== undefined) {
|
|
424
|
+
this.modelState.setSourceRoot(this.modelState.sourceUri, this.modelState.sourceRoot);
|
|
425
|
+
}
|
|
426
|
+
await this.checkpoint('resolve');
|
|
427
|
+
let merged: TSourceModel | undefined;
|
|
428
|
+
const { transition } = this.inDirection(direction);
|
|
429
|
+
if (transition) {
|
|
430
|
+
const outcome = await this.modelState.conflictResolver.resolve<TSourceModel>(
|
|
431
|
+
transition.from,
|
|
432
|
+
transition.to,
|
|
433
|
+
async () => this.modelState.sourceModel
|
|
434
|
+
);
|
|
435
|
+
if (outcome.status === 'merged') {
|
|
436
|
+
merged = outcome.merged;
|
|
437
|
+
} else if (outcome.status !== 'no-op' && outcome.status !== 'already-applied') {
|
|
438
|
+
this.modelState.logger.warn(
|
|
439
|
+
`${direction === 'undo' ? 'Undo' : 'Redo'} of an operation skipped (${outcome.status}); a foreign edit changed a shared field`
|
|
440
|
+
);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
await this.undoOrRedoSideEffects(direction, merged);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Write `model` once through the state's write path, gated on the versions
|
|
449
|
+
* read when the operation opened, a conflict reconciled as for any write of
|
|
450
|
+
* the state; `false` when the reconcile dropped it.
|
|
451
|
+
*/
|
|
452
|
+
protected async write(model: TSourceModel): Promise<boolean> {
|
|
453
|
+
this.dropped = false;
|
|
454
|
+
await this.modelState.updateSourceModel(model, this.baseVersionAtOpen);
|
|
455
|
+
return !this.dropped;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Run the recorded steps of an undo or redo against fresh copies of the
|
|
460
|
+
* built roots, made on demand as in an operation and discarded unwritten,
|
|
461
|
+
* then write `merged`, the resolved transition, through {@link write}: that
|
|
462
|
+
* write is what changes the model, and a step that edits `sourceRoot` must
|
|
463
|
+
* not reach the root every reader shares. A recording command executed here
|
|
464
|
+
* throws.
|
|
465
|
+
*
|
|
466
|
+
* When a step or the write throws, or the write's reconcile drops it, the
|
|
467
|
+
* steps already run are run the other way, newest first, and the error is
|
|
468
|
+
* rethrown with nothing written; GLSP's command stack then flushes, as it
|
|
469
|
+
* does for any undo or redo that fails.
|
|
470
|
+
*/
|
|
471
|
+
protected async undoOrRedoSideEffects(direction: UndoRedoDirection, merged?: TSourceModel): Promise<void> {
|
|
472
|
+
this.workingRoots.clear();
|
|
473
|
+
this.trace.clear();
|
|
474
|
+
this.writtenRoot = undefined;
|
|
475
|
+
this.undoingOrRedoing = true;
|
|
476
|
+
this.open();
|
|
477
|
+
const { steps, run } = this.inDirection(direction);
|
|
478
|
+
const done: OperationSideEffect[] = [];
|
|
479
|
+
try {
|
|
480
|
+
for (const step of steps) {
|
|
481
|
+
await this.checkpoint(direction);
|
|
482
|
+
await run(step);
|
|
483
|
+
done.unshift(step);
|
|
484
|
+
}
|
|
485
|
+
if (merged === undefined) {
|
|
486
|
+
// Nothing to write, but a step may have rebuilt the document: close on its built root, not the one opened on.
|
|
487
|
+
this.modelState.captureBuiltRoot();
|
|
488
|
+
} else if (!(await this.write(merged))) {
|
|
489
|
+
throw new DiagramEditDroppedError(`The ${direction} of an operation was dropped: a foreign edit changed a field it writes`);
|
|
490
|
+
}
|
|
491
|
+
} catch (error: unknown) {
|
|
492
|
+
await this.compensate(done, direction);
|
|
493
|
+
throw error;
|
|
494
|
+
} finally {
|
|
495
|
+
this.close();
|
|
496
|
+
this.undoingOrRedoing = false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Run `steps`, which a failed undo or redo already ran, the other way, each
|
|
502
|
+
* on its own as {@link rollback} does; the undo or redo's own error is the
|
|
503
|
+
* one thrown.
|
|
504
|
+
*/
|
|
505
|
+
protected async compensate(steps: readonly OperationSideEffect[], direction: UndoRedoDirection): Promise<void> {
|
|
506
|
+
const { revert } = this.inDirection(direction);
|
|
507
|
+
await this.runEach(
|
|
508
|
+
steps.map(step => () => revert(step)),
|
|
509
|
+
`Reverting a failed ${direction}`,
|
|
510
|
+
'compensate'
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* The transition and the recorded steps as `direction` runs them: an undo
|
|
516
|
+
* runs the transition backwards and each step's undo, newest first, and is
|
|
517
|
+
* reverted by each step's redo; a redo the other way round.
|
|
518
|
+
*/
|
|
519
|
+
protected inDirection(direction: UndoRedoDirection): {
|
|
520
|
+
readonly transition?: OperationTransition<TSourceModel>;
|
|
521
|
+
readonly steps: readonly OperationSideEffect[];
|
|
522
|
+
run(step: OperationSideEffect): MaybePromise<void>;
|
|
523
|
+
revert(step: OperationSideEffect): MaybePromise<void>;
|
|
524
|
+
} {
|
|
525
|
+
if (direction === 'redo') {
|
|
526
|
+
return { transition: this.transition, steps: [...this.steps], run: step => step.redo(), revert: step => step.undo() };
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
transition: this.transition && { from: this.transition.to, to: this.transition.from },
|
|
530
|
+
steps: [...this.steps].reverse(),
|
|
531
|
+
run: step => step.undo(),
|
|
532
|
+
revert: step => step.redo()
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The copy's stand-in for `original`: its target is the copy of
|
|
538
|
+
* `original`'s target once one exists, read when asked so a copy made later
|
|
539
|
+
* in the operation is found, else that target itself. A handler compares a
|
|
540
|
+
* target by identity with nodes it resolved from a copy.
|
|
541
|
+
*/
|
|
542
|
+
protected buildWorkingReference(original: Reference): Reference {
|
|
543
|
+
const trace = this.trace;
|
|
544
|
+
return {
|
|
545
|
+
$refText: original.$refText,
|
|
546
|
+
$refNode: original.$refNode,
|
|
547
|
+
get ref(): AstNode | undefined {
|
|
548
|
+
const target = original.ref;
|
|
549
|
+
return target && (trace.get(target) ?? target);
|
|
550
|
+
},
|
|
551
|
+
get error(): LinkingError | undefined {
|
|
552
|
+
return original.error;
|
|
553
|
+
},
|
|
554
|
+
get $nodeDescription(): AstNodeDescription | undefined {
|
|
555
|
+
return original.$nodeDescription;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** {@link buildWorkingReference} for a reference with several targets. */
|
|
561
|
+
protected buildWorkingMultiReference(original: MultiReference): MultiReference {
|
|
562
|
+
const trace = this.trace;
|
|
563
|
+
return {
|
|
564
|
+
$refText: original.$refText,
|
|
565
|
+
$refNode: original.$refNode,
|
|
566
|
+
get items(): MultiReferenceItem[] {
|
|
567
|
+
return original.items.map(item => ({ ref: trace.get(item.ref) ?? item.ref, $nodeDescription: item.$nodeDescription }));
|
|
568
|
+
},
|
|
569
|
+
get error(): LinkingError | undefined {
|
|
570
|
+
return original.error;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Replace every multi-reference under `working` with a working one.
|
|
577
|
+
* `AstUtils.copyAstNode` rebuilds single references only and leaves a
|
|
578
|
+
* multi-reference object shared with the built root, whose targets are
|
|
579
|
+
* built nodes.
|
|
580
|
+
*/
|
|
581
|
+
protected rebuildWorkingMultiReferences(working: AstNode): void {
|
|
582
|
+
for (const node of AstUtils.streamAst(working)) {
|
|
583
|
+
const properties = node as GenericAstNode;
|
|
584
|
+
for (const [name, value] of Object.entries(properties)) {
|
|
585
|
+
if (name.startsWith('$')) {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (isMultiReference(value)) {
|
|
589
|
+
properties[name] = this.buildWorkingMultiReference(value);
|
|
590
|
+
} else if (Array.isArray(value) && value.some(element => isMultiReference(element))) {
|
|
591
|
+
properties[name] = value.map((element: unknown) =>
|
|
592
|
+
isMultiReference(element) ? this.buildWorkingMultiReference(element) : element
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function isCommandArray(value: unknown): value is Command[] {
|
|
601
|
+
return Array.isArray(value) && value.every(element => typeof element === 'object' && element !== null && 'execute' in element);
|
|
602
|
+
}
|