Skip to content

Commit

Permalink
Implement app reinitialization
Browse files Browse the repository at this point in the history
With a few TODOs here and there, but hey, no more login-in!
  • Loading branch information
sleepyfran committed Aug 26, 2024
1 parent d0382c5 commit 01316f0
Show file tree
Hide file tree
Showing 17 changed files with 342 additions and 32 deletions.
20 changes: 2 additions & 18 deletions packages/core/types/src/broadcast/media-provider.broadcast.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
import type {
AuthenticationInfo,
FolderMetadata,
ProviderStartArgs,
ProviderMetadata,
ProviderStatus,
} from "../model";

type FileBasedStartInput = {
_tag: "file-based";
metadata: ProviderMetadata;
authInfo: AuthenticationInfo;
rootFolder: FolderMetadata;
};

type ApiBasedStartInput = {
_tag: "api-based";
metadata: ProviderMetadata;
authInfo: AuthenticationInfo;
};

type StartInput = FileBasedStartInput | ApiBasedStartInput;

/**
* Defines the schema for messages flowing from the main thread to the media
* provider worker.
Expand All @@ -30,7 +14,7 @@ type MainThreadActionsSchema = {
* APIs with the given authentication information that was previously obtained
* by the provider's auth process.
*/
start: StartInput;
start: ProviderStartArgs;

/**
* Stops the media provider with the given name, if it is currently running.
Expand Down
28 changes: 28 additions & 0 deletions packages/core/types/src/model/provider-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { AuthenticationInfo } from "./authentication";
import type { FolderMetadata } from "./file-system";

/**
* ID of a file-based provider.
*/
Expand Down Expand Up @@ -88,3 +91,28 @@ export type ProviderStatus =
}
| { _tag: "errored"; error: ProviderError }
| { _tag: "stopped" };

/**
* Defines the parameters required to start a file-based provider.
*/
type FileBasedStartArgs = {
_tag: "file-based";
metadata: ProviderMetadata;
authInfo: AuthenticationInfo;
rootFolder: FolderMetadata;
};

/**
* Defines the parameters required to start an API-based provider.
*/
type ApiBasedStartArgs = {
_tag: "api-based";
metadata: ProviderMetadata;
authInfo: AuthenticationInfo;
};

/**
* Defines the parameters required to start a provider, which can be either file-based
* or API-based.
*/
export type ProviderStartArgs = FileBasedStartArgs | ApiBasedStartArgs;
23 changes: 23 additions & 0 deletions packages/core/types/src/services/app-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Effect } from "effect";

/**
* Service that encapsulates the orchestration for initializing the application
* during a cold boot.
*/
export type IAppInit = {
/**
* Initializes the application by performing reading the last known state of
* the providers from the storage and re-starting them with the last
* used credentials. If the credentials have expired, then the initialization
* of the provider will fail and the user will have to re-add the provider.
*/
readonly init: Effect.Effect<void>;
};

/**
* Tag to identify the AppInit service.
*/
export class AppInit extends Effect.Tag("@echo/core-types/AppInit")<
AppInit,
IAppInit
>() {}
2 changes: 2 additions & 0 deletions packages/core/types/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export * from "./active-media-provider-cache";
export * from "./app-init";
export * from "./authentication";
export * from "./broadcast-channel";
export * from "./crypto";
export * from "./database";
export * from "./library";
export * from "./local-storage";
export * from "./metadata-provider";
export * from "./media-player";
export * from "./media-provider";
Expand Down
51 changes: 51 additions & 0 deletions packages/core/types/src/services/local-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Effect, Option } from "effect";

/**
* Defines all the possible namespaces that can be used to store data in the
* local storage.
*/
export type LocalStorageNamespace = "media-provider-start-args";

