@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.
- package/dist/autosave/autosave-config-resolver.d.ts +45 -0
- package/dist/autosave/autosave-config-resolver.d.ts.map +1 -0
- package/dist/autosave/autosave-config-resolver.js +107 -0
- package/dist/autosave/autosave-middleware.d.ts +37 -0
- package/dist/autosave/autosave-middleware.d.ts.map +1 -0
- package/dist/autosave/autosave-middleware.js +297 -0
- package/dist/autosave/autosave-registry.d.ts +15 -0
- package/dist/autosave/autosave-registry.d.ts.map +1 -0
- package/dist/autosave/autosave-registry.js +33 -0
- package/dist/autosave/autosave-sync-middleware.d.ts +24 -0
- package/dist/autosave/autosave-sync-middleware.d.ts.map +1 -0
- package/dist/autosave/autosave-sync-middleware.js +98 -0
- package/dist/autosave/index.d.ts +8 -0
- package/dist/autosave/index.d.ts.map +1 -0
- package/dist/autosave/index.js +7 -0
- package/dist/core/store-manager.d.ts +4 -1
- package/dist/core/store-manager.d.ts.map +1 -1
- package/dist/core/store-manager.js +28 -15
- package/dist/es2015/autosave/autosave-config-resolver.js +110 -0
- package/dist/es2015/autosave/autosave-middleware.js +311 -0
- package/dist/es2015/autosave/autosave-registry.js +33 -0
- package/dist/es2015/autosave/autosave-sync-middleware.js +98 -0
- package/dist/es2015/autosave/index.js +7 -0
- package/dist/es2015/core/store-manager.js +28 -15
- package/dist/es2015/fs/commands/fs-commands.js +36 -7
- package/dist/es2015/fs/core/fs-adapter.js +16 -5
- package/dist/es2015/index.js +1 -0
- package/dist/es2015/plugin/store-plugin-factory.js +26 -1
- package/dist/fs/commands/fs-commands.d.ts +13 -2
- package/dist/fs/commands/fs-commands.d.ts.map +1 -1
- package/dist/fs/commands/fs-commands.js +39 -7
- package/dist/fs/core/fs-adapter.d.ts +4 -1
- package/dist/fs/core/fs-adapter.d.ts.map +1 -1
- package/dist/fs/core/fs-adapter.js +16 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/plugin/store-plugin-factory.d.ts +1 -0
- package/dist/plugin/store-plugin-factory.d.ts.map +1 -1
- package/dist/plugin/store-plugin-factory.js +26 -1
- 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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Store Manager Implementation
|
|
3
3
|
*/
|
|
4
|
-
import { type Store, type Reducer } from '
|
|
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,
|
|
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 {
|
|
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
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}
|