@hamak/ui-store-impl 0.3.0 → 0.4.0
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/CHANGELOG.md +18 -0
- package/dist/es2015/fs/commands/fs-commands.js +311 -0
- package/dist/es2015/fs/commands/structure-commands.js +21 -0
- package/dist/es2015/fs/core/fs-adapter.js +123 -0
- package/dist/es2015/fs/core/fs-facade.js +78 -0
- package/dist/es2015/fs/index.js +9 -0
- package/dist/es2015/fs/utils/data-updater.js +244 -0
- package/dist/es2015/fs/utils/deep-equal.js +28 -0
- package/dist/es2015/index.js +1 -0
- package/dist/fs/commands/fs-commands.d.ts +96 -0
- package/dist/fs/commands/fs-commands.d.ts.map +1 -0
- package/dist/fs/commands/fs-commands.js +311 -0
- package/dist/fs/commands/structure-commands.d.ts +76 -0
- package/dist/fs/commands/structure-commands.d.ts.map +1 -0
- package/dist/fs/commands/structure-commands.js +21 -0
- package/dist/fs/core/fs-adapter.d.ts +57 -0
- package/dist/fs/core/fs-adapter.d.ts.map +1 -0
- package/dist/fs/core/fs-adapter.js +123 -0
- package/dist/fs/core/fs-facade.d.ts +37 -0
- package/dist/fs/core/fs-facade.d.ts.map +1 -0
- package/dist/fs/core/fs-facade.js +78 -0
- package/dist/fs/index.d.ts +7 -0
- package/dist/fs/index.d.ts.map +1 -0
- package/dist/fs/index.js +9 -0
- package/dist/fs/utils/data-updater.d.ts +36 -0
- package/dist/fs/utils/data-updater.d.ts.map +1 -0
- package/dist/fs/utils/data-updater.js +248 -0
- package/dist/fs/utils/deep-equal.d.ts +5 -0
- package/dist/fs/utils/deep-equal.d.ts.map +1 -0
- package/dist/fs/utils/deep-equal.js +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +8 -5
- package/src/fs/commands/fs-commands.ts +406 -0
- package/src/fs/commands/structure-commands.ts +105 -0
- package/src/fs/core/fs-adapter.ts +180 -0
- package/src/fs/core/fs-facade.ts +100 -0
- package/src/fs/index.ts +11 -0
- package/src/fs/utils/data-updater.ts +273 -0
- package/src/fs/utils/deep-equal.ts +35 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { isDraft, original } from 'immer';
|
|
2
|
+
import { navigate, ensurePath, areSameItineraryStep } from '@hamak/navigation-utils';
|
|
3
|
+
/**
|
|
4
|
+
* Type guard for checking if a value is an object
|
|
5
|
+
*/
|
|
6
|
+
function isObject(o) {
|
|
7
|
+
if (o === undefined || o === null) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
return (isDraft(o) && typeof original(o) === 'object')
|
|
11
|
+
|| typeof o === 'object';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Type guard for checking if a value is an array
|
|
15
|
+
*/
|
|
16
|
+
function isArray(o) {
|
|
17
|
+
if (o === undefined || o === null) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return (isDraft(o) && Array.isArray(original(o)))
|
|
21
|
+
|| Array.isArray(o);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* DataUpdater class handles operations on data structures using itineraries
|
|
25
|
+
*/
|
|
26
|
+
export class DataUpdater {
|
|
27
|
+
/**
|
|
28
|
+
* Delete a value at the specified itinerary
|
|
29
|
+
*/
|
|
30
|
+
executeDelete(data, itinerary) {
|
|
31
|
+
if (itinerary === undefined) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const { parent, value: step } = itinerary;
|
|
35
|
+
const parentNode = navigate(data, parent);
|
|
36
|
+
if (!(parentNode === undefined)) {
|
|
37
|
+
if (isArray(parentNode)) {
|
|
38
|
+
if (step.type === 'position') {
|
|
39
|
+
parentNode.splice(step.position, 1);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// TODO log expected PositionStep
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (isObject(parentNode)) {
|
|
46
|
+
if (step.type === 'property') {
|
|
47
|
+
if (parentNode !== null) {
|
|
48
|
+
delete parentNode[step.propertyName];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// TODO log expected PropertyStep
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// TODO log expected object
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// TODO log Parent Node not found
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Set a value at the specified itinerary
|
|
65
|
+
*/
|
|
66
|
+
executeSet(data, itinerary, value, prototypeToParent) {
|
|
67
|
+
if (itinerary === undefined) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const { parent, value: step } = itinerary;
|
|
71
|
+
const parentNode = ensurePath(data, parent, prototypeToParent);
|
|
72
|
+
if (!(parentNode === undefined)) {
|
|
73
|
+
if (isArray(parentNode)) {
|
|
74
|
+
if (step.type === 'position') {
|
|
75
|
+
parentNode[step.position] = value;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// TODO log expected PositionStep
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (isObject(parentNode)) {
|
|
82
|
+
if (step.type === 'property') {
|
|
83
|
+
if (parentNode !== null) {
|
|
84
|
+
parentNode[step.propertyName] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// TODO log expected PropertyStep
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// TODO log expected object
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// TODO log Parent Node not found
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Add a value at the specified itinerary
|
|
101
|
+
*/
|
|
102
|
+
executeAdd(data, itinerary, value, prototypeToParent) {
|
|
103
|
+
const targetNode = ensurePath(data, itinerary, prototypeToParent);
|
|
104
|
+
if (!(targetNode === undefined)) {
|
|
105
|
+
if (isArray(targetNode)) {
|
|
106
|
+
targetNode.push(value);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// TODO log expected object
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// TODO log Parent Node not found
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Insert a value at the specified position
|
|
118
|
+
*/
|
|
119
|
+
executeInsert(data, itinerary, value, prototypeToParent) {
|
|
120
|
+
const { value: step, parent } = itinerary;
|
|
121
|
+
if (step.type === 'position') {
|
|
122
|
+
const parentNode = ensurePath(data, parent, prototypeToParent);
|
|
123
|
+
if (parentNode !== undefined) {
|
|
124
|
+
if (isArray(parentNode)) {
|
|
125
|
+
parentNode.splice(step.position, 0, value);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// TODO log expected object
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// TODO log Parent Node not found
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// TODO log expecting position step
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Rebase a pending command's itinerary on top of an altering command.
|
|
141
|
+
* Returns null if the pending command should be dropped (e.g., targets a deleted element).
|
|
142
|
+
* Enhanced: Handles rebasing for nested itineraries, not just at the first divergence.
|
|
143
|
+
*/
|
|
144
|
+
rebasePendingCommandOn(alteringCmd, pendingCmd) {
|
|
145
|
+
// Convert itinerary stack to array (root to leaf)
|
|
146
|
+
function itineraryToArray(it) {
|
|
147
|
+
const arr = [];
|
|
148
|
+
let current = it;
|
|
149
|
+
while (current) {
|
|
150
|
+
arr.push(current.value);
|
|
151
|
+
current = current.parent;
|
|
152
|
+
}
|
|
153
|
+
return arr.reverse();
|
|
154
|
+
}
|
|
155
|
+
// Convert array back to itinerary stack (leaf to root)
|
|
156
|
+
function arrayToItinerary(arr) {
|
|
157
|
+
// Assumes arr.length > 0
|
|
158
|
+
let it = { parent: undefined, value: arr[0] };
|
|
159
|
+
for (let i = 1; i < arr.length; i++) {
|
|
160
|
+
it = { parent: it, value: arr[i] };
|
|
161
|
+
}
|
|
162
|
+
return it;
|
|
163
|
+
}
|
|
164
|
+
// Rebase a single position step if needed
|
|
165
|
+
function rebasePositionStep(alteringStep, alteringCmd, pendingStep) {
|
|
166
|
+
if (pendingStep.type !== 'position' || alteringStep.type !== 'position')
|
|
167
|
+
return pendingStep;
|
|
168
|
+
let { position: pendingIdx } = pendingStep;
|
|
169
|
+
const { position: alteringIdx } = alteringStep;
|
|
170
|
+
if (alteringCmd.name === 'insert-at' && pendingIdx >= alteringIdx) {
|
|
171
|
+
pendingIdx += 1;
|
|
172
|
+
}
|
|
173
|
+
else if (alteringCmd.name === 'delete-at') {
|
|
174
|
+
if (pendingIdx > alteringIdx) {
|
|
175
|
+
pendingIdx -= 1;
|
|
176
|
+
}
|
|
177
|
+
// If pendingIdx === alteringIdx, leave unchanged (may be dropped by batch context)
|
|
178
|
+
}
|
|
179
|
+
return pendingIdx !== pendingStep.position
|
|
180
|
+
? Object.assign(Object.assign({}, pendingStep), { position: pendingIdx }) : pendingStep;
|
|
181
|
+
}
|
|
182
|
+
const alteringArr = itineraryToArray(alteringCmd.itinerary);
|
|
183
|
+
const pendingArr = itineraryToArray(pendingCmd.itinerary);
|
|
184
|
+
// Find first index where steps differ
|
|
185
|
+
let commonLength = 0;
|
|
186
|
+
while (commonLength < alteringArr.length &&
|
|
187
|
+
commonLength < pendingArr.length &&
|
|
188
|
+
areSameItineraryStep(alteringArr[commonLength], pendingArr[commonLength])) {
|
|
189
|
+
commonLength++;
|
|
190
|
+
}
|
|
191
|
+
// Rebase all position steps after the divergence point
|
|
192
|
+
const rebasedArr = pendingArr.slice();
|
|
193
|
+
for (let i = 0; i < alteringArr.length; i++) {
|
|
194
|
+
if (alteringArr[i].type === 'position' &&
|
|
195
|
+
pendingArr.length > i &&
|
|
196
|
+
pendingArr[i].type === 'position') {
|
|
197
|
+
// Only rebase if the pending itinerary matches the altering up to this step
|
|
198
|
+
const isDescendant = alteringArr
|
|
199
|
+
.slice(0, i)
|
|
200
|
+
.every((step, j) => areSameItineraryStep(step, pendingArr[j]));
|
|
201
|
+
if (isDescendant) {
|
|
202
|
+
rebasedArr[i] = rebasePositionStep(alteringArr[i], alteringCmd, pendingArr[i]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// If no itinerary, return as is
|
|
207
|
+
if (rebasedArr.length === 0)
|
|
208
|
+
return pendingCmd;
|
|
209
|
+
return Object.assign(Object.assign({}, pendingCmd), { itinerary: arrayToItinerary(rebasedArr) });
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Executes a batch of commands, rebasing each on previous mutations.
|
|
213
|
+
* @param data The data object to mutate
|
|
214
|
+
* @param command The batch update command
|
|
215
|
+
*/
|
|
216
|
+
executeBatch(data, command) {
|
|
217
|
+
const { commands } = command;
|
|
218
|
+
const applied = [];
|
|
219
|
+
for (let i = 0; i < commands.length; i++) {
|
|
220
|
+
let rebased = commands[i];
|
|
221
|
+
for (let j = 0; j < applied.length && rebased != null; j++) {
|
|
222
|
+
rebased = this.rebasePendingCommandOn(applied[j], rebased);
|
|
223
|
+
}
|
|
224
|
+
if (rebased != null) {
|
|
225
|
+
switch (rebased.name) {
|
|
226
|
+
case 'add-at':
|
|
227
|
+
this.executeAdd(data, rebased.itinerary, rebased.value, rebased.prototypes);
|
|
228
|
+
break;
|
|
229
|
+
case 'insert-at':
|
|
230
|
+
this.executeInsert(data, rebased.itinerary, rebased.value, rebased.prototypes);
|
|
231
|
+
break;
|
|
232
|
+
case 'set-at':
|
|
233
|
+
this.executeSet(data, rebased.itinerary, rebased.value, rebased.prototypes);
|
|
234
|
+
break;
|
|
235
|
+
case 'delete-at':
|
|
236
|
+
this.executeDelete(data, rebased.itinerary);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
applied.push(rebased);
|
|
240
|
+
}
|
|
241
|
+
// If rebased is null, skip this command (it targets a deleted element)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep equality check for objects and arrays
|
|
3
|
+
*/
|
|
4
|
+
export function deepEqual(obj1, obj2) {
|
|
5
|
+
if (obj1 === obj2) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (obj1 === null || obj2 === null || typeof obj1 !== 'object' || typeof obj2 !== 'object') {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const keys1 = Object.keys(obj1);
|
|
15
|
+
const keys2 = Object.keys(obj2);
|
|
16
|
+
if (keys1.length !== keys2.length) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
for (const key of keys1) {
|
|
20
|
+
if (!keys2.includes(key)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (!deepEqual(obj1[key], obj2[key])) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
package/dist/es2015/index.js
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { FileNode, FileSystemNode, FileSystemState, FileContentSchema } from '@hamak/ui-store-api';
|
|
2
|
+
import { StructureNodeCommand } from './structure-commands';
|
|
3
|
+
import { DataUpdater } from '../utils/data-updater';
|
|
4
|
+
/**
|
|
5
|
+
* Base interface for filesystem commands
|
|
6
|
+
*/
|
|
7
|
+
export interface FsCommandBase {
|
|
8
|
+
name: 'mkdir' | 'set-file' | 'remove' | 'update-file-content' | 'set-file-content';
|
|
9
|
+
path: string | string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create directory command
|
|
13
|
+
*/
|
|
14
|
+
export interface MkdirCommand extends FsCommandBase {
|
|
15
|
+
name: 'mkdir';
|
|
16
|
+
parents?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Remove file or directory command
|
|
20
|
+
*/
|
|
21
|
+
export interface RemoveCommand extends FsCommandBase {
|
|
22
|
+
name: 'remove';
|
|
23
|
+
recursive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Set file command (create or update)
|
|
27
|
+
*/
|
|
28
|
+
export interface SetFileCommand extends FsCommandBase {
|
|
29
|
+
name: 'set-file';
|
|
30
|
+
content: any;
|
|
31
|
+
schema: FileContentSchema;
|
|
32
|
+
override?: boolean;
|
|
33
|
+
contentIsPresent?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Update file content with structure command
|
|
37
|
+
*/
|
|
38
|
+
export interface UpdateFileContentCommand extends FsCommandBase {
|
|
39
|
+
name: 'update-file-content';
|
|
40
|
+
contentCommand: StructureNodeCommand;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Set file content directly
|
|
44
|
+
*/
|
|
45
|
+
export interface SetFileContentCommand extends FsCommandBase {
|
|
46
|
+
name: 'set-file-content';
|
|
47
|
+
content: any;
|
|
48
|
+
fromRemote?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Union type for all filesystem commands
|
|
52
|
+
*/
|
|
53
|
+
export type FileSystemCommand = MkdirCommand | RemoveCommand | SetFileCommand | UpdateFileContentCommand | SetFileContentCommand;
|
|
54
|
+
/**
|
|
55
|
+
* Convert path to steps array
|
|
56
|
+
*/
|
|
57
|
+
export declare function pathSteps(path: string | string[]): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Get parent path steps
|
|
60
|
+
*/
|
|
61
|
+
export declare function parentPathSteps(path: string | string[]): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Get filesystem node at path
|
|
64
|
+
*/
|
|
65
|
+
export declare function getFileSystemNode(fsNode: FileSystemNode, path: string | string[]): FileSystemNode | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* FileSystem command handler - executes filesystem operations
|
|
68
|
+
*/
|
|
69
|
+
export declare class FileSystemCommandHandler {
|
|
70
|
+
readonly contentCommandHandler: FileContentCommandHandler;
|
|
71
|
+
execute(state: FileSystemState, command: FileSystemCommand): FileSystemState;
|
|
72
|
+
private executeMkdir;
|
|
73
|
+
private executeRemove;
|
|
74
|
+
private executeSetFile;
|
|
75
|
+
private executeContentCommand;
|
|
76
|
+
private contentMayChange;
|
|
77
|
+
private executeSetContentCommand;
|
|
78
|
+
protected current<T = any>(o: T): T;
|
|
79
|
+
protected original<T = any>(o: T): T | undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* File content command handler - handles structure commands on file content
|
|
83
|
+
*/
|
|
84
|
+
export declare class FileContentCommandHandler {
|
|
85
|
+
readonly updater: DataUpdater;
|
|
86
|
+
execute(file: FileNode, command: StructureNodeCommand): void;
|
|
87
|
+
private executeDelete;
|
|
88
|
+
private executeSet;
|
|
89
|
+
private executeAdd;
|
|
90
|
+
private executeInsert;
|
|
91
|
+
/**
|
|
92
|
+
* Executes a batch of commands, rebasing each on previous mutations.
|
|
93
|
+
*/
|
|
94
|
+
executeBatch(file: FileNode, command: any): void;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=fs-commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-commands.d.ts","sourceRoot":"","sources":["../../../src/fs/commands/fs-commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { fileSystemNodeInitialState } from '@hamak/ui-store-api';
|
|
2
|
+
import { DataUpdater } from '../utils/data-updater';
|
|
3
|
+
import { produce, current, isDraft, original } from 'immer';
|
|
4
|
+
import { deepEqual } from '../utils/deep-equal';
|
|
5
|
+
/**
|
|
6
|
+
* Convert path to steps array
|
|
7
|
+
*/
|
|
8
|
+
export function pathSteps(path) {
|
|
9
|
+
if (Array.isArray(path)) {
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
return path.split('/').filter(s => s.trim().length > 0);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get parent path steps
|
|
16
|
+
*/
|
|
17
|
+
export function parentPathSteps(path) {
|
|
18
|
+
const steps = pathSteps(path);
|
|
19
|
+
return steps.length > 0 ? steps.slice(0, steps.length - 1) : steps;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get filesystem node at path
|
|
23
|
+
*/
|
|
24
|
+
export function getFileSystemNode(fsNode, path) {
|
|
25
|
+
const steps = pathSteps(path);
|
|
26
|
+
let result = fsNode;
|
|
27
|
+
for (let i = 0; i < steps.length; i++) {
|
|
28
|
+
const step = steps[i];
|
|
29
|
+
if (result === undefined) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
else if (result.type === 'directory') {
|
|
33
|
+
result = result.children[step];
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create a directory node
|
|
43
|
+
*/
|
|
44
|
+
function createDirectoryNode(step) {
|
|
45
|
+
return {
|
|
46
|
+
type: 'directory',
|
|
47
|
+
state: fileSystemNodeInitialState(),
|
|
48
|
+
name: step,
|
|
49
|
+
children: {}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a file node
|
|
54
|
+
*/
|
|
55
|
+
function createFileNode(name, content, schema, state) {
|
|
56
|
+
return { type: 'file', name, content, schema, state };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* FileSystem command handler - executes filesystem operations
|
|
60
|
+
*/
|
|
61
|
+
export class FileSystemCommandHandler {
|
|
62
|
+
constructor() {
|
|
63
|
+
this.contentCommandHandler = new FileContentCommandHandler();
|
|
64
|
+
}
|
|
65
|
+
execute(state, command) {
|
|
66
|
+
switch (command.name) {
|
|
67
|
+
case 'mkdir': return this.executeMkdir(state, command);
|
|
68
|
+
case 'set-file': return this.executeSetFile(state, command);
|
|
69
|
+
case 'update-file-content': return this.executeContentCommand(state, command);
|
|
70
|
+
case 'set-file-content': return this.executeSetContentCommand(state, command);
|
|
71
|
+
case 'remove': return this.executeRemove(state, command);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
executeMkdir(state, command) {
|
|
75
|
+
const { path, parents } = command;
|
|
76
|
+
const steps = pathSteps(path);
|
|
77
|
+
return produce(state, draft => {
|
|
78
|
+
const { root } = draft;
|
|
79
|
+
let dir = root;
|
|
80
|
+
for (let i = 0; i < steps.length; i++) {
|
|
81
|
+
const step = steps[i];
|
|
82
|
+
if (i + 1 === steps.length) {
|
|
83
|
+
// Last step is the element to create
|
|
84
|
+
if (dir.children[step] === undefined) {
|
|
85
|
+
dir.children[step] = createDirectoryNode(step);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
let child = dir.children[step];
|
|
90
|
+
if (child === undefined) {
|
|
91
|
+
if (parents === true) {
|
|
92
|
+
child = createDirectoryNode(step);
|
|
93
|
+
dir.children[step] = child;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (child.type === 'directory') {
|
|
100
|
+
dir = child;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
dir = child;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
executeRemove(state, command) {
|
|
111
|
+
const { path, recursive } = command;
|
|
112
|
+
const steps = pathSteps(path);
|
|
113
|
+
return produce(state, draft => {
|
|
114
|
+
const { root } = draft;
|
|
115
|
+
const parentDir = getFileSystemNode(root, steps.slice(0, steps.length - 1));
|
|
116
|
+
if (parentDir === undefined) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (parentDir.type === 'directory') {
|
|
120
|
+
const last = steps[steps.length - 1];
|
|
121
|
+
const child = parentDir.children[last];
|
|
122
|
+
if (child?.type === 'directory'
|
|
123
|
+
&& Object.keys(child.children).length > 0
|
|
124
|
+
&& !(recursive === true)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
delete parentDir.children[last];
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
executeSetFile(state, command) {
|
|
132
|
+
const { path, content, schema, override, contentIsPresent } = command;
|
|
133
|
+
const steps = pathSteps(path);
|
|
134
|
+
return produce(state, draft => {
|
|
135
|
+
const { root } = draft;
|
|
136
|
+
const parentPath = steps.slice(0, steps.length - 1);
|
|
137
|
+
const parentDir = getFileSystemNode(root, parentPath);
|
|
138
|
+
if (parentDir === undefined) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (parentDir.type === 'directory') {
|
|
142
|
+
const last = steps[steps.length - 1];
|
|
143
|
+
const child = parentDir.children[last];
|
|
144
|
+
if (child === undefined || override === true) {
|
|
145
|
+
parentDir.children[last] = createFileNode(last, content, schema, fileSystemNodeInitialState(contentIsPresent));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.warn(`File already exists at location`, path);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.warn(`Parent file is not directory`, parentPath);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
executeContentCommand(state, command) {
|
|
157
|
+
const { path, contentCommand } = command;
|
|
158
|
+
return produce(state, draft => {
|
|
159
|
+
const { root } = draft;
|
|
160
|
+
const fileNode = getFileSystemNode(root, path);
|
|
161
|
+
if (fileNode !== undefined) {
|
|
162
|
+
if (fileNode.type === 'file') {
|
|
163
|
+
this.contentCommandHandler.execute(fileNode, contentCommand);
|
|
164
|
+
const previousFile = getFileSystemNode(state.root, path);
|
|
165
|
+
const previousContent = previousFile?.type === "file" ? previousFile.content : undefined;
|
|
166
|
+
this.contentMayChange(fileNode);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.warn(`Not a file at location`, path);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.warn(`No file found at location`, path);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
contentMayChange(fileNode) {
|
|
178
|
+
const changed = !deepEqual(this.original(fileNode.content), this.current(fileNode.content));
|
|
179
|
+
if (changed) {
|
|
180
|
+
if (fileNode.state === undefined) {
|
|
181
|
+
fileNode.state = fileSystemNodeInitialState();
|
|
182
|
+
}
|
|
183
|
+
fileNode.state.contentHistory.push(this.original(fileNode.content));
|
|
184
|
+
// check compared to memo modification status
|
|
185
|
+
if (fileNode.state.memo !== undefined) {
|
|
186
|
+
const memo = fileNode.state.memo;
|
|
187
|
+
memo.modified = !deepEqual(this.current(memo.originalContent), this.current(fileNode.content));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
executeSetContentCommand(state, command) {
|
|
192
|
+
const { path, content, fromRemote } = command;
|
|
193
|
+
return produce(state, draft => {
|
|
194
|
+
const { root } = draft;
|
|
195
|
+
const fileNode = getFileSystemNode(root, path);
|
|
196
|
+
if (fileNode !== undefined) {
|
|
197
|
+
if (fileNode.type === 'file') {
|
|
198
|
+
fileNode.content = content;
|
|
199
|
+
if (fileNode.state === undefined) {
|
|
200
|
+
fileNode.state = fileSystemNodeInitialState(true);
|
|
201
|
+
}
|
|
202
|
+
if (fromRemote) {
|
|
203
|
+
fileNode.state.contentLoaded = true;
|
|
204
|
+
if (fileNode.state.memo === undefined) {
|
|
205
|
+
fileNode.state.memo = { originalContent: { value: content }, modified: false };
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const memo = fileNode.state.memo;
|
|
209
|
+
memo.originalContent = { value: content };
|
|
210
|
+
memo.modified = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
this.contentMayChange(fileNode);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.warn(`Not a file at location`, path);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
console.warn(`No file found at location`, path);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
current(o) {
|
|
227
|
+
if (o === null || o === undefined) {
|
|
228
|
+
return o;
|
|
229
|
+
}
|
|
230
|
+
return isDraft(o) ? current(o) : o;
|
|
231
|
+
}
|
|
232
|
+
original(o) {
|
|
233
|
+
if (o === null || o === undefined) {
|
|
234
|
+
return o;
|
|
235
|
+
}
|
|
236
|
+
return isDraft(o) ? original(o) : o;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* File content command handler - handles structure commands on file content
|
|
241
|
+
*/
|
|
242
|
+
export class FileContentCommandHandler {
|
|
243
|
+
constructor() {
|
|
244
|
+
this.updater = new DataUpdater();
|
|
245
|
+
}
|
|
246
|
+
execute(file, command) {
|
|
247
|
+
switch (command.name) {
|
|
248
|
+
case 'add-at':
|
|
249
|
+
{
|
|
250
|
+
this.executeAdd(file, command);
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case 'insert-at':
|
|
254
|
+
{
|
|
255
|
+
this.executeInsert(file, command);
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
case 'set-at':
|
|
259
|
+
{
|
|
260
|
+
this.executeSet(file, command);
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
case 'delete-at':
|
|
264
|
+
{
|
|
265
|
+
this.executeDelete(file, command);
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
case 'batch-update':
|
|
269
|
+
{
|
|
270
|
+
this.executeBatch(file, command);
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
executeDelete(file, command) {
|
|
276
|
+
const { itinerary } = command;
|
|
277
|
+
if (itinerary === undefined) {
|
|
278
|
+
file.content = undefined;
|
|
279
|
+
}
|
|
280
|
+
const { content } = file;
|
|
281
|
+
this.updater.executeDelete(content, itinerary);
|
|
282
|
+
}
|
|
283
|
+
executeSet(file, command) {
|
|
284
|
+
const { itinerary, value } = command;
|
|
285
|
+
if (itinerary === undefined) {
|
|
286
|
+
file.content = value;
|
|
287
|
+
}
|
|
288
|
+
const { content } = file;
|
|
289
|
+
this.updater.executeSet(content, itinerary, value);
|
|
290
|
+
}
|
|
291
|
+
executeAdd(file, command) {
|
|
292
|
+
const { itinerary, value } = command;
|
|
293
|
+
const { content } = file;
|
|
294
|
+
this.updater.executeAdd(content, itinerary, value);
|
|
295
|
+
}
|
|
296
|
+
executeInsert(file, command) {
|
|
297
|
+
const { itinerary, value } = command;
|
|
298
|
+
if (itinerary === undefined) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const { content } = file;
|
|
302
|
+
this.updater.executeInsert(content, itinerary, value);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Executes a batch of commands, rebasing each on previous mutations.
|
|
306
|
+
*/
|
|
307
|
+
executeBatch(file, command) {
|
|
308
|
+
const { content } = file;
|
|
309
|
+
this.updater.executeBatch(content, command);
|
|
310
|
+
}
|
|
311
|
+
}
|