/**
* Service that allows to cache active media providers and observe changes to
* them.
*/
export type ILocalStorage = {
/**
* Sets the given value in the local storage under the specified namespace and
* key, overwriting any existing value. The value must be serializable, since
* it will always be stored as a string.
*/
readonly set: <T>(
namespace: LocalStorageNamespace,
key: string,
value: T,
) => Effect.Effect<void>;

/**
* Attempts to retrieve and deserialize a value from the local storage under
* the specified namespace and key. If the value does not exist, `None` will
* be returned.
*/
readonly get: <T>(
namespace: LocalStorageNamespace,
key: string,
) => Effect.Effect<Option.Option<T>>;

/**
* Deletes the value stored in the local storage under the specified namespace
* and key. If the value does not exist, this operation is a no-op.
*/
readonly remove: (
namespace: LocalStorageNamespace,
key: string,
) => Effect.Effect<void>;
};

/**
* Tag to identify the LocalStorage service.
*/
export class LocalStorage extends Effect.Tag("@echo/core-types/LocalStorage")<
LocalStorage,
ILocalStorage
>() {}
33 changes: 33 additions & 0 deletions packages/infrastructure/browser-local-storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { LocalStorage, type LocalStorageNamespace } from "@echo/core-types";
import { Effect, Layer, Option } from "effect";

const createKey = (namespace: LocalStorageNamespace, key: string) =>
`${namespace}:${key}`;

const make = LocalStorage.of({
set: (namespace, key, value) =>
Effect.sync(() => {
localStorage.setItem(createKey(namespace, key), JSON.stringify(value));
}),

get: <T>(namespace: LocalStorageNamespace, key: string) =>
Effect.sync(() => {
const item = localStorage.getItem(createKey(namespace, key));

// TODO: Use Effect's schema here to ensure that we're actually properly parsing the value.
return Option.fromNullable(item).pipe(
Option.map((value) => JSON.parse(value) as unknown as T),
);
}),

remove: (namespace, key) =>
Effect.sync(() => {
localStorage.removeItem(createKey(namespace, key));
}),
});

