@hamak/ui-store-impl 0.4.16 → 0.5.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.
Files changed (41) hide show
  1. package/dist/autosave/autosave-config-resolver.d.ts +45 -0
  2. package/dist/autosave/autosave-config-resolver.d.ts.map +1 -0
  3. package/dist/autosave/autosave-config-resolver.js +107 -0
  4. package/dist/autosave/autosave-middleware.d.ts +37 -0
  5. package/dist/autosave/autosave-middleware.d.ts.map +1 -0
  6. package/dist/autosave/autosave-middleware.js +297 -0
  7. package/dist/autosave/autosave-registry.d.ts +15 -0
  8. package/dist/autosave/autosave-registry.d.ts.map +1 -0
  9. package/dist/autosave/autosave-registry.js +33 -0
  10. package/dist/autosave/autosave-sync-middleware.d.ts +24 -0
  11. package/dist/autosave/autosave-sync-middleware.d.ts.map +1 -0
  12. package/dist/autosave/autosave-sync-middleware.js +98 -0
  13. package/dist/autosave/index.d.ts +8 -0
  14. package/dist/autosave/index.d.ts.map +1 -0
  15. package/dist/autosave/index.js +7 -0
  16. package/dist/core/store-manager.d.ts +4 -1
  17. package/dist/core/store-manager.d.ts.map +1 -1
  18. package/dist/core/store-manager.js +28 -15
  19. package/dist/es2015/autosave/autosave-config-resolver.js +110 -0
  20. package/dist/es2015/autosave/autosave-middleware.js +311 -0
  21. package/dist/es2015/autosave/autosave-registry.js +33 -0
  22. package/dist/es2015/autosave/autosave-sync-middleware.js +98 -0
  23. package/dist/es2015/autosave/index.js +7 -0
  24. package/dist/es2015/core/store-manager.js +28 -15
  25. package/dist/es2015/fs/commands/fs-commands.js +36 -7
  26. package/dist/es2015/fs/core/fs-adapter.js +16 -5
  27. package/dist/es2015/index.js +1 -0
  28. package/dist/es2015/plugin/store-plugin-factory.js +26 -1
  29. package/dist/fs/commands/fs-commands.d.ts +13 -2
  30. package/dist/fs/commands/fs-commands.d.ts.map +1 -1
  31. package/dist/fs/commands/fs-commands.js +39 -7
  32. package/dist/fs/core/fs-adapter.d.ts +4 -1
  33. package/dist/fs/core/fs-adapter.d.ts.map +1 -1
  34. package/dist/fs/core/fs-adapter.js +16 -5
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/plugin/store-plugin-factory.d.ts +1 -0
  39. package/dist/plugin/store-plugin-factory.d.ts.map +1 -1
  40. package/dist/plugin/store-plugin-factory.js +26 -1
  41. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/autosave/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Autosave Implementation
3
+ */
4
+ export * from './autosave-registry';
5
+ export * from './autosave-config-resolver';
6
+ export * from './autosave-middleware';
7
+ export * from './autosave-sync-middleware';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Store Manager Implementation
3
3
  */
4
- import { type Store, type Reducer } from 'redux';
4
+ import { type Store, type Reducer } from '@reduxjs/toolkit';
5
5
  import type { IStoreManager, StoreConfig, RootState, AppAction } from '@hamak/ui-store-api';
6
6
  import { MiddlewareRegistry } from './middleware-registry';
7
7
  import { ReducerRegistry } from './reducer-registry';
