Skip to content

Commit a45c926

Browse files
bitmagejamesgpearce
authored andcommitted
add tests, fix prefix parsing, fix empty storage handling
1 parent aa5cb90 commit a45c926

4 files changed

Lines changed: 228 additions & 2 deletions

File tree

src/persisters/persister-durable-object-storage/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const createDurableObjectStoragePersister = ((
4343
key: string,
4444
): [type: string, ...ids: Ids] | undefined => {
4545
if (strStartsWith(key, storagePrefix)) {
46-
const type = slice(key, storagePrefix.length, 1);
46+
const type = slice(key, storagePrefix.length, storagePrefix.length + 1);
4747
return type == T || type == V
4848
? [
4949
type,
@@ -54,7 +54,7 @@ export const createDurableObjectStoragePersister = ((
5454
};
5555

5656
const getPersisted = async (): Promise<
57-
PersistedContent<PersistsType.MergeableStoreOnly>
57+
PersistedContent<PersistsType.MergeableStoreOnly> | undefined
5858
> => {
5959
const tables: TablesStamp<true> = stampNewObjectWithHash();
6060
const values: ValuesStamp<true> = stampNewObjectWithHash();
@@ -100,6 +100,9 @@ export const createDurableObjectStoragePersister = ((
100100
: 0,
101101
),
102102
);
103+
if (Object.keys(tables[0]).length === 0 && Object.keys(values[0]).length === 0) {
104+
return undefined;
105+
}
103106
return [tables, values];
104107
};
105108

test/unit/persisters/__snapshots__/mergeable.test.ts.snap

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,96 @@ exports[`Persists to/from customSynchronizer > saves 2`] = `
662662
]
663663
`;
664664

665+
exports[`Persists to/from durableObjectStorage > autoSaves > delCell 1`] = `undefined`;
666+
667+
exports[`Persists to/from durableObjectStorage > autoSaves > delValue 1`] = `undefined`;
668+
669+
exports[`Persists to/from durableObjectStorage > autoSaves > initial 1`] = `undefined`;
670+
671+
exports[`Persists to/from durableObjectStorage > autoSaves > setTables 1`] = `undefined`;
672+
673+
exports[`Persists to/from durableObjectStorage > autoSaves > setValues 1`] = `undefined`;
674+
675+
exports[`Persists to/from durableObjectStorage > loads 1`] = `
676+
[
677+
[
678+
{
679+
"t1": [
680+
{
681+
"r1": [
682+
{
683+
"c1": [
684+
1,
685+
"_",
686+
4065945599,
687+
],
688+
},
689+
"",
690+
1279994494,
691+
],
692+
},
693+
"",
694+
1293085726,
695+
],
696+
},
697+
"",
698+
4033596827,
699+
],
700+
[
701+
{
702+
"v1": [
703+
1,
704+
"_",
705+
4065945599,
706+
],
707+
},
708+
"",
709+
2304392760,
710+
],
711+
]
712+
`;
713+
714+
exports[`Persists to/from durableObjectStorage > saves 1`] = `
715+
[
716+
[
717+
{
718+
"t1": [
719+
{
720+
"r1": [
721+
{
722+
"c1": [
723+
1,
724+
"Nn1JUF-----7JQY8",
725+
1003668370,
726+
],
727+
},
728+
"",
729+
550994372,
730+
],
731+
},
732+
"",
733+
1072852846,
734+
],
735+
},
736+
"",
737+
1771939739,
738+
],
739+
[
740+
{
741+
"v1": [
742+
1,
743+
"Nn1JUF----07JQY8",
744+
1130939691,
745+
],
746+
},
747+
"",
748+
3877632732,
749+
],
750+
]
751+
`;
752+
753+
exports[`Persists to/from durableObjectStorage > saves 2`] = `undefined`;
754+
665755
exports[`Persists to/from file > autoLoads 1`] = `
666756
[
667757
[

test/unit/persisters/common/mocks.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
createOpfsPersister,
2929
createSessionPersister,
3030
} from 'tinybase/persisters/persister-browser';
31+
import {createDurableObjectStoragePersister} from 'tinybase/persisters/persister-durable-object-storage';
3132
import {createFilePersister} from 'tinybase/persisters/persister-file';
3233
import {createIndexedDbPersister} from 'tinybase/persisters/persister-indexed-db';
3334
import {createRemotePersister} from 'tinybase/persisters/persister-remote';
@@ -778,3 +779,133 @@ export const mockAutomerge: Persistable<DocHandle<any>> = {
778779
testMissing: false,
779780
testAutoLoad: true,
780781
};
782+
783+
// Mock DurableObjectStorage - simple Map-based implementation
784+
class MockDurableObjectStorage {
785+
private data = new Map<string, any>();
786+
787+
async get<T>(key: string): Promise<T | undefined>;
788+
async get<T>(keys: string[]): Promise<Map<string, T>>;
789+
async get<T>(
790+
keyOrKeys: string | string[],
791+
): Promise<T | undefined | Map<string, T>> {
792+
if (Array.isArray(keyOrKeys)) {
793+
const result = new Map<string, T>();
794+
for (const key of keyOrKeys) {
795+
const value = this.data.get(key);
796+
if (value !== undefined) result.set(key, value);
797+
}
798+
return result;
799+
}
800+
return this.data.get(keyOrKeys);
801+
}
802+
803+
async put(entries: Record<string, any>): Promise<void> {
804+
for (const [key, value] of Object.entries(entries)) {
805+
this.data.set(key, value);
806+
}
807+
}
808+
809+
async list<T>(options?: {prefix?: string}): Promise<Map<string, T>> {
810+
const result = new Map<string, T>();
811+
const prefix = options?.prefix ?? '';
812+
for (const [key, value] of this.data.entries()) {
813+
if (key.startsWith(prefix)) {
814+
result.set(key, value);
815+
}
816+
}
817+
return result;
818+
}
819+
820+
async delete(key: string): Promise<boolean> {
821+
return this.data.delete(key);
822+
}
823+
824+
clear(): void {
825+
this.data.clear();
826+
}
827+
}
828+
829+
const STORAGE_PREFIX = 'tinybase_';
830+
const T = 't';
831+
const V = 'v';
832+
833+
// Key construction matching the persister's format
834+
const constructStorageKey = (type: string, ...ids: string[]) =>
835+
STORAGE_PREFIX + type + JSON.stringify(ids).slice(1, -1);
836+
837+
export const mockDurableObjectStorage: Persistable<MockDurableObjectStorage> = {
838+
autoLoadPause: 10,
839+
getLocation: async () => new MockDurableObjectStorage(),
840+
getLocationMethod: ['getStorage', (storage) => storage],
841+
getPersister: (
842+
store: Store | MergeableStore,
843+
storage: MockDurableObjectStorage,
844+
) =>
845+
createDurableObjectStoragePersister(
846+
store as MergeableStore,
847+
storage as unknown as DurableObjectStorage,
848+
STORAGE_PREFIX,
849+
),
850+
get: async (
851+
storage: MockDurableObjectStorage,
852+
): Promise<MergeableContent | void> => {
853+
const entries = await storage.list({prefix: STORAGE_PREFIX});
854+
if (entries.size > 0) {
855+
return undefined;
856+
}
857+
},
858+
set: async (
859+
storage: MockDurableObjectStorage,
860+
content: Content | MergeableContent,
861+
): Promise<void> => {
862+
// Convert MergeableContent to the key-value format the persister uses
863+
const [[tablesObj, tablesHlc, tablesHash], [valuesObj, valuesHlc, valuesHash]] =
864+
content as MergeableContent;
865+
const entries: Record<string, any> = {};
866+
867+
// Store tables root
868+
entries[constructStorageKey(T)] = [0, tablesHlc, tablesHash];
869+
870+
// Process tables
871+
Object.entries(tablesObj).forEach(
872+
([tableId, [tableObj, tableHlc, tableHash]]: any) => {
873+
entries[constructStorageKey(T, tableId)] = [0, tableHlc, tableHash];
874+
Object.entries(tableObj).forEach(
875+
([rowId, [rowObj, rowHlc, rowHash]]: any) => {
876+
entries[constructStorageKey(T, tableId, rowId)] = [
877+
0,
878+
rowHlc,
879+
rowHash,
880+
];
881+
Object.entries(rowObj).forEach(([cellId, cellStamp]) => {
882+
entries[constructStorageKey(T, tableId, rowId, cellId)] =
883+
cellStamp;
884+
});
885+
},
886+
);
887+
},
888+
);
889+
890+
// Store values root
891+
entries[constructStorageKey(V)] = [0, valuesHlc, valuesHash];
892+
893+
// Process values
894+
Object.entries(valuesObj).forEach(([valueId, valueStamp]) => {
895+
entries[constructStorageKey(V, valueId)] = valueStamp;
896+
});
897+
898+
await storage.put(entries);
899+
},
900+
write: async (
901+
_storage: MockDurableObjectStorage,
902+
_rawContent: any,
903+
): Promise<void> => {
904+
// Not used for DO storage
905+
},
906+
del: async (storage: MockDurableObjectStorage): Promise<void> => {
907+
storage.clear();
908+
},
909+
testMissing: false,
910+
testAutoLoad: false,
911+
};

test/unit/persisters/mergeable.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {MERGEABLE_VARIANTS} from './common/databases.ts';
1515
import {
1616
getMockDatabases,
1717
mockCustomSynchronizer,
18+
mockDurableObjectStorage,
1819
mockFile,
1920
mockLocalStorage,
2021
mockLocalSynchronizer,
@@ -40,6 +41,7 @@ describe.each([
4041
['opfs', mockOpfs],
4142
['localStorage', mockLocalStorage],
4243
['sessionStorage', mockSessionStorage],
44+
['durableObjectStorage', mockDurableObjectStorage],
4345
['localSynchronizer', mockLocalSynchronizer],
4446
['customSynchronizer', mockCustomSynchronizer],
4547
...getMockDatabases(MERGEABLE_VARIANTS),

0 commit comments

Comments
 (0)