From dc948f8f33da1ac5d0b9d095fca53abd9ed5bc22 Mon Sep 17 00:00:00 2001 From: sleepyfran Date: Mon, 29 Jul 2024 01:29:24 +0200 Subject: [PATCH] Migrate provider status to Rx --- packages/components/provider-status/index.ts | 1 + .../{state => provider-status}/package.json | 12 +- .../provider-status/src/ProviderStatus.tsx | 36 +++++ .../{state => provider-status}/tsconfig.json | 0 packages/components/state/index.ts | 1 - .../components/state/src/provider.state.ts | 95 ------------ packages/components/state/src/vite-env.d.ts | 1 - packages/core/types/src/services/index.ts | 1 + .../types/src/services/provider-status.ts | 26 ++++ .../src/broadcast-channel.ts | 2 +- packages/services/bootstrap/package.json | 2 - packages/services/bootstrap/src/layers.ts | 4 - .../services/media-provider-status/index.ts | 26 ++++ .../media-provider-status/package.json | 15 ++ .../media-provider-status/tsconfig.json | 6 + packages/services/player/src/player.ts | 1 + packages/web/package.json | 5 +- packages/web/src/App.tsx | 140 ++++++++---------- .../components/template/package.json.hbs | 8 +- yarn.lock | 9 +- 20 files changed, 192 insertions(+), 199 deletions(-) create mode 100644 packages/components/provider-status/index.ts rename packages/components/{state => provider-status}/package.json (54%) create mode 100644 packages/components/provider-status/src/ProviderStatus.tsx rename packages/components/{state => provider-status}/tsconfig.json (100%) delete mode 100644 packages/components/state/index.ts delete mode 100644 packages/components/state/src/provider.state.ts delete mode 100644 packages/components/state/src/vite-env.d.ts create mode 100644 packages/core/types/src/services/provider-status.ts create mode 100644 packages/services/media-provider-status/index.ts create mode 100644 packages/services/media-provider-status/package.json create mode 100644 packages/services/media-provider-status/tsconfig.json diff --git a/packages/components/provider-status/index.ts b/packages/components/provider-status/index.ts new file mode 100644 index 0000000..15c98dc --- /dev/null +++ b/packages/components/provider-status/index.ts @@ -0,0 +1 @@ +export * from "./src/ProviderStatus"; diff --git a/packages/components/state/package.json b/packages/components/provider-status/package.json similarity index 54% rename from packages/components/state/package.json rename to packages/components/provider-status/package.json index 287533d..88250e7 100644 --- a/packages/components/state/package.json +++ b/packages/components/provider-status/package.json @@ -1,5 +1,5 @@ { - "name": "@echo/components-state", + "name": "@echo/components-provider-status", "private": true, "version": "1.0.0", "scripts": { @@ -7,13 +7,15 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@echo/components-effect-bridge": "^1.0.0", "@echo/core-types": "^1.0.0", "@echo/services-bootstrap": "^1.0.0", - "effect": "^3.5.8", - "jotai": "^2.8.3" + "@echo/services-media-provider-status": "^1.0.0", + "@effect-rx/rx": "^0.33.8", + "@effect-rx/rx-react": "^0.30.11", + "effect": "^3.2.8" }, "devDependencies": { - "react": "^18.2.0" + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22" } } \ No newline at end of file diff --git a/packages/components/provider-status/src/ProviderStatus.tsx b/packages/components/provider-status/src/ProviderStatus.tsx new file mode 100644 index 0000000..5b89945 --- /dev/null +++ b/packages/components/provider-status/src/ProviderStatus.tsx @@ -0,0 +1,36 @@ +import { MediaProviderStatusLive } from "@echo/services-media-provider-status"; +import { MainLive } from "@echo/services-bootstrap"; +import { Rx } from "@effect-rx/rx"; +import { Layer, Match } from "effect"; +import { useRxValue } from "@effect-rx/rx-react"; +import { MediaProviderStatus } from "@echo/core-types"; + +const runtime = Rx.runtime( + MediaProviderStatusLive.pipe(Layer.provide(MainLive)), +); + +const providerStatus = runtime.subscriptionRef(MediaProviderStatus.observe); + +export const ProviderStatus = () => { + const status = useRxValue(providerStatus); + + return Match.value(status).pipe( + Match.tag("Initial", () =>
Loading provider status...
), + Match.tag("Success", ({ value: providerState }) => ( +
+ {[...providerState.entries()].map(([providerId, providerState]) => ( +
+

{providerId}

+
{JSON.stringify(providerState, null, 2)}
+
+ ))} +
+ )), + Match.tag("Failure", () => ( +
+ Something went wrong observing the provider statuses. +
+ )), + Match.exhaustive, + ); +}; diff --git a/packages/components/state/tsconfig.json b/packages/components/provider-status/tsconfig.json similarity index 100% rename from packages/components/state/tsconfig.json rename to packages/components/provider-status/tsconfig.json diff --git a/packages/components/state/index.ts b/packages/components/state/index.ts deleted file mode 100644 index 93bac36..0000000 --- a/packages/components/state/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./src/provider.state"; diff --git a/packages/components/state/src/provider.state.ts b/packages/components/state/src/provider.state.ts deleted file mode 100644 index 49a3f6a..0000000 --- a/packages/components/state/src/provider.state.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { useOnMountEffect } from "@echo/components-effect-bridge"; -import { - MediaProviderMainThreadBroadcastChannel, - type MediaProviderBroadcastSchema, - type ProviderMetadata, - type ProviderStatus, -} from "@echo/core-types"; -import { MainLive } from "@echo/services-bootstrap"; -import { Effect, Fiber } from "effect"; -import { atom, useAtom } from "jotai"; -import { useCallback, useRef } from "react"; - -/** - * State derived from the provider status that is shared via the broadcast - * channel. - */ -export type ProviderState = { - status: ProviderStatus; -}; - -/** - * Map of provider ID to the state of the provider. - */ -export type StateByProvider = Map; - -const stateAtom = atom(new Map()); -const writableProviderStateAtom = atom( - null, - (_, set, input: { metadata: ProviderMetadata; status: ProviderStatus }) => { - set(stateAtom, (currentMap) => { - const updatedMap = new Map(currentMap); - updatedMap.set(input.metadata.id, { - status: input.status, - }); - return updatedMap; - }); - }, -); - -/** - * Atom that holds the state of the provider. This state is shared across the - * application and derived from the provider status shared via the broadcast - * channel. - */ -export const providerStateAtom = atom((get) => [...get(stateAtom).entries()]); - -/** - * Hook that subscribes to the provider state and populates the atom's value - * with it. This hook does not return anything, but it will listen to provider - * state updates done through the broadcast channel and update the atom's value. - * In order to actually use the provider state, you should use the atom itself. - */ -export const useProviderStateSubscriber = () => { - const [_, setProviderState] = useAtom(writableProviderStateAtom); - const listener = useCallback( - ( - update: MediaProviderBroadcastSchema["mainThread"]["resolvers"]["reportStatus"], - ) => { - setProviderState({ - metadata: update.metadata, - status: update.status, - }); - }, - [setProviderState], - ); - const _effect = useRef | null>(null); - if (_effect.current === null) { - _effect.current = createProviderStatusListener(listener); - } - - useOnMountEffect(_effect.current!); -}; - -const createProviderStatusListener = ( - listener: ( - update: MediaProviderBroadcastSchema["mainThread"]["resolvers"]["reportStatus"], - ) => void, -) => - Effect.gen(function* () { - const broadcastChannel = yield* MediaProviderMainThreadBroadcastChannel; - - // TODO: Move somewhere else. - const reportStatusFiber = yield* broadcastChannel.registerResolver( - "reportStatus", - (status) => { - listener(status); - return Effect.void; - }, - ); - - // TODO: Is this safe? Does this produce any leaks? - yield* Fiber.join(reportStatusFiber); - }).pipe(Effect.provide(MainLive)); diff --git a/packages/components/state/src/vite-env.d.ts b/packages/components/state/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/packages/components/state/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/packages/core/types/src/services/index.ts b/packages/core/types/src/services/index.ts index 69016ed..20d510e 100644 --- a/packages/core/types/src/services/index.ts +++ b/packages/core/types/src/services/index.ts @@ -6,3 +6,4 @@ export * from "./library"; export * from "./metadata-provider"; export * from "./mediaProvider"; export * from "./player"; +export * from "./provider-status"; diff --git a/packages/core/types/src/services/provider-status.ts b/packages/core/types/src/services/provider-status.ts new file mode 100644 index 0000000..634977c --- /dev/null +++ b/packages/core/types/src/services/provider-status.ts @@ -0,0 +1,26 @@ +import { Effect, SubscriptionRef } from "effect"; +import type { ProviderId, ProviderStatus } from "../model"; + +/** + * Defines the state of all currently active providers. + */ +export type StateByProvider = Map; + +/** + * Service that listen to the status of all providers and allows to observe + * changes to them. + */ +export type IMediaProviderStatus = { + /** + * Returns a subscription ref that holds the current status of a specific + * provider, while also allowing to observe changes to it. + */ + readonly observe: SubscriptionRef.SubscriptionRef; +}; + +/** + * Tag to identify the MediaProviderStatus service. + */ +export class MediaProviderStatus extends Effect.Tag( + "@echo/core-types/MediaProviderStatus", +)() {} diff --git a/packages/infrastructure/broadcast-channel/src/broadcast-channel.ts b/packages/infrastructure/broadcast-channel/src/broadcast-channel.ts index 86f37a2..777a964 100644 --- a/packages/infrastructure/broadcast-channel/src/broadcast-channel.ts +++ b/packages/infrastructure/broadcast-channel/src/broadcast-channel.ts @@ -56,7 +56,7 @@ const createBroadcastChannel = ( }); }), registerResolver: (actionId, resolver) => - Effect.fork( + Effect.forkDaemon( Effect.gen(function* () { const channel = yield* _broadcastChannel.get; diff --git a/packages/services/bootstrap/package.json b/packages/services/bootstrap/package.json index 1f7d674..eaadfa7 100644 --- a/packages/services/bootstrap/package.json +++ b/packages/services/bootstrap/package.json @@ -15,8 +15,6 @@ "@echo/infrastructure-dexie-database": "^1.0.0", "@echo/infrastructure-mmb-metadata-provider": "^1.0.0", "@echo/infrastructure-onedrive-provider": "^1.0.0", - "@echo/services-library": "^1.0.0", - "@echo/services-player": "^1.0.0", "@echo/workers-media-provider": "^1.0.0", "effect": "^3.5.8" } diff --git a/packages/services/bootstrap/src/layers.ts b/packages/services/bootstrap/src/layers.ts index 9d1dbf0..1ea0ed1 100644 --- a/packages/services/bootstrap/src/layers.ts +++ b/packages/services/bootstrap/src/layers.ts @@ -5,11 +5,9 @@ import { } from "@echo/infrastructure-broadcast-channel"; import { BrowserCryptoLive } from "@echo/infrastructure-browser-crypto"; import { DexieDatabaseLive } from "@echo/infrastructure-dexie-database"; -import { LibraryLive } from "@echo/services-library"; import { MmbMetadataProviderLive } from "@echo/infrastructure-mmb-metadata-provider"; import { LazyLoadedProviderLive } from "./loaders/provider"; import { AppConfigLive } from "./app-config"; -import { PlayerLive } from "@echo/services-player"; /** * Exports a layer that can provide all dependencies that are needed in the @@ -18,9 +16,7 @@ import { PlayerLive } from "@echo/services-player"; export const MainLive = MediaProviderMainThreadBroadcastChannelLive.pipe( Layer.provideMerge(MediaProviderWorkerBroadcastChannelLive), Layer.provideMerge(BrowserCryptoLive), - Layer.provideMerge(PlayerLive), Layer.provideMerge(LazyLoadedProviderLive), - Layer.provideMerge(LibraryLive), Layer.provideMerge(DexieDatabaseLive), Layer.provideMerge(AppConfigLive), ); diff --git a/packages/services/media-provider-status/index.ts b/packages/services/media-provider-status/index.ts new file mode 100644 index 0000000..c79823a --- /dev/null +++ b/packages/services/media-provider-status/index.ts @@ -0,0 +1,26 @@ +import { + MediaProviderStatus, + MediaProviderMainThreadBroadcastChannel, + type StateByProvider, +} from "@echo/core-types"; +import { Effect, Layer, Ref, SubscriptionRef } from "effect"; + +export const MediaProviderStatusLive = Layer.effect( + MediaProviderStatus, + Effect.gen(function* () { + const stateByProviderRef = yield* SubscriptionRef.make( + new Map(), + ); + const broadcastChannel = yield* MediaProviderMainThreadBroadcastChannel; + + yield* broadcastChannel.registerResolver("reportStatus", (status) => { + return Ref.update(stateByProviderRef, (current) => { + return new Map(current).set(status.metadata.id, status.status); + }); + }); + + return MediaProviderStatus.of({ + observe: stateByProviderRef, + }); + }), +); diff --git a/packages/services/media-provider-status/package.json b/packages/services/media-provider-status/package.json new file mode 100644 index 0000000..4c961b5 --- /dev/null +++ b/packages/services/media-provider-status/package.json @@ -0,0 +1,15 @@ +{ + "name": "@echo/services-media-provider-status", + "private": true, + "version": "1.0.0", + "description": "Contains the implementation for the MediaProviderStatus service", + "main": "index.js", + "scripts": { + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@echo/core-types": "^1.0.0", + "effect": "^3.2.8" + } +} \ No newline at end of file diff --git a/packages/services/media-provider-status/tsconfig.json b/packages/services/media-provider-status/tsconfig.json new file mode 100644 index 0000000..fd0fe35 --- /dev/null +++ b/packages/services/media-provider-status/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.json", + "include": [ + "index.ts" + ] +} \ No newline at end of file diff --git a/packages/services/player/src/player.ts b/packages/services/player/src/player.ts index 0bd17fd..5e338ba 100644 --- a/packages/services/player/src/player.ts +++ b/packages/services/player/src/player.ts @@ -6,6 +6,7 @@ const PlayerLiveWithState = Layer.effect( Player, Effect.gen(function* () { const state = yield* PlayerStateRef; + // TODO: Remove all this and switch to subscription ref. const statePubSub = yield* PubSub.dropping({ capacity: 1, replay: 1, diff --git a/packages/web/package.json b/packages/web/package.json index 4543524..f39f3b6 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -12,13 +12,12 @@ }, "dependencies": { "@echo/components-effect-bridge": "^1.0.0", - "@echo/components-state": "^1.0.0", + "@echo/components-provider-status": "^1.0.0", "@echo/core-types": "^1.0.0", "@echo/services-bootstrap": "^1.0.0", "@effect-rx/rx": "0.33.8", "@effect-rx/rx-react": "0.30.11", - "effect": "^3.5.8", - "jotai": "^2.8.3" + "effect": "^3.5.8" }, "devDependencies": { "@types/react": "^18.2.66", diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index a76f437..d8adce7 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -1,30 +1,23 @@ import { AvailableProviders, MediaProviderMainThreadBroadcastChannel, - Library, type Authentication, type AuthenticationInfo, type MediaProvider, type ProviderMetadata, type FolderMetadata, - type Track, } from "@echo/core-types"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo } from "react"; import { useEffectCallback, useEffectTs, useOnMountEffect, - useStream, } from "@echo/components-effect-bridge"; -import { - useProviderStateSubscriber, - providerStateAtom, -} from "@echo/components-state"; import { Effect, Fiber, Match } from "effect"; import { LazyLoadedProvider, MainLive } from "@echo/services-bootstrap"; -import { useAtom } from "jotai"; import { Rx } from "@effect-rx/rx"; import { useRx } from "@effect-rx/rx-react"; +import { ProviderStatus } from "@echo/components-provider-status"; const mainRuntime = Rx.runtime(MainLive); const loadProvider = mainRuntime.fn((metadata: ProviderMetadata) => @@ -37,31 +30,35 @@ const useProviderLoader = () => useRx(loadProvider); export const App = () => { const [loadStatus, loadProvider] = useProviderLoader(); - useProviderStateSubscriber(); const onProviderSelected = useCallback( (metadata: ProviderMetadata) => loadProvider(metadata), [loadProvider], ); - return Match.value(loadStatus).pipe( - Match.tag("Initial", () => ( - - )), - Match.tag( - "Success", - ({ value: { metadata, authentication, createMediaProvider } }) => ( - - ), - ), - Match.tag("Failure", () => ( -
Failed to load provider.
- )), - Match.exhaustive, + return ( +
+ {Match.value(loadStatus).pipe( + Match.tag("Initial", () => ( + + )), + Match.tag( + "Success", + ({ value: { metadata, authentication, createMediaProvider } }) => ( + + ), + ), + Match.tag("Failure", () => ( +
Failed to load provider.
+ )), + Match.exhaustive, + )} + +
); }; @@ -221,57 +218,42 @@ const FolderSelector = ({ )(selectRootState); }; -const ProviderStatus = () => { - const [providerState] = useAtom(providerStateAtom); - - return ( -
- {providerState.map(([providerId, providerState]) => ( -
-

{providerId}

-
{JSON.stringify(providerState, null, 2)}
-
- ))} -
- ); -}; - -const observeLibrary = Effect.gen(function* () { - const library = yield* Library; - return yield* library.observeAlbums(); -}).pipe(Effect.provide(MainLive)); +// 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 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; - } - }; +// 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); -}; +// return matcher.pipe( +// Match.tag("empty", () =>