@@ -11,9 +11,12 @@ export declare class StoreManager implements IStoreManager {
11
11
  private reducerRegistry;
12
12
  private initialized;
13
13
  private config;
14
+ private fileSystemAdapter;
14
15
  constructor();
15
16
  getMiddlewareRegistry(): MiddlewareRegistry;
16
17
  getReducerRegistry(): ReducerRegistry;
18
+ setFileSystemAdapter(adapter: any): void;
19
+ getFileSystemAdapter(): any;
17
20
  isInitialized(): boolean;
18
21
  initialize(config?: StoreConfig): Store<RootState, AppAction>;
19
22
  getStore(): Store<RootState, AppAction>;
@@ -1 +1 @@
1
- {"version":3,"file":"store-manager.d.ts","sourceRoot":"","sources":["../../src/core/store-manager.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAyC,KAAK,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,OAAO,CAAC;AACxF,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAA4B;;IAY1C,qBAAqB;IAIrB,kBAAkB;IAIlB,aAAa,IAAI,OAAO;IAIxB,UAAU,CAAC,MAAM,GAAE,WAAgB,GAAG,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC;IA0DjE,QAAQ,IAAI,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC;IAOvC,QAAQ,CAAC,CAAC,SAAS,SAAS,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC;IAI3C,QAAQ,CAAC,CAAC,GAAG,SAAS,KAAK,CAAC;IAI5B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAI3C,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,IAAI;IAIhE,OAAO,IAAI,IAAI;CAKhB"}
1
+ {"version":3,"file":"store-manager.d.ts","sourceRoot":"","sources":["../../src/core/store-manager.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAkB,KAAK,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,iBAAiB,CAAoB;;IAY7C,qBAAqB;IAIrB,kBAAkB;IAIlB,oBAAoB,CAAC,OAAO,EAAE,GAAG;IAIjC,oBAAoB;IAIpB,aAAa,IAAI,OAAO;IAIxB,UAAU,CAAC,MAAM,GAAE,WAAgB,GAAG,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC;IAwDjE,QAAQ,IAAI,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC;IAOvC,QAAQ,CAAC,CAAC,SAAS,SAAS,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC;IAI3C,QAAQ,CAAC,CAAC,GAAG,SAAS,KAAK,CAAC;IAI5B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAI3C,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,IAAI;IAIhE,OAAO,IAAI,IAAI;CAKhB"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Store Manager Implementation
3
3
  */
4
- import { createStore, applyMiddleware, compose } from 'redux';
4
+ import { configureStore } from '@reduxjs/toolkit';
5
5
  import { MiddlewareRegistry } from './middleware-registry';
6
6
  import { ReducerRegistry } from './reducer-registry';
7
7
  export class StoreManager {
@@ -9,6 +9,7 @@ export class StoreManager {
9
9
  this.store = null;
10
10
  this.initialized = false;
11
11
  this.config = null;
12
+ this.fileSystemAdapter = null;
12
13
  this.middlewareRegistry = new MiddlewareRegistry();
13
14
  this.reducerRegistry = new ReducerRegistry((rootReducer) => {
14
15
  // Hot replacement callback
@@ -23,6 +24,12 @@ export class StoreManager {
23
24
  getReducerRegistry() {
24
25
  return this.reducerRegistry;
25
26
  }
27
+ setFileSystemAdapter(adapter) {
28
+ this.fileSystemAdapter = adapter;
29
+ }
30
+ getFileSystemAdapter() {
31
+ return this.fileSystemAdapter;
32
+ }
26
33
  isInitialized() {
27
34
  return this.initialized;
28
35
  }
@@ -39,20 +46,26 @@ export class StoreManager {
39
46
  const rootReducer = this.reducerRegistry.getCombinedReducer();
40
47
  // Create enhancers
41
48
  const enhancers = config.enhancers || [];
42
- // Setup Redux DevTools
43
- let composeEnhancers = compose;
44
- if (config.devTools !== false && typeof window !== 'undefined') {
45
- const devToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
46
- if (devToolsExtension) {
47
- composeEnhancers = devToolsExtension({
48
- trace: true,
49
- traceLimit: 25,
50
- });
51
- }
52
- }
53
- // Create store
54
- const enhancer = composeEnhancers(applyMiddleware(...middleware), ...enhancers);
55
- this.store = createStore(rootReducer, config.preloadedState, enhancer);
49
+ // Create store using configureStore (modern Redux Toolkit API)
50
+ this.store = configureStore({
51
+ reducer: rootReducer,
52
+ preloadedState: config.preloadedState,
53
+ // Use only our custom middleware (disabling Redux Toolkit defaults to maintain exact behavior)
54
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware({
55
+ // Disable all default middleware to maintain exact parity with old createStore behavior
56
+ thunk: false,
57
+ serializableCheck: false,
58
+ immutableCheck: false,
59
+ actionCreatorCheck: false,
60
+ }).concat(...middleware),
61
+ // Add custom enhancers if provided
62
+ enhancers: (getDefaultEnhancers) => getDefaultEnhancers().concat(enhancers),
63
+ // Configure DevTools with trace support
64
+ devTools: config.devTools !== false ? {
65
+ trace: true,
66
+ traceLimit: 25,
67
+ } : false,
68
+ });
56
69
  this.initialized = true;
57
70
  console.log('[StoreManager] Store initialized with:', {
58
71
  reducers: this.reducerRegistry.getAllRegistrations().map(r => r.key),
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Autosave Configuration Resolver
3
+ *
4
+ * Resolves autosave configuration with hierarchical inheritance.
5
+ * Configuration priority (highest to lowest):
6
+ * 1. File node extension state config
7
+ * 2. Parent folder config (recursive, respects inherit flag)
8
+ * 3. Default config
9
+ */
10
+ import { AUTOSAVE_EXTENSION_KEY, DEFAULT_AUTOSAVE_CONFIG, } from '@hamak/ui-store-api';
11
+ /**
12
+ * Get autosave extension state from a node
13
+ */
14
+ export function getAutosaveExtensionState(node) {
15
+ var _a;
16
+ if (!((_a = node === null || node === void 0 ? void 0 : node.state) === null || _a === void 0 ? void 0 : _a.extensionStates)) {
17
+ return undefined;
18
+ }
19
+ return node.state.extensionStates[AUTOSAVE_EXTENSION_KEY];
20
+ }
21
+ /**
22
+ * Get autosave config from a node's extension state
23
+ */
24
+ export function getAutosaveConfig(node) {
25
+ var _a;
26
+ return (_a = getAutosaveExtensionState(node)) === null || _a === void 0 ? void 0 : _a.config;
27
+ }
28
+ /**
29
+ * Resolve autosave configuration for a path with inheritance
30
+ *
31
+ * @param root - Root directory node
32
+ * @param path - Path segments to the target node
33
+ * @param defaultConfig - Default configuration to use if none found
34
+ * @returns Resolved configuration
35
+ */
36
+ export function resolveAutosaveConfig(root, path, defaultConfig = DEFAULT_AUTOSAVE_CONFIG) {
37
+ const configs = [];
38
+ // Walk down the path, collecting configs
39
+ let currentNode = root;
40
+ for (let i = 0; i <= path.length; i++) {
41
+ if (!currentNode)
42
+ break;
43
+ const config = getAutosaveConfig(currentNode);
44
+ if (config) {
45
+ configs.push(config);
46
+ // If inherit is false, stop here
47
+ if (config.inherit === false) {
48
+ break;
49
+ }
50
+ }
51
+ // Move to next segment
52
+ if (i < path.length && currentNode.type === 'directory') {
53
+ currentNode = currentNode.children[path[i]];
54
+ }
55
+ }
56
+ // Merge configs from root to target (later configs override earlier)
57
+ // Start with default, then apply each config in order
58
+ let result = Object.assign({}, defaultConfig);
59
+ for (const config of configs) {
60
+ result = mergeConfigs(result, config);
61
+ }
62
+ return result;
63
+ }
64
+ /**
65
+ * Merge two autosave configs (source overrides target for defined values)
66
+ */
67
+ function mergeConfigs(target, source) {
68
+ var _a, _b, _c, _d, _e, _f;
69
+ return {
70
+ enabled: source.enabled,
71
+ debounceMs: (_a = source.debounceMs) !== null && _a !== void 0 ? _a : target.debounceMs,
72
+ maxWaitMs: (_b = source.maxWaitMs) !== null && _b !== void 0 ? _b : target.maxWaitMs,
73
+ saveOnBlur: (_c = source.saveOnBlur) !== null && _c !== void 0 ? _c : target.saveOnBlur,
74
+ retryOnFailure: (_d = source.retryOnFailure) !== null && _d !== void 0 ? _d : target.retryOnFailure,
75
+ maxRetries: (_e = source.maxRetries) !== null && _e !== void 0 ? _e : target.maxRetries,
76
+ inherit: (_f = source.inherit) !== null && _f !== void 0 ? _f : target.inherit,
77
+ };
78
+ }
79
+ /**
80
+ * Check if autosave is effectively enabled for a path
81
+ */
82
+ export function isAutosaveEnabled(root, path, defaultConfig) {
83
+ const config = resolveAutosaveConfig(root, path, defaultConfig);
84
+ return config.enabled;
85
+ }
86
+ /**
87
+ * Get node at path
88
+ */
89
+ export function getNodeAtPath(root, path) {
90
+ let current = root;
91
+ for (const segment of path) {
92
+ if (!current || current.type !== 'directory') {
93
+ return undefined;
94
+ }
95
+ current = current.children[segment];
96
+ }
97
+ return current;
98
+ }
99
+ /**
100
+ * Convert path array to string key
101
+ */
102
+ export function pathToKey(path) {
103
+ return path.join('/');
104
+ }
105
+ /**
106
+ * Convert string key back to path array
107
+ */
108
+ export function keyToPath(key) {
109
+ return key === '' ? [] : key.split('/');
110
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Autosave Middleware
3
+ *
4
+ * Core middleware that orchestrates autosave operations:
5
+ * 1. Detects file content changes
6
+ * 2. Manages debounce timers per file
7
+ * 3. Delegates save operations to registered providers
8
+ * 4. Handles retries on failure
9
+ */
10
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
11
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
12
+ return new (P || (P = Promise))(function (resolve, reject) {
13
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
14
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
15
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
16
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
17
+ });
18
+ };
19
+ import { AutosaveActionTypes, autosaveActions, DEFAULT_AUTOSAVE_CONFIG, } from '@hamak/ui-store-api';
20
+ import { resolveAutosaveConfig, getNodeAtPath, pathToKey, } from './autosave-config-resolver';
21
+ /**
22
+ * Content change action types to monitor
23
+ */
24
+ const CONTENT_CHANGE_ACTION_TYPES = [
25
+ 'set-file-content',
26
+ 'update-file-content',
27
+ ];
28
+ /**
29
+ * Check if an action is a content change action
30
+ */
31
+ function isContentChangeAction(action) {
32
+ // Check for filesystem command actions
33
+ if (action.type === '@@fs/COMMAND' && action.command) {
34
+ return CONTENT_CHANGE_ACTION_TYPES.includes(action.command.name);
35
+ }
36
+ // Check for direct content change actions from fs-adapter
37
+ if (typeof action.type === 'string') {
38
+ const type = action.type.toLowerCase();
39
+ return (type.includes('setfilecontent') ||
40
+ type.includes('updatefilecontent') ||
41
+ type.includes('set_file_content') ||
42
+ type.includes('update_file_content'));
43
+ }
44
+ return false;
45
+ }
46
+ /**
47
+ * Check if content change is from remote (should not trigger autosave)
48
+ */
49
+ function isFromRemote(action) {
50
+ var _a, _b;
51
+ if (((_a = action.command) === null || _a === void 0 ? void 0 : _a.fromRemote) === true) {
52
+ return true;
53
+ }
54
+ if (((_b = action.payload) === null || _b === void 0 ? void 0 : _b.fromRemote) === true) {
55
+ return true;
56
+ }
57
+ if (action.fromRemote === true) {
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+ /**
63
+ * Get path from action
64
+ */
65
+ function getPathFromAction(action) {
66
+ var _a, _b;
67
+ // Try command path
68
+ if ((_a = action.command) === null || _a === void 0 ? void 0 : _a.path) {
69
+ return Array.isArray(action.command.path)
70
+ ? action.command.path
71
+ : [action.command.path];
72
+ }
73
+ // Try payload path
74
+ if ((_b = action.payload) === null || _b === void 0 ? void 0 : _b.path) {
75
+ return Array.isArray(action.payload.path)
76
+ ? action.payload.path
77
+ : [action.payload.path];
78
+ }
79
+ // Try direct path
80
+ if (action.path) {
81
+ return Array.isArray(action.path) ? action.path : [action.path];
82
+ }
83
+ return undefined;
84
+ }
85
+ /**
86
+ * Create autosave middleware
87
+ */
88
+ export function createAutosaveMiddleware(config) {
89
+ const { registry, fsSliceName = 'fileSystem', defaultConfig = {}, onSaveStart, onSaveSuccess, onSaveError, } = config;
90
+ const mergedDefaultConfig = Object.assign(Object.assign({}, DEFAULT_AUTOSAVE_CONFIG), defaultConfig);
91
+ // Track pending saves by path key
92
+ const pendingSaves = new Map();
93
+ /**
94
+ * Get filesystem root from state
95
+ */
96
+ function getRoot(state) {
97
+ var _a;
98
+ return (_a = state[fsSliceName]) === null || _a === void 0 ? void 0 : _a.root;
99
+ }
100
+ /**
101
+ * Clear timers for a path
102
+ */
103
+ function clearTimers(pathKey) {
104
+ const pending = pendingSaves.get(pathKey);
105
+ if (pending) {
106
+ clearTimeout(pending.debounceTimer);
107
+ if (pending.maxWaitTimer) {
108
+ clearTimeout(pending.maxWaitTimer);
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * Cancel pending save
114
+ */
115
+ function cancelPending(pathKey) {
116
+ clearTimers(pathKey);
117
+ pendingSaves.delete(pathKey);
118
+ }
119
+ /**
120
+ * Schedule autosave for a path
121
+ */
122
+ function scheduleAutosave(store, path, providerId, config) {
123
+ var _a, _b, _c;
124
+ const pathKey = pathToKey(path);
125
+ const now = Date.now();
126
+ const debounceMs = (_a = config.debounceMs) !== null && _a !== void 0 ? _a : DEFAULT_AUTOSAVE_CONFIG.debounceMs;
127
+ const maxWaitMs = (_b = config.maxWaitMs) !== null && _b !== void 0 ? _b : DEFAULT_AUTOSAVE_CONFIG.maxWaitMs;
128
+ // Clear existing debounce timer (but keep max wait if exists)
129
+ const existing = pendingSaves.get(pathKey);
130
+ if (existing) {
131
+ clearTimeout(existing.debounceTimer);
132
+ }
133
+ // Create debounce timer
134
+ const debounceTimer = setTimeout(() => {
135
+ store.dispatch(autosaveActions.debounceExpired(path));
136
+ }, debounceMs);
137
+ // Create max wait timer if not exists
138
+ let maxWaitTimer = existing === null || existing === void 0 ? void 0 : existing.maxWaitTimer;
139
+ if (!maxWaitTimer) {
140
+ maxWaitTimer = setTimeout(() => {
141
+ store.dispatch(autosaveActions.debounceExpired(path));
142
+ }, maxWaitMs);
143
+ }
144
+ // Store pending save
145
+ pendingSaves.set(pathKey, {
146
+ path,
147
+ providerId,
148
+ debounceTimer,
149
+ maxWaitTimer,
150
+ retryCount: (_c = existing === null || existing === void 0 ? void 0 : existing.retryCount) !== null && _c !== void 0 ? _c : 0,
151
+ config,
152
+ });
153
+ // Dispatch change detected
154
+ store.dispatch(autosaveActions.changeDetected(path, now, now + debounceMs, providerId));
155
+ }
156
+ /**
157
+ * Execute save operation
158
+ */
159
+ function executeSave(store, path) {
160
+ return __awaiter(this, void 0, void 0, function* () {
161
+ var _a, _b, _c;
162
+ const pathKey = pathToKey(path);
163
+ const pending = pendingSaves.get(pathKey);
164
+ if (!pending) {
165
+ return;
166
+ }
167
+ // Clear timers
168
+ clearTimers(pathKey);
169
+ // Get current state
170
+ const state = store.getState();
171
+ const root = getRoot(state);
172
+ if (!root) {
173
+ pendingSaves.delete(pathKey);
174
+ return;
175
+ }
176
+ // Get node
177
+ const node = getNodeAtPath(root, path);
178
+ if (!node || node.type !== 'file') {
179
+ pendingSaves.delete(pathKey);
180
+ return;
181
+ }
182
+ // Get provider
183
+ const provider = registry.get(pending.providerId);
184
+ if (!provider) {
185
+ pendingSaves.delete(pathKey);
186
+ return;
187
+ }
188
+ // Dispatch save started
189
+ store.dispatch(autosaveActions.saveStarted(path, pending.providerId));
190
+ onSaveStart === null || onSaveStart === void 0 ? void 0 : onSaveStart(path);
191
+ try {
192
+ // Execute save via provider
193
+ const result = yield provider.save(path, node.content, node, store.dispatch);
194
+ if (result.success) {
195
+ // Success
196
+ pendingSaves.delete(pathKey);
197
+ store.dispatch(autosaveActions.saveSucceeded(path, result.timestamp));
198
+ onSaveSuccess === null || onSaveSuccess === void 0 ? void 0 : onSaveSuccess(path, result);
199
+ }
200
+ else {
201
+ // Provider reported failure
202
+ handleSaveFailure(store, path, (_a = result.error) !== null && _a !== void 0 ? _a : { code: 'UNKNOWN', message: 'Save failed' });
203
+ }
204
+ }
205
+ catch (error) {
206
+ // Exception during save
207
+ handleSaveFailure(store, path, {
208
+ code: (_b = error.code) !== null && _b !== void 0 ? _b : 'ERROR',
209
+ message: (_c = error.message) !== null && _c !== void 0 ? _c : 'Unknown error',
210
+ });
211
+ }
212
+ });
213
+ }
214
+ /**
215
+ * Handle save failure with retry logic
216
+ */
217
+ function handleSaveFailure(store, path, error) {
218
+ var _a;
219
+ const pathKey = pathToKey(path);
220
+ const pending = pendingSaves.get(pathKey);
221
+ if (!pending) {
222
+ return;
223
+ }
224
+ const retryCount = pending.retryCount + 1;
225
+ const maxRetries = (_a = pending.config.maxRetries) !== null && _a !== void 0 ? _a : DEFAULT_AUTOSAVE_CONFIG.maxRetries;
226
+ const shouldRetry = pending.config.retryOnFailure !== false && retryCount < maxRetries;
227
+ // Dispatch failure
228
+ store.dispatch(autosaveActions.saveFailed(path, error, retryCount));
229
+ onSaveError === null || onSaveError === void 0 ? void 0 : onSaveError(path, error);
230
+ if (shouldRetry) {
231
+ // Schedule retry with exponential backoff
232
+ const retryDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 10000);
233
+ const retryAt = Date.now() + retryDelay;
234
+ store.dispatch(autosaveActions.retryScheduled(path, retryAt, retryCount));
235
+ // Update pending save with retry timer
236
+ pending.retryCount = retryCount;
237
+ pending.debounceTimer = setTimeout(() => {
238
+ executeSave(store, path);
239
+ }, retryDelay);
240
+ pendingSaves.set(pathKey, pending);
241
+ }
242
+ else {
243
+ // No more retries
244
+ pendingSaves.delete(pathKey);
245
+ }
246
+ }
247
+ /**
248
+ * The middleware
249
+ */
250
+ const autosaveMiddleware = (store) => (next) => (action) => {
251
+ const result = next(action);
252
+ const typedAction = action;
253
+ // ─────────────────────────────────────────────────────────────────
254
+ // Handle content changes
255
+ // ─────────────────────────────────────────────────────────────────
256
+ if (isContentChangeAction(typedAction) && !isFromRemote(typedAction)) {
257
+ const path = getPathFromAction(typedAction);
258
+ if (path) {
259
+ const state = store.getState();
260
+ const root = getRoot(state);
261
+ if (root) {
262
+ // Resolve config
263
+ const autosaveConfig = resolveAutosaveConfig(root, path, mergedDefaultConfig);
264
+ if (autosaveConfig.enabled) {
265
+ // Get node
266
+ const node = getNodeAtPath(root, path);
267
+ if (node) {
268
+ // Find provider
269
+ const provider = registry.findProvider(path, node);
270
+ if (provider) {
271
+ scheduleAutosave(store, path, provider.id, autosaveConfig);
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
277
+ }
278
+ // ─────────────────────────────────────────────────────────────────
279
+ // Handle autosave actions
280
+ // ─────────────────────────────────────────────────────────────────
281
+ switch (typedAction.type) {
282
+ case AutosaveActionTypes.DEBOUNCE_EXPIRED: {
283
+ const path = typedAction.payload.path;
284
+ void executeSave(store, path);
285
+ break;
286
+ }
287
+ case AutosaveActionTypes.FLUSH_PENDING: {
288
+ const path = typedAction.payload.path;
289
+ const pathKey = pathToKey(path);
290
+ if (pendingSaves.has(pathKey)) {
291
+ void executeSave(store, path);
292
+ }
293
+ break;
294
+ }
295
+ case AutosaveActionTypes.FLUSH_ALL_PENDING: {
296
+ const paths = Array.from(pendingSaves.values()).map((p) => p.path);
297
+ for (const path of paths) {
298
+ void executeSave(store, path);
299
+ }
300
+ break;
301
+ }
302
+ case AutosaveActionTypes.CANCEL_PENDING: {
303
+ const path = typedAction.payload.path;
304
+ cancelPending(pathToKey(path));
305
+ break;
306
+ }
307
+ }
308
+ return result;
309
+ };
310
+ return autosaveMiddleware;
311
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Autosave Provider Registry Implementation
3
+ */
4
+ export class AutosaveProviderRegistry {
5
+ constructor() {
6
+ this.providers = new Map();
7
+ }
8
+ register(provider) {
9
+ if (this.providers.has(provider.id)) {
10
+ console.warn(`[AutosaveRegistry] Provider "${provider.id}" already registered, overwriting.`);
11
+ }
12
+ this.providers.set(provider.id, provider);
13
+ }
14
+ unregister(providerId) {
15
+ this.providers.delete(providerId);
16
+ }
17
+ get(providerId) {
18
+ return this.providers.get(providerId);
19
+ }
20
+ findProvider(path, node) {
21
+ // Get all providers that support this path, sorted by priority (descending)
22
+ const supportingProviders = Array.from(this.providers.values())
23
+ .filter((provider) => provider.supports(path, node))
24
+ .sort((a, b) => { var _a, _b; return ((_a = b.priority) !== null && _a !== void 0 ? _a : 0) - ((_b = a.priority) !== null && _b !== void 0 ? _b : 0); });
25
+ return supportingProviders[0];
26
+ }
27
+ getAll() {
28
+ return Array.from(this.providers.values());
29
+ }
30
+ has(providerId) {
31
+ return this.providers.has(providerId);
32
+ }
33
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Autosave Sync Middleware
3
+ *
4
+ * Updates FileSystemNode extension states based on autosave lifecycle actions.
5
+ * This middleware runs after the main autosave middleware and updates the
6
+ * state so components can display autosave status.
7
+ */
8
+ import { AutosaveActionTypes, autosaveActions, } from '@hamak/ui-store-api';
9
+ /**
10
+ * Create autosave sync middleware
11
+ */
12
+ export function createAutosaveSyncMiddleware(config) {
13
+ const { updateExtensionState } = config;
14
+ const autosaveSyncMiddleware = (store) => (next) => (action) => {
15
+ const result = next(action);
16
+ const typedAction = action;
17
+ // Only process autosave actions
18
+ if (!autosaveActions.isAutosaveAction(typedAction)) {
19
+ return result;
20
+ }
21
+ const dispatch = store.dispatch;
22
+ switch (typedAction.type) {
23
+ case AutosaveActionTypes.SET_CONFIG: {
24
+ const { path, config: autosaveConfig } = typedAction.payload;
25
+ dispatch(updateExtensionState(path, {
26
+ config: autosaveConfig,
27
+ effectivelyEnabled: autosaveConfig.enabled,
28
+ status: autosaveConfig.enabled ? 'idle' : 'disabled',
29
+ }));
30
+ break;
31
+ }
32
+ case AutosaveActionTypes.CLEAR_CONFIG: {
33
+ const { path } = typedAction.payload;
34
+ dispatch(updateExtensionState(path, {
35
+ config: undefined,
36
+ // Status will be recomputed based on parent config
37
+ }));
38
+ break;
39
+ }
40
+ case AutosaveActionTypes.CHANGE_DETECTED: {
41
+ const { path, detectedAt, providerId } = typedAction.payload;
42
+ dispatch(updateExtensionState(path, {
43
+ status: 'pending',
44
+ pendingSince: detectedAt,
45
+ providerId,
46
+ }));
47
+ break;
48
+ }
49
+ case AutosaveActionTypes.SAVE_STARTED: {
50
+ const { path, providerId } = typedAction.payload;
51
+ dispatch(updateExtensionState(path, {
52
+ status: 'saving',
53
+ lastSaveAttempt: Date.now(),
54
+ providerId,
55
+ }));
56
+ break;
57
+ }
58
+ case AutosaveActionTypes.SAVE_SUCCEEDED: {
59
+ const { path, timestamp } = typedAction.payload;
60
+ dispatch(updateExtensionState(path, {
61
+ status: 'idle',
62
+ lastSaveSuccess: timestamp,
63
+ lastSaveError: undefined,
64
+ retryCount: 0,
65
+ pendingSince: undefined,
66
+ }));
67
+ break;
68
+ }
69
+ case AutosaveActionTypes.SAVE_FAILED: {
70
+ const { path, error } = typedAction.payload;
71
+ dispatch(updateExtensionState(path, {
72
+ status: 'error',
73
+ lastSaveError: error,
74
+ retryCount: error.attempt,
75
+ }));
76
+ break;
77
+ }
78
+ case AutosaveActionTypes.RETRY_SCHEDULED: {
79
+ const { path, attempt } = typedAction.payload;
80
+ dispatch(updateExtensionState(path, {
81
+ status: 'pending',
82
+ retryCount: attempt,
83
+ }));
84
+ break;
85
+ }
86
+ case AutosaveActionTypes.CANCEL_PENDING: {
87
+ const { path } = typedAction.payload;
88
+ dispatch(updateExtensionState(path, {
89
+ status: 'idle',
90
+ pendingSince: undefined,
91
+ }));
92
+ break;
93
+ }
94
+ }
95
+ return result;
96
+ };
97
+ return autosaveSyncMiddleware;
98
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Autosave Implementation
3
+ */
4
+ export * from './autosave-registry';
5
+ export * from './autosave-config-resolver';
6
+ export * from './autosave-middleware';
7
+ export * from './autosave-sync-middleware';