/**
* Implementation of the local storage service that uses the browser's local
* storage to store data.
*/
export const BrowserLocalStorageLive = Layer.succeed(LocalStorage, make);
15 changes: 15 additions & 0 deletions packages/infrastructure/browser-local-storage/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@echo/infrastructure-browser-local-storage",
"private": true,
"version": "1.0.0",
"description": "Contains the BrowserLocalStorage related infrastructure",
"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.6.5"
}
}
7 changes: 7 additions & 0 deletions packages/infrastructure/browser-local-storage/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.json",
"include": [
"src",
"index.ts"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as Machine from "@effect/experimental/Machine";
import {
ActiveMediaProviderCache,
AddProviderWorkflow,
LocalStorage,
MediaProviderMainThreadBroadcastChannel,
type Authentication,
type AuthenticationError,
Expand Down Expand Up @@ -65,11 +66,10 @@ export const addProviderWorkflow = Machine.makeWith<MachineState>()(
const state = previousState ?? { _tag: "Idle" };

const activeMediaProviderCache = yield* ActiveMediaProviderCache;

const broadcastChannel = yield* MediaProviderMainThreadBroadcastChannel;
const providerLazyLoader = yield* LazyLoadedProvider;
const mediaPlayerLazyLoader = yield* LazyLoadedMediaPlayer;

const broadcastChannel = yield* MediaProviderMainThreadBroadcastChannel;
const localStorage = yield* LocalStorage;

return Machine.procedures.make(state).pipe(
/*
Expand Down Expand Up @@ -158,12 +158,18 @@ export const addProviderWorkflow = Machine.makeWith<MachineState>()(
return [{}, state];
}

yield* broadcastChannel.send("start", {
_tag: "file-based",
const startArgs = {
_tag: "file-based" as const,
metadata: state.providerMetadata,
rootFolder: request.rootFolder,
authInfo: state.authInfo,
});
rootFolder: request.rootFolder,
};
yield* broadcastChannel.send("start", startArgs);
yield* localStorage.set(
"media-provider-start-args",
state.providerMetadata.id,
startArgs,
);

return [{}, { _tag: "Done" as const }];
}),
Expand Down
1 change: 1 addition & 0 deletions packages/services/app-init/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AppInitLive } from "./src/app-init";
17 changes: 17 additions & 0 deletions packages/services/app-init/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@echo/services-app-init",
"private": true,
"version": "1.0.0",
"description": "Contains the implementation for the AppInit service",
"main": "index.js",
"scripts": {
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@echo/core-auth": "^1.0.0",
"@echo/core-types": "^1.0.0",
"@echo/services-bootstrap": "^1.0.0",
"effect": "^3.6.5"
}
}
106 changes: 106 additions & 0 deletions packages/services/app-init/src/app-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { isValidToken } from "@echo/core-auth";
import {
AppInit,
AvailableProviders,
LocalStorage,
MediaPlayerFactory,
MediaProviderFactory,
MediaProviderMainThreadBroadcastChannel,
type ProviderStartArgs,
type BroadcastChannel,
type ILocalStorage,
type MediaProviderBroadcastSchema,
type ProviderId,
ActiveMediaProviderCache,
type IActiveMediaProviderCache,
} from "@echo/core-types";
import {
LazyLoadedMediaPlayer,
LazyLoadedProvider,
} from "@echo/services-bootstrap";
import { Effect, Layer, Option } from "effect";

const make = Effect.gen(function* () {
const activeMediaProviderCache = yield* ActiveMediaProviderCache;
const broadcastChannel = yield* MediaProviderMainThreadBroadcastChannel;
const lazyLoadedProvider = yield* LazyLoadedProvider;
const lazyLoaderMediaPlayer = yield* LazyLoadedMediaPlayer;
const localStorage = yield* LocalStorage;

return AppInit.of({
init: Effect.gen(function* () {
const allProviderStates = yield* retrieveAllProviderArgs(localStorage);

return yield* Effect.all(
allProviderStates.map((providerStartArgs) =>
Effect.gen(function* () {
const retrievedMetadata = {
id: providerStartArgs.value.metadata.id,
type: providerStartArgs.value.metadata.type,
};

const providerFactory =
yield* lazyLoadedProvider.load(retrievedMetadata);
const mediaPlayerFactory =
yield* lazyLoaderMediaPlayer.load(retrievedMetadata);

return yield* reinitializeProvider(
providerStartArgs.value,
providerFactory.createMediaProvider,
mediaPlayerFactory.createMediaPlayer,
broadcastChannel,
activeMediaProviderCache,
);
}),
),
);
}),
});
});

const retrieveAllProviderArgs = (localStorage: ILocalStorage) =>
Effect.gen(function* () {
const allProviders = yield* Effect.all(
AvailableProviders.map((provider) =>
retrieveProviderArgs(provider.id, localStorage),
),
);
return allProviders.filter(Option.isSome);
});

const retrieveProviderArgs = (
providerId: ProviderId,
localStorage: ILocalStorage,
) =>
localStorage.get<ProviderStartArgs>("media-provider-start-args", providerId);

const reinitializeProvider = (
startArgs: ProviderStartArgs,
createMediaProvider: MediaProviderFactory["createMediaProvider"],
createMediaPlayer: MediaPlayerFactory["createMediaPlayer"],
broadcastChannel: BroadcastChannel<
MediaProviderBroadcastSchema["mainThread"]
>,
activeMediaProviderCache: IActiveMediaProviderCache,
) =>
Effect.gen(function* () {
// TODO: This should attempt to refresh the token if it's expired.
// TODO: Instead of ignoring the initialization with a warning, we should notify the user.
if (!isValidToken(startArgs.authInfo)) {
yield* Effect.logWarning(
`The retrieved token for ${startArgs.metadata.id} is invalid, ignoring initialization.`,
);
}

const mediaProvider = createMediaProvider(startArgs.authInfo);
const mediaPlayer = yield* createMediaPlayer(startArgs.authInfo);

yield* broadcastChannel.send("start", startArgs);
yield* activeMediaProviderCache.add(
startArgs.metadata,
mediaProvider,
mediaPlayer,
);
});

export const AppInitLive = Layer.effect(AppInit, make);
Loading

0 comments on commit 01316f0

Please sign in to comment.