Nothing in your library

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

Failed to load library

), +// Match.exhaustive, +// )(albumStream); +// }; diff --git a/tools/plop-templates/components/template/package.json.hbs b/tools/plop-templates/components/template/package.json.hbs index 110d485..d28efa9 100644 --- a/tools/plop-templates/components/template/package.json.hbs +++ b/tools/plop-templates/components/template/package.json.hbs @@ -6,7 +6,13 @@ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "typecheck": "tsc --noEmit" }, - "dependencies": {}, + "dependencies": { + "@echo/core-types": "^1.0.0", + "@echo/services-bootstrap": "^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" diff --git a/yarn.lock b/yarn.lock index ddccd55..66b1872 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,7 +21,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@effect-rx/rx-react@0.30.11": +"@effect-rx/rx-react@0.30.11", "@effect-rx/rx-react@^0.30.11": version "0.30.11" resolved "https://registry.yarnpkg.com/@effect-rx/rx-react/-/rx-react-0.30.11.tgz#af1638e4505acba7c9a30f8a8dd43b99944eccaf" integrity sha512-Z0HwvrJbzgUvpjgMS5qH2+4dYL0oYKbsx2UJ9S7b1iV8ZippJnLf//FsOqDjzUdXKNLuW1L/h+as70o7DLDrgw== @@ -1075,7 +1075,7 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" -effect@^3.5.8: +effect@^3.2.8, effect@^3.5.8: version "3.5.8" resolved "https://registry.yarnpkg.com/effect/-/effect-3.5.8.tgz#63098d38f538facd4d8b0ca8b81717395c33f694" integrity sha512-gGrRH3BgsUrfOXx4vbD4puRS+/IyK7JiERhuKnjVYurTVDj3J+lPI7raJB9/AAKr59SGEiV7+Sy59/Q/Vfo5mQ== @@ -2083,11 +2083,6 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -jotai@^2.8.3: - version "2.8.3" - resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.8.3.tgz#21b50c89c9ee2d24e694158b925bade1f38641d7" - integrity sha512-pR4plVvdbzB6zyt7VLLHPMAkcRSKhRIvZKd+qkifQLa3CEziEo1uwZjePj4acTmQrboiISBlYSdCz3gWcr1Nkg== - "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"