DBlobs: simplify

This commit is contained in:
Enrico Ros
2024-06-13 01:19:13 -07:00
parent 63e9022b84
commit a765c566c8
3 changed files with 137 additions and 121 deletions
+27 -31
View File
@@ -1,6 +1,6 @@
import Dexie from 'dexie';
import { DBlobAudioItem, dBlobCacheT256, DBlobDBItem, DBlobId, DBlobImageItem, DBlobItem, DBlobMetaDataType, DBlobMimeType } from './dblobs.types';
import { DBlobAsset, DBlobAssetType, DBlobAudioAsset, DBlobDBAsset, DBlobId, DBlobImageAsset, DBlobMimeType } from './dblobs.types';
import { resizeBase64ImageIfNeeded } from '~/common/util/imageUtils';
@@ -12,14 +12,15 @@ import { resizeBase64ImageIfNeeded } from '~/common/util/imageUtils';
* - indexedDB.deleteDatabase('NAME').onsuccess = console.log;
*/
class BigAgiDB extends Dexie {
largeAssets!: Dexie.Table<DBlobDBItem, string>;
largeAssets!: Dexie.Table<DBlobDBAsset, string>;
constructor() {
super('Big-AGI');
this.version(1).stores({
// Index common properties
largeAssets: 'id, uId, wId, cId, sId, [cId+sId], type, [type+cId+sId], data.mimeType, origin.ot, origin.source, createdAt, updatedAt',
// Index common properties (and compound indexes)
largeAssets: 'id, contextId, scopeId, [contextId+scopeId], type, [type+contextId+scopeId], data.mimeType, origin.ot, origin.source, createdAt, updatedAt',
});
// Note: can re-add wId and uId in version 2 if needed
}
}
@@ -38,20 +39,17 @@ const assetsTable = _db.largeAssets;
// CRUD
const DEFAULT_USER_ID = '1';
const DEFAULT_WORKSPACE_ID = '1';
export async function addDBlobItem<T extends DBlobItem>(item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']): Promise<DBlobId> {
export async function addDBlobItem<T extends DBlobAsset>(item: T, contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']): Promise<DBlobId> {
// Auto-Thumbnail: when adding an image, generate a thumbnail-256 cache level
if (item.type === DBlobMetaDataType.IMAGE) {
if (!item.cache?.[dBlobCacheT256]) {
const imageItem = item as DBlobImageItem;
if (item.type === 'image') {
if (!item.cache?.thumb256) {
const imageItem = item as DBlobImageAsset;
const resizedDataForCache = await resizeBase64ImageIfNeeded(imageItem.data.mimeType, imageItem.data.base64, 'thumbnail-256', DBlobMimeType.IMG_WEBP, 0.9)
.catch((error: any) => console.error('addDBlobItem: Error resizing image', error));
if (resizedDataForCache) {
item.cache[dBlobCacheT256] = {
item.cache.thumb256 = {
base64: resizedDataForCache.base64,
mimeType: DBlobMimeType.IMG_WEBP,
};
@@ -63,10 +61,8 @@ export async function addDBlobItem<T extends DBlobItem>(item: T, cId: DBlobDBIte
// returns the id of the added item
return await assetsTable.add({
...item,
uId: DEFAULT_USER_ID,
wId: DEFAULT_WORKSPACE_ID,
cId,
sId,
contextId,
scopeId,
});
} catch (error) {
console.error('addDBlobItem: Error adding item', error);
@@ -78,25 +74,25 @@ export async function getDBlobItemIDs() {
return assetsTable.toCollection().primaryKeys();
}
export async function getItemById<T extends DBlobItem = DBlobItem>(id: DBlobId) {
export async function getItemById<T extends DBlobAsset = DBlobAsset>(id: DBlobId) {
return await assetsTable.get(id) as T | undefined;
}
export async function getDBlobItemsByType<T extends DBlobItem>(type: T['type']) {
export async function getDBlobItemsByType<T extends DBlobAsset>(type: T['type']) {
return await assetsTable.where({ type }).toArray() as unknown as T[];
}
export async function getDBlobItemsByTypeCIdSid<T extends DBlobItem>(type: T['type'], cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) {
const items = await assetsTable.where({ type, cId, sId }).sortBy('createdAt');
export async function getDBlobItemsByTypeContextIdScopeId<T extends DBlobAsset>(type: T['type'], contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']) {
const items = await assetsTable.where({ type, contextId, scopeId }).sortBy('createdAt');
return items.reverse() as unknown as T[];
}
export async function getItemsByMimeType<T extends DBlobItem>(mimeType: T['data']['mimeType']) {
export async function getItemsByMimeType<T extends DBlobAsset>(mimeType: T['data']['mimeType']) {
return await assetsTable.where('data.mimeType').equals(mimeType).toArray() as unknown as T[];
}
export async function updateDBlobItem(id: DBlobId, updates: Partial<DBlobItem>) {
export async function updateDBlobItem(id: DBlobId, updates: Partial<DBlobAsset>) {
return assetsTable.update(id, updates);
}
@@ -108,14 +104,14 @@ export async function deleteDBlobItems(ids: DBlobId[]) {
return assetsTable.bulkDelete(ids);
}
export async function deleteAllDBlobsInScopeId(cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) {
return assetsTable.where({ cId, sId }).delete();
export async function deleteAllDBlobsInScopeId(contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']) {
return assetsTable.where({ contextId, scopeId }).delete();
}
// Specific item types
async function getImageItemById(id: DBlobId) {
return await getItemById<DBlobImageItem>(id);
return await getItemById<DBlobImageAsset>(id);
}
export async function getImageDataURLById(id: DBlobId) {
@@ -139,18 +135,18 @@ export async function getImageBlobURLById(id: DBlobId) {
}
// Example usage:
async function getAllImages(): Promise<DBlobImageItem[]> {
return await getDBlobItemsByType<DBlobImageItem>(DBlobMetaDataType.IMAGE);
async function getAllImages(): Promise<DBlobImageAsset[]> {
return await getDBlobItemsByType<DBlobImageAsset>(DBlobAssetType.IMAGE);
}
async function getAllAudio(): Promise<DBlobAudioItem[]> {
return await getDBlobItemsByType<DBlobAudioItem>(DBlobMetaDataType.AUDIO);
async function getAllAudio(): Promise<DBlobAudioAsset[]> {
return await getDBlobItemsByType<DBlobAudioAsset>(DBlobAssetType.AUDIO);
}
async function getHighResImages() {
return await assetsTable
.where('data.mimeType')
.startsWith('image/')
.and(item => (item as DBlobImageItem).metadata.width > 1920)
.toArray() as DBlobImageItem[];
.and(item => (item as DBlobImageAsset).metadata.width > 1920)
.toArray() as DBlobImageAsset[];
}
+11 -11
View File
@@ -1,14 +1,14 @@
import { useLiveQuery } from 'dexie-react-hooks';
import type { DBlobDBItem, DBlobItem } from './dblobs.types';
import { addDBlobItem, deleteDBlobItem, getDBlobItemsByTypeCIdSid, getItemById, updateDBlobItem } from './dblobs.db';
import type { DBlobAsset, DBlobDBAsset } from './dblobs.types';
import { addDBlobItem, deleteDBlobItem, getDBlobItemsByTypeContextIdScopeId, getItemById, updateDBlobItem } from './dblobs.db';
// export function useDBlobItems<T extends DBlobItem>(type: T['type']): [T[] | undefined, (item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) => Promise<void>, (id: string) => Promise<void>] {
// export function useDBlobItems<T extends DBlobItem>(type: T['type']): [T[] | undefined, (item: T, contextId: DBlobDBItem['contextId'], scopeId: DBlobDBItem['scopeId']) => Promise<void>, (id: string) => Promise<void>] {
// const items = useLiveQuery(() => getDBlobItemsByType<T>(type), [type]);
//
// const addDBlobItemHandler = async (item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) => {
// await addDBlobItem(item, cId, sId);
// const addDBlobItemHandler = async (item: T, contextId: DBlobDBItem['contextId'], scopeId: DBlobDBItem['scopeId']) => {
// await addDBlobItem(item, contextId, scopeId);
// };
//
// const deleteDBlobItemHandler = async (id: string) => {
@@ -18,14 +18,14 @@ import { addDBlobItem, deleteDBlobItem, getDBlobItemsByTypeCIdSid, getItemById,
// return [items, addDBlobItemHandler, deleteDBlobItemHandler];
// }
export function useDBlobItemsByTypeCIdSId<T extends DBlobItem>(type: T['type'], cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']): [T[] | undefined, (item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) => Promise<void>, (id: string) => Promise<void>] {
export function useDBlobItemsByTypeContextIdScopeId<T extends DBlobAsset>(type: T['type'], contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']): [T[] | undefined, (item: T, contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']) => Promise<void>, (id: string) => Promise<void>] {
const items = useLiveQuery(
() => getDBlobItemsByTypeCIdSid<T>(type, cId, sId),
[type, cId, sId],
() => getDBlobItemsByTypeContextIdScopeId<T>(type, contextId, scopeId),
[type, contextId, scopeId],
);
const addDBlobItemHandler = async (item: T, cId: DBlobDBItem['cId'], sId: DBlobDBItem['sId']) => {
await addDBlobItem(item, cId, sId);
const addDBlobItemHandler = async (item: T, contextId: DBlobDBAsset['contextId'], scopeId: DBlobDBAsset['scopeId']) => {
await addDBlobItem(item, contextId, scopeId);
};
const deleteDBlobItemHandler = async (id: string) => {
@@ -36,7 +36,7 @@ export function useDBlobItemsByTypeCIdSId<T extends DBlobItem>(type: T['type'],
}
export function useDBlobItem<T extends DBlobItem>(id: string): [T | undefined, (updates: Partial<T>) => Promise<void>] {
export function useDBlobItem<T extends DBlobAsset>(id: string): [T | undefined, (updates: Partial<T>) => Promise<void>] {
const item = useLiveQuery(
() => getItemById<T>(id),
[id],
+99 -79
View File
@@ -1,17 +1,72 @@
import { nanoid } from 'nanoid';
// Blob
// DB - Assets
export enum DBlobMimeType {
IMG_PNG = 'image/png', IMG_JPEG = 'image/jpeg', IMG_WEBP = 'image/webp',
AUDIO_MPEG = 'audio/mpeg', AUDIO_WAV = 'audio/wav',
// VIDEO_MP4 = 'video/mp4',
// DOCUMENT_PDF = 'application/pdf', DOCUMENT_PLAIN = 'text/plain', DOCUMENT_HTML = 'text/html',
// ...
/**
* This is the asset when stored/loaded in the DB. Carries some more context out of band from the asset itself.
*/
export type DBlobDBAsset = {
contextId: 'global';
scopeId: 'app-chat' | 'app-draw' | 'attachment-drafts';
} & DBlobAsset;
// Assets
export type DBlobAsset = DBlobImageAsset | DBlobAudioAsset; // | DBlobVideoAsset | DBlobDocumentAsset | DBlobTextAsset;
export type DBlobImageAsset = DBlobAssetImplV1<
/* type: */ DBlobAssetType.IMAGE,
/* data: <mime> */ DBlobMimeType.IMG_PNG | DBlobMimeType.IMG_JPEG | DBlobMimeType.IMG_WEBP,
/* metadata: */ ImageAssetMetadata
>;
export type DBlobAudioAsset = DBlobAssetImplV1<
/* type: */ DBlobAssetType.AUDIO,
/* data: <mime> */ DBlobMimeType.AUDIO_MPEG | DBlobMimeType.AUDIO_WAV,
/* metadata: */ AudioAssetMetadata
>;
// type DBlobVideoAsset = DBlobAssetImplV1<DBlobAssetType.VIDEO, DBlobDataMimeType.VIDEO_MP4, VideoAssetMetadata>;
// type DBlobDocumentAsset = DBlobAssetImplV1<DBlobAssetType.DOCUMENT, DBlobDataMimeType.DOCUMENT_PDF, DocumentAssetMetadata>;
// type DBlobTextAsset = DBlobAssetImplV1<DBlobAssetType.TEXT, DBlobDataMimeType.DOCUMENT_PLAIN, {}>;
// DB - Asset Generic Type
interface DBlobAssetImplV1<TType extends DBlobAssetType, TMime extends DBlobMimeType, TMeta extends Record<string, any>> {
id: DBlobId; // Unique identifier
type: TType; // Type of asset, used for discrimination
label: string; // Textual representation
data: DBlobAssetData<TMime>; // Original data as a BlobData object
origin: DBlobAssetOrigin; // Source of the data (e.g., "upload", "generated")
createdAt: Date; // Creation date
updatedAt: Date; // Last updated date
metadata: TMeta; // Flexible metadata for specific .type(s)
// cache: Record<string, DBlobData<DBlobMimeType>>; // Cached conversions as BlobData objects
cache: {
thumb256?: DBlobAssetData<DBlobMimeType.IMG_WEBP>; // Cache for the thumbnail-256 conversion
};
}
interface DBlobData<M extends DBlobMimeType> {
export type DBlobId = string;
export enum DBlobAssetType {
IMAGE = 'image',
AUDIO = 'audio',
// VIDEO = 'video',
// DOCUMENT = 'document',
// EGO = 'ego',
}
// Asset Data
interface DBlobAssetData<M extends DBlobMimeType> {
mimeType: M;
base64: string; // Base64 encoded content (not a data URL)
// NOTE: the data url will be "data:${mimeType};base64,${base64}"
@@ -20,8 +75,17 @@ interface DBlobData<M extends DBlobMimeType> {
altData?: string; // Alternative data for the input (optional)
}
export enum DBlobMimeType {
IMG_PNG = 'image/png', IMG_JPEG = 'image/jpeg', IMG_WEBP = 'image/webp',
AUDIO_MPEG = 'audio/mpeg', AUDIO_WAV = 'audio/wav',
// VIDEO_MP4 = 'video/mp4',
// DOCUMENT_PDF = 'application/pdf', DOCUMENT_PLAIN = 'text/plain', DOCUMENT_HTML = 'text/html',
}
// Item Origin
// Asset Origin
type DBlobAssetOrigin = UserOrigin | GeneratedOrigin;
interface UserOrigin {
ot: 'user';
@@ -76,58 +140,10 @@ interface EgoOrigin {
messageId?: string; // ID of the message (optional)
}*/
// Union type for ItemDataOrigin
type ItemDataOrigin = UserOrigin | GeneratedOrigin;
// Asset Metadata
// Item Base type
interface DBlobBase<TType extends DBlobMetaDataType, TMime extends DBlobMimeType, TMeta extends Record<string, any>> {
id: DBlobId; // Unique identifier
type: TType; // Type of item, used for discrimination
label: string; // Textual representation
data: DBlobData<TMime>; // Original data as a BlobData object
origin: ItemDataOrigin; // Source of the data (e.g., "upload", "generated")
createdAt: Date; // Creation date
updatedAt: Date; // Last updated date
metadata: TMeta; // Flexible metadata for specific .type(s)
cache: Record<string, DBlobData<DBlobMimeType>>; // Cached conversions as BlobData objects
}
export type DBlobId = string;
export function createDBlobBase<TType extends DBlobMetaDataType, TMime extends DBlobMimeType, TMeta extends Record<string, any>>(type: TType, label: string, data: DBlobData<TMime>, origin: ItemDataOrigin, metadata: TMeta): DBlobBase<TType, TMime, TMeta> {
const creationDate = new Date();
return {
id: nanoid(),
type,
label,
data,
origin,
createdAt: creationDate,
updatedAt: creationDate,
metadata,
cache: {},
};
}
export const dBlobCacheT256 = 'thumbnail-256';
// Item Specialization
export enum DBlobMetaDataType {
IMAGE = 'image',
AUDIO = 'audio',
// VIDEO = 'video',
// DOCUMENT = 'document',
// EGO = 'ego',
}
interface ImageMetadata {
interface ImageAssetMetadata {
width: number;
height: number;
averageColor?: string; // Average html color of the image (optional)
@@ -136,7 +152,7 @@ interface ImageMetadata {
description?: string; // Description of the image (optional)
}
interface AudioMetadata {
interface AudioAssetMetadata {
duration: number; // Duration in seconds
sampleRate: number; // Sample rate of the audio
bitrate?: number; // Bitrate of the audio (optional)
@@ -167,25 +183,29 @@ interface DocumentMetadata {
}*/
// Item Data
export type DBlobImageItem = DBlobBase<DBlobMetaDataType.IMAGE, DBlobMimeType.IMG_PNG | DBlobMimeType.IMG_JPEG | DBlobMimeType.IMG_WEBP, ImageMetadata>;
export type DBlobAudioItem = DBlobBase<DBlobMetaDataType.AUDIO, DBlobMimeType.AUDIO_MPEG | DBlobMimeType.AUDIO_WAV, AudioMetadata>;
// type DBlobVideoItem = DBlobBase<ItemDataType.VIDEO, BlobMimeType.VIDEO_MP4, VideoMetadata>;
// type DBlobDocumentItem = DBlobBase<ItemDataType.DOCUMENT, BlobMimeType.DOCUMENT_PDF, DocumentMetadata>;
// type DBlobTextItem = DBlobBase<ItemDataType.TEXT, BlobMimeType.DOCUMENT_PLAIN, {}>;
// DB Item Data
export type DBlobItem = DBlobImageItem | DBlobAudioItem; // | DBlobVideoItem | DBlobDocumentItem | DBlobTextItem | DBlobEgoItem;
export const createDBlobImageItem = (label: string, data: DBlobImageItem['data'], origin: ItemDataOrigin, metadata: ImageMetadata): DBlobImageItem =>
createDBlobBase(DBlobMetaDataType.IMAGE, label, data, origin, metadata);
export type DBlobDBItem = DBlobItem & {
uId: '1';
wId: '1';
cId: 'global';
sId: 'app-chat' | 'app-draw' | 'attachment-drafts'; // scope ID
export function createDBlobImageAsset(label: string, data: DBlobImageAsset['data'], origin: DBlobAssetOrigin, metadata: ImageAssetMetadata): DBlobImageAsset {
return _createAssetImpl(DBlobAssetType.IMAGE, label, data, origin, metadata);
}
function _createAssetImpl<TType extends DBlobAssetType, TMime extends DBlobMimeType, TMeta extends Record<string, any>>(
type: TType,
label: string,
data: DBlobAssetData<TMime>,
origin: DBlobAssetOrigin,
metadata: TMeta,
): DBlobAssetImplV1<TType, TMime, TMeta> {
const creationDate = new Date();
return {
id: nanoid(),
type,
label,
data,
origin,
createdAt: creationDate,
updatedAt: creationDate,
metadata,
cache: {},
};
}