@push.rocks/smartstate 2.0.26 → 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.
@@ -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
- return this.createStatePart<PayloadType>(statePartNameArg, initialArg, initMode);
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
- return this.createStatePart<PayloadType>(statePartNameArg, initialArg, initMode);
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
- await newState.setState({
80
- ...currentState,
81
- ...initialPayloadArg,
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
- this.stateStore = newStateArg;
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
 
@@ -79,7 +83,7 @@ export class StatePart<TStatePartName, TStatePayload> {
79
83
  return;
80
84
  }
81
85
  const createStateHash = async (stateArg: any) => {
82
- return await plugins.smarthashWeb.sha256FromString(plugins.smartjson.stringify(stateArg));
86
+ return await plugins.smarthashWeb.sha256FromString(plugins.smartjson.stableOneWayStringify(stateArg));
83
87
  };
84
88
  const currentHash = await createStateHash(this.stateStore);
85
89
  if (
@@ -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
- // TODO: check viability
102
- setTimeout(async () => {
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
- // Nothing here
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
- const subscription = selectedObservable.subscribe(async (value) => {
161
- if (value) {
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
- const result = await done.promise;
166
- subscription.unsubscribe();
167
- return result;
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