diff --git a/packages/zero-client/src/client/zero-idbname.test.ts b/packages/zero-client/src/client/zero-idb.test.ts similarity index 61% rename from packages/zero-client/src/client/zero-idbname.test.ts rename to packages/zero-client/src/client/zero-idb.test.ts index fcb485ed61..c17068c814 100644 --- a/packages/zero-client/src/client/zero-idbname.test.ts +++ b/packages/zero-client/src/client/zero-idb.test.ts @@ -1,4 +1,5 @@ import {expect, test} from 'vitest'; +import {hasMemStore} from '../../../replicache/src/kv/mem-store.ts'; import {h64} from '../../../shared/src/hash.ts'; import {createSchema} from '../../../zero-schema/src/builder/schema-builder.ts'; import {string, table} from '../../../zero-schema/src/builder/table-builder.ts'; @@ -15,6 +16,18 @@ const schema = createSchema({ ], }); +const schemaV2 = createSchema({ + tables: [ + table('foo') + .columns({ + id: string(), + value: string(), + value2: string(), + }) + .primaryKey('id'), + ], +}); + const userID = 'test-user'; const storageKey = 'test-storage'; @@ -95,3 +108,52 @@ test('idbName generation with URL configuration', async () => { await zero.close(); } }); + +test('delete closes and removes all databases for the same zero instance', async () => { + const userIDForDrop = 'drop-db-user'; + const storageKeyForDrop = 'drop-db-storage'; + + const zOld = new Zero({ + userID: userIDForDrop, + storageKey: storageKeyForDrop, + schema, + kvStore: 'mem', + }); + const oldDBName = zOld.idbName; + await zOld.close(); + + const zCurrent = new Zero({ + userID: userIDForDrop, + storageKey: storageKeyForDrop, + schema: schemaV2, + kvStore: 'mem', + }); + const currentDBName = zCurrent.idbName; + + const zOther = new Zero({ + userID: 'drop-db-other-user', + storageKey: 'drop-db-other-storage', + schema, + kvStore: 'mem', + }); + const otherDBName = zOther.idbName; + + expect(zCurrent.closed).toBe(false); + expect(hasMemStore(oldDBName)).toBe(true); + expect(hasMemStore(currentDBName)).toBe(true); + expect(hasMemStore(otherDBName)).toBe(true); + + const result = await zCurrent.delete(); + + expect(zCurrent.closed).toBe(true); + expect(result.errors).toHaveLength(0); + expect(result.deleted).toContain(oldDBName); + expect(result.deleted).toContain(currentDBName); + expect(hasMemStore(oldDBName)).toBe(false); + expect(hasMemStore(currentDBName)).toBe(false); + + expect(hasMemStore(otherDBName)).toBe(true); + + await zOther.close(); + await zOther.delete(); +}); diff --git a/packages/zero-client/src/client/zero.ts b/packages/zero-client/src/client/zero.ts index e7b335c797..6db317702c 100644 --- a/packages/zero-client/src/client/zero.ts +++ b/packages/zero-client/src/client/zero.ts @@ -1,11 +1,13 @@ import {LogContext, type LogLevel} from '@rocicorp/logger'; import {type Resolver, resolver} from '@rocicorp/resolver'; import {type DeletedClients} from '../../../replicache/src/deleted-clients.ts'; +import {getKVStoreProvider} from '../../../replicache/src/get-kv-store-provider.ts'; import { ReplicacheImpl, type ReplicacheImplOptions, } from '../../../replicache/src/impl.ts'; -import {dropDatabase} from '../../../replicache/src/persist/collect-idb-databases.ts'; +import {dropDatabase as dropReplicacheDatabase} from '../../../replicache/src/persist/collect-idb-databases.ts'; +import {IDBDatabasesStore} from '../../../replicache/src/persist/idb-databases-store.ts'; import type {Puller, PullerResult} from '../../../replicache/src/puller.ts'; import type {Pusher, PusherResult} from '../../../replicache/src/pusher.ts'; import type {ReplicacheOptions} from '../../../replicache/src/replicache-options.ts'; @@ -430,6 +432,7 @@ export class Zero< #totalToConnectStart: number | undefined = undefined; readonly #options: ZeroOptions; + readonly #kvStore: ZeroOptions['kvStore']; /** * Query builders for each table in the schema. @@ -496,6 +499,8 @@ export class Zero< }); } + this.#kvStore = kvStore; + this.pingTimeoutMs = pingTimeoutMs; this.#onlineManager = new OnlineManager(); @@ -1409,7 +1414,9 @@ export class Zero< kind === ErrorKind.InvalidConnectionRequestLastMutationID || kind === ErrorKind.InvalidConnectionRequestBaseCookie ) { - await dropDatabase(this.#rep.idbName); + await dropReplicacheDatabase(this.#rep.idbName, { + kvStore: this.#kvStore, + }); reloadWithReason(lc, this.#reload, kind, serverAheadReloadReason); } } @@ -2387,6 +2394,40 @@ export class Zero< } } + async delete(): Promise<{deleted: string[]; errors: unknown[]}> { + await this.close(); + + const kvStoreProvider = getKVStoreProvider(this.#lc, this.#kvStore); + const idbDatabasesStore = new IDBDatabasesStore(kvStoreProvider.create); + try { + const databases = await idbDatabasesStore.getDatabases(); + const dbNamesToDelete = Object.values(databases) + .filter(database => database.replicacheName === this.#rep.name) + .map(database => database.name); + + const deleteResults = await Promise.allSettled( + dbNamesToDelete.map(async dbName => { + await dropReplicacheDatabase(dbName, {kvStore: this.#kvStore}); + return dbName; + }), + ); + + const deleted: string[] = []; + const errors: unknown[] = []; + for (const result of deleteResults) { + if (result.status === 'fulfilled') { + deleted.push(result.value); + } else { + errors.push(result.reason); + } + } + + return {deleted, errors}; + } finally { + await idbDatabasesStore.close(); + } + } + #addMetric: ( metric: K, value: number,