@push.rocks/smartstate 2.0.27 → 2.0.30
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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/smartstate.classes.smartstate.d.ts +1 -0
- package/dist_ts/smartstate.classes.smartstate.js +28 -7
- package/dist_ts/smartstate.classes.statepart.d.ts +3 -1
- package/dist_ts/smartstate.classes.statepart.js +40 -14
- package/npmextra.json +22 -5
- package/package.json +14 -13
- package/readme.hints.md +10 -2
- package/readme.md +61 -56
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartstate.classes.smartstate.ts +32 -7
- package/ts/smartstate.classes.statepart.ts +46 -15
- package/dist_ts/smartstate.classes.statecollection.d.ts +0 -8
- package/dist_ts/smartstate.classes.statecollection.js +0 -15
- package/dist_ts/smartstate.classes.stateobservable.d.ts +0 -10
- package/dist_ts/smartstate.classes.stateobservable.js +0 -15
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartstate',
|
|
6
|
-
version: '2.0.
|
|
6
|
+
version: '2.0.30',
|
|
7
7
|
description: 'A package for handling and managing state in applications.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLDREQUE0RDtDQUMxRSxDQUFBIn0=
|
|
@@ -7,6 +7,7 @@ export declare class Smartstate<StatePartNameType extends string> {
|
|
|
7
7
|
statePartMap: {
|
|
8
8
|
[key in StatePartNameType]?: StatePart<StatePartNameType, any>;
|
|
9
9
|
};
|
|
10
|
+
private pendingStatePartCreation;
|
|
10
11
|
constructor();
|
|
11
12
|
/**
|
|
12
13
|
* Allows getting and initializing a new statepart
|
|
@@ -6,6 +6,7 @@ import { StatePart } from './smartstate.classes.statepart.js';
|
|
|
6
6
|
export class Smartstate {
|
|
7
7
|
constructor() {
|
|
8
8
|
this.statePartMap = {};
|
|
9
|
+
this.pendingStatePartCreation = new Map();
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
12
|
* Allows getting and initializing a new statepart
|
|
@@ -18,6 +19,11 @@ export class Smartstate {
|
|
|
18
19
|
* @param initMode
|
|
19
20
|
*/
|
|
20
21
|
async getStatePart(statePartNameArg, initialArg, initMode = 'soft') {
|
|
22
|
+
// Return pending creation if one exists to prevent duplicate state parts
|
|
23
|
+
const pending = this.pendingStatePartCreation.get(statePartNameArg);
|
|
24
|
+
if (pending) {
|
|
25
|
+
return pending;
|
|
26
|
+
}
|
|
21
27
|
const existingStatePart = this.statePartMap[statePartNameArg];
|
|
22
28
|
if (existingStatePart) {
|
|
23
29
|
switch (initMode) {
|
|
@@ -25,7 +31,7 @@ export class Smartstate {
|
|
|
25
31
|
throw new Error(`State part '${statePartNameArg}' already exists, but initMode is 'mandatory'`);
|
|
26
32
|
case 'force':
|
|
27
33
|
// Force mode: create new state part
|
|
28
|
-
|
|
34
|
+
break; // Fall through to creation
|
|
29
35
|
case 'soft':
|
|
30
36
|
case 'persistent':
|
|
31
37
|
default:
|
|
@@ -38,7 +44,15 @@ export class Smartstate {
|
|
|
38
44
|
if (!initialArg) {
|
|
39
45
|
throw new Error(`State part '${statePartNameArg}' does not exist and no initial state provided`);
|
|
40
46
|
}
|
|
41
|
-
|
|
47
|
+
}
|
|
48
|
+
const creationPromise = this.createStatePart(statePartNameArg, initialArg, initMode);
|
|
49
|
+
this.pendingStatePartCreation.set(statePartNameArg, creationPromise);
|
|
50
|
+
try {
|
|
51
|
+
const result = await creationPromise;
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
this.pendingStatePartCreation.delete(statePartNameArg);
|
|
42
56
|
}
|
|
43
57
|
}
|
|
44
58
|
/**
|
|
@@ -56,12 +70,19 @@ export class Smartstate {
|
|
|
56
70
|
: null);
|
|
57
71
|
await newState.init();
|
|
58
72
|
const currentState = newState.getState();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
if (initMode === 'persistent' && currentState !== undefined) {
|
|
74
|
+
// Persisted state exists - merge with defaults, persisted values take precedence
|
|
75
|
+
await newState.setState({
|
|
76
|
+
...initialPayloadArg,
|
|
77
|
+
...currentState,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// No persisted state or non-persistent mode
|
|
82
|
+
await newState.setState(initialPayloadArg);
|
|
83
|
+
}
|
|
63
84
|
this.statePartMap[statePartName] = newState;
|
|
64
85
|
return newState;
|
|
65
86
|
}
|
|
66
87
|
}
|
|
67
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
88
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRzdGF0ZS5jbGFzc2VzLnNtYXJ0c3RhdGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHN0YXRlLmNsYXNzZXMuc21hcnRzdGF0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLHlCQUF5QixDQUFDO0FBQ25ELE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQztBQUk5RDs7R0FFRztBQUNILE1BQU0sT0FBTyxVQUFVO0lBS3JCO1FBSk8saUJBQVksR0FBdUUsRUFBRSxDQUFDO1FBRXJGLDZCQUF3QixHQUE0RCxJQUFJLEdBQUcsRUFBRSxDQUFDO0lBRXZGLENBQUM7SUFFaEI7Ozs7Ozs7OztPQVNHO0lBQ0ksS0FBSyxDQUFDLFlBQVksQ0FDdkIsZ0JBQW1DLEVBQ25DLFVBQXdCLEVBQ3hCLFdBQXNCLE1BQU07UUFFNUIseUVBQXlFO1FBQ3pFLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxHQUFHLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztRQUNwRSxJQUFJLE9BQU8sRUFBRSxDQUFDO1lBQ1osT0FBTyxPQUE2RCxDQUFDO1FBQ3ZFLENBQUM7UUFFRCxNQUFNLGlCQUFpQixHQUFHLElBQUksQ0FBQyxZQUFZLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztRQUU5RCxJQUFJLGlCQUFpQixFQUFFLENBQUM7WUFDdEIsUUFBUSxRQUFRLEVBQUUsQ0FBQztnQkFDakIsS0FBSyxXQUFXO29CQUNkLE1BQU0sSUFBSSxLQUFLLENBQ2IsZUFBZSxnQkFBZ0IsK0NBQStDLENBQy9FLENBQUM7Z0JBQ0osS0FBSyxPQUFPO29CQUNWLG9DQUFvQztvQkFDcEMsTUFBTSxDQUFDLDJCQUEyQjtnQkFDcEMsS0FBSyxNQUFNLENBQUM7Z0JBQ1osS0FBSyxZQUFZLENBQUM7Z0JBQ2xCO29CQUNFLDZCQUE2QjtvQkFDN0IsT0FBTyxpQkFBOEQsQ0FBQztZQUMxRSxDQUFDO1FBQ0gsQ0FBQzthQUFNLENBQUM7WUFDTiwyQkFBMkI7WUFDM0IsSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO2dCQUNoQixNQUFNLElBQUksS0FBSyxDQUNiLGVBQWUsZ0JBQWdCLGdEQUFnRCxDQUNoRixDQUFDO1lBQ0osQ0FBQztRQUNILENBQUM7UUFFRCxNQUFNLGVBQWUsR0FBRyxJQUFJLENBQUMsZUFBZSxDQUFjLGdCQUFnQixFQUFFLFVBQVUsRUFBRSxRQUFRLENBQUMsQ0FBQztRQUNsRyxJQUFJLENBQUMsd0JBQXdCLENBQUMsR0FBRyxDQUFDLGdCQUFnQixFQUFFLGVBQWUsQ0FBQyxDQUFDO1FBRXJFLElBQUksQ0FBQztZQUNILE1BQU0sTUFBTSxHQUFHLE1BQU0sZUFBZSxDQUFDO1lBQ3JDLE9BQU8sTUFBTSxDQUFDO1FBQ2hCLENBQUM7Z0JBQVMsQ0FBQztZQUNULElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxNQUFNLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztRQUN6RCxDQUFDO0lBQ0gsQ0FBQztJQUVEOzs7OztPQUtHO0lBQ0ssS0FBSyxDQUFDLGVBQWUsQ0FDM0IsYUFBZ0MsRUFDaEMsaUJBQThCLEVBQzlCLFdBQXNCLE1BQU07UUFFNUIsTUFBTSxRQUFRLEdBQUcsSUFBSSxTQUFTLENBQzVCLGFBQWEsRUFDYixRQUFRLEtBQUssWUFBWTtZQUN2QixDQUFDLENBQUM7Z0JBQ0UsTUFBTSxFQUFFLFlBQVk7Z0JBQ3BCLFNBQVMsRUFBRSxhQUFhO2FBQ3pCO1lBQ0gsQ0FBQyxDQUFDLElBQUksQ0FDVCxDQUFDO1FBQ0YsTUFBTSxRQUFRLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDdEIsTUFBTSxZQUFZLEdBQUcsUUFBUSxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBRXpDLElBQUksUUFBUSxLQUFLLFlBQVksSUFBSSxZQUFZLEtBQUssU0FBUyxFQUFFLENBQUM7WUFDNUQsaUZBQWlGO1lBQ2pGLE1BQU0sUUFBUSxDQUFDLFFBQVEsQ0FBQztnQkFDdEIsR0FBRyxpQkFBaUI7Z0JBQ3BCLEdBQUcsWUFBWTthQUNoQixDQUFDLENBQUM7UUFDTCxDQUFDO2FBQU0sQ0FBQztZQUNOLDRDQUE0QztZQUM1QyxNQUFNLFFBQVEsQ0FBQyxRQUFRLENBQUMsaUJBQWlCLENBQUMsQ0FBQztRQUM3QyxDQUFDO1FBRUQsSUFBSSxDQUFDLFlBQVksQ0FBQyxhQUFhLENBQUMsR0FBRyxRQUFRLENBQUM7UUFDNUMsT0FBTyxRQUFRLENBQUM7SUFDbEIsQ0FBQztDQUNGIn0=
|
|
@@ -5,6 +5,7 @@ export declare class StatePart<TStatePartName, TStatePayload> {
|
|
|
5
5
|
state: plugins.smartrx.rxjs.Subject<TStatePayload>;
|
|
6
6
|
stateStore: TStatePayload | undefined;
|
|
7
7
|
private cumulativeDeferred;
|
|
8
|
+
private pendingCumulativeNotification;
|
|
8
9
|
private webStoreOptions;
|
|
9
10
|
private webStore;
|
|
10
11
|
constructor(nameArg: TStatePartName, webStoreOptionsArg?: plugins.webstore.IWebStoreOptions);
|
|
@@ -50,8 +51,9 @@ export declare class StatePart<TStatePartName, TStatePayload> {
|
|
|
50
51
|
/**
|
|
51
52
|
* waits until a certain part of the state becomes available
|
|
52
53
|
* @param selectorFn
|
|
54
|
+
* @param timeoutMs - optional timeout in milliseconds to prevent indefinite waiting
|
|
53
55
|
*/
|
|
54
|
-
waitUntilPresent<T = TStatePayload>(selectorFn?: (state: TStatePayload) => T): Promise<T>;
|
|
56
|
+
waitUntilPresent<T = TStatePayload>(selectorFn?: (state: TStatePayload) => T, timeoutMs?: number): Promise<T>;
|
|
55
57
|
/**
|
|
56
58
|
* is executed
|
|
57
59
|
*/
|
|
@@ -4,6 +4,7 @@ export class StatePart {
|
|
|
4
4
|
constructor(nameArg, webStoreOptionsArg) {
|
|
5
5
|
this.state = new plugins.smartrx.rxjs.Subject();
|
|
6
6
|
this.cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
|
|
7
|
+
this.pendingCumulativeNotification = null;
|
|
7
8
|
this.webStore = null; // Add WebStore instance
|
|
8
9
|
this.name = nameArg;
|
|
9
10
|
// Initialize WebStore if webStoreOptions are provided
|
|
@@ -40,12 +41,13 @@ export class StatePart {
|
|
|
40
41
|
if (!this.validateState(newStateArg)) {
|
|
41
42
|
throw new Error(`Invalid state structure for state part '${this.name}'`);
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
await this.notifyChange();
|
|
45
|
-
// Save state to WebStore if initialized
|
|
44
|
+
// Save to WebStore first to ensure atomicity - if save fails, memory state remains unchanged
|
|
46
45
|
if (this.webStore) {
|
|
47
46
|
await this.webStore.set(String(this.name), newStateArg);
|
|
48
47
|
}
|
|
48
|
+
// Update in-memory state after successful persistence
|
|
49
|
+
this.stateStore = newStateArg;
|
|
50
|
+
await this.notifyChange();
|
|
49
51
|
return this.stateStore;
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
@@ -81,8 +83,12 @@ export class StatePart {
|
|
|
81
83
|
* creates a cumulative notification by adding a change notification at the end of the call stack;
|
|
82
84
|
*/
|
|
83
85
|
notifyChangeCumulative() {
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
+
// Debounce: clear any pending notification
|
|
87
|
+
if (this.pendingCumulativeNotification) {
|
|
88
|
+
clearTimeout(this.pendingCumulativeNotification);
|
|
89
|
+
}
|
|
90
|
+
this.pendingCumulativeNotification = setTimeout(async () => {
|
|
91
|
+
this.pendingCumulativeNotification = null;
|
|
86
92
|
if (this.stateStore) {
|
|
87
93
|
await this.notifyChange();
|
|
88
94
|
}
|
|
@@ -100,7 +106,8 @@ export class StatePart {
|
|
|
100
106
|
return selectorFn(stateArg);
|
|
101
107
|
}
|
|
102
108
|
catch (e) {
|
|
103
|
-
|
|
109
|
+
console.error(`Selector error in state part '${this.name}':`, e);
|
|
110
|
+
return undefined;
|
|
104
111
|
}
|
|
105
112
|
}));
|
|
106
113
|
return mapped;
|
|
@@ -123,18 +130,37 @@ export class StatePart {
|
|
|
123
130
|
/**
|
|
124
131
|
* waits until a certain part of the state becomes available
|
|
125
132
|
* @param selectorFn
|
|
133
|
+
* @param timeoutMs - optional timeout in milliseconds to prevent indefinite waiting
|
|
126
134
|
*/
|
|
127
|
-
async waitUntilPresent(selectorFn) {
|
|
135
|
+
async waitUntilPresent(selectorFn, timeoutMs) {
|
|
128
136
|
const done = plugins.smartpromise.defer();
|
|
129
137
|
const selectedObservable = this.select(selectorFn);
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
let resolved = false;
|
|
139
|
+
const subscription = selectedObservable.subscribe((value) => {
|
|
140
|
+
if (value && !resolved) {
|
|
141
|
+
resolved = true;
|
|
132
142
|
done.resolve(value);
|
|
133
143
|
}
|
|
134
144
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
let timeoutId;
|
|
146
|
+
if (timeoutMs) {
|
|
147
|
+
timeoutId = setTimeout(() => {
|
|
148
|
+
if (!resolved) {
|
|
149
|
+
resolved = true;
|
|
150
|
+
subscription.unsubscribe();
|
|
151
|
+
done.reject(new Error(`waitUntilPresent timed out after ${timeoutMs}ms`));
|
|
152
|
+
}
|
|
153
|
+
}, timeoutMs);
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const result = await done.promise;
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
subscription.unsubscribe();
|
|
161
|
+
if (timeoutId)
|
|
162
|
+
clearTimeout(timeoutId);
|
|
163
|
+
}
|
|
138
164
|
}
|
|
139
165
|
/**
|
|
140
166
|
* is executed
|
|
@@ -142,7 +168,7 @@ export class StatePart {
|
|
|
142
168
|
async stateSetup(funcArg) {
|
|
143
169
|
const resultPromise = funcArg(this);
|
|
144
170
|
this.cumulativeDeferred.addPromise(resultPromise);
|
|
145
|
-
this.setState(await resultPromise);
|
|
171
|
+
await this.setState(await resultPromise);
|
|
146
172
|
}
|
|
147
173
|
}
|
|
148
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
174
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRzdGF0ZS5jbGFzc2VzLnN0YXRlcGFydC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0c3RhdGUuY2xhc3Nlcy5zdGF0ZXBhcnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSx5QkFBeUIsQ0FBQztBQUNuRCxPQUFPLEVBQUUsV0FBVyxFQUFtQixNQUFNLHFDQUFxQyxDQUFDO0FBRW5GLE1BQU0sT0FBTyxTQUFTO0lBV3BCLFlBQVksT0FBdUIsRUFBRSxrQkFBc0Q7UUFUcEYsVUFBSyxHQUFHLElBQUksT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFpQixDQUFDO1FBRXpELHVCQUFrQixHQUFHLE9BQU8sQ0FBQyxZQUFZLENBQUMsZUFBZSxFQUFFLENBQUM7UUFFNUQsa0NBQTZCLEdBQXlDLElBQUksQ0FBQztRQUczRSxhQUFRLEdBQW9ELElBQUksQ0FBQyxDQUFDLHdCQUF3QjtRQUdoRyxJQUFJLENBQUMsSUFBSSxHQUFHLE9BQU8sQ0FBQztRQUVwQixzREFBc0Q7UUFDdEQsSUFBSSxrQkFBa0IsRUFBRSxDQUFDO1lBQ3ZCLElBQUksQ0FBQyxlQUFlLEdBQUcsa0JBQWtCLENBQUM7UUFDNUMsQ0FBQztJQUNILENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxJQUFJO1FBQ2YsSUFBSSxJQUFJLENBQUMsZUFBZSxFQUFFLENBQUM7WUFDekIsSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLE9BQU8sQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFnQixJQUFJLENBQUMsZUFBZSxDQUFDLENBQUM7WUFDbkYsTUFBTSxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRSxDQUFDO1lBQzNCLE1BQU0sV0FBVyxHQUFHLE1BQU0sSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1lBQy9E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
|
package/npmextra.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
|
|
2
|
+
"@git.zone/tsbundle": {
|
|
3
|
+
"bundles": [
|
|
4
|
+
{
|
|
5
|
+
"from": "./ts/index.ts",
|
|
6
|
+
"to": "./dist_bundle/bundle.js",
|
|
7
|
+
"outputMode": "bundle",
|
|
8
|
+
"bundler": "esbuild",
|
|
9
|
+
"production": true
|
|
10
|
+
}
|
|
11
|
+
]
|
|
5
12
|
},
|
|
6
|
-
"
|
|
13
|
+
"@git.zone/cli": {
|
|
7
14
|
"projectType": "npm",
|
|
8
15
|
"module": {
|
|
9
16
|
"githost": "code.foss.global",
|
|
@@ -24,9 +31,19 @@
|
|
|
24
31
|
"asynchronous state",
|
|
25
32
|
"cumulative notification"
|
|
26
33
|
]
|
|
34
|
+
},
|
|
35
|
+
"release": {
|
|
36
|
+
"registries": [
|
|
37
|
+
"https://verdaccio.lossless.digital",
|
|
38
|
+
"https://registry.npmjs.org"
|
|
39
|
+
],
|
|
40
|
+
"accessLevel": "public"
|
|
27
41
|
}
|
|
28
42
|
},
|
|
29
|
-
"tsdoc": {
|
|
43
|
+
"@git.zone/tsdoc": {
|
|
30
44
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
|
45
|
+
},
|
|
46
|
+
"@ship.zone/szci": {
|
|
47
|
+
"npmGlobalTools": []
|
|
31
48
|
}
|
|
32
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartstate",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.30",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A package for handling and managing state in applications.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -8,18 +8,23 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"author": "Lossless GmbH",
|
|
10
10
|
"license": "MIT",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "(tstest test/ --verbose)",
|
|
13
|
+
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle npm)",
|
|
14
|
+
"buildDocs": "tsdoc"
|
|
15
|
+
},
|
|
11
16
|
"devDependencies": {
|
|
12
|
-
"@git.zone/tsbuild": "^
|
|
13
|
-
"@git.zone/tsbundle": "^2.
|
|
14
|
-
"@git.zone/tsrun": "^
|
|
15
|
-
"@git.zone/tstest": "^
|
|
17
|
+
"@git.zone/tsbuild": "^4.1.2",
|
|
18
|
+
"@git.zone/tsbundle": "^2.8.3",
|
|
19
|
+
"@git.zone/tsrun": "^2.0.1",
|
|
20
|
+
"@git.zone/tstest": "^3.1.8",
|
|
16
21
|
"@push.rocks/tapbundle": "^6.0.3",
|
|
17
|
-
"@types/node": "^
|
|
22
|
+
"@types/node": "^25.2.0"
|
|
18
23
|
},
|
|
19
24
|
"dependencies": {
|
|
20
25
|
"@push.rocks/lik": "^6.2.2",
|
|
21
26
|
"@push.rocks/smarthash": "^3.2.6",
|
|
22
|
-
"@push.rocks/smartjson": "^
|
|
27
|
+
"@push.rocks/smartjson": "^6.0.0",
|
|
23
28
|
"@push.rocks/smartpromise": "^4.2.3",
|
|
24
29
|
"@push.rocks/smartrx": "^3.0.10",
|
|
25
30
|
"@push.rocks/webstore": "^2.0.20"
|
|
@@ -56,9 +61,5 @@
|
|
|
56
61
|
"type": "git",
|
|
57
62
|
"url": "https://code.foss.global/push.rocks/smartstate.git"
|
|
58
63
|
},
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle npm)",
|
|
62
|
-
"buildDocs": "tsdoc"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
64
|
+
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
|
65
|
+
}
|
package/readme.hints.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartstate Implementation Notes
|
|
2
2
|
|
|
3
|
-
## Current API (as of v2.0.
|
|
3
|
+
## Current API (as of v2.0.28+)
|
|
4
4
|
|
|
5
5
|
### State Part Initialization
|
|
6
6
|
- State parts can be created with different init modes: 'soft' (default), 'mandatory', 'force', 'persistent'
|
|
@@ -49,4 +49,12 @@
|
|
|
49
49
|
4. Simplified init mode logic with clear behavior for each mode
|
|
50
50
|
5. Added state validation with extensible validateState() method
|
|
51
51
|
6. Made notifyChange() async to support proper hash comparison
|
|
52
|
-
7. Updated select() to filter undefined states
|
|
52
|
+
7. Updated select() to filter undefined states
|
|
53
|
+
|
|
54
|
+
## Dependency Versions (v2.0.28)
|
|
55
|
+
- @git.zone/tsbuild: ^4.1.2
|
|
56
|
+
- @git.zone/tsbundle: ^2.8.3
|
|
57
|
+
- @git.zone/tsrun: ^2.0.1
|
|
58
|
+
- @git.zone/tstest: ^3.1.8
|
|
59
|
+
- @push.rocks/smartjson: ^6.0.0
|
|
60
|
+
- @types/node: ^25.2.0
|
package/readme.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @push.rocks/smartstate
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
A powerful TypeScript library for elegant state management using RxJS and reactive programming patterns 🚀
|
|
4
|
+
|
|
5
|
+
## Issue Reporting and Security
|
|
6
|
+
|
|
7
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
3
8
|
|
|
4
9
|
## Install
|
|
5
10
|
|
|
@@ -16,15 +21,13 @@ npm install @push.rocks/smartstate --save
|
|
|
16
21
|
yarn add @push.rocks/smartstate
|
|
17
22
|
```
|
|
18
23
|
|
|
19
|
-
This will add `@push.rocks/smartstate` to your project's dependencies.
|
|
20
|
-
|
|
21
24
|
## Usage
|
|
22
25
|
|
|
23
|
-
The `@push.rocks/smartstate` library provides an elegant way to handle state within your JavaScript or TypeScript projects, leveraging the power of Reactive Extensions (RxJS) and a structured state management strategy.
|
|
26
|
+
The `@push.rocks/smartstate` library provides an elegant way to handle state within your JavaScript or TypeScript projects, leveraging the power of Reactive Extensions (RxJS) and a structured state management strategy.
|
|
24
27
|
|
|
25
28
|
### Getting Started
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
Import the necessary components from the library:
|
|
28
31
|
|
|
29
32
|
```typescript
|
|
30
33
|
import { Smartstate, StatePart, StateAction } from '@push.rocks/smartstate';
|
|
@@ -32,7 +35,7 @@ import { Smartstate, StatePart, StateAction } from '@push.rocks/smartstate';
|
|
|
32
35
|
|
|
33
36
|
### Creating a SmartState Instance
|
|
34
37
|
|
|
35
|
-
`Smartstate` acts as the container for your state parts.
|
|
38
|
+
`Smartstate` acts as the container for your state parts. Think of it as the root of your state management structure:
|
|
36
39
|
|
|
37
40
|
```typescript
|
|
38
41
|
const myAppSmartState = new Smartstate<YourStatePartNamesEnum>();
|
|
@@ -42,16 +45,16 @@ const myAppSmartState = new Smartstate<YourStatePartNamesEnum>();
|
|
|
42
45
|
|
|
43
46
|
When creating state parts, you can specify different initialization modes:
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
| Mode | Description |
|
|
49
|
+
|------|-------------|
|
|
50
|
+
| `'soft'` | Default. Returns existing state part if it exists, creates new if not |
|
|
51
|
+
| `'mandatory'` | Requires state part to not exist, throws error if it does |
|
|
52
|
+
| `'force'` | Always creates new state part, overwriting any existing one |
|
|
53
|
+
| `'persistent'` | Like 'soft' but with WebStore persistence using IndexedDB |
|
|
49
54
|
|
|
50
55
|
### Defining State Parts
|
|
51
56
|
|
|
52
|
-
State parts represent separable sections of your state, making it easier to manage and modularize.
|
|
53
|
-
|
|
54
|
-
Define state part names using either enums or string literal types:
|
|
57
|
+
State parts represent separable sections of your state, making it easier to manage and modularize. Define state part names using either enums or string literal types:
|
|
55
58
|
|
|
56
59
|
```typescript
|
|
57
60
|
// Option 1: Using enums
|
|
@@ -64,7 +67,7 @@ enum AppStateParts {
|
|
|
64
67
|
type AppStateParts = 'UserState' | 'SettingsState';
|
|
65
68
|
```
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
Create a state part within your `Smartstate` instance:
|
|
68
71
|
|
|
69
72
|
```typescript
|
|
70
73
|
interface IUserState {
|
|
@@ -77,13 +80,11 @@ const userStatePart = await myAppSmartState.getStatePart<IUserState>(
|
|
|
77
80
|
{ isLoggedIn: false }, // Initial state
|
|
78
81
|
'soft' // Init mode (optional, defaults to 'soft')
|
|
79
82
|
);
|
|
80
|
-
|
|
81
|
-
// Note: Persistent state parts are automatically initialized internally
|
|
82
83
|
```
|
|
83
84
|
|
|
84
85
|
### Subscribing to State Changes
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
Subscribe to changes in a state part to perform actions accordingly:
|
|
87
88
|
|
|
88
89
|
```typescript
|
|
89
90
|
// The select() method automatically filters out undefined states
|
|
@@ -92,7 +93,7 @@ userStatePart.select().subscribe((currentState) => {
|
|
|
92
93
|
});
|
|
93
94
|
```
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
Select a specific part of your state with a selector function:
|
|
96
97
|
|
|
97
98
|
```typescript
|
|
98
99
|
userStatePart.select(state => state.username).subscribe((username) => {
|
|
@@ -116,8 +117,6 @@ const loginUserAction = userStatePart.createAction<ILoginPayload>(async (statePa
|
|
|
116
117
|
});
|
|
117
118
|
|
|
118
119
|
// Dispatch the action to update the state
|
|
119
|
-
loginUserAction.trigger({ username: 'johnDoe' });
|
|
120
|
-
// or await the result
|
|
121
120
|
const newState = await loginUserAction.trigger({ username: 'johnDoe' });
|
|
122
121
|
```
|
|
123
122
|
|
|
@@ -128,14 +127,12 @@ There are two ways to dispatch actions:
|
|
|
128
127
|
```typescript
|
|
129
128
|
// Method 1: Using trigger on the action (returns promise)
|
|
130
129
|
const newState = await loginUserAction.trigger({ username: 'johnDoe' });
|
|
131
|
-
// or fire and forget
|
|
132
|
-
loginUserAction.trigger({ username: 'johnDoe' });
|
|
133
130
|
|
|
134
131
|
// Method 2: Using dispatchAction on the state part (returns promise)
|
|
135
132
|
const newState = await userStatePart.dispatchAction(loginUserAction, { username: 'johnDoe' });
|
|
136
133
|
```
|
|
137
134
|
|
|
138
|
-
Both methods return a Promise with the new state
|
|
135
|
+
Both methods return a Promise with the new state payload.
|
|
139
136
|
|
|
140
137
|
### Additional State Methods
|
|
141
138
|
|
|
@@ -148,20 +145,26 @@ if (currentState) {
|
|
|
148
145
|
console.log('Current user:', currentState.username);
|
|
149
146
|
}
|
|
150
147
|
|
|
151
|
-
// Wait for
|
|
148
|
+
// Wait for state to be present
|
|
152
149
|
await userStatePart.waitUntilPresent();
|
|
153
150
|
|
|
154
151
|
// Wait for a specific property to be present
|
|
155
152
|
await userStatePart.waitUntilPresent(state => state.username);
|
|
156
153
|
|
|
154
|
+
// Wait with a timeout (throws error if condition not met within timeout)
|
|
155
|
+
try {
|
|
156
|
+
await userStatePart.waitUntilPresent(state => state.username, 5000); // 5 second timeout
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Timed out waiting for username');
|
|
159
|
+
}
|
|
160
|
+
|
|
157
161
|
// Setup initial state with async operations
|
|
158
162
|
await userStatePart.stateSetup(async (statePart) => {
|
|
159
|
-
// Perform async initialization
|
|
160
163
|
const userData = await fetchUserData();
|
|
161
164
|
return { ...statePart.getState(), ...userData };
|
|
162
165
|
});
|
|
163
166
|
|
|
164
|
-
// Defer notification to end of call stack
|
|
167
|
+
// Defer notification to end of call stack (debounced)
|
|
165
168
|
userStatePart.notifyChangeCumulative();
|
|
166
169
|
```
|
|
167
170
|
|
|
@@ -172,31 +175,28 @@ userStatePart.notifyChangeCumulative();
|
|
|
172
175
|
```typescript
|
|
173
176
|
const settingsStatePart = await myAppSmartState.getStatePart<ISettingsState>(
|
|
174
177
|
AppStateParts.SettingsState,
|
|
175
|
-
{ theme: 'light' }, // Initial state
|
|
178
|
+
{ theme: 'light' }, // Initial/default state
|
|
176
179
|
'persistent' // Mode
|
|
177
180
|
);
|
|
178
|
-
|
|
179
|
-
// Note: init() is called automatically for persistent mode
|
|
180
181
|
```
|
|
181
182
|
|
|
182
183
|
Persistent state automatically:
|
|
183
184
|
- Saves state changes to IndexedDB
|
|
184
185
|
- Restores state on application restart
|
|
185
|
-
-
|
|
186
|
+
- Merges persisted values with defaults (persisted values take precedence)
|
|
187
|
+
- Ensures atomic writes (persistence happens before memory update)
|
|
186
188
|
|
|
187
189
|
### State Validation
|
|
188
190
|
|
|
189
191
|
`Smartstate` includes built-in state validation to ensure data integrity:
|
|
190
192
|
|
|
191
193
|
```typescript
|
|
192
|
-
// Basic validation (built-in)
|
|
193
|
-
// Ensures state is not null or undefined
|
|
194
|
+
// Basic validation (built-in) ensures state is not null or undefined
|
|
194
195
|
await userStatePart.setState(null); // Throws error: Invalid state structure
|
|
195
196
|
|
|
196
197
|
// Custom validation by extending StatePart
|
|
197
198
|
class ValidatedStatePart<T> extends StatePart<string, T> {
|
|
198
199
|
protected validateState(stateArg: any): stateArg is T {
|
|
199
|
-
// Add your custom validation logic
|
|
200
200
|
return super.validateState(stateArg) && /* your validation */;
|
|
201
201
|
}
|
|
202
202
|
}
|
|
@@ -206,11 +206,12 @@ class ValidatedStatePart<T> extends StatePart<string, T> {
|
|
|
206
206
|
|
|
207
207
|
`Smartstate` includes advanced performance optimizations:
|
|
208
208
|
|
|
209
|
-
-
|
|
210
|
-
-
|
|
211
|
-
-
|
|
212
|
-
-
|
|
213
|
-
-
|
|
209
|
+
- **🔒 Async State Hash Detection**: Uses SHA256 hashing to detect actual state changes, preventing unnecessary notifications when state values haven't truly changed
|
|
210
|
+
- **🚫 Duplicate Prevention**: Identical state updates are automatically filtered out
|
|
211
|
+
- **📦 Cumulative Notifications**: Batch multiple state changes into a single notification using `notifyChangeCumulative()` with automatic debouncing
|
|
212
|
+
- **🎯 Selective Subscriptions**: Use selectors to subscribe only to specific state properties
|
|
213
|
+
- **✨ Undefined State Filtering**: The `select()` method automatically filters out undefined states
|
|
214
|
+
- **⚡ Concurrent Access Safety**: Prevents race conditions when multiple calls request the same state part simultaneously
|
|
214
215
|
|
|
215
216
|
### RxJS Integration
|
|
216
217
|
|
|
@@ -275,7 +276,7 @@ const loginAction = userState.createAction<{ username: string; email: string }>(
|
|
|
275
276
|
async (statePart, payload) => {
|
|
276
277
|
// Simulate API call
|
|
277
278
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
278
|
-
|
|
279
|
+
|
|
279
280
|
return {
|
|
280
281
|
isLoggedIn: true,
|
|
281
282
|
username: payload.username,
|
|
@@ -293,34 +294,38 @@ userState.select(state => state.isLoggedIn).subscribe(isLoggedIn => {
|
|
|
293
294
|
await loginAction.trigger({ username: 'john', email: 'john@example.com' });
|
|
294
295
|
```
|
|
295
296
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
297
|
+
## Key Features
|
|
298
|
+
|
|
299
|
+
| Feature | Description |
|
|
300
|
+
|---------|-------------|
|
|
301
|
+
| 🎯 **Type-safe** | Full TypeScript support with intelligent type inference |
|
|
302
|
+
| ⚡ **Performance optimized** | Async state hash detection prevents unnecessary re-renders |
|
|
303
|
+
| 💾 **Persistent state** | Built-in IndexedDB support for state persistence |
|
|
304
|
+
| 🔄 **Reactive** | Powered by RxJS for elegant async handling |
|
|
305
|
+
| 🧩 **Modular** | Organize state into logical, reusable parts |
|
|
306
|
+
| ✅ **Validated** | Built-in state validation with extensible validation logic |
|
|
307
|
+
| 🎭 **Flexible init modes** | Choose how state parts are initialized |
|
|
308
|
+
| 📦 **Zero config** | Works out of the box with sensible defaults |
|
|
309
|
+
| 🛡️ **Race condition safe** | Concurrent state part creation is handled safely |
|
|
310
|
+
| ⏱️ **Timeout support** | `waitUntilPresent` supports optional timeouts |
|
|
308
311
|
|
|
309
312
|
## License and Legal Information
|
|
310
313
|
|
|
311
|
-
This repository contains open-source code
|
|
314
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
|
312
315
|
|
|
313
316
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
314
317
|
|
|
315
318
|
### Trademarks
|
|
316
319
|
|
|
317
|
-
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein.
|
|
320
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
321
|
+
|
|
322
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
318
323
|
|
|
319
324
|
### Company Information
|
|
320
325
|
|
|
321
|
-
Task Venture Capital GmbH
|
|
322
|
-
Registered at District
|
|
326
|
+
Task Venture Capital GmbH
|
|
327
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
323
328
|
|
|
324
|
-
For any legal inquiries or
|
|
329
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
325
330
|
|
|
326
331
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type TInitMode = 'soft' | 'mandatory' | 'force' | 'persistent';
|
|
|
9
9
|
export class Smartstate<StatePartNameType extends string> {
|
|
10
10
|
public statePartMap: { [key in StatePartNameType]?: StatePart<StatePartNameType, any> } = {};
|
|
11
11
|
|
|
12
|
+
private pendingStatePartCreation: Map<string, Promise<StatePart<StatePartNameType, any>>> = new Map();
|
|
13
|
+
|
|
12
14
|
constructor() {}
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -26,8 +28,14 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
26
28
|
initialArg?: PayloadType,
|
|
27
29
|
initMode: TInitMode = 'soft'
|
|
28
30
|
): Promise<StatePart<StatePartNameType, PayloadType>> {
|
|
31
|
+
// Return pending creation if one exists to prevent duplicate state parts
|
|
32
|
+
const pending = this.pendingStatePartCreation.get(statePartNameArg);
|
|
33
|
+
if (pending) {
|
|
34
|
+
return pending as Promise<StatePart<StatePartNameType, PayloadType>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
const existingStatePart = this.statePartMap[statePartNameArg];
|
|
30
|
-
|
|
38
|
+
|
|
31
39
|
if (existingStatePart) {
|
|
32
40
|
switch (initMode) {
|
|
33
41
|
case 'mandatory':
|
|
@@ -36,7 +44,7 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
36
44
|
);
|
|
37
45
|
case 'force':
|
|
38
46
|
// Force mode: create new state part
|
|
39
|
-
|
|
47
|
+
break; // Fall through to creation
|
|
40
48
|
case 'soft':
|
|
41
49
|
case 'persistent':
|
|
42
50
|
default:
|
|
@@ -50,7 +58,16 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
50
58
|
`State part '${statePartNameArg}' does not exist and no initial state provided`
|
|
51
59
|
);
|
|
52
60
|
}
|
|
53
|
-
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const creationPromise = this.createStatePart<PayloadType>(statePartNameArg, initialArg, initMode);
|
|
64
|
+
this.pendingStatePartCreation.set(statePartNameArg, creationPromise);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await creationPromise;
|
|
68
|
+
return result;
|
|
69
|
+
} finally {
|
|
70
|
+
this.pendingStatePartCreation.delete(statePartNameArg);
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
73
|
|
|
@@ -76,10 +93,18 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
76
93
|
);
|
|
77
94
|
await newState.init();
|
|
78
95
|
const currentState = newState.getState();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
|
|
97
|
+
if (initMode === 'persistent' && currentState !== undefined) {
|
|
98
|
+
// Persisted state exists - merge with defaults, persisted values take precedence
|
|
99
|
+
await newState.setState({
|
|
100
|
+
...initialPayloadArg,
|
|
101
|
+
...currentState,
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
// No persisted state or non-persistent mode
|
|
105
|
+
await newState.setState(initialPayloadArg);
|
|
106
|
+
}
|
|
107
|
+
|
|
83
108
|
this.statePartMap[statePartName] = newState;
|
|
84
109
|
return newState;
|
|
85
110
|
}
|
|
@@ -7,6 +7,8 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
7
7
|
public stateStore: TStatePayload | undefined;
|
|
8
8
|
private cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
|
|
9
9
|
|
|
10
|
+
private pendingCumulativeNotification: ReturnType<typeof setTimeout> | null = null;
|
|
11
|
+
|
|
10
12
|
private webStoreOptions: plugins.webstore.IWebStoreOptions;
|
|
11
13
|
private webStore: plugins.webstore.WebStore<TStatePayload> | null = null; // Add WebStore instance
|
|
12
14
|
|
|
@@ -50,14 +52,16 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
50
52
|
if (!this.validateState(newStateArg)) {
|
|
51
53
|
throw new Error(`Invalid state structure for state part '${this.name}'`);
|
|
52
54
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await this.notifyChange();
|
|
56
|
-
|
|
57
|
-
// Save state to WebStore if initialized
|
|
55
|
+
|
|
56
|
+
// Save to WebStore first to ensure atomicity - if save fails, memory state remains unchanged
|
|
58
57
|
if (this.webStore) {
|
|
59
58
|
await this.webStore.set(String(this.name), newStateArg);
|
|
60
59
|
}
|
|
60
|
+
|
|
61
|
+
// Update in-memory state after successful persistence
|
|
62
|
+
this.stateStore = newStateArg;
|
|
63
|
+
await this.notifyChange();
|
|
64
|
+
|
|
61
65
|
return this.stateStore;
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -98,8 +102,13 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
98
102
|
* creates a cumulative notification by adding a change notification at the end of the call stack;
|
|
99
103
|
*/
|
|
100
104
|
public notifyChangeCumulative() {
|
|
101
|
-
//
|
|
102
|
-
|
|
105
|
+
// Debounce: clear any pending notification
|
|
106
|
+
if (this.pendingCumulativeNotification) {
|
|
107
|
+
clearTimeout(this.pendingCumulativeNotification);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.pendingCumulativeNotification = setTimeout(async () => {
|
|
111
|
+
this.pendingCumulativeNotification = null;
|
|
103
112
|
if (this.stateStore) {
|
|
104
113
|
await this.notifyChange();
|
|
105
114
|
}
|
|
@@ -122,7 +131,8 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
122
131
|
try {
|
|
123
132
|
return selectorFn(stateArg);
|
|
124
133
|
} catch (e) {
|
|
125
|
-
|
|
134
|
+
console.error(`Selector error in state part '${this.name}':`, e);
|
|
135
|
+
return undefined;
|
|
126
136
|
}
|
|
127
137
|
})
|
|
128
138
|
);
|
|
@@ -151,20 +161,41 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
151
161
|
/**
|
|
152
162
|
* waits until a certain part of the state becomes available
|
|
153
163
|
* @param selectorFn
|
|
164
|
+
* @param timeoutMs - optional timeout in milliseconds to prevent indefinite waiting
|
|
154
165
|
*/
|
|
155
166
|
public async waitUntilPresent<T = TStatePayload>(
|
|
156
|
-
selectorFn?: (state: TStatePayload) => T
|
|
167
|
+
selectorFn?: (state: TStatePayload) => T,
|
|
168
|
+
timeoutMs?: number
|
|
157
169
|
): Promise<T> {
|
|
158
170
|
const done = plugins.smartpromise.defer<T>();
|
|
159
171
|
const selectedObservable = this.select(selectorFn);
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
let resolved = false;
|
|
173
|
+
|
|
174
|
+
const subscription = selectedObservable.subscribe((value) => {
|
|
175
|
+
if (value && !resolved) {
|
|
176
|
+
resolved = true;
|
|
162
177
|
done.resolve(value);
|
|
163
178
|
}
|
|
164
179
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
|
|
181
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
182
|
+
if (timeoutMs) {
|
|
183
|
+
timeoutId = setTimeout(() => {
|
|
184
|
+
if (!resolved) {
|
|
185
|
+
resolved = true;
|
|
186
|
+
subscription.unsubscribe();
|
|
187
|
+
done.reject(new Error(`waitUntilPresent timed out after ${timeoutMs}ms`));
|
|
188
|
+
}
|
|
189
|
+
}, timeoutMs);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const result = await done.promise;
|
|
194
|
+
return result;
|
|
195
|
+
} finally {
|
|
196
|
+
subscription.unsubscribe();
|
|
197
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
198
|
+
}
|
|
168
199
|
}
|
|
169
200
|
|
|
170
201
|
/**
|
|
@@ -175,6 +206,6 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
175
206
|
) {
|
|
176
207
|
const resultPromise = funcArg(this);
|
|
177
208
|
this.cumulativeDeferred.addPromise(resultPromise);
|
|
178
|
-
this.setState(await resultPromise);
|
|
209
|
+
await this.setState(await resultPromise);
|
|
179
210
|
}
|
|
180
211
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { StatePart } from './smartstate.classes.statepart';
|
|
2
|
-
/**
|
|
3
|
-
* A StatePartCollection is a collection of StateParts.
|
|
4
|
-
* It can be used for expressing interest in a certain set of StateParts.
|
|
5
|
-
*/
|
|
6
|
-
export declare class StatePartCollection<StatePartNameType, T> extends StatePart<StatePartNameType, T> {
|
|
7
|
-
constructor(nameArg: StatePartNameType);
|
|
8
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StatePartCollection = void 0;
|
|
4
|
-
const smartstate_classes_statepart_1 = require("./smartstate.classes.statepart");
|
|
5
|
-
/**
|
|
6
|
-
* A StatePartCollection is a collection of StateParts.
|
|
7
|
-
* It can be used for expressing interest in a certain set of StateParts.
|
|
8
|
-
*/
|
|
9
|
-
class StatePartCollection extends smartstate_classes_statepart_1.StatePart {
|
|
10
|
-
constructor(nameArg) {
|
|
11
|
-
super(nameArg);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
exports.StatePartCollection = StatePartCollection;
|
|
15
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRzdGF0ZS5jbGFzc2VzLnN0YXRlY29sbGVjdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0c3RhdGUuY2xhc3Nlcy5zdGF0ZWNvbGxlY3Rpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQ0EsaUZBQTJEO0FBRTNEOzs7R0FHRztBQUNILE1BQWEsbUJBQTBDLFNBQVEsd0NBQStCO0lBQzVGLFlBQVksT0FBMEI7UUFDcEMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ2pCLENBQUM7Q0FDRjtBQUpELGtEQUlDIn0=
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State observable observes a StatePart and notifies everyone interested
|
|
3
|
-
*/
|
|
4
|
-
export declare class StateObservable {
|
|
5
|
-
/**
|
|
6
|
-
* creates an observable from a StateCollection
|
|
7
|
-
*/
|
|
8
|
-
static fromStatePartCollection(filterArg?: () => any): void;
|
|
9
|
-
constructor();
|
|
10
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StateObservable = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* State observable observes a StatePart and notifies everyone interested
|
|
6
|
-
*/
|
|
7
|
-
class StateObservable {
|
|
8
|
-
/**
|
|
9
|
-
* creates an observable from a StateCollection
|
|
10
|
-
*/
|
|
11
|
-
static fromStatePartCollection(filterArg) { }
|
|
12
|
-
constructor() { }
|
|
13
|
-
}
|
|
14
|
-
exports.StateObservable = StateObservable;
|
|
15
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRzdGF0ZS5jbGFzc2VzLnN0YXRlb2JzZXJ2YWJsZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0c3RhdGUuY2xhc3Nlcy5zdGF0ZW9ic2VydmFibGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBRUE7O0dBRUc7QUFDSCxNQUFhLGVBQWU7SUFDMUI7O09BRUc7SUFDSSxNQUFNLENBQUMsdUJBQXVCLENBQUMsU0FBcUIsSUFBRyxDQUFDO0lBRS9ELGdCQUFlLENBQUM7Q0FDakI7QUFQRCwwQ0FPQyJ9
|