@hkdigital/lib-core 0.5.87 → 0.5.90

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.
@@ -0,0 +1,152 @@
1
+ export default class ReactiveDataStore {
2
+ /**
3
+ * Constructor
4
+ *
5
+ * @param {Object} [options]
6
+ * @param {Record<string, any>} [options.initialData={}]
7
+ * Initial key-value pairs
8
+ * @param {boolean} [options.strictMode=true]
9
+ * Throw error when accessing uninitialized keys
10
+ * @param {boolean} [options.productionGuard=false]
11
+ * Dev-only mode: no-op on SET, throw on GET in production
12
+ * @param {string} [options.errorPrefix='Data key']
13
+ * Prefix for error messages
14
+ */
15
+ constructor({ initialData, strictMode, productionGuard, errorPrefix }?: {
16
+ initialData?: Record<string, any> | undefined;
17
+ strictMode?: boolean | undefined;
18
+ productionGuard?: boolean | undefined;
19
+ errorPrefix?: string | undefined;
20
+ });
21
+ /**
22
+ * Set a data property value
23
+ *
24
+ * Automatically reactive - effects watching this key will re-run.
25
+ * Uses fine-grained reactivity via SvelteMap.
26
+ *
27
+ * With productionGuard: silent no-op in production (safe to call)
28
+ *
29
+ * @param {string} key - Property key
30
+ * @param {any} value - Property value
31
+ *
32
+ * @example
33
+ * ```javascript
34
+ * store.set('score', 100);
35
+ * store.set('playerName', 'Alice');
36
+ * ```
37
+ */
38
+ set(key: string, value: any): void;
39
+ /**
40
+ * Get a data property value
41
+ *
42
+ * Automatically reactive - creates a dependency on this specific key.
43
+ * The effect will only re-run when THIS key changes.
44
+ *
45
+ * With strictMode: throws if key is not initialized
46
+ * With productionGuard: throws in production (programming error)
47
+ *
48
+ * @param {string} key - Property key
49
+ *
50
+ * @returns {any} Property value
51
+ *
52
+ * @throws {Error} If key not initialized (strictMode)
53
+ * @throws {Error} If accessed in production (productionGuard)
54
+ *
55
+ * @example
56
+ * ```javascript
57
+ * // Reactive - re-runs only when 'score' changes
58
+ * $effect(() => {
59
+ * const score = store.get('score');
60
+ * console.log('Score:', score);
61
+ * });
62
+ * ```
63
+ */
64
+ get(key: string): any;
65
+ /**
66
+ * Get all data properties as plain object
67
+ *
68
+ * Note: Returns a snapshot (plain object), not reactive.
69
+ * Use for serialization or inspection, not for reactive tracking.
70
+ *
71
+ * @returns {Record<string, any>} Plain object with all data
72
+ *
73
+ * @example
74
+ * ```javascript
75
+ * const allData = store.getAll();
76
+ * await saveToServer(allData);
77
+ * ```
78
+ */
79
+ getAll(): Record<string, any>;
80
+ /**
81
+ * Update multiple data properties at once
82
+ *
83
+ * Each property update triggers fine-grained reactivity.
84
+ * Only effects watching the specific changed keys will re-run.
85
+ *
86
+ * @param {Record<string, any>} updates
87
+ * Object with key-value pairs to update
88
+ *
89
+ * @example
90
+ * ```javascript
91
+ * store.update({
92
+ * score: 100,
93
+ * level: 5,
94
+ * completed: true
95
+ * });
96
+ * ```
97
+ */
98
+ update(updates: Record<string, any>): void;
99
+ /**
100
+ * Delete a data property
101
+ *
102
+ * @param {string} key - Property key to delete
103
+ *
104
+ * @returns {boolean} True if the key existed and was deleted
105
+ *
106
+ * @example
107
+ * ```javascript
108
+ * store.delete('temporaryFlag');
109
+ * ```
110
+ */
111
+ delete(key: string): boolean;
112
+ /**
113
+ * Check if data property exists
114
+ *
115
+ * With productionGuard: throws in production (same as get)
116
+ *
117
+ * @param {string} key - Property key to check
118
+ *
119
+ * @returns {boolean} True if the key exists
120
+ *
121
+ * @throws {Error} If accessed in production (productionGuard)
122
+ *
123
+ * @example
124
+ * ```javascript
125
+ * if (store.has('tutorialSeen')) {
126
+ * // Skip tutorial
127
+ * }
128
+ * ```
129
+ */
130
+ has(key: string): boolean;
131
+ /**
132
+ * Clear all data properties
133
+ *
134
+ * @example
135
+ * ```javascript
136
+ * store.clear(); // Reset all data
137
+ * ```
138
+ */
139
+ clear(): void;
140
+ /**
141
+ * Get number of data properties
142
+ *
143
+ * @returns {number} Number of data entries
144
+ *
145
+ * @example
146
+ * ```javascript
147
+ * console.log(`Store has ${store.size} entries`);
148
+ * ```
149
+ */
150
+ get size(): number;
151
+ #private;
152
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Reactive key-value data store with fine-grained reactivity
3
+ *
4
+ * Built on SvelteMap for fine-grained reactivity where effects only re-run
5
+ * when the specific keys they access change.
6
+ *
7
+ * Features:
8
+ * - Fine-grained reactivity using SvelteMap
9
+ * - Strict mode: throws on access to uninitialized keys
10
+ * - Production guard: dev-only data throws on read in production
11
+ * - Initialization from plain object
12
+ *
13
+ * @example
14
+ * ```javascript
15
+ * // Regular data store
16
+ * const store = new ReactiveDataStore({
17
+ * initialData: { score: 0, level: 1 }
18
+ * });
19
+ *
20
+ * store.set('score', 100);
21
+ * const score = store.get('score'); // Reactive access
22
+ *
23
+ * // Dev-only data store
24
+ * const devStore = new ReactiveDataStore({
25
+ * initialData: { autoNav: false },
26
+ * productionGuard: true,
27
+ * errorPrefix: 'Dev data key'
28
+ * });
29
+ *
30
+ * devStore.set('autoNav', true); // No-op in production
31
+ * devStore.get('autoNav'); // Throws in production
32
+ * ```
33
+ */
34
+
35
+ import { SvelteMap } from 'svelte/reactivity';
36
+ import { dev } from '$app/environment';
37
+
38
+ export default class ReactiveDataStore {
39
+ /**
40
+ * Internal reactive map
41
+ * @type {SvelteMap<string, any>}
42
+ */
43
+ #data;
44
+
45
+ /**
46
+ * Throw on access to uninitialized keys
47
+ * @type {boolean}
48
+ */
49
+ #strictMode;
50
+
51
+ /**
52
+ * Guard against production access (for dev-only data)
53
+ * @type {boolean}
54
+ */
55
+ #productionGuard;
56
+
57
+ /**
58
+ * Prefix for error messages
59
+ * @type {string}
60
+ */
61
+ #errorPrefix;
62
+
63
+ /**
64
+ * Constructor
65
+ *
66
+ * @param {Object} [options]
67
+ * @param {Record<string, any>} [options.initialData={}]
68
+ * Initial key-value pairs
69
+ * @param {boolean} [options.strictMode=true]
70
+ * Throw error when accessing uninitialized keys
71
+ * @param {boolean} [options.productionGuard=false]
72
+ * Dev-only mode: no-op on SET, throw on GET in production
73
+ * @param {string} [options.errorPrefix='Data key']
74
+ * Prefix for error messages
75
+ */
76
+ constructor({
77
+ initialData = {},
78
+ strictMode = true,
79
+ productionGuard = false,
80
+ errorPrefix = 'Data key'
81
+ } = {}) {
82
+ this.#strictMode = strictMode;
83
+ this.#productionGuard = productionGuard;
84
+ this.#errorPrefix = errorPrefix;
85
+
86
+ // Initialize map
87
+ this.#data = new SvelteMap();
88
+
89
+ // Only populate initial data in dev mode if guard is enabled
90
+ if (!productionGuard || dev) {
91
+ for (const [key, value] of Object.entries(initialData)) {
92
+ this.#data.set(key, value);
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Set a data property value
99
+ *
100
+ * Automatically reactive - effects watching this key will re-run.
101
+ * Uses fine-grained reactivity via SvelteMap.
102
+ *
103
+ * With productionGuard: silent no-op in production (safe to call)
104
+ *
105
+ * @param {string} key - Property key
106
+ * @param {any} value - Property value
107
+ *
108
+ * @example
109
+ * ```javascript
110
+ * store.set('score', 100);
111
+ * store.set('playerName', 'Alice');
112
+ * ```
113
+ */
114
+ set(key, value) {
115
+ // Production guard: no-op silently (safe to call conditionally)
116
+ if (this.#productionGuard && !dev) {
117
+ return;
118
+ }
119
+
120
+ this.#data.set(key, value);
121
+ }
122
+
123
+ /**
124
+ * Get a data property value
125
+ *
126
+ * Automatically reactive - creates a dependency on this specific key.
127
+ * The effect will only re-run when THIS key changes.
128
+ *
129
+ * With strictMode: throws if key is not initialized
130
+ * With productionGuard: throws in production (programming error)
131
+ *
132
+ * @param {string} key - Property key
133
+ *
134
+ * @returns {any} Property value
135
+ *
136
+ * @throws {Error} If key not initialized (strictMode)
137
+ * @throws {Error} If accessed in production (productionGuard)
138
+ *
139
+ * @example
140
+ * ```javascript
141
+ * // Reactive - re-runs only when 'score' changes
142
+ * $effect(() => {
143
+ * const score = store.get('score');
144
+ * console.log('Score:', score);
145
+ * });
146
+ * ```
147
+ */
148
+ get(key) {
149
+ // Production guard: THROW on read in production
150
+ if (this.#productionGuard && !dev) {
151
+ throw new Error(
152
+ `${this.#errorPrefix} "${key}" accessed in production. ` +
153
+ `This data is only available in development mode.`
154
+ );
155
+ }
156
+
157
+ // Strict mode: validate key exists
158
+ if (this.#strictMode && !this.#data.has(key)) {
159
+ throw new Error(
160
+ `${this.#errorPrefix} "${key}" is not initialized.`
161
+ );
162
+ }
163
+
164
+ return this.#data.get(key);
165
+ }
166
+
167
+ /**
168
+ * Get all data properties as plain object
169
+ *
170
+ * Note: Returns a snapshot (plain object), not reactive.
171
+ * Use for serialization or inspection, not for reactive tracking.
172
+ *
173
+ * @returns {Record<string, any>} Plain object with all data
174
+ *
175
+ * @example
176
+ * ```javascript
177
+ * const allData = store.getAll();
178
+ * await saveToServer(allData);
179
+ * ```
180
+ */
181
+ getAll() {
182
+ if (this.#productionGuard && !dev) {
183
+ return {};
184
+ }
185
+
186
+ return Object.fromEntries(this.#data);
187
+ }
188
+
189
+ /**
190
+ * Update multiple data properties at once
191
+ *
192
+ * Each property update triggers fine-grained reactivity.
193
+ * Only effects watching the specific changed keys will re-run.
194
+ *
195
+ * @param {Record<string, any>} updates
196
+ * Object with key-value pairs to update
197
+ *
198
+ * @example
199
+ * ```javascript
200
+ * store.update({
201
+ * score: 100,
202
+ * level: 5,
203
+ * completed: true
204
+ * });
205
+ * ```
206
+ */
207
+ update(updates) {
208
+ if (this.#productionGuard && !dev) {
209
+ return;
210
+ }
211
+
212
+ for (const [key, value] of Object.entries(updates)) {
213
+ this.#data.set(key, value);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Delete a data property
219
+ *
220
+ * @param {string} key - Property key to delete
221
+ *
222
+ * @returns {boolean} True if the key existed and was deleted
223
+ *
224
+ * @example
225
+ * ```javascript
226
+ * store.delete('temporaryFlag');
227
+ * ```
228
+ */
229
+ delete(key) {
230
+ if (this.#productionGuard && !dev) {
231
+ return false;
232
+ }
233
+
234
+ return this.#data.delete(key);
235
+ }
236
+
237
+ /**
238
+ * Check if data property exists
239
+ *
240
+ * With productionGuard: throws in production (same as get)
241
+ *
242
+ * @param {string} key - Property key to check
243
+ *
244
+ * @returns {boolean} True if the key exists
245
+ *
246
+ * @throws {Error} If accessed in production (productionGuard)
247
+ *
248
+ * @example
249
+ * ```javascript
250
+ * if (store.has('tutorialSeen')) {
251
+ * // Skip tutorial
252
+ * }
253
+ * ```
254
+ */
255
+ has(key) {
256
+ // Production guard: THROW on read in production
257
+ if (this.#productionGuard && !dev) {
258
+ throw new Error(
259
+ `${this.#errorPrefix} "${key}" existence check in production. ` +
260
+ `This data is only available in development mode.`
261
+ );
262
+ }
263
+
264
+ return this.#data.has(key);
265
+ }
266
+
267
+ /**
268
+ * Clear all data properties
269
+ *
270
+ * @example
271
+ * ```javascript
272
+ * store.clear(); // Reset all data
273
+ * ```
274
+ */
275
+ clear() {
276
+ if (this.#productionGuard && !dev) {
277
+ return;
278
+ }
279
+
280
+ this.#data.clear();
281
+ }
282
+
283
+ /**
284
+ * Get number of data properties
285
+ *
286
+ * @returns {number} Number of data entries
287
+ *
288
+ * @example
289
+ * ```javascript
290
+ * console.log(`Store has ${store.size} entries`);
291
+ * ```
292
+ */
293
+ get size() {
294
+ if (this.#productionGuard && !dev) {
295
+ return 0;
296
+ }
297
+
298
+ return this.#data.size;
299
+ }
300
+ }
@@ -1 +1,2 @@
1
1
  export { default as SubscribersCount } from "./classes/subscribers-count/SubscribersCount.js";
2
+ export { default as ReactiveDataStore } from "./classes/reactive-data-store/ReactiveDataStore.svelte.js";
@@ -1 +1,2 @@
1
1
  export { default as SubscribersCount } from './classes/subscribers-count/SubscribersCount.js';
2
+ export { default as ReactiveDataStore } from './classes/reactive-data-store/ReactiveDataStore.svelte.js';
@@ -9,6 +9,8 @@ export default class PageMachine {
9
9
  * Optional list of valid routes (for validation/dev tools)
10
10
  * @param {Record<string, any>} [config.initialData={}]
11
11
  * Initial data properties (use KEY_ constants for keys)
12
+ * @param {Record<string, any>} [config.initialDevData={}]
13
+ * Initial dev data properties (use KEY_DEV_ constants for keys)
12
14
  * @param {import('../../../logging/client.js').Logger} [config.logger]
13
15
  * Logger instance (optional)
14
16
  *
@@ -17,6 +19,7 @@ export default class PageMachine {
17
19
  * const ROUTE_INTRO = '/intro/start';
18
20
  * const KEY_INTRO_COMPLETED = 'intro-completed';
19
21
  * const KEY_SCORE = 'score';
22
+ * const KEY_DEV_AUTO_NAVIGATION = 'dev-auto-navigation';
20
23
  *
21
24
  * const machine = new PageMachine({
22
25
  * startPath: ROUTE_INTRO,
@@ -24,14 +27,18 @@ export default class PageMachine {
24
27
  * initialData: {
25
28
  * [KEY_INTRO_COMPLETED]: false,
26
29
  * [KEY_SCORE]: 0
30
+ * },
31
+ * initialDevData: {
32
+ * [KEY_DEV_AUTO_NAVIGATION]: false
27
33
  * }
28
34
  * });
29
35
  * ```
30
36
  */
31
- constructor({ startPath, routes, initialData, logger }: {
37
+ constructor({ startPath, routes, initialData, initialDevData, logger }: {
32
38
  startPath: string;
33
39
  routes?: string[] | undefined;
34
40
  initialData?: Record<string, any> | undefined;
41
+ initialDevData?: Record<string, any> | undefined;
35
42
  logger?: import("../../../logging/client.js").Logger | undefined;
36
43
  });
37
44
  /**
@@ -63,132 +70,69 @@ export default class PageMachine {
63
70
  */
64
71
  get routes(): string[];
65
72
  /**
66
- * Set a data property value
73
+ * Get the reactive data store
67
74
  *
68
- * Automatically reactive - effects watching this key will re-run.
69
- * Uses fine-grained reactivity, so only effects watching this specific
70
- * key will be triggered.
75
+ * Provides read-only access to the data store instance.
76
+ * Access all data methods through this property.
71
77
  *
72
- * @param {string} key - Property key (use KEY_ constant)
73
- * @param {any} value - Property value
74
- *
75
- * @example
76
- * ```javascript
77
- * const KEY_HAS_STRONG_PROFILE = 'has-strong-profile';
78
- * const KEY_PROFILE_SCORE = 'profile-score';
79
- *
80
- * machine.setData(KEY_HAS_STRONG_PROFILE, true);
81
- * machine.setData(KEY_PROFILE_SCORE, 85);
82
- * ```
83
- */
84
- setData(key: string, value: any): void;
85
- /**
86
- * Get a data property value
87
- *
88
- * Automatically reactive - creates a dependency on this specific key.
89
- * The effect will only re-run when THIS key changes, not when other
90
- * keys change.
91
- *
92
- * @param {string} key - Property key (use KEY_ constant)
93
- *
94
- * @returns {any} Property value or undefined
78
+ * @returns {ReactiveDataStore} The data store
95
79
  *
96
80
  * @example
97
81
  * ```javascript
98
82
  * const KEY_SCORE = 'score';
99
83
  *
100
- * // Reactive - re-runs only when KEY_SCORE changes
84
+ * // Set data
85
+ * machine.data.set(KEY_SCORE, 100);
86
+ *
87
+ * // Get data (reactive)
101
88
  * $effect(() => {
102
- * const score = machine.getData(KEY_SCORE);
89
+ * const score = machine.data.get(KEY_SCORE);
103
90
  * console.log('Score:', score);
104
91
  * });
105
- * ```
106
- */
107
- getData(key: string): any;
108
- /**
109
- * Get all data properties as plain object
110
- *
111
- * Note: This returns a snapshot (plain object), not a reactive map.
112
- * Use this for serialization or server sync, not for reactive tracking.
113
92
  *
114
- * @returns {Record<string, any>} Plain object with all data
115
- *
116
- * @example
117
- * ```javascript
118
- * const allData = machine.getAllData();
119
- * await playerService.saveData(allData);
93
+ * // Other operations
94
+ * machine.data.update({ [KEY_SCORE]: 200 });
95
+ * machine.data.has(KEY_SCORE);
96
+ * machine.data.delete(KEY_SCORE);
97
+ * machine.data.clear();
98
+ * console.log(machine.data.size);
120
99
  * ```
121
100
  */
122
- getAllData(): Record<string, any>;
101
+ get data(): ReactiveDataStore;
123
102
  /**
124
- * Update multiple data properties at once
103
+ * Get the reactive dev data store
125
104
  *
126
- * Each property update triggers fine-grained reactivity.
105
+ * Provides read-only access to the dev data store instance.
106
+ * Access all dev data methods through this property.
127
107
  *
128
- * @param {Record<string, any>} dataUpdates
129
- * Object with key-value pairs (use KEY_ constants for keys)
108
+ * Dev data is only available in development mode. In production:
109
+ * - SET operations are silent no-ops
110
+ * - GET/HAS operations throw errors (programming errors)
130
111
  *
131
- * @example
132
- * ```javascript
133
- * const KEY_HAS_STRONG_PROFILE = 'has-strong-profile';
134
- * const KEY_PROFILE_SCORE = 'profile-score';
135
- * const KEY_MATCHED_SECTOR = 'matched-sector';
136
- *
137
- * machine.updateData({
138
- * [KEY_HAS_STRONG_PROFILE]: true,
139
- * [KEY_PROFILE_SCORE]: 85,
140
- * [KEY_MATCHED_SECTOR]: 'technology'
141
- * });
142
- * ```
143
- */
144
- updateData(dataUpdates: Record<string, any>): void;
145
- /**
146
- * Delete a data property
147
- *
148
- * @param {string} key - Property key to delete (use KEY_ constant)
149
- *
150
- * @returns {boolean} True if the key existed and was deleted
112
+ * @returns {ReactiveDataStore} The dev data store
151
113
  *
152
114
  * @example
153
115
  * ```javascript
154
- * const KEY_TEMPORARY_FLAG = 'temporary-flag';
116
+ * const KEY_DEV_AUTO_NAV = 'dev-auto-navigation';
155
117
  *
156
- * machine.deleteData(KEY_TEMPORARY_FLAG);
157
- * ```
158
- */
159
- deleteData(key: string): boolean;
160
- /**
161
- * Check if data property exists
162
- *
163
- * @param {string} key - Property key to check (use KEY_ constant)
164
- *
165
- * @returns {boolean} True if the key exists
166
- *
167
- * @example
168
- * ```javascript
169
- * const KEY_TUTORIAL_SEEN = 'tutorial-seen';
118
+ * // Set dev data (no-op in production)
119
+ * machine.devData.set(KEY_DEV_AUTO_NAV, true);
170
120
  *
171
- * if (machine.hasData(KEY_TUTORIAL_SEEN)) {
172
- * // Skip tutorial
173
- * }
174
- * ```
175
- */
176
- hasData(key: string): boolean;
177
- /**
178
- * Clear all data properties
121
+ * // Get dev data (throws in production)
122
+ * $effect(() => {
123
+ * const autoNav = machine.devData.get(KEY_DEV_AUTO_NAV);
124
+ * console.log('Auto-nav:', autoNav);
125
+ * });
179
126
  *
180
- * @example
181
- * ```javascript
182
- * machine.clearData(); // Reset all game data
127
+ * // Other operations
128
+ * machine.devData.update({ [KEY_DEV_AUTO_NAV]: false });
129
+ * machine.devData.has(KEY_DEV_AUTO_NAV);
130
+ * machine.devData.delete(KEY_DEV_AUTO_NAV);
131
+ * machine.devData.clear();
132
+ * console.log(machine.devData.size);
183
133
  * ```
184
134
  */
185
- clearData(): void;
186
- /**
187
- * Get number of data properties
188
- *
189
- * @returns {number} Number of data entries
190
- */
191
- get dataSize(): number;
135
+ get devData(): ReactiveDataStore;
192
136
  /**
193
137
  * Check if a route has been visited
194
138
  *
@@ -293,3 +237,4 @@ export default class PageMachine {
293
237
  redirectToStartPath(): void;
294
238
  #private;
295
239
  }
240
+ import { ReactiveDataStore } from '../../classes.js';
@@ -8,12 +8,14 @@
8
8
  * - Current route tracking and synchronization
9
9
  * - Start path management
10
10
  * - Data properties for business/domain state (using SvelteMap)
11
+ * - Dev data properties for dev-mode helpers (using SvelteMap)
11
12
  * - Visited routes tracking (using SvelteSet)
12
13
  * - Fine-grained reactivity without manual revision tracking
13
14
  *
14
15
  * Best practices:
15
16
  * - Use constants for routes: `const ROUTE_INTRO = '/intro/start'`
16
17
  * - Use KEY_ constants for data: `const KEY_SCORE = 'score'`
18
+ * - Use KEY_DEV_ constants for dev data: `const KEY_DEV_AUTO_NAV = 'dev-auto-navigation'`
17
19
  *
18
20
  * Basic usage:
19
21
  * ```javascript
@@ -21,6 +23,7 @@
21
23
  * const ROUTE_INTRO = '/intro/start';
22
24
  * const KEY_SCORE = 'score';
23
25
  * const KEY_TUTORIAL_SEEN = 'tutorial-seen';
26
+ * const KEY_DEV_AUTO_NAVIGATION = 'dev-auto-navigation';
24
27
  *
25
28
  * const machine = new PageMachine({
26
29
  * startPath: ROUTE_INTRO,
@@ -48,6 +51,10 @@
48
51
  * const score = machine.getData(KEY_SCORE);
49
52
  * console.log('Score changed:', score);
50
53
  * });
54
+ *
55
+ * // Dev-mode helpers (only available in dev)
56
+ * machine.setDevData(KEY_DEV_AUTO_NAVIGATION, true);
57
+ * const autoNavEnabled = machine.getDevData(KEY_DEV_AUTO_NAVIGATION);
51
58
  * ```
52
59
  *
53
60
  * Animations and page-specific logic should use $effect in pages:
@@ -62,7 +69,8 @@
62
69
  * });
63
70
  * ```
64
71
  */
65
- import { SvelteMap, SvelteSet } from 'svelte/reactivity';
72
+ import { SvelteSet } from 'svelte/reactivity';
73
+ import { ReactiveDataStore } from '../../classes.js';
66
74
 
67
75
  export default class PageMachine {
68
76
  /**
@@ -91,12 +99,17 @@ export default class PageMachine {
91
99
  #routes = [];
92
100
 
93
101
  /**
94
- * Reactive map for business/domain data
95
- * Uses SvelteMap for fine-grained reactivity
96
- * @type {SvelteMap<string, any>}
102
+ * Reactive data store for business/domain data
103
+ * @type {ReactiveDataStore}
97
104
  */
98
105
  #data;
99
106
 
107
+ /**
108
+ * Reactive data store for dev-mode helper data
109
+ * @type {ReactiveDataStore}
110
+ */
111
+ #devData;
112
+
100
113
  /**
101
114
  * Reactive set for visited routes
102
115
  * Uses SvelteSet for automatic reactivity
@@ -114,6 +127,8 @@ export default class PageMachine {
114
127
  * Optional list of valid routes (for validation/dev tools)
115
128
  * @param {Record<string, any>} [config.initialData={}]
116
129
  * Initial data properties (use KEY_ constants for keys)
130
+ * @param {Record<string, any>} [config.initialDevData={}]
131
+ * Initial dev data properties (use KEY_DEV_ constants for keys)
117
132
  * @param {import('../../../logging/client.js').Logger} [config.logger]
118
133
  * Logger instance (optional)
119
134
  *
@@ -122,6 +137,7 @@ export default class PageMachine {
122
137
  * const ROUTE_INTRO = '/intro/start';
123
138
  * const KEY_INTRO_COMPLETED = 'intro-completed';
124
139
  * const KEY_SCORE = 'score';
140
+ * const KEY_DEV_AUTO_NAVIGATION = 'dev-auto-navigation';
125
141
  *
126
142
  * const machine = new PageMachine({
127
143
  * startPath: ROUTE_INTRO,
@@ -129,11 +145,14 @@ export default class PageMachine {
129
145
  * initialData: {
130
146
  * [KEY_INTRO_COMPLETED]: false,
131
147
  * [KEY_SCORE]: 0
148
+ * },
149
+ * initialDevData: {
150
+ * [KEY_DEV_AUTO_NAVIGATION]: false
132
151
  * }
133
152
  * });
134
153
  * ```
135
154
  */
136
- constructor({ startPath, routes = [], initialData = {}, logger = null }) {
155
+ constructor({ startPath, routes = [], initialData = {}, initialDevData = {}, logger = null }) {
137
156
  if (!startPath) {
138
157
  throw new Error('PageMachine requires startPath parameter');
139
158
  }
@@ -144,13 +163,17 @@ export default class PageMachine {
144
163
  this.#current = startPath;
145
164
 
146
165
  // Initialize reactive data structures
147
- this.#data = new SvelteMap();
148
- this.#visitedRoutes = new SvelteSet();
166
+ this.#data = new ReactiveDataStore({
167
+ initialData
168
+ });
149
169
 
150
- // Populate initial data
151
- for (const [key, value] of Object.entries(initialData)) {
152
- this.#data.set(key, value);
153
- }
170
+ this.#devData = new ReactiveDataStore({
171
+ initialData: initialDevData,
172
+ productionGuard: true,
173
+ errorPrefix: 'Dev data key'
174
+ });
175
+
176
+ this.#visitedRoutes = new SvelteSet();
154
177
 
155
178
  // Mark start path as visited
156
179
  this.#visitedRoutes.add(startPath);
@@ -235,156 +258,75 @@ export default class PageMachine {
235
258
  /* ===== Data Properties (Business/Domain State) ===== */
236
259
 
237
260
  /**
238
- * Set a data property value
261
+ * Get the reactive data store
239
262
  *
240
- * Automatically reactive - effects watching this key will re-run.
241
- * Uses fine-grained reactivity, so only effects watching this specific
242
- * key will be triggered.
263
+ * Provides read-only access to the data store instance.
264
+ * Access all data methods through this property.
243
265
  *
244
- * @param {string} key - Property key (use KEY_ constant)
245
- * @param {any} value - Property value
246
- *
247
- * @example
248
- * ```javascript
249
- * const KEY_HAS_STRONG_PROFILE = 'has-strong-profile';
250
- * const KEY_PROFILE_SCORE = 'profile-score';
251
- *
252
- * machine.setData(KEY_HAS_STRONG_PROFILE, true);
253
- * machine.setData(KEY_PROFILE_SCORE, 85);
254
- * ```
255
- */
256
- setData(key, value) {
257
- this.#data.set(key, value);
258
- }
259
-
260
- /**
261
- * Get a data property value
262
- *
263
- * Automatically reactive - creates a dependency on this specific key.
264
- * The effect will only re-run when THIS key changes, not when other
265
- * keys change.
266
- *
267
- * @param {string} key - Property key (use KEY_ constant)
268
- *
269
- * @returns {any} Property value or undefined
266
+ * @returns {ReactiveDataStore} The data store
270
267
  *
271
268
  * @example
272
269
  * ```javascript
273
270
  * const KEY_SCORE = 'score';
274
271
  *
275
- * // Reactive - re-runs only when KEY_SCORE changes
272
+ * // Set data
273
+ * machine.data.set(KEY_SCORE, 100);
274
+ *
275
+ * // Get data (reactive)
276
276
  * $effect(() => {
277
- * const score = machine.getData(KEY_SCORE);
277
+ * const score = machine.data.get(KEY_SCORE);
278
278
  * console.log('Score:', score);
279
279
  * });
280
- * ```
281
- */
282
- getData(key) {
283
- return this.#data.get(key);
284
- }
285
-
286
- /**
287
- * Get all data properties as plain object
288
- *
289
- * Note: This returns a snapshot (plain object), not a reactive map.
290
- * Use this for serialization or server sync, not for reactive tracking.
291
280
  *
292
- * @returns {Record<string, any>} Plain object with all data
293
- *
294
- * @example
295
- * ```javascript
296
- * const allData = machine.getAllData();
297
- * await playerService.saveData(allData);
281
+ * // Other operations
282
+ * machine.data.update({ [KEY_SCORE]: 200 });
283
+ * machine.data.has(KEY_SCORE);
284
+ * machine.data.delete(KEY_SCORE);
285
+ * machine.data.clear();
286
+ * console.log(machine.data.size);
298
287
  * ```
299
288
  */
300
- getAllData() {
301
- return Object.fromEntries(this.#data);
289
+ get data() {
290
+ return this.#data;
302
291
  }
303
292
 
304
- /**
305
- * Update multiple data properties at once
306
- *
307
- * Each property update triggers fine-grained reactivity.
308
- *
309
- * @param {Record<string, any>} dataUpdates
310
- * Object with key-value pairs (use KEY_ constants for keys)
311
- *
312
- * @example
313
- * ```javascript
314
- * const KEY_HAS_STRONG_PROFILE = 'has-strong-profile';
315
- * const KEY_PROFILE_SCORE = 'profile-score';
316
- * const KEY_MATCHED_SECTOR = 'matched-sector';
317
- *
318
- * machine.updateData({
319
- * [KEY_HAS_STRONG_PROFILE]: true,
320
- * [KEY_PROFILE_SCORE]: 85,
321
- * [KEY_MATCHED_SECTOR]: 'technology'
322
- * });
323
- * ```
324
- */
325
- updateData(dataUpdates) {
326
- for (const [key, value] of Object.entries(dataUpdates)) {
327
- this.#data.set(key, value);
328
- }
329
- }
293
+ /* ===== Dev Data Properties (Dev-Mode Helpers) ===== */
330
294
 
331
295
  /**
332
- * Delete a data property
333
- *
334
- * @param {string} key - Property key to delete (use KEY_ constant)
296
+ * Get the reactive dev data store
335
297
  *
336
- * @returns {boolean} True if the key existed and was deleted
337
- *
338
- * @example
339
- * ```javascript
340
- * const KEY_TEMPORARY_FLAG = 'temporary-flag';
341
- *
342
- * machine.deleteData(KEY_TEMPORARY_FLAG);
343
- * ```
344
- */
345
- deleteData(key) {
346
- return this.#data.delete(key);
347
- }
348
-
349
- /**
350
- * Check if data property exists
298
+ * Provides read-only access to the dev data store instance.
299
+ * Access all dev data methods through this property.
351
300
  *
352
- * @param {string} key - Property key to check (use KEY_ constant)
301
+ * Dev data is only available in development mode. In production:
302
+ * - SET operations are silent no-ops
303
+ * - GET/HAS operations throw errors (programming errors)
353
304
  *
354
- * @returns {boolean} True if the key exists
305
+ * @returns {ReactiveDataStore} The dev data store
355
306
  *
356
307
  * @example
357
308
  * ```javascript
358
- * const KEY_TUTORIAL_SEEN = 'tutorial-seen';
309
+ * const KEY_DEV_AUTO_NAV = 'dev-auto-navigation';
359
310
  *
360
- * if (machine.hasData(KEY_TUTORIAL_SEEN)) {
361
- * // Skip tutorial
362
- * }
363
- * ```
364
- */
365
- hasData(key) {
366
- return this.#data.has(key);
367
- }
368
-
369
- /**
370
- * Clear all data properties
311
+ * // Set dev data (no-op in production)
312
+ * machine.devData.set(KEY_DEV_AUTO_NAV, true);
371
313
  *
372
- * @example
373
- * ```javascript
374
- * machine.clearData(); // Reset all game data
375
- * ```
376
- */
377
- clearData() {
378
- this.#data.clear();
379
- }
380
-
381
- /**
382
- * Get number of data properties
314
+ * // Get dev data (throws in production)
315
+ * $effect(() => {
316
+ * const autoNav = machine.devData.get(KEY_DEV_AUTO_NAV);
317
+ * console.log('Auto-nav:', autoNav);
318
+ * });
383
319
  *
384
- * @returns {number} Number of data entries
320
+ * // Other operations
321
+ * machine.devData.update({ [KEY_DEV_AUTO_NAV]: false });
322
+ * machine.devData.has(KEY_DEV_AUTO_NAV);
323
+ * machine.devData.delete(KEY_DEV_AUTO_NAV);
324
+ * machine.devData.clear();
325
+ * console.log(machine.devData.size);
326
+ * ```
385
327
  */
386
- get dataSize() {
387
- return this.#data.size;
328
+ get devData() {
329
+ return this.#devData;
388
330
  }
389
331
 
390
332
  /* ===== Visited Routes Tracking ===== */
@@ -78,8 +78,12 @@ const KEY_TUTORIAL_SEEN = 'tutorial-seen';
78
78
  const KEY_HIGHEST_LEVEL = 'highest-level';
79
79
  const KEY_DIFFICULTY = 'difficulty';
80
80
 
81
+ // Dev data keys (use KEY_DEV_ prefix)
82
+ const KEY_DEV_AUTO_NAVIGATION = 'dev-auto-navigation';
83
+ const KEY_DEV_SKIP_ANIMATIONS = 'dev-skip-animations';
84
+
81
85
  export class PuzzleState extends PageMachine {
82
- #gameLogic;
86
+ #logic;
83
87
 
84
88
  constructor() {
85
89
  // Call PageMachine constructor with route config
@@ -96,14 +100,18 @@ export class PuzzleState extends PageMachine {
96
100
  [KEY_TUTORIAL_SEEN]: false,
97
101
  [KEY_HIGHEST_LEVEL]: 1,
98
102
  [KEY_DIFFICULTY]: 'normal'
103
+ },
104
+ initialDevData: {
105
+ [KEY_DEV_AUTO_NAVIGATION]: false,
106
+ [KEY_DEV_SKIP_ANIMATIONS]: false
99
107
  }
100
108
  });
101
109
 
102
- this.#gameLogic = new PuzzleGameLogic();
110
+ this.#logic = new PuzzleGameLogic();
103
111
  }
104
112
 
105
- get gameLogic() {
106
- return this.#gameLogic;
113
+ get logic() {
114
+ return this.#logic;
107
115
  }
108
116
 
109
117
  // Computed properties for convenience
@@ -161,7 +169,7 @@ export class PuzzleState extends PageMachine {
161
169
  }
162
170
 
163
171
  reset() {
164
- this.#gameLogic = new PuzzleGameLogic();
172
+ this.#logic = new PuzzleGameLogic();
165
173
  }
166
174
  }
167
175
 
@@ -209,12 +217,12 @@ export const [createOrGetPuzzleState, createPuzzleState, getPuzzleState] =
209
217
  {:else if puzzleState.isOnTutorial}
210
218
  <TutorialView />
211
219
  {:else if puzzleState.isOnLevel1}
212
- <Level1View gameLogic={puzzleState.gameLogic} />
220
+ <Level1View gameLogic={puzzleState.logic} />
213
221
  {:else if puzzleState.isOnLevel2}
214
- <Level2View gameLogic={puzzleState.gameLogic} />
222
+ <Level2View gameLogic={puzzleState.logic} />
215
223
  {:else if puzzleState.isComplete}
216
224
  <CompleteView
217
- score={puzzleState.gameLogic.score}
225
+ score={puzzleState.logic.score}
218
226
  highestLevel={puzzleState.highestLevel} />
219
227
  {/if}
220
228
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-core",
3
- "version": "0.5.87",
3
+ "version": "0.5.90",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"
@@ -1 +0,0 @@
1
- export { default as SubScribersCount } from "./SubscribersCount.js";
@@ -1 +0,0 @@
1
- export { default as SubScribersCount } from './SubscribersCount.js';