@hamak/ui-store-impl 0.3.0 → 0.4.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/es2015/fs/commands/fs-commands.js +311 -0
  3. package/dist/es2015/fs/commands/structure-commands.js +21 -0
  4. package/dist/es2015/fs/core/fs-adapter.js +123 -0
  5. package/dist/es2015/fs/core/fs-facade.js +78 -0
  6. package/dist/es2015/fs/index.js +9 -0
  7. package/dist/es2015/fs/utils/data-updater.js +244 -0
  8. package/dist/es2015/fs/utils/deep-equal.js +28 -0
  9. package/dist/es2015/index.js +1 -0
  10. package/dist/fs/commands/fs-commands.d.ts +96 -0
  11. package/dist/fs/commands/fs-commands.d.ts.map +1 -0
  12. package/dist/fs/commands/fs-commands.js +311 -0
  13. package/dist/fs/commands/structure-commands.d.ts +76 -0
  14. package/dist/fs/commands/structure-commands.d.ts.map +1 -0
  15. package/dist/fs/commands/structure-commands.js +21 -0
  16. package/dist/fs/core/fs-adapter.d.ts +57 -0
  17. package/dist/fs/core/fs-adapter.d.ts.map +1 -0
  18. package/dist/fs/core/fs-adapter.js +123 -0
  19. package/dist/fs/core/fs-facade.d.ts +37 -0
  20. package/dist/fs/core/fs-facade.d.ts.map +1 -0
  21. package/dist/fs/core/fs-facade.js +78 -0
  22. package/dist/fs/index.d.ts +7 -0
  23. package/dist/fs/index.d.ts.map +1 -0
  24. package/dist/fs/index.js +9 -0
  25. package/dist/fs/utils/data-updater.d.ts +36 -0
  26. package/dist/fs/utils/data-updater.d.ts.map +1 -0
  27. package/dist/fs/utils/data-updater.js +248 -0
  28. package/dist/fs/utils/deep-equal.d.ts +5 -0
  29. package/dist/fs/utils/deep-equal.d.ts.map +1 -0
  30. package/dist/fs/utils/deep-equal.js +28 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/package.json +8 -5
  35. package/src/fs/commands/fs-commands.ts +405 -0
  36. package/src/fs/commands/structure-commands.ts +105 -0
  37. package/src/fs/core/fs-adapter.ts +180 -0
  38. package/src/fs/core/fs-facade.ts +100 -0
  39. package/src/fs/index.ts +11 -0
  40. package/src/fs/utils/data-updater.ts +273 -0
  41. package/src/fs/utils/deep-equal.ts +35 -0
  42. package/src/index.ts +1 -0
