@@ -14,6 +14,14 @@ export const SETTING_TYPES = {
1414 displayName : 'contact/display-name' ,
1515} as const ;
1616
17+ /**
18+ * Dynamic setting prefixes — one event per key, keyed by content.itemKey.
19+ * Event type is shared for all settings with the same prefix.
20+ */
21+ const DYNAMIC_PREFIXES : Record < string , { eventType : string ; contentKey : string ; contentValue : string } > = {
22+ 'autoConvert-' : { eventType : 'settings/auto-convert' , contentKey : 'itemKey' , contentValue : 'method' } ,
23+ } ;
24+
1725export type SettingKey = keyof typeof SETTING_TYPES ;
1826
1927export type DateFormat = 'DD.MM.YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY-MM-DD' ;
@@ -70,6 +78,16 @@ function applySideEffects (values: SettingsValues, key: SettingKey): void {
7078 }
7179}
7280
81+ /** Find the dynamic prefix config for a key, or null */
82+ function findDynamicPrefix ( key : string ) : { prefix : string ; eventType : string ; contentKey : string ; contentValue : string ; suffix : string } | null {
83+ for ( const [ prefix , config ] of Object . entries ( DYNAMIC_PREFIXES ) ) {
84+ if ( key . startsWith ( prefix ) ) {
85+ return { prefix, ...config , suffix : key . slice ( prefix . length ) } ;
86+ }
87+ }
88+ return null ;
89+ }
90+
7391/** @internal */
7492let _connection : pryv . Connection | null = null ;
7593/** @internal */
@@ -78,6 +96,10 @@ let _streamId: string | null = null;
7896let _cache : Partial < Record < SettingKey , any > > = { } ;
7997/** @internal */
8098let _values : SettingsValues = { ...DEFAULTS } ;
99+ /** @internal — dynamic settings: key → value */
100+ let _dynamicValues : Record < string , any > = { } ;
101+ /** @internal — dynamic settings: key → cached event */
102+ let _dynamicCache : Record < string , any > = { } ;
81103/** @internal */
82104let _hooked = false ;
83105
@@ -87,18 +109,38 @@ async function load (): Promise<void> {
87109 const browser = browserDefaults ( ) ;
88110 _values = { ...DEFAULTS , ...browser } ;
89111 _cache = { } ;
112+ _dynamicValues = { } ;
113+ _dynamicCache = { } ;
114+
115+ // Collect all event types to fetch (typed + dynamic)
116+ const typedEventTypes = Object . values ( SETTING_TYPES ) as string [ ] ;
117+ const dynamicEventTypes = Object . values ( DYNAMIC_PREFIXES ) . map ( c => c . eventType ) ;
118+ const allTypes = [ ...typedEventTypes , ...dynamicEventTypes ] ;
90119
91120 const settingsEvents : any [ ] = await _connection . apiOne (
92121 'events.get' ,
93- { streams : [ _streamId ] , types : Object . values ( SETTING_TYPES ) , limit : 100 } ,
122+ { streams : [ _streamId ] , types : allTypes , limit : 200 } ,
94123 'events'
95124 ) ;
96125
97126 for ( const event of settingsEvents ) {
127+ // Try typed settings first
98128 const key = keyForType ( event . type ) ;
99129 if ( key && ! _cache [ key ] ) {
100130 _cache [ key ] = event ;
101131 ( _values as any ) [ key ] = event . content ;
132+ continue ;
133+ }
134+
135+ // Try dynamic settings
136+ for ( const [ prefix , config ] of Object . entries ( DYNAMIC_PREFIXES ) ) {
137+ if ( event . type === config . eventType && event . content ?. [ config . contentKey ] ) {
138+ const dynKey = prefix + event . content [ config . contentKey ] ;
139+ if ( ! _dynamicCache [ dynKey ] ) {
140+ _dynamicCache [ dynKey ] = event ;
141+ _dynamicValues [ dynKey ] = event . content [ config . contentValue ] ;
142+ }
143+ }
102144 }
103145 }
104146
@@ -109,13 +151,16 @@ async function load (): Promise<void> {
109151/**
110152 * HDSSettings — singleton managing user settings as individual Pryv events.
111153 *
112- * Each setting is stored as its own event with a specific type
113- * (e.g. `settings/preferredLocales`) in the application's baseStream.
154+ * Supports two kinds of settings:
155+ * - **Typed settings**: fixed keys (theme, dateFormat, etc.) with specific event types.
156+ * - **Dynamic settings**: prefix-based keys (autoConvert-{itemKey}) stored as events
157+ * with a shared event type and keyed by content field.
114158 *
115159 * Usage:
116160 * await HDSSettings.hookToApplication(app);
117161 * const locale = HDSSettings.get('preferredLocales');
118162 * await HDSSettings.set('theme', 'dark');
163+ * await HDSSettings.setDynamic('autoConvert-wellbeing-mood', 'billings');
119164 */
120165const HDSSettings = {
121166
@@ -139,21 +184,37 @@ const HDSSettings = {
139184 } ,
140185
141186 /**
142- * Get the current value for a setting.
187+ * Get the current value for a typed setting.
188+ * Also checks dynamic settings for prefix-based keys (e.g. 'autoConvert-wellbeing-mood').
143189 */
144- get < K extends SettingKey > ( key : K ) : SettingsValues [ K ] {
145- return _values [ key ] ;
190+ get ( key : string ) : any {
191+ if ( key in _dynamicValues ) return _dynamicValues [ key ] ;
192+ return ( _values as any ) [ key ] ;
146193 } ,
147194
148195 /**
149- * Get all current settings values.
196+ * Get all current typed settings values.
150197 */
151198 getAll ( ) : Readonly < SettingsValues > {
152199 return { ..._values } ;
153200 } ,
154201
155202 /**
156- * Set a setting value — persists to HDS server and updates cache.
203+ * Get all dynamic settings with a given prefix.
204+ * Returns a map of suffix → value (e.g. { 'wellbeing-mood': 'billings' }).
205+ */
206+ getDynamic ( prefix : string ) : Record < string , any > {
207+ const result : Record < string , any > = { } ;
208+ for ( const [ key , value ] of Object . entries ( _dynamicValues ) ) {
209+ if ( key . startsWith ( prefix ) ) {
210+ result [ key . slice ( prefix . length ) ] = value ;
211+ }
212+ }
213+ return result ;
214+ } ,
215+
216+ /**
217+ * Set a typed setting value — persists to HDS server and updates cache.
157218 */
158219 async set < K extends SettingKey > ( key : K , value : SettingsValues [ K ] ) : Promise < void > {
159220 if ( ! _connection || ! _streamId ) {
@@ -183,6 +244,52 @@ const HDSSettings = {
183244 applySideEffects ( _values , key ) ;
184245 } ,
185246
247+ /**
248+ * Set a dynamic setting value — persists to HDS server.
249+ * Key must match a known prefix (e.g. 'autoConvert-wellbeing-mood').
250+ * Pass null to delete the setting.
251+ */
252+ async setDynamic ( key : string , value : any ) : Promise < void > {
253+ if ( ! _connection || ! _streamId ) {
254+ throw new Error ( 'HDSSettings: call hookToApplication() or hookToConnection() first' ) ;
255+ }
256+
257+ const dp = findDynamicPrefix ( key ) ;
258+ if ( ! dp ) throw new Error ( `Unknown dynamic setting prefix for key: "${ key } "` ) ;
259+
260+ const existing = _dynamicCache [ key ] ;
261+
262+ if ( value === null || value === undefined ) {
263+ // Delete
264+ if ( existing ) {
265+ await _connection . apiOne ( 'events.delete' , { id : existing . id } , 'eventDeletion' ) ;
266+ delete _dynamicCache [ key ] ;
267+ delete _dynamicValues [ key ] ;
268+ }
269+ return ;
270+ }
271+
272+ const content = { [ dp . contentKey ] : dp . suffix , [ dp . contentValue ] : value } ;
273+
274+ if ( existing ) {
275+ const updated = await _connection . apiOne (
276+ 'events.update' ,
277+ { id : existing . id , update : { content } } ,
278+ 'event'
279+ ) ;
280+ _dynamicCache [ key ] = updated ;
281+ } else {
282+ const created = await _connection . apiOne (
283+ 'events.create' ,
284+ { streamIds : [ _streamId ] , type : dp . eventType , content } ,
285+ 'event'
286+ ) ;
287+ _dynamicCache [ key ] = created ;
288+ }
289+
290+ _dynamicValues [ key ] = value ;
291+ } ,
292+
186293 /**
187294 * Whether settings have been loaded from the server.
188295 */
@@ -205,23 +312,33 @@ const HDSSettings = {
205312 _streamId = null ;
206313 _cache = { } ;
207314 _values = { ...DEFAULTS } ;
315+ _dynamicValues = { } ;
316+ _dynamicCache = { } ;
208317 _hooked = false ;
209318 } ,
210319
211320 /**
212321 * @internal Test-only: inject a setting value and mark as hooked.
213- * Allows testing code paths that depend on HDSSettings without a Pryv connection .
322+ * Works for both typed and dynamic keys .
214323 */
215324 _testInject ( key : string , value : any ) : void {
216- ( _values as any ) [ key ] = value ;
325+ if ( findDynamicPrefix ( key ) ) {
326+ _dynamicValues [ key ] = value ;
327+ } else {
328+ ( _values as any ) [ key ] = value ;
329+ }
217330 _hooked = true ;
218331 } ,
219332
220333 /**
221334 * @internal Test-only: remove an injected setting.
222335 */
223336 _testClear ( key : string ) : void {
224- delete ( _values as any ) [ key ] ;
337+ if ( findDynamicPrefix ( key ) ) {
338+ delete _dynamicValues [ key ] ;
339+ } else {
340+ delete ( _values as any ) [ key ] ;
341+ }
225342 } ,
226343} ;
227344
0 commit comments