-
Notifications
You must be signed in to change notification settings - Fork 615
feat: preserve stores on schema version or rollup address change #24631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mverzilli
wants to merge
25
commits into
merge-train/fairies-v5
Choose a base branch
from
martin/change-store-version-behavior
base: merge-train/fairies-v5
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
6bad978
docs: design for PXE store selection by identity
mverzilli 7c3c29d
docs: implementation plan for PXE store selection by identity
mverzilli 071b0af
feat(kv-store): store identity slug helper and DataStoreConfig.l1ChainId
mverzilli f8076f4
fix(stdlib): honor 'throw' mismatch policy on rollup address change
mverzilli 4634961
feat(kv-store): sqlite-opfs stores selected by (chain, rollup, schema…
mverzilli 38e6382
feat(kv-store): list and delete sqlite-opfs stores
mverzilli d3b2535
feat(kv-store): opt-in identity-partitioned lmdb-v2 stores
mverzilli 6509dd1
feat(pxe): node PXE and embedded wallet stores partitioned by identity
mverzilli 4e0036d
feat(pxe): browser PXE and embedded wallet stores partitioned by iden…
mverzilli 31093a9
docs: changelog for identity-partitioned PXE stores
mverzilli 0bdd2e4
fix(stdlib): separate rollup-address mismatch policy and fix zero-add…
mverzilli d4b69f1
docs: align store-selection docs with rollup-policy split and sibling…
mverzilli 1551290
docs: mark implementation plan as historical after final-review amend…
mverzilli 0452dbe
chore: remove local planning docs from version control
mverzilli f7dae32
feat(pxe): identity-partitioned store helper for node-side stores
mverzilli 4935622
refactor(pxe): open pxe_data and wallet_data via the identity store h…
mverzilli d39c985
refactor(kv-store): contain store-identity logic to sqlite-opfs and p…
mverzilli d5c96d9
docs: cover embedded-wallet data locations in store-selection migrati…
mverzilli d4f8bdb
refactor(pxe): polish identity-store helper logging, exports, and wal…
mverzilli 6864ad0
fix(kv-store): throw on incomplete store identity instead of defaulti…
mverzilli 3b3e56b
fix(pxe): require a full identity in openStoreForIdentity
mverzilli 5a637ce
fix(wallets): make wallet_data chain-agnostic with a schema-only iden…
mverzilli 17f6a9d
docs: wallet_data is chain-agnostic in the store-selection migration …
mverzilli 7a5d236
refactor(wallets): drop unused node-info destructure in browser entry…
mverzilli b4fe53c
test(kv-store): cover missing rollup address in identity throw; note …
mverzilli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,4 @@ | ||
| export * from './store.js'; | ||
| export * from './factory.js'; | ||
| export { storeIdentitySlug } from '../store_identity.js'; | ||
| export type { StoreIdentity } from '../store_identity.js'; |
136 changes: 136 additions & 0 deletions
136
yarn-project/kv-store/src/sqlite-opfs/create_store.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { EthAddress } from '@aztec/foundation/eth-address'; | ||
|
|
||
| import { mockLogger } from '../interfaces/utils.js'; | ||
| import { AztecSQLiteOPFSStore, StoreIdentityMismatchError, createStore, effectiveStoreName } from './index.js'; | ||
| import { deleteStore, listStores, storePoolDirectory } from './manage.js'; | ||
|
|
||
| const configFor = (rollupAddress: EthAddress, l1ChainId = 31337) => ({ | ||
| dataDirectory: 'test', | ||
| dataStoreMapSizeKb: 1024, | ||
| rollupAddress, | ||
| l1ChainId, | ||
| }); | ||
|
|
||
| describe('sqlite-opfs createStore', () => { | ||
| it('rejects when a required identity component is missing', async () => { | ||
| const addr = EthAddress.random(); | ||
| const { l1ChainId: _l1ChainId, ...configWithoutChainId } = configFor(addr); | ||
| const { rollupAddress: _rollupAddress, ...configWithoutRollup } = configFor(addr); | ||
|
|
||
| await expect(createStore('incomplete_test', configWithoutChainId, 1, mockLogger)).rejects.toThrow( | ||
| /without a complete identity/, | ||
| ); | ||
| await expect(createStore('incomplete_test', configWithoutRollup, 1, mockLogger)).rejects.toThrow( | ||
| /without a complete identity/, | ||
| ); | ||
| await expect(createStore('incomplete_test', configFor(addr), undefined, mockLogger)).rejects.toThrow( | ||
| /without a complete identity/, | ||
| ); | ||
|
|
||
| const storeName = effectiveStoreName('incomplete_test', { | ||
| l1ChainId: 31337, | ||
| rollupAddress: addr, | ||
| schemaVersion: 1, | ||
| }); | ||
| expect(await listStores()).not.toContain(storeName); | ||
| }); | ||
|
|
||
| it('keeps data intact when switching rollup addresses back and forth', async () => { | ||
| const addrA = EthAddress.random(); | ||
| const addrB = EthAddress.random(); | ||
|
|
||
| const storeA = await createStore('roundtrip_test', configFor(addrA), 1, mockLogger); | ||
| await storeA.openSingleton<string>('payload').set('data-for-A'); | ||
| await storeA.close(); | ||
|
|
||
| const storeB = await createStore('roundtrip_test', configFor(addrB), 1, mockLogger); | ||
| expect(await storeB.openSingleton<string>('payload').getAsync()).toBeUndefined(); | ||
| await storeB.openSingleton<string>('payload').set('data-for-B'); | ||
| await storeB.close(); | ||
|
|
||
| const reopenedA = await createStore('roundtrip_test', configFor(addrA), 1, mockLogger); | ||
| expect(await reopenedA.openSingleton<string>('payload').getAsync()).toEqual('data-for-A'); | ||
| await reopenedA.close(); | ||
|
|
||
| const reopenedB = await createStore('roundtrip_test', configFor(addrB), 1, mockLogger); | ||
| expect(await reopenedB.openSingleton<string>('payload').getAsync()).toEqual('data-for-B'); | ||
| await reopenedB.close(); | ||
| }); | ||
|
|
||
| it('opens two different stores concurrently in the same tab', async () => { | ||
| const addr = EthAddress.random(); | ||
| const pxeStore = await createStore('pxe_data', configFor(addr), 1, mockLogger); | ||
| const walletStore = await createStore('wallet_data', configFor(addr), 1, mockLogger); | ||
|
|
||
| await pxeStore.openSingleton<string>('k').set('pxe'); | ||
| await walletStore.openSingleton<string>('k').set('wallet'); | ||
| expect(await pxeStore.openSingleton<string>('k').getAsync()).toEqual('pxe'); | ||
| expect(await walletStore.openSingleton<string>('k').getAsync()).toEqual('wallet'); | ||
|
|
||
| await pxeStore.close(); | ||
| await walletStore.close(); | ||
| }); | ||
|
|
||
| it('separates stores by schema version', async () => { | ||
| const addr = EthAddress.random(); | ||
| const v1 = await createStore('schema_test', configFor(addr), 1, mockLogger); | ||
| await v1.openSingleton<string>('k').set('v1-data'); | ||
| await v1.close(); | ||
|
|
||
| const v2 = await createStore('schema_test', configFor(addr), 2, mockLogger); | ||
| expect(await v2.openSingleton<string>('k').getAsync()).toBeUndefined(); | ||
| await v2.close(); | ||
|
|
||
| const v1Again = await createStore('schema_test', configFor(addr), 1, mockLogger); | ||
| expect(await v1Again.openSingleton<string>('k').getAsync()).toEqual('v1-data'); | ||
| await v1Again.close(); | ||
| }); | ||
|
|
||
| it('refuses to open on a recorded-identity mismatch and leaves data untouched', async () => { | ||
| const addr = EthAddress.random(); | ||
| const store = await createStore('mismatch_test', configFor(addr), 1, mockLogger); | ||
| await store.openSingleton<string>('payload').set('precious'); | ||
| // Simulate a naming bug by corrupting the recorded identity. | ||
| await store.openSingleton<string>('dbVersion').set('garbage'); | ||
| await store.close(); | ||
|
|
||
| await expect(createStore('mismatch_test', configFor(addr), 1, mockLogger)).rejects.toThrow( | ||
| StoreIdentityMismatchError, | ||
| ); | ||
|
|
||
| // The refusal must not have modified the store: read it raw, bypassing the identity check. | ||
| const storeName = effectiveStoreName('mismatch_test', { l1ChainId: 31337, rollupAddress: addr, schemaVersion: 1 }); | ||
| const raw = await AztecSQLiteOPFSStore.open(mockLogger, storeName, false, storePoolDirectory(storeName)); | ||
| expect(await raw.openSingleton<string>('payload').getAsync()).toEqual('precious'); | ||
| await raw.close(); | ||
| }); | ||
|
|
||
| it('lists created stores and deletes them', async () => { | ||
| const addr = EthAddress.random(); | ||
| const store = await createStore('managed_test', configFor(addr), 1, mockLogger); | ||
| await store.openSingleton<string>('k').set('v'); | ||
| await store.close(); | ||
|
|
||
| const storeName = effectiveStoreName('managed_test', { l1ChainId: 31337, rollupAddress: addr, schemaVersion: 1 }); | ||
| expect(await listStores()).toContain(storeName); | ||
|
|
||
| await deleteStore(storeName); | ||
| expect(await listStores()).not.toContain(storeName); | ||
|
|
||
| // Recreating after deletion starts empty. | ||
| const fresh = await createStore('managed_test', configFor(addr), 1, mockLogger); | ||
| expect(await fresh.openSingleton<string>('k').getAsync()).toBeUndefined(); | ||
| await fresh.close(); | ||
| }); | ||
|
|
||
| it('refuses to delete a store that is currently open', async () => { | ||
| const addr = EthAddress.random(); | ||
| const store = await createStore('locked_test', configFor(addr), 1, mockLogger); | ||
| const storeName = effectiveStoreName('locked_test', { l1ChainId: 31337, rollupAddress: addr, schemaVersion: 1 }); | ||
|
|
||
| await expect(deleteStore(storeName)).rejects.toThrow(); | ||
|
|
||
| await store.close(); | ||
| await deleteStore(storeName); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,27 +1,87 @@ | ||||||||||
| import type { EthAddress } from '@aztec/foundation/eth-address'; | ||||||||||
| import { type Logger, createLogger } from '@aztec/foundation/log'; | ||||||||||
| import { DatabaseVersion } from '@aztec/stdlib/database-version/version'; | ||||||||||
| import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; | ||||||||||
|
|
||||||||||
| import { initStoreForRollupAndSchemaVersion } from '../utils.js'; | ||||||||||
| import { StoreIdentityMismatchError, effectiveStoreName } from '../store_identity.js'; | ||||||||||
| import { storePoolDirectory } from './manage.js'; | ||||||||||
| import { AztecSQLiteOPFSStore } from './store.js'; | ||||||||||
|
|
||||||||||
| export { AztecSQLiteOPFSStore } from './store.js'; | ||||||||||
| export { SqliteEncryptionError } from './errors.js'; | ||||||||||
| export type { SqliteEncryptionErrorCode } from './errors.js'; | ||||||||||
| export { OPFS_POOL_DIR_PREFIX, deleteStore, listStores, storePoolDirectory } from './manage.js'; | ||||||||||
| export { StoreIdentityMismatchError, effectiveStoreName, storeIdentitySlug } from '../store_identity.js'; | ||||||||||
| export type { StoreIdentity } from '../store_identity.js'; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Opens the persistent store selected by `name` and the identity `(config.l1ChainId, config.rollupAddress, | ||||||||||
| * schemaVersion)`. A store exists per identity: reopening with the same identity returns the same data, a | ||||||||||
| * different identity selects a different (possibly fresh) store. Nothing is ever cleared. | ||||||||||
| * | ||||||||||
| * @throws If `schemaVersion`, `config.rollupAddress`, or `config.l1ChainId` is missing — an incomplete identity | ||||||||||
| * must never silently default to the zero identity. | ||||||||||
| */ | ||||||||||
| export async function createStore( | ||||||||||
| name: string, | ||||||||||
| config: DataStoreConfig, | ||||||||||
| config: DataStoreConfig & { l1ChainId?: number }, | ||||||||||
| schemaVersion: number | undefined = undefined, | ||||||||||
| log: Logger = createLogger('kv-store'), | ||||||||||
| ) { | ||||||||||
| const { dataDirectory } = config; | ||||||||||
| log.info( | ||||||||||
| dataDirectory | ||||||||||
| ? `Creating ${name} SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB` | ||||||||||
| : `Creating ${name} ephemeral SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB`, | ||||||||||
| const { rollupAddress, l1ChainId } = config; | ||||||||||
| if (schemaVersion === undefined || rollupAddress === undefined || l1ChainId === undefined) { | ||||||||||
| const missing = [ | ||||||||||
| l1ChainId === undefined && 'l1ChainId', | ||||||||||
| rollupAddress === undefined && 'rollupAddress', | ||||||||||
| schemaVersion === undefined && 'schemaVersion', | ||||||||||
| ].filter((component): component is string => component !== false); | ||||||||||
| throw new Error(`Cannot open store '${name}' without a complete identity: missing ${missing.join(', ')}`); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const storeName = effectiveStoreName(name, { l1ChainId, rollupAddress, schemaVersion }); | ||||||||||
| log.info(`Creating ${storeName} SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB`); | ||||||||||
| const store = await AztecSQLiteOPFSStore.open( | ||||||||||
| createLogger('kv-store:sqlite-opfs'), | ||||||||||
| storeName, | ||||||||||
| false, | ||||||||||
| storePoolDirectory(storeName), | ||||||||||
| ); | ||||||||||
| const store = await AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), name, false); | ||||||||||
| return initStoreForRollupAndSchemaVersion(store, schemaVersion, config.rollupAddress, log); | ||||||||||
| try { | ||||||||||
| await assertStoreIdentity(store, storeName, schemaVersion, rollupAddress); | ||||||||||
| } catch (err) { | ||||||||||
| // The store handle owns a worker and OPFS locks; release them before surfacing the refusal. | ||||||||||
| await store.close().catch(() => {}); | ||||||||||
| throw err; | ||||||||||
| } | ||||||||||
| return store; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Belt-and-braces invariant check: the identity is part of the physical store name, so the recorded version | ||||||||||
| * can only disagree if there is a store-naming bug. Refuses to open on mismatch; never clears. | ||||||||||
|
Comment on lines
+60
to
+61
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| */ | ||||||||||
| async function assertStoreIdentity( | ||||||||||
| store: AztecSQLiteOPFSStore, | ||||||||||
| storeName: string, | ||||||||||
| schemaVersion: number, | ||||||||||
| rollupAddress: EthAddress, | ||||||||||
| ): Promise<void> { | ||||||||||
| const expected = new DatabaseVersion(schemaVersion, rollupAddress); | ||||||||||
| const singleton = store.openSingleton<string>('dbVersion'); | ||||||||||
| const stored = await singleton.getAsync(); | ||||||||||
| if (stored === undefined) { | ||||||||||
| await singleton.set(expected.toBuffer().toString('utf-8')); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
| let storedVersion: DatabaseVersion; | ||||||||||
| try { | ||||||||||
| storedVersion = DatabaseVersion.fromBuffer(Buffer.from(stored, 'utf-8')); | ||||||||||
| } catch { | ||||||||||
| throw new StoreIdentityMismatchError(storeName, expected.toString(), stored); | ||||||||||
| } | ||||||||||
| if (!storedVersion.equals(expected)) { | ||||||||||
| throw new StoreIdentityMismatchError(storeName, expected.toString(), storedVersion.toString()); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export function openTmpStore(ephemeral: boolean = false): Promise<AztecSQLiteOPFSStore> { | ||||||||||
|
|
||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /** Prefix for the per-store OPFS SAH pool directories owned by this package. */ | ||
| export const OPFS_POOL_DIR_PREFIX = '.aztec-kv-'; | ||
|
|
||
| /** | ||
| * OPFS directory holding a store's SAH pool. One directory per store: the SAH-pool VFS allows only one | ||
| * concurrent instance per directory and its capacity does not grow automatically, so sharing a pool across | ||
| * stores would make concurrently opened stores contend for locks and orphaned stores exhaust pool slots. | ||
| */ | ||
| export function storePoolDirectory(effectiveName: string): string { | ||
| return `${OPFS_POOL_DIR_PREFIX}${effectiveName}`; | ||
| } | ||
|
|
||
| /** | ||
| * Lists the effective names (logical name + identity slug) of every persistent sqlite-opfs store in this | ||
| * origin, by enumerating the per-store pool directories. Includes stores created under identities other than | ||
| * the current one — that is the point: wallets can surface and clean up data for networks no longer in use. | ||
| */ | ||
| export async function listStores(): Promise<string[]> { | ||
| const root = await navigator.storage.getDirectory(); | ||
| const names: string[] = []; | ||
| for await (const [entryName, handle] of root.entries()) { | ||
| if (handle.kind === 'directory' && entryName.startsWith(OPFS_POOL_DIR_PREFIX)) { | ||
| names.push(entryName.slice(OPFS_POOL_DIR_PREFIX.length)); | ||
| } | ||
| } | ||
| return names; | ||
| } | ||
|
|
||
| /** | ||
| * Permanently deletes a store by effective name (as returned by {@link listStores}). The store must be closed: | ||
| * an open store's SAH pool holds locks on the directory and the removal will reject. | ||
| */ | ||
| export async function deleteStore(effectiveName: string): Promise<void> { | ||
| const root = await navigator.storage.getDirectory(); | ||
| await root.removeEntry(storePoolDirectory(effectiveName), { recursive: true }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { EthAddress } from '@aztec/foundation/eth-address'; | ||
|
|
||
| import { effectiveStoreName, storeIdentitySlug } from './store_identity.js'; | ||
|
|
||
| describe('storeIdentitySlug', () => { | ||
| it('composes chain id, rollup address and schema version', () => { | ||
| const rollupAddress = EthAddress.fromString('0x1234567890abcdef1234567890abcdef12345678'); | ||
| expect(storeIdentitySlug({ l1ChainId: 31337, rollupAddress, schemaVersion: 12 })).toEqual( | ||
| '31337-0x1234567890abcdef1234567890abcdef12345678-v12', | ||
| ); | ||
| }); | ||
|
|
||
| it('composes the exact slug format for the zero identity', () => { | ||
| expect(storeIdentitySlug({ l1ChainId: 0, rollupAddress: EthAddress.ZERO, schemaVersion: 0 })).toEqual( | ||
| '0-0x0000000000000000000000000000000000000000-v0', | ||
| ); | ||
| }); | ||
|
|
||
| it('normalizes the rollup address to lowercase hex', () => { | ||
| const rollupAddress = EthAddress.fromString('0x1234567890ABCDEF1234567890ABCDEF12345678'); | ||
| expect(storeIdentitySlug({ l1ChainId: 0, rollupAddress, schemaVersion: 1 })).toEqual( | ||
| `0-0x1234567890abcdef1234567890abcdef12345678-v1`, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('effectiveStoreName', () => { | ||
| it('joins the logical name and the slug with an underscore', () => { | ||
| const rollupAddress = EthAddress.fromString('0x1234567890abcdef1234567890abcdef12345678'); | ||
| expect(effectiveStoreName('pxe_data', { l1ChainId: 1, rollupAddress, schemaVersion: 2 })).toEqual( | ||
| 'pxe_data_1-0x1234567890abcdef1234567890abcdef12345678-v2', | ||
| ); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import type { EthAddress } from '@aztec/foundation/eth-address'; | ||
|
|
||
| /** The coordinates that determine which physical store a logical store name maps to. */ | ||
| export type StoreIdentity = { | ||
| /** Chain ID of the L1 the rollup is deployed to. */ | ||
| l1ChainId: number; | ||
| /** Address of the rollup contract the store's data pertains to. */ | ||
| rollupAddress: EthAddress; | ||
| /** Schema version of the data held in the store. */ | ||
| schemaVersion: number; | ||
| }; | ||
|
|
||
| /** | ||
| * Composes the store-name discriminator for a store identity. Two identities map to the same physical store iff | ||
| * their slugs are equal, so the format must stay stable: `<l1ChainId>-<rollupAddress>-v<schemaVersion>`. | ||
| */ | ||
| export function storeIdentitySlug({ l1ChainId, rollupAddress, schemaVersion }: StoreIdentity): string { | ||
| return `${l1ChainId}-${rollupAddress.toString()}-v${schemaVersion}`; | ||
| } | ||
|
|
||
| /** Composes the physical store name for a logical store name and identity. */ | ||
| export function effectiveStoreName(name: string, identity: StoreIdentity): string { | ||
| return `${name}_${storeIdentitySlug(identity)}`; | ||
| } | ||
|
|
||
| /** | ||
| * Thrown when a store's recorded identity does not match the identity it was opened under. Since the identity is | ||
| * part of the physical store name, this can only indicate a store-naming bug; the store is left untouched. | ||
| */ | ||
| export class StoreIdentityMismatchError extends Error { | ||
| constructor( | ||
| public readonly storeName: string, | ||
| public readonly expected: string, | ||
| public readonly actual: string, | ||
| ) { | ||
| super( | ||
| `Store '${storeName}' records identity ${actual} but was opened as ${expected}. ` + | ||
| `Refusing to open; data was NOT modified.`, | ||
| ); | ||
| this.name = 'StoreIdentityMismatchError'; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.