@superbright/indexeddb-orm 0.1.1

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/src/storage.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { getDB } from "./db";
2
+
3
+ export interface AsyncStringStorage {
4
+ getItem(name: string): Promise<string | null>;
5
+ setItem(name: string, value: string): Promise<void>;
6
+ removeItem(name: string): Promise<void>;
7
+ }
8
+
9
+ type storeKeys = "property" | "user" | "app";
10
+
11
+ // Generic typed KV helpers (store raw objects in the ORM's kv table)
12
+ export async function kvGet<T>(key: storeKeys): Promise<T | null> {
13
+ const db = await getDB();
14
+ const row = await db.kv.get(key);
15
+ return (row?.value as T | undefined) ?? null;
16
+ }
17
+ export async function kvSet<T>(key: storeKeys, value: T): Promise<void> {
18
+ const db = await getDB();
19
+ await db.kv.put({ key, value });
20
+ }
21
+ export async function kvRemove(key: storeKeys): Promise<void> {
22
+ const db = await getDB();
23
+ await db.kv.delete(key);
24
+ }
25
+
26
+ export function createORMStringStorage(opts?: {
27
+ prefix?: string;
28
+ fallbackToMemory?: boolean;
29
+ }): AsyncStringStorage {
30
+ const mem = new Map<string, string>(); // optional fallback
31
+ const prefix = opts?.prefix ?? "";
32
+
33
+ const toKey = (name: string) => `${prefix}${name}`;
34
+
35
+ return {
36
+ async getItem(name) {
37
+ try {
38
+ const db = await getDB();
39
+ const row = await db.kv.get(toKey(name));
40
+ return row ? JSON.stringify(row.value) : null;
41
+ } catch (e) {
42
+ if (opts?.fallbackToMemory) return mem.get(toKey(name)) ?? null;
43
+ throw e;
44
+ }
45
+ },
46
+ async setItem(name, value) {
47
+ try {
48
+ const db = await getDB();
49
+ await db.kv.put({ key: toKey(name), value: JSON.parse(value) });
50
+ } catch (e) {
51
+ if (opts?.fallbackToMemory) {
52
+ mem.set(toKey(name), value);
53
+ return;
54
+ }
55
+ throw e;
56
+ }
57
+ },
58
+ async removeItem(name) {
59
+ try {
60
+ const db = await getDB();
61
+ await db.kv.delete(toKey(name));
62
+ } catch (e) {
63
+ if (opts?.fallbackToMemory) {
64
+ mem.delete(toKey(name));
65
+ return;
66
+ }
67
+ throw e;
68
+ }
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,133 @@
1
+ import { propertyStore, type PropertyData, type TourContactData } from "../api/properties";
2
+
3
+ // Zustand-compatible interface for the property store
4
+ export interface PropertyStoreInterface {
5
+ data: Record<string, PropertyData>;
6
+ propertySlug: string | null;
7
+ propertyId: string | null;
8
+ hasPreviouslySearched: string[];
9
+
10
+ // Async methods
11
+ setData: (value: Record<string, PropertyData>) => Promise<void>;
12
+ setPropertySlug: (slug: string) => Promise<void>;
13
+ setPropertyId: (id: string) => Promise<void>;
14
+ removeData: (key: string) => Promise<void>;
15
+ clearData: () => Promise<void>;
16
+ setHasPreviouslySearched: (slug: string) => Promise<void>;
17
+ toggleFavorite: (unitId: string) => Promise<void>;
18
+ markUnitAsViewed: (unitId: string, slug: string) => Promise<void>;
19
+ setTourContactedOn: () => Promise<void>;
20
+ getTourContactedOn: () => Promise<string | null>;
21
+ setQuestionnaireResults: (results: unknown) => Promise<void>;
22
+ setTourContactData: (data: TourContactData) => Promise<void>;
23
+
24
+ // Utility methods
25
+ getUnitState: (unitId: string) => Promise<{
26
+ isFavorite: boolean;
27
+ viewedDate: string;
28
+ }>;
29
+ getPropertyData: (propertyId?: string) => Promise<PropertyData | null>;
30
+ initializeProperty: (propertyId: string, slug: string) => Promise<void>;
31
+
32
+ // State hydration
33
+ hydrate: () => Promise<void>;
34
+ }
35
+
36
+ // Factory function to create a property store instance
37
+ export function createPropertyStore(): PropertyStoreInterface {
38
+ let currentState = {
39
+ data: {} as Record<string, PropertyData>,
40
+ propertySlug: null as string | null,
41
+ propertyId: null as string | null,
42
+ hasPreviouslySearched: [] as string[],
43
+ };
44
+
45
+ const store: PropertyStoreInterface = {
46
+ get data() { return currentState.data; },
47
+ get propertySlug() { return currentState.propertySlug; },
48
+ get propertyId() { return currentState.propertyId; },
49
+ get hasPreviouslySearched() { return currentState.hasPreviouslySearched; },
50
+
51
+ async hydrate() {
52
+ const state = await propertyStore.getFullState();
53
+ currentState = state;
54
+ },
55
+
56
+ async setData(value) {
57
+ await propertyStore.setData(value);
58
+ currentState.data = value;
59
+ },
60
+
61
+ async setPropertySlug(slug) {
62
+ await propertyStore.setPropertySlug(slug);
63
+ currentState.propertySlug = slug;
64
+ },
65
+
66
+ async setPropertyId(id) {
67
+ await propertyStore.setPropertyId(id);
68
+ currentState.propertyId = id;
69
+ },
70
+
71
+ async removeData(key) {
72
+ await propertyStore.removeData(key);
73
+ const { [key]: removed, ...rest } = currentState.data;
74
+ currentState.data = rest;
75
+ },
76
+
77
+ async clearData() {
78
+ await propertyStore.clearData();
79
+ currentState.data = {};
80
+ },
81
+
82
+ async setHasPreviouslySearched(slug) {
83
+ await propertyStore.setHasPreviouslySearched(slug);
84
+ currentState.hasPreviouslySearched = Array.from(
85
+ new Set([...currentState.hasPreviouslySearched, slug])
86
+ );
87
+ },
88
+
89
+ async toggleFavorite(unitId) {
90
+ await propertyStore.toggleFavorite(unitId);
91
+ // Refresh state after update
92
+ await store.hydrate();
93
+ },
94
+
95
+ async markUnitAsViewed(unitId, slug) {
96
+ await propertyStore.markUnitAsViewed(unitId, slug);
97
+ // Refresh state after update
98
+ await store.hydrate();
99
+ },
100
+
101
+ async setTourContactedOn() {
102
+ await propertyStore.setTourContactedOn();
103
+ // Refresh state after update
104
+ await store.hydrate();
105
+ },
106
+
107
+ getTourContactedOn: propertyStore.getTourContactedOn.bind(propertyStore),
108
+
109
+ async setQuestionnaireResults(results) {
110
+ await propertyStore.setQuestionnaireResults(results);
111
+ // Refresh state after update
112
+ await store.hydrate();
113
+ },
114
+
115
+ async setTourContactData(data) {
116
+ await propertyStore.setTourContactData(data);
117
+ // Refresh state after update
118
+ await store.hydrate();
119
+ },
120
+
121
+ getUnitState: propertyStore.getUnitState.bind(propertyStore),
122
+ getPropertyData: propertyStore.getPropertyData.bind(propertyStore),
123
+ initializeProperty: async (propertyId, slug) => {
124
+ await propertyStore.initializeProperty(propertyId, slug);
125
+ await store.hydrate();
126
+ },
127
+ };
128
+
129
+ return store;
130
+ }
131
+
132
+ // Singleton instance for convenience
133
+ export const defaultPropertyStore = createPropertyStore();
@@ -0,0 +1,507 @@
1
+ import { z } from "zod";
2
+ import { kvGet, kvSet } from "../storage";
3
+
4
+ // Combined store schemas
5
+ export const UnitDataSchema = z.object({
6
+ isFavorite: z.boolean().optional(),
7
+ viewedDate: z.string().optional(),
8
+ });
9
+
10
+ export const ViewedUnitSchema = z.object({
11
+ unitId: z.string(),
12
+ viewedDate: z.string(),
13
+ });
14
+
15
+ export const TourContactDataSchema = z.object({
16
+ timezone: z.string(),
17
+ favouriteUnits: z.array(z.string()).optional(),
18
+ preferences: z.record(z.unknown()),
19
+ });
20
+
21
+ export const PropertyDataSchema = z.object({
22
+ id: z.string(),
23
+ slug: z.string(),
24
+ favoritedUnits: z.array(z.string()),
25
+ tourContactedOn: z.string().nullable(),
26
+ viewedUnits: z.array(ViewedUnitSchema),
27
+ questionnaireResults: z.unknown().nullable().optional(),
28
+ tourContactData: TourContactDataSchema.nullable().optional(),
29
+ });
30
+
31
+ export const FiltersSchema = z.object({
32
+ availability: z.union([z.string(), z.array(z.string())]).nullable().optional(),
33
+ bedrooms: z.array(z.number()).nullable().optional(),
34
+ cost: z.number().nullable().optional(),
35
+ highlights: z.array(z.string()).optional(),
36
+ });
37
+
38
+ export const QueryParamsSchema = z.object({
39
+ limit: z.number().default(10),
40
+ page: z.number().default(1),
41
+ availability: z.array(z.string()).optional(),
42
+ bedrooms: z.array(z.number()).optional(),
43
+ cost: z.number().nullable().optional(),
44
+ highlights: z.array(z.string()).optional(),
45
+ });
46
+
47
+ export const UnifiedStoreDataSchema = z.object({
48
+ // Property-related data
49
+ properties: z.record(PropertyDataSchema),
50
+ currentPropertyId: z.string().nullable(),
51
+ currentPropertySlug: z.string().nullable(),
52
+ hasPreviouslySearched: z.array(z.string()),
53
+
54
+ // App-level data
55
+ filters: FiltersSchema,
56
+ tempFilters: FiltersSchema,
57
+ apiFilters: QueryParamsSchema,
58
+ resultsMode: z.enum(["all", "bestFit", "closestMatch", "favorites"]),
59
+ resolvedQuestionnaireValues: z.record(z.array(z.string())),
60
+ sortBy: z.enum(["relevance", "newest", "priceLowToHigh", "priceHighToLow"]),
61
+ filtersLoaded: z.boolean(),
62
+ });
63
+
64
+ export type UnitData = z.infer<typeof UnitDataSchema>;
65
+ export type ViewedUnit = z.infer<typeof ViewedUnitSchema>;
66
+ export type TourContactData = z.infer<typeof TourContactDataSchema>;
67
+ export type PropertyData = z.infer<typeof PropertyDataSchema>;
68
+ export type Filters = z.infer<typeof FiltersSchema>;
69
+ export type QueryParams = z.infer<typeof QueryParamsSchema>;
70
+ export type UnifiedStoreData = z.infer<typeof UnifiedStoreDataSchema>;
71
+ export type ResultsMode = "all" | "bestFit" | "closestMatch" | "favorites";
72
+ export type SortBy = "relevance" | "newest" | "priceLowToHigh" | "priceHighToLow";
73
+
74
+ // Default values
75
+ const defaultFilters: Filters = {
76
+ availability: undefined,
77
+ bedrooms: undefined,
78
+ cost: undefined,
79
+ highlights: undefined,
80
+ };
81
+
82
+ const defaultUnifiedStoreData: UnifiedStoreData = {
83
+ // Property data
84
+ properties: {},
85
+ currentPropertyId: null,
86
+ currentPropertySlug: null,
87
+ hasPreviouslySearched: [],
88
+
89
+ // App data
90
+ filters: defaultFilters,
91
+ tempFilters: defaultFilters,
92
+ apiFilters: {
93
+ limit: 10,
94
+ page: 1,
95
+ },
96
+ resultsMode: "all",
97
+ resolvedQuestionnaireValues: {},
98
+ sortBy: "relevance",
99
+ filtersLoaded: false,
100
+ };
101
+
102
+ // Unified store class
103
+ export class UnifiedStore {
104
+ private async getState(): Promise<UnifiedStoreData> {
105
+ const state = await kvGet<UnifiedStoreData>("app");
106
+ if (!state) {
107
+ return defaultUnifiedStoreData;
108
+ }
109
+
110
+ // Ensure all required fields exist (defensive programming)
111
+ return {
112
+ ...defaultUnifiedStoreData,
113
+ ...state,
114
+ properties: state.properties ?? {},
115
+ };
116
+ }
117
+
118
+ private async setState(updater: (state: UnifiedStoreData) => UnifiedStoreData): Promise<void> {
119
+ const currentState = await this.getState();
120
+ const newState = updater(currentState);
121
+ await kvSet("app", newState);
122
+ }
123
+
124
+ // === PROPERTY OPERATIONS ===
125
+
126
+ async initializeProperty(propertyId: string, slug: string): Promise<void> {
127
+ await this.setState(state => {
128
+ if (state.properties && state.properties[propertyId]) {
129
+ return {
130
+ ...state,
131
+ currentPropertyId: propertyId,
132
+ currentPropertySlug: slug
133
+ };
134
+ }
135
+
136
+ return {
137
+ ...state,
138
+ currentPropertyId: propertyId,
139
+ currentPropertySlug: slug,
140
+ properties: {
141
+ ...state.properties,
142
+ [propertyId]: {
143
+ id: propertyId,
144
+ slug,
145
+ favoritedUnits: [],
146
+ tourContactedOn: null,
147
+ viewedUnits: [],
148
+ questionnaireResults: null,
149
+ tourContactData: null,
150
+ },
151
+ },
152
+ };
153
+ });
154
+ }
155
+
156
+ async setCurrentProperty(propertyId: string, slug?: string): Promise<void> {
157
+ await this.setState(state => ({
158
+ ...state,
159
+ currentPropertyId: propertyId,
160
+ currentPropertySlug: slug || state.currentPropertySlug,
161
+ }));
162
+ }
163
+
164
+ async setCurrentPropertySlug(slug: string): Promise<void> {
165
+ await this.setState(state => ({
166
+ ...state,
167
+ currentPropertySlug: slug
168
+ }));
169
+ }
170
+
171
+ async setHasPreviouslySearched(slug: string): Promise<void> {
172
+ await this.setState(state => ({
173
+ ...state,
174
+ hasPreviouslySearched: Array.from(
175
+ new Set([...state.hasPreviouslySearched, slug])
176
+ ),
177
+ }));
178
+ }
179
+
180
+ async toggleFavorite(unitId: string): Promise<void> {
181
+ await this.setState(state => {
182
+ const propertyId = state.currentPropertyId;
183
+ if (!propertyId) return state;
184
+
185
+ const property = state.properties[propertyId];
186
+ if (!property) return state;
187
+
188
+ const isFavorited = property.favoritedUnits.includes(unitId);
189
+ const updatedFavoritedUnits = isFavorited
190
+ ? property.favoritedUnits.filter((id) => id !== unitId)
191
+ : [...property.favoritedUnits, unitId];
192
+
193
+ return {
194
+ ...state,
195
+ properties: {
196
+ ...state.properties,
197
+ [propertyId]: {
198
+ ...property,
199
+ favoritedUnits: updatedFavoritedUnits,
200
+ },
201
+ },
202
+ };
203
+ });
204
+ }
205
+
206
+ async markUnitAsViewed(unitId: string, slug: string): Promise<void> {
207
+ const today = new Date();
208
+ const formattedDate = `${String(today.getMonth() + 1).padStart(
209
+ 2,
210
+ "0"
211
+ )}/${String(today.getDate()).padStart(2, "0")}`;
212
+
213
+ await this.setState(state => {
214
+ const propertyId = state.currentPropertyId;
215
+ if (!propertyId) return state;
216
+
217
+ const property = state.properties[propertyId];
218
+ if (!property) return state;
219
+
220
+ const updatedViewedUnits = [
221
+ ...property.viewedUnits.filter((u) => u.unitId !== unitId),
222
+ { unitId, viewedDate: formattedDate },
223
+ ];
224
+
225
+ return {
226
+ ...state,
227
+ properties: {
228
+ ...state.properties,
229
+ [propertyId]: {
230
+ ...property,
231
+ viewedUnits: updatedViewedUnits,
232
+ },
233
+ },
234
+ };
235
+ });
236
+
237
+ if (typeof window !== 'undefined') {
238
+ window.open(`//${slug}`, "_blank");
239
+ }
240
+ }
241
+
242
+ async setTourContactedOn(): Promise<void> {
243
+ await this.setState(state => {
244
+ const propertyId = state.currentPropertyId;
245
+ if (!propertyId) return state;
246
+
247
+ const property = state.properties[propertyId];
248
+ if (!property) return state;
249
+
250
+ return {
251
+ ...state,
252
+ properties: {
253
+ ...state.properties,
254
+ [propertyId]: {
255
+ ...property,
256
+ tourContactedOn: new Date().toISOString(),
257
+ },
258
+ },
259
+ };
260
+ });
261
+ }
262
+
263
+ async getTourContactedOn(): Promise<string | null> {
264
+ const state = await this.getState();
265
+ const propertyId = state.currentPropertyId;
266
+ if (!propertyId) return null;
267
+
268
+ return state.properties[propertyId]?.tourContactedOn ?? null;
269
+ }
270
+
271
+ async setQuestionnaireResults(results: unknown): Promise<void> {
272
+ await this.setState(state => {
273
+ const propertyId = state.currentPropertyId;
274
+ if (!propertyId) return state;
275
+
276
+ const property = state.properties[propertyId];
277
+ if (!property) return state;
278
+
279
+ return {
280
+ ...state,
281
+ properties: {
282
+ ...state.properties,
283
+ [propertyId]: {
284
+ ...property,
285
+ questionnaireResults: results,
286
+ },
287
+ },
288
+ };
289
+ });
290
+ }
291
+
292
+ async setTourContactData(data: TourContactData): Promise<void> {
293
+ await this.setState(state => {
294
+ const propertyId = state.currentPropertyId;
295
+ if (!propertyId) return state;
296
+
297
+ const property = state.properties[propertyId];
298
+ if (!property) return state;
299
+
300
+ return {
301
+ ...state,
302
+ properties: {
303
+ ...state.properties,
304
+ [propertyId]: {
305
+ ...property,
306
+ tourContactData: data,
307
+ },
308
+ },
309
+ };
310
+ });
311
+ }
312
+
313
+
314
+
315
+
316
+
317
+
318
+
319
+ // === FILTER OPERATIONS ===
320
+
321
+ async setFilters(filters: Partial<Filters>): Promise<void> {
322
+ await this.setState(state => ({
323
+ ...state,
324
+ filters: { ...state.filters, ...filters }
325
+ }));
326
+ }
327
+
328
+ async setTempFilters(filters: Partial<Filters>): Promise<void> {
329
+ await this.setState(state => ({
330
+ ...state,
331
+ tempFilters: { ...state.tempFilters, ...filters }
332
+ }));
333
+ }
334
+
335
+ async setFiltersToDefault(): Promise<void> {
336
+ await this.setState(state => ({ ...state, filters: defaultFilters }));
337
+ }
338
+
339
+ async setApiFilters(filters: Partial<QueryParams>): Promise<void> {
340
+ await this.setState(state => ({
341
+ ...state,
342
+ apiFilters: { ...state.apiFilters, ...filters }
343
+ }));
344
+ }
345
+
346
+ async handleTempFilterChange<K extends keyof Filters>(
347
+ key: K,
348
+ value: Filters[K]
349
+ ): Promise<void> {
350
+ await this.setState(state => ({
351
+ ...state,
352
+ tempFilters: { ...state.tempFilters, [key]: value }
353
+ }));
354
+ }
355
+
356
+ async commitTempFilterChange<K extends keyof Filters>(
357
+ key: K,
358
+ defaultValue: Filters[K]
359
+ ): Promise<void> {
360
+ const state = await this.getState();
361
+ const value = state.tempFilters[key] ?? defaultValue;
362
+
363
+ await this.handleTempFilterChange(key, value);
364
+ await this.submitFilterUpdate();
365
+ }
366
+
367
+ async handleFilterCommitIndexDB(newFilters: Partial<Filters>): Promise<void> {
368
+ await this.setState(state => {
369
+ const apiParams: QueryParams = {
370
+ ...state.apiFilters,
371
+ availability: (newFilters.availability as string[]) || [],
372
+ bedrooms: newFilters.bedrooms || [],
373
+ cost: newFilters.cost || null,
374
+ highlights: newFilters.highlights || [],
375
+ };
376
+ return {
377
+ ...state,
378
+ filters: { ...state.filters, ...newFilters },
379
+ apiFilters: apiParams,
380
+ };
381
+ });
382
+ }
383
+
384
+ async commitAvailabilityChange(): Promise<void> {
385
+ await this.submitFilterUpdate();
386
+ }
387
+
388
+ async submitFilterUpdate(): Promise<void> {
389
+ await this.setState(state => {
390
+ const apiParams: QueryParams = {
391
+ ...state.apiFilters,
392
+ availability: (state.filters.availability as string[]) || [],
393
+ bedrooms: state.filters.bedrooms || [],
394
+ cost: state.filters.cost || null,
395
+ highlights: state.filters.highlights || [],
396
+ };
397
+
398
+ return {
399
+ ...state,
400
+ apiFilters: apiParams,
401
+ };
402
+ });
403
+ }
404
+
405
+ // === RESULTS AND SORTING ===
406
+
407
+ async setResultsMode(mode: ResultsMode): Promise<void> {
408
+ await this.setState(state => ({ ...state, resultsMode: mode }));
409
+ }
410
+
411
+ async setSortBy(sortBy: SortBy): Promise<void> {
412
+ await this.setState(state => ({ ...state, sortBy }));
413
+ }
414
+
415
+ // === QUESTIONNAIRE ===
416
+
417
+ async setResolvedQuestionnaireValues(name: string, values: string[]): Promise<void> {
418
+ await this.setState(state => ({
419
+ ...state,
420
+ resolvedQuestionnaireValues: {
421
+ ...state.resolvedQuestionnaireValues,
422
+ [name]: values
423
+ }
424
+ }));
425
+ }
426
+
427
+ // === UTILITY METHODS ===
428
+
429
+ async getUnitState(unitId: string): Promise<{
430
+ isFavorite: boolean;
431
+ viewedDate: string;
432
+ }> {
433
+ const state = await this.getState();
434
+ const property = state.currentPropertyId ? state.properties[state.currentPropertyId] : null;
435
+
436
+ return {
437
+ isFavorite: property?.favoritedUnits.includes(unitId) ?? false,
438
+ viewedDate:
439
+ property?.viewedUnits.find((u) => u.unitId === unitId)?.viewedDate ?? "",
440
+ };
441
+ }
442
+
443
+ async getResultsUrl(): Promise<string | null> {
444
+ const state = await this.getState();
445
+ return state.currentPropertySlug ? `/${state.currentPropertySlug}/results` : null;
446
+ }
447
+
448
+ async getCurrentProperty(): Promise<PropertyData | null> {
449
+ const state = await this.getState();
450
+ return state.currentPropertyId ? state.properties[state.currentPropertyId] ?? null : null;
451
+ }
452
+
453
+ async getPropertyData(propertyId?: string): Promise<PropertyData | null> {
454
+ const state = await this.getState();
455
+ const id = propertyId ?? state.currentPropertyId;
456
+ return id ? state.properties[id] ?? null : null;
457
+ }
458
+
459
+ async getFullState(): Promise<UnifiedStoreData> {
460
+ return this.getState();
461
+ }
462
+
463
+ async initialize(): Promise<void> {
464
+ const state = await this.getState();
465
+ if (Object.keys(state.properties).length === 0 && !state.filtersLoaded) {
466
+ await this.setState(state => ({
467
+ ...defaultUnifiedStoreData,
468
+ ...state,
469
+ filtersLoaded: true
470
+ }));
471
+ }
472
+ }
473
+
474
+ async loadPersistedFilters(): Promise<void> {
475
+ // Mark filters as loaded since we're always loading from IndexedDB
476
+ await this.setState(state => ({ ...state, filtersLoaded: true }));
477
+ }
478
+
479
+ // === LEGACY COMPATIBILITY METHODS ===
480
+
481
+ // Property store compatibility
482
+ async setData(value: Record<string, PropertyData>): Promise<void> {
483
+ await this.setState(state => ({ ...state, properties: value }));
484
+ }
485
+
486
+ async setPropertySlug(slug: string): Promise<void> {
487
+ await this.setCurrentPropertySlug(slug);
488
+ }
489
+
490
+ async setPropertyId(id: string): Promise<void> {
491
+ await this.setCurrentProperty(id);
492
+ }
493
+
494
+ async removeData(key: string): Promise<void> {
495
+ await this.setState(state => {
496
+ const { [key]: removed, ...rest } = state.properties;
497
+ return { ...state, properties: rest };
498
+ });
499
+ }
500
+
501
+ async clearData(): Promise<void> {
502
+ await this.setState(state => ({ ...state, properties: {} }));
503
+ }
504
+ }
505
+
506
+ // Export singleton instance
507
+ export const store = new UnifiedStore();