Skip to content

Commit

Permalink
Re-implement library in Rx
Browse files Browse the repository at this point in the history
  • Loading branch information
sleepyfran committed Aug 21, 2024
1 parent 6dfe15e commit 2830809
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 93 deletions.
1 change: 1 addition & 0 deletions packages/components/library/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { UserLibraryWithSuspense as UserLibrary } from "./src/Library";
21 changes: 21 additions & 0 deletions packages/components/library/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
47 changes: 47 additions & 0 deletions packages/components/library/src/Library.tsx
Original file line number Diff line number Diff line change
@@ -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<string | undefined>(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 (
<div>
<br />
<audio src={src} autoPlay controls />
{albums.map((album) => (
<div key={album.id}>
<h3>{album.name}</h3>
<p>{album.artist.name}</p>
<button onClick={() => playFirstTrack(album.tracks)}>Play</button>
<hr />
</div>
))}
</div>
);
};

export const UserLibraryWithSuspense = () => (
<Suspense fallback="Loading library...">
<UserLibrary />
</Suspense>
);
1 change: 1 addition & 0 deletions packages/components/library/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
7 changes: 7 additions & 0 deletions packages/components/library/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"include": [
"src",
"index.ts"
],
"extends": "../../../tsconfig.json"
}
6 changes: 6 additions & 0 deletions packages/core/types/src/model/album.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/types/src/model/track.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -48,7 +48,7 @@ export type Track = {
/**
* Album that the track belongs to.
*/
album: Album;
albumId: AlbumId;

/**
* Name of the track.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Album, "artist"> & {
export type DatabaseAlbum = Omit<Album, "artist" | "tracks"> & {
artistId: Artist["id"];
};

Expand Down
16 changes: 15 additions & 1 deletion packages/core/types/src/services/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -92,5 +104,7 @@ export type Table<
/**
* Streams all records from the table.
*/
readonly observe: () => Effect.Effect<Stream.Stream<TSchema>>;
readonly observe: () => Effect.Effect<
Stream.Stream<TSchema[], DatabaseObserveError>
>;
};
13 changes: 8 additions & 5 deletions packages/core/types/src/services/library.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<Album[], NonExistingArtistReferenced>
>;
};

/**
* Tag to identify the library service.
*/
export const Library = Context.GenericTag<Library>("@echo/core-types/Library");
export class Library extends Effect.Tag("@echo/core-types/Library")<
Library,
ILibrary
>() {}
29 changes: 14 additions & 15 deletions packages/infrastructure/dexie-database/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<TSchema[]>,
).subscribe(
(items) => emit.single(items),
(error) =>
emit.fail(new DatabaseObserveError(tableName, error as unknown)),
() => emit.end(),
);

const pubsub = yield* PubSub.sliding<TSchema>(500);

const subscription = liveQuery(
() => table.toArray() as unknown as Promise<TSchema[]>,
).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);
});
}),
});

Expand Down
58 changes: 29 additions & 29 deletions packages/services/library/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {
type Album,
Database,
Library,
NonExistingArtistReferenced,
type Track,
} from "@echo/core-types";
import { Effect, Layer, Option, Stream } from "effect";

Expand All @@ -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: [],
})),
};
}),
),
),
),
);
}),
Expand Down
1 change: 1 addition & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 2 additions & 40 deletions packages/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<div>
<AddProvider />
<ProviderStatus />
<UserLibrary />
</div>
);

// const observeLibrary = Effect.gen(function* () {
// const library = yield* Library;
// return yield* library.observeAlbums();
// }).pipe(Effect.provide(MainLive));

// const UserLibrary = () => {
// const [src, setSrc] = useState<string | undefined>(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", () => <h1>Nothing in your library</h1>),
// Match.tag("items", ({ items }) => (
// <div>
// <audio src={src} autoPlay controls />
// {items.map(([album, tracks]) => (
// <div key={album.id}>
// <h1>{album.name}</h1>
// <p>{album.artist.name}</p>
// <button onClick={() => playFirstTrack(tracks)}>Play</button>
// <hr />
// </div>
// ))}
// </div>
// )),
// Match.tag("failure", () => <h1>Failed to load library</h1>),
// Match.exhaustive,
// )(albumStream);
// };

0 comments on commit 2830809

Please sign in to comment.