Skip to content

Commit

Permalink
Merge pull request #294 from braden-w:feature/whi-86-feat-decouple-re…
Browse files Browse the repository at this point in the history
…cording-blobs-from-indexeddb-for-future

feat: decouple recording blobs from indexedDb for future usage in blob storage, local file system
  • Loading branch information
braden-w authored Aug 15, 2024
2 parents 59999b6 + d58fe69 commit 5973cb1
Showing 1 changed file with 133 additions and 19 deletions.
152 changes: 133 additions & 19 deletions apps/app/src/lib/services/RecordingDbServiceIndexedDbLive.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,88 @@ import type { Recording } from './RecordingDbService';
import { RecordingsDbService } from './RecordingDbService';

const DB_NAME = 'RecordingDB' as const;
const DB_VERSION = 1 as const;
const RECORDING_STORE = 'recordings' as const;
const DB_VERSION = 2 as const;

interface RecordingsDbSchema extends DBSchema {
recordings: {
const RECORDING_METADATA_STORE = 'recordingMetadata' as const;
const RECORDING_BLOB_STORE = 'recordingBlobs' as const;
const DEPRECATED_RECORDING_STORE = 'recordings' as const;

interface RecordingsDbSchemaV2 extends DBSchema {
[RECORDING_METADATA_STORE]: {
key: Recording['id'];
value: Omit<Recording, 'blob'>;
};
[RECORDING_BLOB_STORE]: {
key: Recording['id'];
value: { id: Recording['id']; blob: Blob };
};
}

interface RecordingsDbSchemaV1 extends DBSchema {
[DEPRECATED_RECORDING_STORE]: {
key: Recording['id'];
value: Recording;
};
}

type RecordingsDbSchema = RecordingsDbSchemaV2 & RecordingsDbSchemaV1;

export const RecordingsDbServiceLiveIndexedDb = Layer.effect(
RecordingsDbService,
Effect.sync(() => {
const db = openDB<RecordingsDbSchema>(DB_NAME, DB_VERSION, {
upgrade(db) {
const isRecordingStoreObjectStoreExists = db.objectStoreNames.contains(RECORDING_STORE);
if (!isRecordingStoreObjectStoreExists) {
db.createObjectStore(RECORDING_STORE, { keyPath: 'id' });
const dbPromise = openDB<RecordingsDbSchema>(DB_NAME, DB_VERSION, {
async upgrade(db, oldVersion, newVersion, transaction) {
if (oldVersion === 0) {
// Fresh install
transaction.db.createObjectStore(RECORDING_METADATA_STORE, { keyPath: 'id' });
transaction.db.createObjectStore(RECORDING_BLOB_STORE, { keyPath: 'id' });
}

if (oldVersion === 1 && newVersion === 2) {
// Upgrade from v1 to v2
const recordingsStore = transaction.objectStore(DEPRECATED_RECORDING_STORE);
const metadataStore = transaction.db.createObjectStore(RECORDING_METADATA_STORE, {
keyPath: 'id',
});
const blobStore = transaction.db.createObjectStore(RECORDING_BLOB_STORE, {
keyPath: 'id',
});

const recordings = await recordingsStore.getAll();
await Promise.all(
recordings.map(async (recording) => {
const { blob, ...metadata } = recording;
await Promise.all([
metadataStore.add(metadata),
blobStore.add({ id: recording.id, blob }),
]);
}),
);

// Delete the old store after migration
transaction.db.deleteObjectStore(DEPRECATED_RECORDING_STORE);
await transaction.done;
}
},
});

return {
addRecording: (recording) =>
Effect.tryPromise({
try: async () => (await db).add(RECORDING_STORE, recording),
try: async () => {
const { blob, ...metadata } = recording;
const tx = (await dbPromise).transaction(
[RECORDING_METADATA_STORE, RECORDING_BLOB_STORE],
'readwrite',
);
const recordingMetadataStore = tx.objectStore(RECORDING_METADATA_STORE);
const recordingBlobStore = tx.objectStore(RECORDING_BLOB_STORE);
await Promise.all([
recordingMetadataStore.add(metadata),
recordingBlobStore.add({ id: recording.id, blob }),
tx.done,
]);
},
catch: (error) =>
new WhisperingError({
title: 'Error adding recording to indexedDB',
Expand All @@ -39,17 +96,35 @@ export const RecordingsDbServiceLiveIndexedDb = Layer.effect(
}),
updateRecording: (recording) =>
Effect.tryPromise({
try: async () => (await db).put(RECORDING_STORE, $state.snapshot(recording)),
try: async () => {
const { blob, ...metadata } = recording;
await Promise.all([
(await dbPromise).put(RECORDING_METADATA_STORE, metadata),
(await dbPromise).put(RECORDING_BLOB_STORE, { id: recording.id, blob }),
]);
},
catch: (error) =>
new WhisperingError({
title: 'Error editing recording in indexedDB',
title: 'Error updating recording in indexedDB',
description: error instanceof Error ? error.message : 'Please try again.',
error,
}),
}),
deleteRecordingById: (id) =>
Effect.tryPromise({
try: async () => (await db).delete(RECORDING_STORE, id),
try: async () => {
const tx = (await dbPromise).transaction(
[RECORDING_METADATA_STORE, RECORDING_BLOB_STORE],
'readwrite',
);
const recordingMetadataStore = tx.objectStore(RECORDING_METADATA_STORE);
const recordingBlobStore = tx.objectStore(RECORDING_BLOB_STORE);
await Promise.all([
recordingMetadataStore.delete(id),
recordingBlobStore.delete(id),
tx.done,
]);
},
catch: (error) =>
new WhisperingError({
title: 'Error deleting recording from indexedDB',
Expand All @@ -60,28 +135,67 @@ export const RecordingsDbServiceLiveIndexedDb = Layer.effect(
deleteRecordingsById: (ids) =>
Effect.tryPromise({
try: async () => {
const tx = (await db).transaction(RECORDING_STORE, 'readwrite');
await Promise.all([...ids.map((id) => tx.store.delete(id)), tx.done]);
const tx = (await dbPromise).transaction(
[RECORDING_METADATA_STORE, RECORDING_BLOB_STORE],
'readwrite',
);
const recordingMetadataStore = tx.objectStore(RECORDING_METADATA_STORE);
const recordingBlobStore = tx.objectStore(RECORDING_BLOB_STORE);
for (const id of ids) {
await recordingMetadataStore.delete(id);
await recordingBlobStore.delete(id);
}
await tx.done;
},
catch: (error) =>
new WhisperingError({
title: 'Error deleting recording from indexedDB',
title: 'Error deleting recordings from indexedDB',
description: error instanceof Error ? error.message : 'Please try again.',
error,
}),
}),
getAllRecordings: Effect.tryPromise({
try: async () => (await db).getAll(RECORDING_STORE),
try: async () => {
const tx = (await dbPromise).transaction(
[RECORDING_METADATA_STORE, RECORDING_BLOB_STORE],
'readonly',
);
const recordingMetadataStore = tx.objectStore(RECORDING_METADATA_STORE);
const recordingBlobStore = tx.objectStore(RECORDING_BLOB_STORE);
const metadata = await recordingMetadataStore.getAll();
const blobs = await recordingBlobStore.getAll();
await tx.done;
return metadata
.map((recording) => {
const blob = blobs.find((blob) => blob.id === recording.id)?.blob;
return blob ? { ...recording, blob } : null;
})
.filter((r) => r !== null);
},
catch: (error) =>
new WhisperingError({
title: 'Error getting all recordings from indexedDB',
title: 'Error getting recordings from indexedDB',
description: error instanceof Error ? error.message : 'Please try again.',
error,
}),
}),
getRecording: (id) =>
Effect.tryPromise({
try: async () => (await db).get(RECORDING_STORE, id),
try: async () => {
const tx = (await dbPromise).transaction(
[RECORDING_METADATA_STORE, RECORDING_BLOB_STORE],
'readonly',
);
const recordingMetadataStore = tx.objectStore(RECORDING_METADATA_STORE);
const recordingBlobStore = tx.objectStore(RECORDING_BLOB_STORE);
const metadata = await recordingMetadataStore.get(id);
const blobData = await recordingBlobStore.get(id);
await tx.done;
if (metadata && blobData) {
return { ...metadata, blob: blobData.blob };
}
return null;
},
catch: (error) =>
new WhisperingError({
title: 'Error getting recording from indexedDB',
Expand Down

0 comments on commit 5973cb1

Please sign in to comment.