Skip to content

Commit 5eb8d2b

Browse files
committed
✨ implement item holder caching
1 parent df4e52c commit 5eb8d2b

File tree

4 files changed

+216
-4
lines changed

4 files changed

+216
-4
lines changed

packages/hyper_storage/lib/src/hyper_storage.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,5 +378,6 @@ class HyperStorage extends _HyperStorageImpl {
378378
removeAllListeners();
379379
_instance = null;
380380
await backend.close();
381+
await super.close();
381382
}
382383
}

packages/hyper_storage/lib/src/hyper_storage_container.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ final class HyperStorageContainer extends StorageContainer with ItemHolderMixin,
229229
Future<void> close() async {
230230
removeAllListeners();
231231
await backend.close();
232+
await super.close();
232233
}
233234

234235
@override

packages/hyper_storage/lib/src/item_holder.dart

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ final class JsonItemHolder<E extends Object> extends SerializableItemHolder<E> {
227227
/// type holders. Each item holder is automatically linked to the container's
228228
/// change notification system.
229229
mixin ItemHolderMixin on BaseStorage {
230+
/// Internal cache of item holders by their keys.
231+
final Map<String, ItemHolder> _holders = {};
232+
230233
/// Encodes a key for storage. This method should be implemented by the class
231234
/// using this mixin if key encoding is required (e.g., for containers).
232235
/// The default implementation in BaseStorage returns the key unchanged.
@@ -280,7 +283,19 @@ mixin ItemHolderMixin on BaseStorage {
280283
required ToJson<E> toJson,
281284
}) {
282285
validateKey(key);
283-
return JsonItemHolder<E>(this, encodeKey(key), fromJson: fromJson, toJson: toJson);
286+
final existing = _holders[key];
287+
if (existing != null) {
288+
if (existing is JsonItemHolder<E>) {
289+
return existing;
290+
} else {
291+
throw ArgumentError(
292+
'An ItemHolder with key "$key" already exists with a different type: ${existing.runtimeType}.',
293+
);
294+
}
295+
}
296+
final holder = JsonItemHolder<E>(this, encodeKey(key), fromJson: fromJson, toJson: toJson);
297+
_holders[key] = holder;
298+
return holder;
284299
}
285300

286301
/// Creates an item holder for storing a single serializable object at the specified key.
@@ -318,7 +333,19 @@ mixin ItemHolderMixin on BaseStorage {
318333
required DeserializeCallback<E> deserialize,
319334
}) {
320335
validateKey(key);
321-
return SerializableItemHolder<E>(this, encodeKey(key), serialize: serialize, deserialize: deserialize);
336+
final existing = _holders[key];
337+
if (existing != null) {
338+
if (existing is SerializableItemHolder<E>) {
339+
return existing;
340+
} else {
341+
throw ArgumentError(
342+
'An ItemHolder with key "$key" already exists with a different type: ${existing.runtimeType}.',
343+
);
344+
}
345+
}
346+
final holder = SerializableItemHolder<E>(this, encodeKey(key), serialize: serialize, deserialize: deserialize);
347+
_holders[key] = holder;
348+
return holder;
322349
}
323350

324351
/// Creates an item holder for storing a single primitive value at the specified key.
@@ -362,7 +389,20 @@ mixin ItemHolderMixin on BaseStorage {
362389
}
363390
// Only run generic type validation if custom getter/setter are not provided.
364391
if (set == null || get == null) _validateGenericType<E>();
365-
return ItemHolder<E>(this, encodeKey(key), setter: set, getter: get);
392+
final existing = _holders[key];
393+
if (existing != null) {
394+
if (existing is ItemHolder<E>) {
395+
return existing;
396+
} else {
397+
throw ArgumentError(
398+
'An ItemHolder with key "$key" already exists with a different type: ${existing.runtimeType}.',
399+
);
400+
}
401+
}
402+
403+
final holder = ItemHolder<E>(this, encodeKey(key), setter: set, getter: get);
404+
_holders[key] = holder;
405+
return holder;
366406
}
367407

368408
/// Creates a custom item holder using the provided factory function.
@@ -393,7 +433,19 @@ mixin ItemHolderMixin on BaseStorage {
393433
required H Function(StorageBackend backend, String key) create,
394434
}) {
395435
validateKey(key);
396-
return create(backend, encodeKey(key));
436+
final existing = _holders[key];
437+
if (existing != null) {
438+
if (existing is H) {
439+
return existing;
440+
} else {
441+
throw ArgumentError(
442+
'An ItemHolder with key "$key" already exists with a different type: ${existing.runtimeType}.',
443+
);
444+
}
445+
}
446+
final holder = create(backend, encodeKey(key));
447+
_holders[key] = holder;
448+
return holder;
397449
}
398450

399451
bool _validateGenericType<E>() {
@@ -410,4 +462,13 @@ mixin ItemHolderMixin on BaseStorage {
410462
_ => throw UnsupportedError('Type $E is not supported'),
411463
};
412464
}
465+
466+
/// Disposes all item holders created by this mixin.
467+
@mustCallSuper
468+
Future<void> close() async {
469+
for (final holder in _holders.values) {
470+
holder.dispose();
471+
}
472+
_holders.clear();
473+
}
413474
}

