@hamak/ui-remote-resource 0.8.1 → 0.9.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/api/actions/resource-action-factory.d.ts +11 -1
- package/dist/api/actions/resource-action-factory.d.ts.map +1 -1
- package/dist/api/actions/resource-action-factory.js +18 -0
- package/dist/api/actions/resource-action-types.d.ts +27 -2
- package/dist/api/actions/resource-action-types.d.ts.map +1 -1
- package/dist/api/actions/resource-action-types.js +2 -0
- package/dist/api/types/resource-attributes.d.ts +7 -0
- package/dist/api/types/resource-attributes.d.ts.map +1 -1
- package/dist/impl/index.d.ts +5 -0
- package/dist/impl/index.d.ts.map +1 -1
- package/dist/impl/index.js +3 -0
- package/dist/impl/middleware/refresh-middleware.d.ts +21 -0
- package/dist/impl/middleware/refresh-middleware.d.ts.map +1 -0
- package/dist/impl/middleware/refresh-middleware.js +34 -0
- package/dist/impl/middleware/request-sequencer.d.ts +19 -0
- package/dist/impl/middleware/request-sequencer.d.ts.map +1 -0
- package/dist/impl/middleware/request-sequencer.js +33 -0
- package/dist/impl/middleware/resource-middleware.d.ts +6 -0
- package/dist/impl/middleware/resource-middleware.d.ts.map +1 -1
- package/dist/impl/middleware/resource-middleware.js +26 -11
- package/dist/impl/middleware/sync-middleware.d.ts.map +1 -1
- package/dist/impl/middleware/sync-middleware.js +17 -4
- package/dist/impl/plugin/resource-plugin-factory.d.ts.map +1 -1
- package/dist/impl/plugin/resource-plugin-factory.js +32 -1
- package/dist/impl/providers/plain-http-resource-provider.d.ts +51 -0
- package/dist/impl/providers/plain-http-resource-provider.d.ts.map +1 -0
- package/dist/impl/providers/plain-http-resource-provider.js +112 -0
- package/dist/spi/providers/i-resource-provider.d.ts +11 -0
- package/dist/spi/providers/i-resource-provider.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResourceCallRequestAction, ResourceCallSuccessAction, ResourceCallFailureAction, InvalidateResourceAction } from './resource-action-types.js';
|
|
1
|
+
import type { ResourceCallRequestAction, ResourceCallSuccessAction, ResourceCallFailureAction, InvalidateResourceAction, RefreshResourceAction } from './resource-action-types.js';
|
|
2
2
|
import type { ResourceOperation, ErrorObject } from '../types/resource-types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Factory class for creating resource actions
|
|
@@ -36,5 +36,15 @@ export declare class ResourceActionFactory {
|
|
|
36
36
|
* Create invalidate action
|
|
37
37
|
*/
|
|
38
38
|
invalidate(path: string | string[]): InvalidateResourceAction;
|
|
39
|
+
/**
|
|
40
|
+
* Create a refresh action for a query-bound node.
|
|
41
|
+
*
|
|
42
|
+
* @param path - Stable path of the bound node to refresh
|
|
43
|
+
* @param paramsPatch - New/changed criteria (merged or replacing per `merge`)
|
|
44
|
+
* @param opts.merge - Merge the patch over stored params (default true)
|
|
45
|
+
*/
|
|
46
|
+
refresh(path: string | string[], paramsPatch?: Record<string, any>, opts?: {
|
|
47
|
+
merge?: boolean;
|
|
48
|
+
}): RefreshResourceAction;
|
|
39
49
|
}
|
|
40
50
|
//# sourceMappingURL=resource-action-factory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-action-factory.d.ts","sourceRoot":"","sources":["../../../src/api/actions/resource-action-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,
|
|
1
|
+
{"version":3,"file":"resource-action-factory.d.ts","sourceRoot":"","sources":["../../../src/api/actions/resource-action-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,qBAAqB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAU9E;;GAEG;AACH,qBAAa,qBAAqB;IAChC;;OAEG;IACH,WAAW,CACT,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,iBAAiB,EAC5B,MAAM,CAAC,EAAE,GAAG,EACZ,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,EAAE,CAAC,EAAE,MAAM,GACV,yBAAyB;IAa5B;;OAEG;IACH,KAAK,CACH,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,GAAG,EACZ,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,EAAE,CAAC,EAAE,MAAM,GACV,yBAAyB;IAI5B;;OAEG;IACH,MAAM,CACJ,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,GAAG,EACZ,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,EAAE,CAAC,EAAE,MAAM,GACV,yBAAyB;IAI5B;;OAEG;IACH,MAAM,CACJ,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,GAAG,EACZ,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,EAAE,CAAC,EAAE,MAAM,GACV,yBAAyB;IAI5B;;OAEG;IACH,MAAM,CACJ,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,GAAG,EACZ,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,EAAE,CAAC,EAAE,MAAM,GACV,yBAAyB;IAI5B;;OAEG;IACH,WAAW,CACT,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EACvB,IAAI,EAAE,GAAG,EACT,QAAQ,EAAE,GAAG,GAAG,SAAS,EACzB,OAAO,EAAE,yBAAyB,GACjC,yBAAyB;IAc5B;;OAEG;IACH,WAAW,CACT,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EACvB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,yBAAyB,GACjC,yBAAyB;IAa5B;;OAEG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,wBAAwB;IAQ7D;;;;;;OAMG;IACH,OAAO,CACL,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EACvB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GACzB,qBAAqB;CAWzB"}
|
|
@@ -90,4 +90,22 @@ export class ResourceActionFactory {
|
|
|
90
90
|
payload: { path }
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a refresh action for a query-bound node.
|
|
95
|
+
*
|
|
96
|
+
* @param path - Stable path of the bound node to refresh
|
|
97
|
+
* @param paramsPatch - New/changed criteria (merged or replacing per `merge`)
|
|
98
|
+
* @param opts.merge - Merge the patch over stored params (default true)
|
|
99
|
+
*/
|
|
100
|
+
refresh(path, paramsPatch, opts) {
|
|
101
|
+
return {
|
|
102
|
+
type: Types.REFRESH_RESOURCE,
|
|
103
|
+
id: generateId(),
|
|
104
|
+
payload: {
|
|
105
|
+
path,
|
|
106
|
+
paramsPatch,
|
|
107
|
+
merge: opts?.merge ?? true
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
93
111
|
}
|
|
@@ -6,7 +6,8 @@ export declare enum ResourceActionTypes {
|
|
|
6
6
|
RESOURCE_CALL_REQUEST = "@@resource/CALL_REQUEST",
|
|
7
7
|
RESOURCE_CALL_SUCCESS = "@@resource/CALL_SUCCESS",
|
|
8
8
|
RESOURCE_CALL_FAILURE = "@@resource/CALL_FAILURE",
|
|
9
|
-
INVALIDATE_RESOURCE = "@@resource/INVALIDATE"
|
|
9
|
+
INVALIDATE_RESOURCE = "@@resource/INVALIDATE",
|
|
10
|
+
REFRESH_RESOURCE = "@@resource/REFRESH"
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* Generic resource call request action
|
|
@@ -101,5 +102,29 @@ export interface InvalidateResourceAction {
|
|
|
101
102
|
path: string | string[];
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Refresh a query-bound resource node.
|
|
107
|
+
*
|
|
108
|
+
* Re-runs the query that produced the node at `path`, recovering its
|
|
109
|
+
* `endpointId` + previous params from the node's stored binding and re-issuing
|
|
110
|
+
* a fetch to the same path. Use to refresh a filtered listing when the user
|
|
111
|
+
* changes a criterion.
|
|
112
|
+
*/
|
|
113
|
+
export interface RefreshResourceAction {
|
|
114
|
+
type: ResourceActionTypes.REFRESH_RESOURCE;
|
|
115
|
+
id: string;
|
|
116
|
+
payload: {
|
|
117
|
+
/** Stable path of the bound node to refresh */
|
|
118
|
+
path: string | string[];
|
|
119
|
+
/** New/changed criteria to apply (see `merge`) */
|
|
120
|
+
paramsPatch?: Record<string, any>;
|
|
121
|
+
/**
|
|
122
|
+
* When true, shallow-merge `paramsPatch` over the node's stored params.
|
|
123
|
+
* When false, `paramsPatch` replaces them (falls back to stored params if
|
|
124
|
+
* no patch is given). Defaults to true.
|
|
125
|
+
*/
|
|
126
|
+
merge?: boolean;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export type ResourceAction = ResourceCallRequestAction | ResourceCallSuccessAction | ResourceCallFailureAction | InvalidateResourceAction | RefreshResourceAction;
|
|
105
130
|
//# sourceMappingURL=resource-action-types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-action-types.d.ts","sourceRoot":"","sources":["../../../src/api/actions/resource-action-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;GAEG;AACH,oBAAY,mBAAmB;IAE7B,qBAAqB,4BAA4B;IAGjD,qBAAqB,4BAA4B;IACjD,qBAAqB,4BAA4B;IAGjD,mBAAmB,0BAA0B;
|
|
1
|
+
{"version":3,"file":"resource-action-types.d.ts","sourceRoot":"","sources":["../../../src/api/actions/resource-action-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,4 +10,6 @@ export var ResourceActionTypes;
|
|
|
10
10
|
ResourceActionTypes["RESOURCE_CALL_FAILURE"] = "@@resource/CALL_FAILURE";
|
|
11
11
|
// Invalidate cache
|
|
12
12
|
ResourceActionTypes["INVALIDATE_RESOURCE"] = "@@resource/INVALIDATE";
|
|
13
|
+
// Refresh a bound node by re-running its query (optionally with new criteria)
|
|
14
|
+
ResourceActionTypes["REFRESH_RESOURCE"] = "@@resource/REFRESH";
|
|
13
15
|
})(ResourceActionTypes || (ResourceActionTypes = {}));
|
|
@@ -14,6 +14,13 @@ export interface RemoteResourceAttributes {
|
|
|
14
14
|
expiresAt?: number;
|
|
15
15
|
/** Endpoint ID that populated this file */
|
|
16
16
|
endpointId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* High-level params that produced this node (the "query binding").
|
|
19
|
+
* Persisted so the node can be re-fetched/refreshed with changed criteria —
|
|
20
|
+
* the endpoint's payloadMapper deterministically re-derives the wire query
|
|
21
|
+
* from these. See `ResourceActionFactory.refresh`.
|
|
22
|
+
*/
|
|
23
|
+
params?: Record<string, any>;
|
|
17
24
|
/** Resource-specific metadata */
|
|
18
25
|
metadata?: {
|
|
19
26
|
status?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-attributes.d.ts","sourceRoot":"","sources":["../../../src/api/types/resource-attributes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAErE;;;;;;;GAOG;AACH,MAAM,WAAW,wBAAwB;IACvC,2BAA2B;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,iCAAiC;IACjC,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAC;IAEF,+BAA+B;IAC/B,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAElC,6BAA6B;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,yEAAyE;IACzE,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IAEjB,wDAAwD;IACxD,IAAI,EAAE,SAAS,CAAC;IAEhB,+BAA+B;IAC/B,SAAS,EAAE,MAAM,EAAE,CAAC;IAEpB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B"}
|
|
1
|
+
{"version":3,"file":"resource-attributes.d.ts","sourceRoot":"","sources":["../../../src/api/types/resource-attributes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAErE;;;;;;;GAOG;AACH,MAAM,WAAW,wBAAwB;IACvC,2BAA2B;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAE7B,iCAAiC;IACjC,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAC;IAEF,+BAA+B;IAC/B,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAElC,6BAA6B;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,yEAAyE;IACzE,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IAEjB,wDAAwD;IACxD,IAAI,EAAE,SAAS,CAAC;IAEhB,+BAA+B;IAC/B,SAAS,EAAE,MAAM,EAAE,CAAC;IAEpB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B"}
|
package/dist/impl/index.d.ts
CHANGED
|
@@ -8,12 +8,17 @@ export { RestResourceProvider } from './providers/rest-resource-provider.js';
|
|
|
8
8
|
export type { RestProviderConfig } from './providers/rest-resource-provider.js';
|
|
9
9
|
export { MockResourceProvider } from './providers/mock-resource-provider.js';
|
|
10
10
|
export type { MockProviderConfig, EndpointMockConfig, MockConfigMap, MockCallHistoryEntry } from './providers/mock-resource-provider.js';
|
|
11
|
+
export { PlainHttpResourceProvider } from './providers/plain-http-resource-provider.js';
|
|
12
|
+
export type { PlainHttpProviderConfig } from './providers/plain-http-resource-provider.js';
|
|
11
13
|
export { ResourceRegistry } from './registry/resource-registry.js';
|
|
12
14
|
export { EntityRegistry } from './registry/entity-registry.js';
|
|
13
15
|
export { createResourceMiddleware } from './middleware/resource-middleware.js';
|
|
14
16
|
export { createSyncMiddleware } from './middleware/sync-middleware.js';
|
|
15
17
|
export { createEntityMiddleware } from './middleware/entity-middleware.js';
|
|
16
18
|
export { createEntitySyncMiddleware } from './middleware/entity-sync-middleware.js';
|
|
19
|
+
export { createRefreshMiddleware } from './middleware/refresh-middleware.js';
|
|
20
|
+
export type { RefreshMiddlewareConfig } from './middleware/refresh-middleware.js';
|
|
21
|
+
export { RequestSequencer, pathKey } from './middleware/request-sequencer.js';
|
|
17
22
|
export type { FileSystemNodeActions } from '@hamak/ui-store-impl';
|
|
18
23
|
export { ResourceAutosaveProvider } from './autosave/resource-autosave-provider.js';
|
|
19
24
|
export type { ResourceAutosaveProviderConfig } from './autosave/resource-autosave-provider.js';
|
package/dist/impl/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/impl/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AAGvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,YAAY,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACvB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,YAAY,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACrB,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/impl/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AAGvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,YAAY,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACvB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,YAAY,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACrB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,YAAY,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC;AAGxF,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,YAAY,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC/E,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG3E,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAGlE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,YAAY,EAAE,8BAA8B,EAAE,MAAM,uCAAuC,CAAC"}
|
package/dist/impl/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { STORE_MANAGER_TOKEN, MIDDLEWARE_REGISTRY_TOKEN, STORE_EXTENSIONS_TOKEN
|
|
|
8
8
|
// Built-in providers (exported for convenience and testing)
|
|
9
9
|
export { RestResourceProvider } from './providers/rest-resource-provider.js';
|
|
10
10
|
export { MockResourceProvider } from './providers/mock-resource-provider.js';
|
|
11
|
+
export { PlainHttpResourceProvider } from './providers/plain-http-resource-provider.js';
|
|
11
12
|
// Registry implementations (internal, but exported for testing)
|
|
12
13
|
export { ResourceRegistry } from './registry/resource-registry.js';
|
|
13
14
|
export { EntityRegistry } from './registry/entity-registry.js';
|
|
@@ -16,5 +17,7 @@ export { createResourceMiddleware } from './middleware/resource-middleware.js';
|
|
|
16
17
|
export { createSyncMiddleware } from './middleware/sync-middleware.js';
|
|
17
18
|
export { createEntityMiddleware } from './middleware/entity-middleware.js';
|
|
18
19
|
export { createEntitySyncMiddleware } from './middleware/entity-sync-middleware.js';
|
|
20
|
+
export { createRefreshMiddleware } from './middleware/refresh-middleware.js';
|
|
21
|
+
export { RequestSequencer, pathKey } from './middleware/request-sequencer.js';
|
|
19
22
|
// Autosave provider
|
|
20
23
|
export { ResourceAutosaveProvider } from './autosave/resource-autosave-provider.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Middleware } from 'redux';
|
|
2
|
+
import { type ResourceAction } from '../../api/index.js';
|
|
3
|
+
export interface RefreshMiddlewareConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Read the file node at a path from the current root store state.
|
|
6
|
+
* Returns the node (with `state.extensionStates`) or undefined.
|
|
7
|
+
*/
|
|
8
|
+
readNode: (state: any, path: string | string[]) => any | undefined;
|
|
9
|
+
/** Called when a refresh targets a node with no remote binding. */
|
|
10
|
+
onError?: (error: any, action: ResourceAction) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Refresh middleware.
|
|
14
|
+
*
|
|
15
|
+
* Handles REFRESH_RESOURCE: recovers a node's stored query binding
|
|
16
|
+
* (`endpointId` + `params`) and re-issues a fetch to the SAME path with the
|
|
17
|
+
* (optionally merged) new criteria. The existing resource + sync middleware
|
|
18
|
+
* then re-fetch and overwrite the node content in place.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createRefreshMiddleware<S = any>(config: RefreshMiddlewareConfig): Middleware<{}, S>;
|
|
21
|
+
//# sourceMappingURL=refresh-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh-middleware.d.ts","sourceRoot":"","sources":["../../../src/impl/middleware/refresh-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,WAAW,CAAC;AAGnB,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,GAAG,SAAS,CAAC;IAEnE,mEAAmE;IACnE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;CACxD;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,GAAG,GAAG,EAC7C,MAAM,EAAE,uBAAuB,GAC9B,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,CA2CnB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ResourceActionTypes, ResourceActionFactory, REMOTE_RESOURCE_EXTENSION_KEY } from '../../api/index.js';
|
|
2
|
+
import { Pathway } from '@hamak/navigation-utils';
|
|
3
|
+
/**
|
|
4
|
+
* Refresh middleware.
|
|
5
|
+
*
|
|
6
|
+
* Handles REFRESH_RESOURCE: recovers a node's stored query binding
|
|
7
|
+
* (`endpointId` + `params`) and re-issues a fetch to the SAME path with the
|
|
8
|
+
* (optionally merged) new criteria. The existing resource + sync middleware
|
|
9
|
+
* then re-fetch and overwrite the node content in place.
|
|
10
|
+
*/
|
|
11
|
+
export function createRefreshMiddleware(config) {
|
|
12
|
+
const { readNode, onError } = config;
|
|
13
|
+
const actionFactory = new ResourceActionFactory();
|
|
14
|
+
return (store) => (next) => (action) => {
|
|
15
|
+
if (action.type !== ResourceActionTypes.REFRESH_RESOURCE) {
|
|
16
|
+
return next(action);
|
|
17
|
+
}
|
|
18
|
+
const result = next(action);
|
|
19
|
+
const { path, paramsPatch, merge } = action.payload;
|
|
20
|
+
const node = readNode(store.getState(), path);
|
|
21
|
+
const attrs = node?.state?.extensionStates?.[REMOTE_RESOURCE_EXTENSION_KEY];
|
|
22
|
+
if (!attrs?.endpointId) {
|
|
23
|
+
onError?.(new Error(`[ui-remote-resource] refresh: node at '${Pathway.of(path).toString()}' has no remote binding (was it fetched with a stable path?)`), action);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
const prevParams = attrs.params ?? {};
|
|
27
|
+
const nextParams = merge === false
|
|
28
|
+
? paramsPatch ?? prevParams
|
|
29
|
+
: { ...prevParams, ...(paramsPatch ?? {}) };
|
|
30
|
+
// Re-issue a fetch to the same node — resource + sync middleware do the rest.
|
|
31
|
+
store.dispatch(actionFactory.callRequest(attrs.endpointId, 'fetch', nextParams, path));
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-path monotonic request sequencer.
|
|
3
|
+
*
|
|
4
|
+
* Guards against out-of-order responses when a node is re-fetched rapidly
|
|
5
|
+
* (e.g. a user flipping filters): each request to a path gets an increasing
|
|
6
|
+
* token, and a response is only applied if its token is still the latest one
|
|
7
|
+
* issued for that path. A slow earlier response therefore can't clobber a
|
|
8
|
+
* newer filter's result.
|
|
9
|
+
*/
|
|
10
|
+
export declare class RequestSequencer {
|
|
11
|
+
private readonly latest;
|
|
12
|
+
/** Issue the next token for a path key. */
|
|
13
|
+
next(key: string): number;
|
|
14
|
+
/** True if `token` is the most recent one issued for the path key. */
|
|
15
|
+
isCurrent(key: string, token: number): boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Normalize a path (string or segments) to a stable map key. */
|
|
18
|
+
export declare function pathKey(path: string | string[]): string;
|
|
19
|
+
//# sourceMappingURL=request-sequencer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-sequencer.d.ts","sourceRoot":"","sources":["../../../src/impl/middleware/request-sequencer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IAEpD,2CAA2C;IAC3C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAMzB,sEAAsE;IACtE,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;CAG/C;AAED,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,CAEvD"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-path monotonic request sequencer.
|
|
3
|
+
*
|
|
4
|
+
* Guards against out-of-order responses when a node is re-fetched rapidly
|
|
5
|
+
* (e.g. a user flipping filters): each request to a path gets an increasing
|
|
6
|
+
* token, and a response is only applied if its token is still the latest one
|
|
7
|
+
* issued for that path. A slow earlier response therefore can't clobber a
|
|
8
|
+
* newer filter's result.
|
|
9
|
+
*/
|
|
10
|
+
export class RequestSequencer {
|
|
11
|
+
constructor() {
|
|
12
|
+
Object.defineProperty(this, "latest", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: new Map()
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Issue the next token for a path key. */
|
|
20
|
+
next(key) {
|
|
21
|
+
const token = (this.latest.get(key) ?? 0) + 1;
|
|
22
|
+
this.latest.set(key, token);
|
|
23
|
+
return token;
|
|
24
|
+
}
|
|
25
|
+
/** True if `token` is the most recent one issued for the path key. */
|
|
26
|
+
isCurrent(key, token) {
|
|
27
|
+
return (this.latest.get(key) ?? 0) === token;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Normalize a path (string or segments) to a stable map key. */
|
|
31
|
+
export function pathKey(path) {
|
|
32
|
+
return Array.isArray(path) ? path.join('/') : path;
|
|
33
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { Middleware } from 'redux';
|
|
2
2
|
import { type ResourceAction } from '../../api/index.js';
|
|
3
|
+
import { RequestSequencer } from './request-sequencer.js';
|
|
3
4
|
export interface ResourceMiddlewareConfig {
|
|
4
5
|
registry: any;
|
|
5
6
|
onError?: (error: any, action: ResourceAction) => void;
|
|
6
7
|
onSuccess?: (result: any, action: ResourceAction) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Optional out-of-order guard. When provided, a response is dropped if a newer
|
|
10
|
+
* request to the same storage path was issued while it was in flight.
|
|
11
|
+
*/
|
|
12
|
+
sequencer?: RequestSequencer;
|
|
7
13
|
}
|
|
8
14
|
/**
|
|
9
15
|
* Resource middleware
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-middleware.d.ts","sourceRoot":"","sources":["../../../src/impl/middleware/resource-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"resource-middleware.d.ts","sourceRoot":"","sources":["../../../src/impl/middleware/resource-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,gBAAgB,EAAW,MAAM,qBAAqB,CAAC;AAGhE,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,GAAG,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IACvD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAC1D;;;OAGG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,GAAG,EAC9C,MAAM,EAAE,wBAAwB,GAC/B,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,CAwFnB"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { ResourceActionTypes, ResourceActionFactory } from '../../api/index.js';
|
|
2
|
+
import { pathKey } from './request-sequencer.js';
|
|
2
3
|
/**
|
|
3
4
|
* Resource middleware
|
|
4
5
|
* Intercepts RESOURCE_CALL_REQUEST actions and executes API calls via providers
|
|
5
6
|
*/
|
|
6
7
|
export function createResourceMiddleware(config) {
|
|
7
|
-
const { registry, onError, onSuccess } = config;
|
|
8
|
+
const { registry, onError, onSuccess, sequencer } = config;
|
|
8
9
|
const actionFactory = new ResourceActionFactory();
|
|
9
10
|
return (store) => (next) => async (action) => {
|
|
10
11
|
// Only handle RESOURCE_CALL_REQUEST actions
|
|
@@ -15,9 +16,15 @@ export function createResourceMiddleware(config) {
|
|
|
15
16
|
next(action);
|
|
16
17
|
const requestAction = action;
|
|
17
18
|
const { endpointId, operation, params, path } = requestAction.payload;
|
|
19
|
+
// Resolve the storage path and claim an ordering token up front, so a
|
|
20
|
+
// response that loses the race to a newer request to the same node is
|
|
21
|
+
// dropped instead of overwriting fresher content.
|
|
22
|
+
const endpointDef = registry.getEndpoint(endpointId);
|
|
23
|
+
const storagePath = determinePath(path, endpointDef, params, operation);
|
|
24
|
+
const key = pathKey(storagePath);
|
|
25
|
+
const token = sequencer?.next(key);
|
|
26
|
+
const isStale = () => sequencer !== undefined && token !== undefined && !sequencer.isCurrent(key, token);
|
|
18
27
|
try {
|
|
19
|
-
// Lookup endpoint
|
|
20
|
-
const endpointDef = registry.getEndpoint(endpointId);
|
|
21
28
|
if (!endpointDef) {
|
|
22
29
|
throw new Error(`Endpoint not found: ${endpointId}`);
|
|
23
30
|
}
|
|
@@ -32,21 +39,24 @@ export function createResourceMiddleware(config) {
|
|
|
32
39
|
: { params }; // Default: just use params
|
|
33
40
|
// Execute provider call
|
|
34
41
|
const result = await provider.call(endpointDef.endpoint, callParams, operation);
|
|
42
|
+
// Drop the result if a newer request to the same node superseded it
|
|
43
|
+
if (isStale()) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
35
46
|
// Transform response (transformer receives entire action)
|
|
36
47
|
const transformedData = endpointDef.responseTransformer
|
|
37
48
|
? endpointDef.responseTransformer(result.data, result.metadata, requestAction)
|
|
38
49
|
: result.data;
|
|
39
|
-
// Determine storage path
|
|
40
|
-
const storagePath = determinePath(path, endpointDef, params, operation);
|
|
41
50
|
// Dispatch success action
|
|
42
51
|
const successAction = actionFactory.callSuccess(endpointId, storagePath, transformedData, result.metadata, requestAction);
|
|
43
52
|
store.dispatch(successAction);
|
|
44
53
|
onSuccess?.(result, requestAction);
|
|
45
54
|
}
|
|
46
55
|
catch (error) {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
// Drop a stale failure too, so it can't surface over newer content
|
|
57
|
+
if (isStale()) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
50
60
|
// Dispatch failure action
|
|
51
61
|
const failureAction = actionFactory.callFailure(endpointId, storagePath, normalizeError(error), requestAction);
|
|
52
62
|
store.dispatch(failureAction);
|
|
@@ -86,12 +96,17 @@ function generateFilename(params, operation) {
|
|
|
86
96
|
return `${operation}-${Date.now()}`;
|
|
87
97
|
}
|
|
88
98
|
/**
|
|
89
|
-
* Normalize error to consistent format
|
|
99
|
+
* Normalize error to consistent format.
|
|
100
|
+
*
|
|
101
|
+
* Handles both axios-shaped errors (`error.response.status` / `.data`) and
|
|
102
|
+
* plain-HTTP-shaped errors thrown by PlainHttpResourceProvider
|
|
103
|
+
* (`error.status` / `.body`), so `code` carries the HTTP status either way.
|
|
90
104
|
*/
|
|
91
105
|
function normalizeError(error) {
|
|
106
|
+
const status = error.response?.status ?? error.status;
|
|
92
107
|
return {
|
|
93
108
|
message: error.message || 'Unknown error',
|
|
94
|
-
code: error.code
|
|
95
|
-
details: error.response?.data
|
|
109
|
+
code: error.code ?? (status != null ? String(status) : undefined),
|
|
110
|
+
details: error.response?.data ?? error.body ?? error
|
|
96
111
|
};
|
|
97
112
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-middleware.d.ts","sourceRoot":"","sources":["../../../src/impl/middleware/sync-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-middleware.d.ts","sourceRoot":"","sources":["../../../src/impl/middleware/sync-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAQxC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAGlE,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,SAAS,EAAE,qBAAqB,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,GAAG,EAC1C,MAAM,EAAE,oBAAoB,GAC3B,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,CAwBnB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ResourceActionTypes } from '../../api/index.js';
|
|
1
|
+
import { ResourceActionTypes, REMOTE_RESOURCE_EXTENSION_KEY } from '../../api/index.js';
|
|
2
2
|
import { Pathway } from '@hamak/navigation-utils';
|
|
3
3
|
/**
|
|
4
4
|
* Sync middleware
|
|
@@ -38,11 +38,13 @@ function handleRequestAction(store, action, fsActions) {
|
|
|
38
38
|
if (parentSegments.length > 0) {
|
|
39
39
|
store.dispatch(fsActions.mkdir(parentSegments, true));
|
|
40
40
|
}
|
|
41
|
-
//
|
|
41
|
+
// Ensure the node exists (no-op if already present — keeps prior content for
|
|
42
|
+
// stale-while-revalidate), then flag it loading without clobbering content.
|
|
42
43
|
store.dispatch(fsActions.setFile(path, null, 'xs:any', { override: false, contentIsPresent: false }));
|
|
44
|
+
store.dispatch(fsActions.setContentLoading(path, true));
|
|
43
45
|
}
|
|
44
46
|
function handleSuccessAction(store, action, fsActions) {
|
|
45
|
-
const { path, data } = action.payload;
|
|
47
|
+
const { path, data, endpointId, metadata } = action.payload;
|
|
46
48
|
const { request } = action;
|
|
47
49
|
// Normalize path using Pathway utility
|
|
48
50
|
const pathway = Pathway.of(path);
|
|
@@ -55,10 +57,21 @@ function handleSuccessAction(store, action, fsActions) {
|
|
|
55
57
|
// Set file content from remote (automatically sets contentLoaded=true and clears loading)
|
|
56
58
|
// Schema should come from endpoint definition if available
|
|
57
59
|
const schema = request.meta?.schema || 'xs:any';
|
|
58
|
-
// First ensure file exists with proper schema
|
|
60
|
+
// First ensure file exists with proper schema (override resets node state,
|
|
61
|
+
// clearing the loading flag set on the request)
|
|
59
62
|
store.dispatch(fsActions.setFile(path, data, schema, { override: true, contentIsPresent: true }));
|
|
60
63
|
// Then set content from remote to mark it as loaded from remote source
|
|
61
64
|
store.dispatch(fsActions.setFileContent(path, data, true));
|
|
65
|
+
// Persist the query binding so the node can be refreshed later with new
|
|
66
|
+
// criteria (re-fetch via REFRESH_RESOURCE). setFile(override) above reset
|
|
67
|
+
// extension state, so this write must follow it.
|
|
68
|
+
store.dispatch(fsActions.setExtensionState(path, REMOTE_RESOURCE_EXTENSION_KEY, {
|
|
69
|
+
endpointId,
|
|
70
|
+
params: request?.payload?.params,
|
|
71
|
+
lastFetched: Date.now(),
|
|
72
|
+
lastOperation: request?.payload?.operation,
|
|
73
|
+
metadata
|
|
74
|
+
}));
|
|
62
75
|
}
|
|
63
76
|
function handleFailureAction(store, action, fsActions) {
|
|
64
77
|
const { path, error } = action.payload;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-plugin-factory.d.ts","sourceRoot":"","sources":["../../../src/impl/plugin/resource-plugin-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"resource-plugin-factory.d.ts","sourceRoot":"","sources":["../../../src/impl/plugin/resource-plugin-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAiBtD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,QAAQ,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,UAAU,qBAAqB;IAC7B,OAAO,CAAC,QAAQ,EAAE,GAAG,GAAG,IAAI,CAAC;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,UAAU,eAAe;IACvB,OAAO,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,KAAK,EAAE;QACL,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;KACvC,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAGD,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAC3D,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,GAAE,oBAAyB,GAChC,YAAY,CAgKd"}
|
|
@@ -8,6 +8,9 @@ import { createResourceMiddleware } from '../middleware/resource-middleware.js';
|
|
|
8
8
|
import { createSyncMiddleware } from '../middleware/sync-middleware.js';
|
|
9
9
|
import { createEntityMiddleware } from '../middleware/entity-middleware.js';
|
|
10
10
|
import { createEntitySyncMiddleware } from '../middleware/entity-sync-middleware.js';
|
|
11
|
+
import { createRefreshMiddleware } from '../middleware/refresh-middleware.js';
|
|
12
|
+
import { RequestSequencer } from '../middleware/request-sequencer.js';
|
|
13
|
+
import { Pathway } from '@hamak/navigation-utils';
|
|
11
14
|
// Dependency injection tokens (for local use)
|
|
12
15
|
export const RESOURCE_REGISTRY_TOKEN = 'RESOURCE_REGISTRY';
|
|
13
16
|
export const ENTITY_REGISTRY_TOKEN = 'ENTITY_REGISTRY';
|
|
@@ -67,22 +70,50 @@ export function createResourcePlugin(config = {}) {
|
|
|
67
70
|
}
|
|
68
71
|
const fsActions = fsAdapter.getActions();
|
|
69
72
|
const fsSliceName = fsAdapter.sliceName;
|
|
73
|
+
// Shared per-path ordering guard for in-flight resource calls
|
|
74
|
+
const sequencer = new RequestSequencer();
|
|
75
|
+
// Read a node from the fs slice by path (for refresh binding lookup)
|
|
76
|
+
const readNode = (state, path) => {
|
|
77
|
+
const root = state?.[fsSliceName]?.root;
|
|
78
|
+
if (!root) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return Pathway.of(path)
|
|
82
|
+
.getSegments()
|
|
83
|
+
.reduce((acc, step) => {
|
|
84
|
+
if (!acc || acc.type === 'file') {
|
|
85
|
+
return acc;
|
|
86
|
+
}
|
|
87
|
+
return acc.children?.[step];
|
|
88
|
+
}, root);
|
|
89
|
+
};
|
|
70
90
|
// Create middleware with FileSystemAdapter actions
|
|
71
91
|
const entityMiddleware = createEntityMiddleware({
|
|
72
92
|
entityRegistry,
|
|
73
93
|
fsSliceName,
|
|
74
94
|
onError: config.onError
|
|
75
95
|
});
|
|
96
|
+
const refreshMiddleware = createRefreshMiddleware({
|
|
97
|
+
readNode,
|
|
98
|
+
onError: config.onError
|
|
99
|
+
});
|
|
76
100
|
const resourceMiddleware = createResourceMiddleware({
|
|
77
101
|
registry: resourceRegistry,
|
|
78
102
|
onError: config.onError,
|
|
79
|
-
onSuccess: config.onSuccess
|
|
103
|
+
onSuccess: config.onSuccess,
|
|
104
|
+
sequencer
|
|
80
105
|
});
|
|
81
106
|
const entitySyncMiddleware = createEntitySyncMiddleware();
|
|
82
107
|
const resourceSyncMiddleware = createSyncMiddleware({
|
|
83
108
|
fsActions
|
|
84
109
|
});
|
|
85
110
|
const middlewareRegistry = resolve(MIDDLEWARE_REGISTRY_TOKEN);
|
|
111
|
+
middlewareRegistry.register({
|
|
112
|
+
id: 'refresh-middleware',
|
|
113
|
+
middleware: refreshMiddleware,
|
|
114
|
+
priority: 120,
|
|
115
|
+
plugin: 'ui-remote-resource'
|
|
116
|
+
});
|
|
86
117
|
middlewareRegistry.register({
|
|
87
118
|
id: 'entity-middleware',
|
|
88
119
|
middleware: entityMiddleware,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { IResourceProvider, ResourceCallParams, ResourceCallResult, HttpMethod } from '../../spi/index.js';
|
|
2
|
+
import type { ResourceOperation } from '../../api/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for the plain HTTP resource provider
|
|
5
|
+
*/
|
|
6
|
+
export interface PlainHttpProviderConfig {
|
|
7
|
+
/** Base URL the endpoint paths are resolved against */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
/** Request timeout in milliseconds (uses AbortSignal.timeout when set) */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
/** Default headers applied to every request */
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Default HTTP verb per CRUD operation, used when the call doesn't specify
|
|
15
|
+
* `params.method`. Overrides the built-in CRUD mapping for the given ops.
|
|
16
|
+
*/
|
|
17
|
+
methodMap?: Partial<Record<ResourceOperation, HttpMethod>>;
|
|
18
|
+
/**
|
|
19
|
+
* Last-chance hook to mutate the RequestInit before the call (auth, signing,
|
|
20
|
+
* etc.). Return a new init to replace it, or mutate in place and return void.
|
|
21
|
+
*/
|
|
22
|
+
onRequest?: (req: RequestInit, url: string) => RequestInit | void;
|
|
23
|
+
/** Custom fetch implementation (tests / SSR). Defaults to globalThis.fetch. */
|
|
24
|
+
fetchImpl?: typeof fetch;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Plain HTTP resource provider (provider type `'http'`).
|
|
28
|
+
*
|
|
29
|
+
* For endpoints that don't follow REST resource semantics — e.g. a data-listing
|
|
30
|
+
* / query API, a `POST /search` returning a collection, or an RPC-style call.
|
|
31
|
+
* The HTTP verb is decoupled from the CRUD `operation`: a `payloadMapper` can
|
|
32
|
+
* set `params.method` explicitly, so a "list" doesn't have to masquerade as a
|
|
33
|
+
* `create`. Zero dependencies — uses the native `fetch`/`URL`/`URLSearchParams`.
|
|
34
|
+
*/
|
|
35
|
+
export declare class PlainHttpResourceProvider implements IResourceProvider {
|
|
36
|
+
private readonly config;
|
|
37
|
+
readonly type = "http";
|
|
38
|
+
constructor(config?: PlainHttpProviderConfig);
|
|
39
|
+
call<TData = any>(endpoint: string, params: ResourceCallParams, operation: ResourceOperation): Promise<ResourceCallResult<TData>>;
|
|
40
|
+
/**
|
|
41
|
+
* Parse the response body by content type: JSON when the server says so,
|
|
42
|
+
* otherwise text. 204/empty bodies resolve to null.
|
|
43
|
+
*/
|
|
44
|
+
private parseBody;
|
|
45
|
+
/**
|
|
46
|
+
* Build the request URL: substitute `:param` path placeholders, resolve
|
|
47
|
+
* against `baseUrl` when configured, then append serialized query params.
|
|
48
|
+
*/
|
|
49
|
+
private buildUrl;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=plain-http-resource-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plain-http-resource-provider.d.ts","sourceRoot":"","sources":["../../../src/impl/providers/plain-http-resource-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACX,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC,CAAC;IAE3D;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,WAAW,GAAG,IAAI,CAAC;IAElE,+EAA+E;IAC/E,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAaD;;;;;;;;GAQG;AACH,qBAAa,yBAA0B,YAAW,iBAAiB;IAGrD,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,QAAQ,CAAC,IAAI,UAAU;gBAEM,MAAM,GAAE,uBAA4B;IAE3D,IAAI,CAAC,KAAK,GAAG,GAAG,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,EAC1B,SAAS,EAAE,iBAAiB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;IA8CrC;;;OAGG;YACW,SAAS;IAWvB;;;OAGG;IACH,OAAO,CAAC,QAAQ;CAgCjB"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default CRUD→verb mapping, matching REST semantics so this provider is a
|
|
3
|
+
* drop-in for plain CRUD endpoints too.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_METHODS = {
|
|
6
|
+
fetch: 'GET',
|
|
7
|
+
create: 'POST',
|
|
8
|
+
update: 'PUT',
|
|
9
|
+
delete: 'DELETE'
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Plain HTTP resource provider (provider type `'http'`).
|
|
13
|
+
*
|
|
14
|
+
* For endpoints that don't follow REST resource semantics — e.g. a data-listing
|
|
15
|
+
* / query API, a `POST /search` returning a collection, or an RPC-style call.
|
|
16
|
+
* The HTTP verb is decoupled from the CRUD `operation`: a `payloadMapper` can
|
|
17
|
+
* set `params.method` explicitly, so a "list" doesn't have to masquerade as a
|
|
18
|
+
* `create`. Zero dependencies — uses the native `fetch`/`URL`/`URLSearchParams`.
|
|
19
|
+
*/
|
|
20
|
+
export class PlainHttpResourceProvider {
|
|
21
|
+
constructor(config = {}) {
|
|
22
|
+
Object.defineProperty(this, "config", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: config
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "type", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: 'http'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async call(endpoint, params, operation) {
|
|
36
|
+
const method = params.method ??
|
|
37
|
+
this.config.methodMap?.[operation] ??
|
|
38
|
+
DEFAULT_METHODS[operation];
|
|
39
|
+
const url = this.buildUrl(endpoint, params.params, params.query);
|
|
40
|
+
const hasBody = method !== 'GET' && method !== 'HEAD' && params.body != null;
|
|
41
|
+
let init = {
|
|
42
|
+
method,
|
|
43
|
+
headers: {
|
|
44
|
+
...(hasBody ? { 'Content-Type': 'application/json' } : {}),
|
|
45
|
+
...this.config.headers,
|
|
46
|
+
...params.headers
|
|
47
|
+
},
|
|
48
|
+
body: hasBody ? JSON.stringify(params.body) : undefined,
|
|
49
|
+
signal: this.config.timeoutMs
|
|
50
|
+
? AbortSignal.timeout(this.config.timeoutMs)
|
|
51
|
+
: undefined
|
|
52
|
+
};
|
|
53
|
+
init = this.config.onRequest?.(init, url) ?? init;
|
|
54
|
+
const fetchImpl = this.config.fetchImpl ?? fetch;
|
|
55
|
+
const response = await fetchImpl(url, init);
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const body = await response.text().catch(() => undefined);
|
|
58
|
+
throw Object.assign(new Error(`HTTP ${response.status} for ${method} ${url}`), { status: response.status, body });
|
|
59
|
+
}
|
|
60
|
+
const data = (await this.parseBody(response));
|
|
61
|
+
return {
|
|
62
|
+
data,
|
|
63
|
+
metadata: {
|
|
64
|
+
status: response.status,
|
|
65
|
+
headers: Object.fromEntries(response.headers)
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Parse the response body by content type: JSON when the server says so,
|
|
71
|
+
* otherwise text. 204/empty bodies resolve to null.
|
|
72
|
+
*/
|
|
73
|
+
async parseBody(response) {
|
|
74
|
+
if (response.status === 204) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
78
|
+
if (contentType.includes('application/json')) {
|
|
79
|
+
return response.json();
|
|
80
|
+
}
|
|
81
|
+
return response.text();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the request URL: substitute `:param` path placeholders, resolve
|
|
85
|
+
* against `baseUrl` when configured, then append serialized query params.
|
|
86
|
+
*/
|
|
87
|
+
buildUrl(endpoint, pathParams, query) {
|
|
88
|
+
let url = endpoint;
|
|
89
|
+
if (pathParams) {
|
|
90
|
+
for (const [key, value] of Object.entries(pathParams)) {
|
|
91
|
+
url = url.replace(`:${key}`, encodeURIComponent(String(value)));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const base = this.config.baseUrl
|
|
95
|
+
? new URL(url, this.config.baseUrl).toString()
|
|
96
|
+
: url;
|
|
97
|
+
if (!query) {
|
|
98
|
+
return base;
|
|
99
|
+
}
|
|
100
|
+
const search = new URLSearchParams();
|
|
101
|
+
for (const [key, value] of Object.entries(query)) {
|
|
102
|
+
if (value !== undefined && value !== null) {
|
|
103
|
+
search.append(key, String(value));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const qs = search.toString();
|
|
107
|
+
if (!qs) {
|
|
108
|
+
return base;
|
|
109
|
+
}
|
|
110
|
+
return `${base}${base.includes('?') ? '&' : '?'}${qs}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ResourceOperation } from '../../api/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* HTTP method, for providers that talk plain HTTP and need the verb decoupled
|
|
4
|
+
* from CRUD intent (e.g. a non-REST listing API where a "fetch" is a POST).
|
|
5
|
+
*/
|
|
6
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
|
|
2
7
|
/**
|
|
3
8
|
* Parameters extracted from action and mapped to API call
|
|
4
9
|
*/
|
|
@@ -11,6 +16,12 @@ export interface ResourceCallParams {
|
|
|
11
16
|
headers?: Record<string, string>;
|
|
12
17
|
/** Query parameters */
|
|
13
18
|
query?: Record<string, any>;
|
|
19
|
+
/**
|
|
20
|
+
* Explicit HTTP method override, decoupled from the CRUD `operation`.
|
|
21
|
+
* Honored by plain-HTTP providers (e.g. `PlainHttpResourceProvider`); ignored
|
|
22
|
+
* by `RestResourceProvider`, which keeps its fixed CRUD→verb mapping.
|
|
23
|
+
*/
|
|
24
|
+
method?: HttpMethod;
|
|
14
25
|
}
|
|
15
26
|
/**
|
|
16
27
|
* Result from a resource provider call
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i-resource-provider.d.ts","sourceRoot":"","sources":["../../../src/spi/providers/i-resource-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,gDAAgD;IAChD,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,uBAAuB;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"i-resource-provider.d.ts","sourceRoot":"","sources":["../../../src/spi/providers/i-resource-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,gDAAgD;IAChD,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,uBAAuB;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,KAAK,GAAG,GAAG;IAC7C,oBAAoB;IACpB,IAAI,EAAE,KAAK,CAAC;IACZ,gDAAgD;IAChD,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,IAAI,CAAC,KAAK,GAAG,GAAG,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,EAC1B,SAAS,EAAE,iBAAiB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;CACvC"}
|