diff --git a/packages/components/library/index.ts b/packages/components/library/index.ts new file mode 100644 index 0000000..5d080ec --- /dev/null +++ b/packages/components/library/index.ts @@ -0,0 +1 @@ +export { UserLibraryWithSuspense as UserLibrary } from "./src/Library"; diff --git a/packages/components/library/package.json b/packages/components/library/package.json new file mode 100644 index 0000000..ddc8cfd --- /dev/null +++ b/packages/components/library/package.json @@ -0,0 +1,21 @@ +{ + "name": "@echo/components-library", + "private": true, + "version": "1.0.0", + "scripts": { + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@echo/core-types": "^1.0.0", + "@echo/services-bootstrap": "^1.0.0", + "@echo/services-library": "^1.0.0", + "@effect-rx/rx": "^0.33.8", + "@effect-rx/rx-react": "^0.30.11", + "effect": "^3.2.8" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22" + } +} \ No newline at end of file diff --git a/packages/components/library/src/Library.tsx b/packages/components/library/src/Library.tsx new file mode 100644 index 0000000..0419cf2 --- /dev/null +++ b/packages/components/library/src/Library.tsx @@ -0,0 +1,47 @@ +import { Library, type Track } from "@echo/core-types"; +import { MainLive } from "@echo/services-bootstrap"; +import { Rx } from "@effect-rx/rx"; +import { Layer, Stream } from "effect"; +import { Suspense, useState } from "react"; +import { LibraryLive } from "@echo/services-library"; +import { useRxSuspenseSuccess } from "@effect-rx/rx-react"; + +const runtime = Rx.runtime(LibraryLive.pipe(Layer.provide(MainLive))); +const observeLibrary = runtime.rx(Stream.unwrap(Library.observeAlbums())); + +const UserLibrary = () => { + const [src, setSrc] = useState(undefined); + const albums = useRxSuspenseSuccess(observeLibrary).value; + + const playFirstTrack = (tracks: Track[]) => { + const track = tracks[0]; + switch (track.resource.type) { + case "file": + setSrc(track.resource.uri); + break; + case "api": + break; + } + }; + + return ( +
+
+
+ ); +}; + +export const UserLibraryWithSuspense = () => ( + + + +); diff --git a/packages/components/library/src/vite-env.d.ts b/packages/components/library/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/components/library/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/components/library/tsconfig.json b/packages/components/library/tsconfig.json new file mode 100644 index 0000000..25a3349 --- /dev/null +++ b/packages/components/library/tsconfig.json @@ -0,0 +1,7 @@ +{ + "include": [ + "src", + "index.ts" + ], + "extends": "../../../tsconfig.json" +} \ No newline at end of file diff --git a/packages/core/types/src/model/album.ts b/packages/core/types/src/model/album.ts index f6edc28..6053ed6 100644 --- a/packages/core/types/src/model/album.ts +++ b/packages/core/types/src/model/album.ts @@ -1,5 +1,6 @@ import { Brand, Option } from "effect"; import type { Artist } from "./artist"; +import type { Track } from "./track"; /** * Wrapper around a string to represent an album id. @@ -30,6 +31,11 @@ export type Album = { */ name: string; + /** + * All tracks that are part of the album. + */ + tracks: Track[]; + /** * URL to an image of the album. This is typically the album's cover art on a * third-party service. If none is available, this field is omitted. diff --git a/packages/core/types/src/model/track.ts b/packages/core/types/src/model/track.ts index 692d0ea..6536157 100644 --- a/packages/core/types/src/model/track.ts +++ b/packages/core/types/src/model/track.ts @@ -1,5 +1,5 @@ import type { Artist } from "./artist"; -import type { Album } from "./album"; +import type { AlbumId } from "./album"; import { Brand } from "effect"; import type { ApiBasedProviderId, @@ -48,7 +48,7 @@ export type Track = { /** * Album that the track belongs to. */ - album: Album; + albumId: AlbumId; /** * Name of the track. diff --git a/packages/core/types/src/services/database/database-models.ts b/packages/core/types/src/services/database/database-models.ts index d73e060..8f76b34 100644 --- a/packages/core/types/src/services/database/database-models.ts +++ b/packages/core/types/src/services/database/database-models.ts @@ -4,7 +4,7 @@ import type { Album, Artist, Track } from "../../model"; * Represents an album in the database, which is synced with the model but * references other tables by their IDs instead of duplicating the data. */ -export type DatabaseAlbum = Omit & { +export type DatabaseAlbum = Omit & { artistId: Artist["id"]; }; diff --git a/packages/core/types/src/services/database/database.ts b/packages/core/types/src/services/database/database.ts index daedafd..2920937 100644 --- a/packages/core/types/src/services/database/database.ts +++ b/packages/core/types/src/services/database/database.ts @@ -5,6 +5,18 @@ import type { DatabaseTrack, } from "./database-models.ts"; +/** + * Error that is thrown when the database raises an unexpected error while + * observing a table. + */ +export class DatabaseObserveError extends Error { + constructor(tableName: string, error: unknown) { + super( + `Database raised an unexpected error while observing the ${tableName} table. Error: ${JSON.stringify(error)}`, + ); + } +} + /** * Keys for all the available tables in the database with their associated * data type. @@ -92,5 +104,7 @@ export type Table< /** * Streams all records from the table. */ - readonly observe: () => Effect.Effect>; + readonly observe: () => Effect.Effect< + Stream.Stream + >; }; diff --git a/packages/core/types/src/services/library.ts b/packages/core/types/src/services/library.ts index 7ffbfb7..eebd81d 100644 --- a/packages/core/types/src/services/library.ts +++ b/packages/core/types/src/services/library.ts @@ -1,5 +1,5 @@ -import { Context, Effect, Stream } from "effect"; -import type { Album, ArtistId, Track } from "../model"; +import { Effect, Stream } from "effect"; +import type { Album, ArtistId } from "../model"; /** * Error that is thrown when an album references an artist that does not exist. @@ -15,16 +15,19 @@ export class NonExistingArtistReferenced extends Error { /** * Service that provides access to the user's library. */ -export type Library = { +export type ILibrary = { /** * Returns a stream of albums that are currently stored in the database. */ readonly observeAlbums: () => Effect.Effect< - Stream.Stream<[Album, Track[]], NonExistingArtistReferenced> + Stream.Stream >; }; /** * Tag to identify the library service. */ -export const Library = Context.GenericTag("@echo/core-types/Library"); +export class Library extends Effect.Tag("@echo/core-types/Library")< + Library, + ILibrary +>() {} diff --git a/packages/infrastructure/dexie-database/src/database.ts b/packages/infrastructure/dexie-database/src/database.ts index 6845315..f1048e9 100644 --- a/packages/infrastructure/dexie-database/src/database.ts +++ b/packages/infrastructure/dexie-database/src/database.ts @@ -6,9 +6,10 @@ import { type Tables, type Table, type DatabaseAlbum, + DatabaseObserveError, } from "@echo/core-types"; import Dexie, { type Table as DexieTable, liveQuery } from "dexie"; -import { Effect, Layer, Option, PubSub, Ref, Stream } from "effect"; +import { Effect, Layer, Option, Ref, Stream } from "effect"; /** * Implementation of the Database service using Dexie.js. @@ -102,22 +103,20 @@ const createTable = < ); }), observe: () => - Effect.gen(function* () { + Effect.sync(() => { const table = db[tableName]; + return Stream.async((emit) => { + const subscription = liveQuery( + () => table.toArray() as unknown as Promise, + ).subscribe( + (items) => emit.single(items), + (error) => + emit.fail(new DatabaseObserveError(tableName, error as unknown)), + () => emit.end(), + ); - const pubsub = yield* PubSub.sliding(500); - - const subscription = liveQuery( - () => table.toArray() as unknown as Promise, - ).subscribe( - (items) => Effect.runPromise(pubsub.publishAll(items)), // next - () => Effect.runPromise(pubsub.shutdown), // error - () => Effect.runPromise(pubsub.shutdown), // complete - ); - - return Stream.fromPubSub(pubsub).pipe( - Stream.ensuring(Effect.sync(subscription.unsubscribe)), - ); + return Effect.sync(subscription.unsubscribe); + }); }), }); diff --git a/packages/services/library/index.ts b/packages/services/library/index.ts index e7e4c6f..43cc8cc 100644 --- a/packages/services/library/index.ts +++ b/packages/services/library/index.ts @@ -1,9 +1,7 @@ import { - type Album, Database, Library, NonExistingArtistReferenced, - type Track, } from "@echo/core-types"; import { Effect, Layer, Option, Stream } from "effect"; @@ -24,37 +22,39 @@ export const LibraryLive = Layer.effect( const allAlbums = yield* albumsTable.observe(); return allAlbums.pipe( - Stream.mapEffect((album) => - Effect.gen(function* () { - const artist = yield* artistsTable.byId(album.artistId); - const tracks = yield* tracksTable.filtered({ - filter: { - albumId: album.id, - }, - }); + Stream.mapEffect((albums) => + Effect.all( + albums.map((album) => + Effect.gen(function* () { + const artist = yield* artistsTable.byId(album.artistId); + const tracks = yield* tracksTable.filtered({ + filter: { + albumId: album.id, + }, + }); - if (Option.isNone(artist)) { - return yield* Effect.fail( - new NonExistingArtistReferenced(album.name, album.artistId), - ); - } + if (Option.isNone(artist)) { + return yield* Effect.fail( + new NonExistingArtistReferenced( + album.name, + album.artistId, + ), + ); + } - return [ - { - ...album, - artist: artist.value, - }, - tracks.map((track) => ({ - ...track, - album: { + return { ...album, artist: artist.value, - }, - mainArtist: artist.value, - secondaryArtists: [], - })), - ] as [Album, Track[]]; - }), + tracks: tracks.map((track) => ({ + ...track, + albumId: album.id, + mainArtist: artist.value, + secondaryArtists: [], + })), + }; + }), + ), + ), ), ); }), diff --git a/packages/web/package.json b/packages/web/package.json index 9f315f1..9075ba3 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@echo/components-add-provider": "^1.0.0", + "@echo/components-library": "^1.0.0", "@echo/components-provider-status": "^1.0.0", "@echo/core-types": "^1.0.0", "@echo/services-bootstrap": "^1.0.0", diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index adeb372..3f5cc5d 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -1,49 +1,11 @@ import { AddProvider } from "@echo/components-add-provider"; +import { UserLibrary } from "@echo/components-library"; import { ProviderStatus } from "@echo/components-provider-status"; export const App = () => (
+
); - -// const observeLibrary = Effect.gen(function* () { -// const library = yield* Library; -// return yield* library.observeAlbums(); -// }).pipe(Effect.provide(MainLive)); - -// const UserLibrary = () => { -// const [src, setSrc] = useState(undefined); -// const [albumStream, matcher] = useStream(observeLibrary); - -// const playFirstTrack = (tracks: Track[]) => { -// const track = tracks[0]; -// switch (track.resource.type) { -// case "file": -// setSrc(track.resource.uri); -// break; -// case "api": -// break; -// } -// }; - -// return matcher.pipe( -// Match.tag("empty", () =>

Nothing in your library

), -// Match.tag("items", ({ items }) => ( -//
-//
-// )), -// Match.tag("failure", () =>

Failed to load library

), -// Match.exhaustive, -// )(albumStream); -// };