@@ -0,0 +1,100 @@
1
+ import { createSelector } from '@reduxjs/toolkit';
2
+ import {
3
+ DirectoryNode,
4
+ FileNode,
5
+ FileSystemNode,
6
+ FileSystemState,
7
+ Selector
8
+ } from '@hamak/ui-store-api';
9
+ import { FileSystemAdapter, FileSystemNodeActions } from './fs-adapter';
10
+ import { Path, Pathway, PathwayResolver, RelativePathwayResolver } from '@hamak/shared-utils';
11
+
12
+ /**
13
+ * Selector that returns a FileSystemNode generally the root of the Filesystem Overlay
14
+ */
15
+ export interface StoreFileSystemOverlaySelector<S> {
16
+ /**
17
+ * The key of the overlay
18
+ */
19
+ key : string;
20
+ selector : Selector<S, FileSystemNode | undefined>
21
+ }
22
+
23
+ /**
24
+ * StoreFileSystemFacade - High-level API for filesystem operations
25
+ */
26
+ export class StoreFileSystemFacade<S = any> {
27
+ constructor(
28
+ private fileSystemSelector: Selector<S, FileSystemState | undefined>,
29
+ private adapter : FileSystemAdapter
30
+ ) {}
31
+
32
+ getActions() : FileSystemNodeActions {
33
+ return this.adapter.getActions()
34
+ }
35
+
36
+ createSelector(path : string | string[]) : Selector<S, FileSystemNode | undefined>{
37
+ return this.adapter.createSelector(this.fileSystemSelector, path)
38
+ }
39
+
40
+ createFileSelector(path : string | string[]) : Selector<S, FileNode | undefined>{
41
+ const nodeSelector = this.adapter.createSelector(this.fileSystemSelector, path)
42
+ return createSelector([nodeSelector], (f: FileSystemNode | undefined) => {
43
+ if(f?.type === 'file'){
44
+ return f as FileNode
45
+ }else {
46
+ return undefined
47
+ }
48
+ })
49
+ }
50
+
51
+ selectFromRoot(state : S, path : string[] = []) : FileSystemNode | undefined {
52
+ const fileSystemState = this.fileSystemSelector(state)
53
+ if(fileSystemState === undefined || fileSystemState === null){
54
+ return undefined
55
+ } else {
56
+ const root = fileSystemState.root
57
+ return (path ?? []).reduce((acc, step) => {
58
+ if(acc === undefined || acc === null) {
59
+ return undefined
60
+ } else {
61
+ switch (acc.type){
62
+ case "directory" : return acc.children[step];
63
+ case "file" : return undefined
64
+ }
65
+ }
66
+ }, root as FileSystemNode | undefined)
67
+ }
68
+ }
69
+
70
+ selectFileFromRoot(state : S, path : string[] = []) : FileNode | undefined {
71
+ const fsNode = this.selectFromRoot(state, path)
72
+ if(fsNode === undefined){
73
+ return undefined
74
+ } else {
75
+ if(fsNode.type === "file"){
76
+ return fsNode
77
+ } else {
78
+ throw new Error(`Expecting file at path '${Pathway.of(path).toString()}' but got '${fsNode.type }'`)
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+
85
+ /**
86
+ * StoreFileSystemPathwayResolver - Pathway resolver for filesystem state
87
+ */
88
+ export class StoreFileSystemPathwayResolver<S = any> implements PathwayResolver<FileSystemNode> {
89
+ constructor(private state : S, private fs : StoreFileSystemFacade<S>) {
90
+ }
91
+
92
+ resolve(path: string | string[] | Pathway): FileSystemNode | undefined {
93
+ const segments = Pathway.of(path).getSegments()
94
+ return this.fs.selectFromRoot(this.state, segments)
95
+ }
96
+
97
+ relativeTo(path : Path) : RelativePathwayResolver<FileSystemNode> {
98
+ return new RelativePathwayResolver<FileSystemNode>(this, path)
99
+ }
100
+ }
@@ -0,0 +1,11 @@
1
+ // Core filesystem functionality
2
+ export * from './core/fs-adapter';
3
+ export * from './core/fs-facade';
4
+
5
+ // Commands
6
+ export * from './commands/fs-commands';
7
+ export * from './commands/structure-commands';
8
+
9
+ // Utilities
10
+ export * from './utils/data-updater';
11
+ export * from './utils/deep-equal';
@@ -0,0 +1,273 @@
1
+ import { isDraft, original } from 'immer';
2
+ import {
3
+ Itinerary,
4
+ StackElement,
5
+ navigate,
6
+ ensurePath,
7
+ areSameItineraryStep,
8
+ ItineraryStep
9
+ } from '@hamak/shared-utils';
10
+ import {
11
+ UnitaryStructureNodeCommand,
12
+ BatchUpdateStructureNodeCommand
13
+ } from '../commands/structure-commands';
14
+
15
+ /**
16
+ * Type guard for checking if a value is an object
17
+ */
18
+ function isObject(o: any): o is Record<any, any> {
19
+ if (o === undefined || o === null) {
20
+ return false
21
+ }
22
+
23
+ return (isDraft(o) && typeof original(o) === 'object')
24
+ || typeof o === 'object'
25
+ }
26
+
27
+ /**
28
+ * Type guard for checking if a value is an array
29
+ */
30
+ function isArray(o: any): o is Array<any> {
31
+ if (o === undefined || o === null) {
32
+ return false
33
+ }
34
+ return (isDraft(o) && Array.isArray(original(o)))
35
+ || Array.isArray(o)
36
+ }
37
+
38
+ /**
39
+ * DataUpdater class handles operations on data structures using itineraries
40
+ */
41
+ export class DataUpdater {
42
+
43
+ /**
44
+ * Delete a value at the specified itinerary
45
+ */
46
+ public executeDelete(data: any, itinerary: Itinerary | undefined) {
47
+ if (itinerary === undefined) {
48
+ return
49
+ }
50
+
51
+ const {parent, value: step} = itinerary
52
+ const parentNode = navigate(data, parent)
53
+ if (!(parentNode === undefined)) {
54
+ if (isArray(parentNode)) {
55
+ if (step.type === 'position') {
56
+ parentNode.splice(step.position, 1)
57
+ } else {
58
+ // TODO log expected PositionStep
59
+ }
60
+ } else if (isObject(parentNode)) {
61
+ if (step.type === 'property') {
62
+ if (parentNode !== null) {
63
+ delete parentNode[step.propertyName]
64
+ }
65
+ } else {
66
+ // TODO log expected PropertyStep
67
+ }
68
+ } else {
69
+ // TODO log expected object
70
+ }
71
+ } else {
72
+ // TODO log Parent Node not found
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Set a value at the specified itinerary
78
+ */
79
+ public executeSet(data: any, itinerary: Itinerary | undefined, value: any, prototypeToParent?: StackElement<any>) {
80
+ if (itinerary === undefined) {
81
+ return
82
+ }
83
+
84
+ const {parent, value: step} = itinerary
85
+
86
+ const parentNode = ensurePath(data, parent, prototypeToParent)
87
+ if (!(parentNode === undefined)) {
88
+ if (isArray(parentNode)) {
89
+ if (step.type === 'position') {
90
+ parentNode[step.position] = value
91
+ } else {
92
+ // TODO log expected PositionStep
93
+ }
94
+ } else if (isObject(parentNode)) {
95
+ if (step.type === 'property') {
96
+ if (parentNode !== null) {
97
+ parentNode[step.propertyName] = value
98
+ }
99
+ } else {
100
+ // TODO log expected PropertyStep
101
+ }
102
+ } else {
103
+ // TODO log expected object
104
+ }
105
+ } else {
106
+ // TODO log Parent Node not found
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Add a value at the specified itinerary
112
+ */
113
+ public executeAdd(data: any, itinerary: Itinerary | undefined, value: any, prototypeToParent?: StackElement<any>) {
114
+ const targetNode = ensurePath(data, itinerary, prototypeToParent)
115
+ if (!(targetNode === undefined)) {
116
+ if (isArray(targetNode)) {
117
+ targetNode.push(value)
118
+ } else {
119
+ // TODO log expected object
120
+ }
121
+ } else {
122
+ // TODO log Parent Node not found
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Insert a value at the specified position
128
+ */
129
+ public executeInsert(data: any, itinerary: Itinerary, value: any, prototypeToParent?: StackElement<any>) {
130
+ const {value : step, parent} = itinerary
131
+ if(step.type === 'position'){
132
+ const parentNode = ensurePath(data, parent, prototypeToParent)
133
+ if (parentNode !== undefined) {
134
+ if (isArray(parentNode)) {
135
+ parentNode.splice(step.position, 0, value)
136
+ } else {
137
+ // TODO log expected object
138
+ }
139
+ } else {
140
+ // TODO log Parent Node not found
141
+ }
142
+ }else{
143
+ // TODO log expecting position step
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Rebase a pending command's itinerary on top of an altering command.
149
+ * Returns null if the pending command should be dropped (e.g., targets a deleted element).
150
+ * Enhanced: Handles rebasing for nested itineraries, not just at the first divergence.
151
+ */
152
+ public rebasePendingCommandOn(
153
+ alteringCmd: UnitaryStructureNodeCommand,
154
+ pendingCmd: UnitaryStructureNodeCommand
155
+ ): UnitaryStructureNodeCommand | null {
156
+ // Convert itinerary stack to array (root to leaf)
157
+ function itineraryToArray(it: Itinerary | undefined): ItineraryStep[] {
158
+ const arr: ItineraryStep[] = [];
159
+ let current = it;
160
+ while (current) {
161
+ arr.push(current.value);
162
+ current = current.parent;
163
+ }
164
+ return arr.reverse();
165
+ }
166
+
167
+ // Convert array back to itinerary stack (leaf to root)
168
+ function arrayToItinerary(arr: ItineraryStep[]): Itinerary {
169
+ // Assumes arr.length > 0
170
+ let it: Itinerary = { parent: undefined, value: arr[0] };
171
+ for (let i = 1; i < arr.length; i++) {
172
+ it = { parent: it, value: arr[i] };
173
+ }
174
+ return it;
175
+ }
176
+
177
+ // Rebase a single position step if needed
178
+ function rebasePositionStep(
179
+ alteringStep: ItineraryStep,
180
+ alteringCmd: UnitaryStructureNodeCommand,
181
+ pendingStep: ItineraryStep
182
+ ): ItineraryStep {
183
+ if (pendingStep.type !== 'position' || alteringStep.type !== 'position') return pendingStep;
184
+ let { position: pendingIdx } = pendingStep;
185
+ const { position: alteringIdx } = alteringStep;
186
+
187
+ if (alteringCmd.name === 'insert-at' && pendingIdx >= alteringIdx) {
188
+ pendingIdx += 1;
189
+ } else if (alteringCmd.name === 'delete-at') {
190
+ if (pendingIdx > alteringIdx) {
191
+ pendingIdx -= 1;
192
+ }
193
+ // If pendingIdx === alteringIdx, leave unchanged (may be dropped by batch context)
194
+ }
195
+ return pendingIdx !== pendingStep.position
196
+ ? { ...pendingStep, position: pendingIdx }
197
+ : pendingStep;
198
+ }
199
+
200
+ const alteringArr = itineraryToArray(alteringCmd.itinerary);
201
+ const pendingArr = itineraryToArray(pendingCmd.itinerary);
202
+
203
+ // Find first index where steps differ
204
+ let commonLength = 0;
205
+ while (
206
+ commonLength < alteringArr.length &&
207
+ commonLength < pendingArr.length &&
208
+ areSameItineraryStep(alteringArr[commonLength], pendingArr[commonLength])
209
+ ) {
210
+ commonLength++;
211
+ }
212
+
213
+ // Rebase all position steps after the divergence point
214
+ const rebasedArr = pendingArr.slice();
215
+ for (let i = 0; i < alteringArr.length; i++) {
216
+ if (
217
+ alteringArr[i].type === 'position' &&
218
+ pendingArr.length > i &&
219
+ pendingArr[i].type === 'position'
220
+ ) {
221
+ // Only rebase if the pending itinerary matches the altering up to this step
222
+ const isDescendant = alteringArr
223
+ .slice(0, i)
224
+ .every((step, j) => areSameItineraryStep(step, pendingArr[j]));
225
+ if (isDescendant) {
226
+ rebasedArr[i] = rebasePositionStep(alteringArr[i], alteringCmd, pendingArr[i]);
227
+ }
228
+ }
229
+ }
230
+
231
+ // If no itinerary, return as is
232
+ if (rebasedArr.length === 0) return pendingCmd;
233
+
234
+ return {
235
+ ...pendingCmd,
236
+ itinerary: arrayToItinerary(rebasedArr),
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Executes a batch of commands, rebasing each on previous mutations.
242
+ * @param data The data object to mutate
243
+ * @param command The batch update command
244
+ */
245
+ public executeBatch(data: any, command: BatchUpdateStructureNodeCommand): void {
246
+ const {commands} = command
247
+ const applied: UnitaryStructureNodeCommand[] = [];
248
+ for (let i = 0; i < commands.length; i++) {
249
+ let rebased: UnitaryStructureNodeCommand | null = commands[i];
250
+ for (let j = 0; j < applied.length && rebased != null; j++) {
251
+ rebased = this.rebasePendingCommandOn(applied[j], rebased);
252
+ }
253
+ if (rebased != null) {
254
+ switch (rebased.name) {
255
+ case 'add-at':
256
+ this.executeAdd(data, rebased.itinerary, rebased.value, rebased.prototypes);
257
+ break;
258
+ case 'insert-at':
259
+ this.executeInsert(data, rebased.itinerary, rebased.value, rebased.prototypes);
260
+ break;
261
+ case 'set-at':
262
+ this.executeSet(data, rebased.itinerary, rebased.value, rebased.prototypes);
263
+ break;
264
+ case 'delete-at':
265
+ this.executeDelete(data, rebased.itinerary);
266
+ break;
267
+ }
268
+ applied.push(rebased);
269
+ }
270
+ // If rebased is null, skip this command (it targets a deleted element)
271
+ }
272
+ }
273
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Deep equality check for objects and arrays
3
+ */
4
+ export function deepEqual(obj1: any, obj2: any): boolean {
5
+ if (obj1 === obj2) {
6
+ return true;
7
+ }
8
+
9
+ if (obj1 === null || obj2 === null || typeof obj1 !== 'object' || typeof obj2 !== 'object') {
10
+ return false;
11
+ }
12
+
13
+ if (Array.isArray(obj1) !== Array.isArray(obj2)) {
14
+ return false;
15
+ }
16
+
17
+ const keys1 = Object.keys(obj1);
18
+ const keys2 = Object.keys(obj2);
19
+
20
+ if (keys1.length !== keys2.length) {
21
+ return false;
22
+ }
23
+
24
+ for (const key of keys1) {
25
+ if (!keys2.includes(key)) {
26
+ return false;
27
+ }
28
+
29
+ if (!deepEqual(obj1[key], obj2[key])) {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ return true;
35
+ }
package/src/index.ts CHANGED
@@ -6,3 +6,4 @@
6
6
  export * from './core';
7
7
  export * from './middleware';
8
8
  export * from './plugin';
9
+ export * from './fs';