packages/hyper_storage/test/item_holder_test.dart

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,144 @@ void main() {
553553
expect(customHolder.key, 'test___user');
554554
});
555555
});
556+
557+
group('ItemHolder caching', () {
558+
test('itemHolder returns same instance for same key', () {
559+
final holder1 = container.itemHolder<String>('cache-test');
560+
final holder2 = container.itemHolder<String>('cache-test');
561+
expect(identical(holder1, holder2), true);
562+
});
563+
564+
test('itemHolder throws ArgumentError for type mismatch', () {
565+
// Create a holder with String type
566+
container.itemHolder<String>('type-mismatch');
567+
568+
// Attempt to create another holder with same key but different type
569+
expect(
570+
() => container.itemHolder<int>('type-mismatch'),
571+
throwsA(
572+
isA<ArgumentError>().having(
573+
(e) => e.message,
574+
'message',
575+
contains('An ItemHolder with key "type-mismatch" already exists with a different type'),
576+
),
577+
),
578+
);
579+
});
580+
581+
test('jsonItemHolder returns same instance for same key', () {
582+
final holder1 = container.jsonItemHolder<User>(
583+
'user-cache',
584+
fromJson: User.fromJson,
585+
toJson: (user) => user.toJson(),
586+
);
587+
final holder2 = container.jsonItemHolder<User>(
588+
'user-cache',
589+
fromJson: User.fromJson,
590+
toJson: (user) => user.toJson(),
591+
);
592+
expect(identical(holder1, holder2), true);
593+
});
594+
595+
test('jsonItemHolder throws ArgumentError for type mismatch', () {
596+
// Create a JsonItemHolder with User type
597+
container.jsonItemHolder<User>(
598+
'json-mismatch',
599+
fromJson: User.fromJson,
600+
toJson: (user) => user.toJson(),
601+
);
602+
603+
// Attempt to create another JsonItemHolder with same key but different type
604+
expect(
605+
() => container.jsonItemHolder<Map<String, dynamic>>(
606+
'json-mismatch',
607+
fromJson: (json) => json,
608+
toJson: (data) => data,
609+
),
610+
throwsA(
611+
isA<ArgumentError>().having(
612+
(e) => e.message,
613+
'message',
614+
contains('An ItemHolder with key "json-mismatch" already exists with a different type'),
615+
),
616+
),
617+
);
618+
});
619+
620+
test('serializableItemHolder returns same instance for same key', () {
621+
final holder1 = container.serializableItemHolder<User>(
622+
'serializable-cache',
623+
serialize: (user) => user.serialize(),
624+
deserialize: User.deserialize,
625+
);
626+
final holder2 = container.serializableItemHolder<User>(
627+
'serializable-cache',
628+
serialize: (user) => user.serialize(),
629+
deserialize: User.deserialize,
630+
);
631+
expect(identical(holder1, holder2), true);
632+
});
633+
634+
test('serializableItemHolder throws ArgumentError for type mismatch', () {
635+
// Create a SerializableItemHolder with User type
636+
container.serializableItemHolder<User>(
637+
'serializable-mismatch',
638+
serialize: (user) => user.serialize(),
639+
deserialize: User.deserialize,
640+
);
641+
642+
// Attempt to create another SerializableItemHolder with same key but different type
643+
expect(
644+
() => container.serializableItemHolder<String>(
645+
'serializable-mismatch',
646+
serialize: (s) => s,
647+
deserialize: (s) => s,
648+
),
649+
throwsA(
650+
isA<ArgumentError>().having(
651+
(e) => e.message,
652+
'message',
653+
contains('An ItemHolder with key "serializable-mismatch" already exists with a different type'),
654+
),
655+
),
656+
);
657+
});
658+
659+
test('customItemHolder returns same instance for same key', () {
660+
final holder1 = container.customItemHolder<CustomUserHolder, User>(
661+
'custom-cache',
662+
create: (backend, key) => CustomUserHolder(container, backend, key),
663+
);
664+
final holder2 = container.customItemHolder<CustomUserHolder, User>(
665+
'custom-cache',
666+
create: (backend, key) => CustomUserHolder(container, backend, key),
667+
);
668+
expect(identical(holder1, holder2), true);
669+
});
670+
671+
test('customItemHolder throws ArgumentError for type mismatch', () {
672+
// Create a CustomItemHolder
673+
container.customItemHolder<CustomUserHolder, User>(
674+
'custom-mismatch',
675+
create: (backend, key) => CustomUserHolder(container, backend, key),
676+
);
677+
678+
// Attempt to create another CustomItemHolder with same key but different holder type
679+
expect(
680+
() => container.customItemHolder<AnotherCustomHolder, String>(
681+
'custom-mismatch',
682+
create: (backend, key) => AnotherCustomHolder(container, backend, key),
683+
),
684+
throwsA(
685+
isA<ArgumentError>().having(
686+
(e) => e.message,
687+
'message',
688+
contains('An ItemHolder with key "custom-mismatch" already exists with a different type'),
689+
),
690+
),
691+
);
692+
});
693+
});
556694
});
557695
}
558696

@@ -574,3 +712,14 @@ class CustomUserHolder extends ItemHolder<User> {
574712

575713
String get key => _encodedKey;
576714
}
715+
716+
// Another custom ItemHolder for testing type mismatch
717+
class AnotherCustomHolder extends ItemHolder<String> {
718+
AnotherCustomHolder(HyperStorageContainer parent, StorageBackend backend, String encodedKey)
719+
: super(
720+
parent,
721+
encodedKey,
722+
getter: (backend, key) => backend.getString(key),
723+
setter: (backend, key, value) => backend.setString(key, value),
724+
);
725+
}

0 commit comments

Comments
 (0)