Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/extension/scripts/extension-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export function extensionEnvironment(environment = process.env) {
connectUrl: deployment.connectUrl,
loopbackUrl: deployment.loopbackUrl,
readerOrigin: deployment.origin,
// Opt-in mdbase-next SDK backend (MDBASE_SDK=next); Connect stays the default.
sdk: environment.MDBASE_SDK === "next" ? "next" : "connect",
};
}

Expand Down
30 changes: 20 additions & 10 deletions apps/extension/src/connect-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {
ReaderPortableApplicationSession,
type MdbaseAppManifest,
type ReaderConnectSnapshot,
type ReaderPortableSession,
} from "@mdbase-reader/connect";
import { ReaderNextApplicationSession, readerSdkBackend } from "@mdbase-reader/connect/next";

import { chromeStorageMirror } from "./chrome-storage.js";
import { lastCollectionKey } from "./collection-memory.js";
Expand All @@ -14,21 +16,29 @@ import type { KeyValueStorage } from "@mdbase-reader/platform";
export { rememberCollection, rememberedCollection } from "./collection-memory.js";

export interface ExtensionSession {
readonly session: ReaderPortableApplicationSession;
readonly session: ReaderPortableSession;
/** Reader's mutation journal, kept beside the grants so any extension context can recover it. */
readonly journalStorage: KeyValueStorage;
}

export async function createExtensionSession(): Promise<ExtensionSession> {
const storage = await chromeStorageMirror();
const session = new ReaderPortableApplicationSession({
serverUrl: environment.connectUrl,
loopbackUrl: environment.loopbackUrl,
manifest: manifest as MdbaseAppManifest,
storage,
// Interactive capture should not wait the SDK's ten-minute file-index default.
timeouts: { watchStartMs: 60_000, fileIndexMs: 30_000, uploadMs: 120_000 },
});
const session: ReaderPortableSession =
readerSdkBackend(null, environment.sdk) === "next"
? // The client key is a non-extractable WebCrypto key kept in IndexedDB.
new ReaderNextApplicationSession({
serverUrl: environment.connectUrl,
app: { name: manifest.id, version: chrome.runtime.getManifest().version },
storage,
})
: new ReaderPortableApplicationSession({
serverUrl: environment.connectUrl,
loopbackUrl: environment.loopbackUrl,
manifest: manifest as MdbaseAppManifest,
storage,
// Interactive capture should not wait the SDK's ten-minute file-index default.
timeouts: { watchStartMs: 60_000, fileIndexMs: 30_000, uploadMs: 120_000 },
});
return {
session,
journalStorage: {
Expand All @@ -41,7 +51,7 @@ export async function createExtensionSession(): Promise<ExtensionSession> {

/** Selects the collection chosen last time, if it is still authorized. */
export async function restoreCollection(
session: ReaderPortableApplicationSession,
session: ReaderPortableSession,
): Promise<ReaderConnectSnapshot> {
const snapshot = session.getSnapshot();
if (snapshot.status !== "unselected") {
Expand Down
2 changes: 2 additions & 0 deletions apps/extension/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export interface ExtensionEnvironment {
readonly connectUrl: string;
readonly loopbackUrl: string;
readonly readerOrigin: string;
/** `next` opts into the mdbase-next SDK backend (MDBASE_SDK=next at build time). */
readonly sdk?: "connect" | "next";
}

declare const __READER_EXTENSION_ENVIRONMENT__: ExtensionEnvironment;
Expand Down
7 changes: 2 additions & 5 deletions apps/extension/src/page-status-connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import {
} from "./connect-session.js";
import { annotationQuotes, drawPageQuotes, type PageQuote } from "./page-annotations.js";

import type {
ReaderConnectedCollection,
ReaderPortableApplicationSession,
} from "@mdbase-reader/connect";
import type { ReaderConnectedCollection, ReaderPortableSession } from "@mdbase-reader/connect";

/**
* The part of page status that needs Connect. The service worker loads it (with the
Expand All @@ -29,7 +26,7 @@ export interface PageStatusConnect {
drawPageQuotes(tabId: number, quotes: readonly PageQuote[], url: string): Promise<unknown>;
}

let session: Promise<ReaderPortableApplicationSession | null> | null = null;
let session: Promise<ReaderPortableSession | null> | null = null;

/** One Connect session per service-worker lifetime, sharing the panel's stored grants. */
async function selectedCollection(): Promise<ReaderConnectedCollection | null> {
Expand Down
7 changes: 2 additions & 5 deletions apps/extension/src/save-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ import { pageText } from "./page-annotations.js";
import { rememberSavedUrls } from "./saved-url-index.js";

import type { PageCapture, PdfCapture, SelectedWebCapture } from "./page-capture.js";
import type {
ReaderConnectedCollection,
ReaderPortableApplicationSession,
} from "@mdbase-reader/connect";
import type { ReaderConnectedCollection, ReaderPortableSession } from "@mdbase-reader/connect";

export const highlightColors = ["yellow", "green", "blue", "pink", "purple"] as const;
export type HighlightColor = (typeof highlightColors)[number];
Expand All @@ -61,7 +58,7 @@ export interface SavedCapture {
readonly notices: readonly string[];
}
export interface SaveCaptureInput {
readonly session: ReaderPortableApplicationSession;
readonly session: ReaderPortableSession;
readonly collection: ReaderConnectedCollection;
readonly capture: PageCapture;
readonly draft: CaptureDraft;
Expand Down
7 changes: 2 additions & 5 deletions apps/extension/src/testing/save-capture-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ import {
} from "../save-capture.js";

import type { PageCapture, SelectedWebCapture } from "../page-capture.js";
import type {
ReaderConnectedCollection,
ReaderPortableApplicationSession,
} from "@mdbase-reader/connect";
import type { ReaderConnectedCollection, ReaderPortableSession } from "@mdbase-reader/connect";

export const capture: SelectedWebCapture = {
kind: "html",
Expand Down Expand Up @@ -119,7 +116,7 @@ export function fixture() {
} as unknown as ReaderConnectedCollection;
const session = {
recoverPendingMutations: vi.fn(() => Promise.resolve([])),
} as unknown as ReaderPortableApplicationSession;
} as unknown as ReaderPortableSession;
const onSource = vi.fn();
const journal = new Map<string, string>();
const storage = {
Expand Down
6 changes: 3 additions & 3 deletions apps/extension/src/use-connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
connectProblemMessage,
type ReaderConnectSnapshot,
type ReaderDirectAccessController,
type ReaderPortableApplicationSession,
type ReaderPortableSession,
} from "@mdbase-reader/connect";
import { useCallback, useEffect, useState } from "react";

Expand All @@ -23,7 +23,7 @@ export interface ConnectLink {
readonly deviceCode: string | null;
readonly directAccess: ReaderDirectAccessController | null;
/** Creates the session (grants load from extension storage) and restores the last collection. */
readonly open: () => Promise<ReaderPortableApplicationSession>;
readonly open: () => Promise<ReaderPortableSession>;
readonly connect: (choose?: boolean) => Promise<void>;
readonly retry: () => Promise<void>;
readonly applySetup: () => Promise<void>;
Expand All @@ -48,7 +48,7 @@ export function useConnect(lock: ActionLock): ConnectLink {
};
}, [extension]);

const open = useCallback(async (): Promise<ReaderPortableApplicationSession> => {
const open = useCallback(async (): Promise<ReaderPortableSession> => {
const created = await createExtensionSession();
setExtension(created);
const problem = connectProblemMessage(await created.session.start());
Expand Down
45 changes: 34 additions & 11 deletions apps/reader/src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {
ReaderApplicationSession,
manifestForApplicationUrl,
type ReaderApplicationSessionOptions,
type ReaderSession,
} from "@mdbase-reader/connect";
import { ReaderNextApplicationSession, readerSdkBackend } from "@mdbase-reader/connect/next";

import bundledManifest from "./generated/mdbase-app.json";

Expand All @@ -17,17 +19,38 @@ const manifest = isLoopbackApplication(location)
? manifestForApplicationUrl(declaredManifest, applicationUrl, callbackUrl.href)
: declaredManifest;

export const readerSession = new ReaderApplicationSession({
serverUrl:
serverParameter ?? import.meta.env.VITE_MDBASE_CONNECT_URL ?? "https://connect.mdbase.dev",
loopbackUrl: import.meta.env.VITE_MDBASE_CONNECT_LOOPBACK_URL ?? "http://127.0.0.1:28485",
manifest,
redirectUri: callbackUrl.href,
fallbackPath: import.meta.env.BASE_URL,
// Application startup includes collection setup verification. A remote connector
// may need more than the SDK's interactive 10-second discovery default.
timeouts: { watchStartMs: 60_000 },
});
const serverUrl =
serverParameter ?? import.meta.env.VITE_MDBASE_CONNECT_URL ?? "https://connect.mdbase.dev";
// Opt-in mdbase-next backend: `?sdk=next` or VITE_MDBASE_SDK=next. Connect stays the default.
const sdkBackend = readerSdkBackend(
new URL(location.href).searchParams.get("sdk"),
import.meta.env.VITE_MDBASE_SDK,
);

export const readerSession: ReaderSession =
sdkBackend === "next"
? new ReaderNextApplicationSession({
serverUrl,
app: {
name: declaredManifest.id,
version: import.meta.env.VITE_MDBASE_READER_BUILD_ID ?? "development",
},
storage: localStorage,
})
: connectSession();

function connectSession(): ReaderApplicationSession {
return new ReaderApplicationSession({
serverUrl,
loopbackUrl: import.meta.env.VITE_MDBASE_CONNECT_LOOPBACK_URL ?? "http://127.0.0.1:28485",
manifest,
redirectUri: callbackUrl.href,
fallbackPath: import.meta.env.BASE_URL,
// Application startup includes collection setup verification. A remote connector
// may need more than the SDK's interactive 10-second discovery default.
timeouts: { watchStartMs: 60_000 },
});
}

function isLoopbackApplication(current: Location): boolean {
return (
Expand Down
2 changes: 2 additions & 0 deletions apps/reader/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ interface ImportMetaEnv {
readonly VITE_MDBASE_FEEDBACK_TURNSTILE_SITE_KEY?: string;
readonly VITE_MDBASE_CONNECT_URL?: string;
readonly VITE_MDBASE_CONNECT_LOOPBACK_URL?: string;
/** `next` opts into the mdbase-next SDK backend; Connect is the default. */
readonly VITE_MDBASE_SDK?: string;
readonly VITE_MDBASE_READER_BUILD_ID?: string;
readonly VITE_MDBASE_EDITOR_URL?: string;
readonly VITE_MDBASE_READER_URL?: string;
Expand Down
4 changes: 2 additions & 2 deletions apps/reader/src/import-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {

import { markFraction, withMarkProgress } from "./mark-activity.js";

import type { ReaderApplicationSession } from "@mdbase-reader/connect";
import type { ReaderSession } from "@mdbase-reader/connect";
export interface ImportState {
plan: MigrationPlan | null;
message: string;
Expand All @@ -37,7 +37,7 @@ export class ImportController {
private listeners = new Set<() => void>();
private active: AbortController | null = null;
private client: ReadwiseClient | null = null;
constructor(private session: ReaderApplicationSession) {}
constructor(private session: ReaderSession) {}
getSnapshot = (): ImportState => this.state;
subscribe = (fn: () => void): (() => void) => {
this.listeners.add(fn);
Expand Down
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export default tseslint.config(
"electron",
"@capacitor/*",
"@mdbase-dev/connect",
"@mdbase-dev/sdk",
"@mdbase-dev/sdk/*",
"@embedpdf/*",
"@readium/*",
"@codemirror/*",
Expand Down
4 changes: 3 additions & 1 deletion packages/connect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./next": "./src/next/index.ts"
},
"scripts": {
"test": "vitest run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json"
},
"dependencies": {
"@mdbase-dev/connect": "0.1.0-beta.125",
"@mdbase-dev/sdk": "file:vendor/mdbase-dev-sdk-c573c96.tgz",
"@mdbase-reader/core": "workspace:*",
"@mdbase-reader/migration": "workspace:*",
"yaml": "^2.9.0"
Expand Down
18 changes: 16 additions & 2 deletions packages/connect/src/application-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
} from "./repositories.js";
import { connectSourceImportRepository } from "./source-imports.js";

import type { MigrationTarget } from "@mdbase-reader/migration";

export type ReaderConnectSnapshot = MdbaseApplicationSessionSnapshot;

export interface ReaderDirectAccessSnapshot {
Expand All @@ -60,7 +62,7 @@ export interface ReaderConnectedCollection {
readonly collectionName: string;
readonly sources: SourceRepository;
readonly sourceImports: SourceImportRepository;
readonly migration: ConnectMigrationTarget;
readonly migration: MigrationTarget;
readonly annotations: AnnotationRepository;
readonly annotationAssets: AnnotationAssetRepository;
readonly documents: DocumentRepository;
Expand All @@ -80,7 +82,19 @@ export interface ReaderApplicationSessionOptions {
readonly timeouts?: MdbaseConnectTimeouts;
}

export class ReaderApplicationSession {
/** What Reader's web app needs from a session, whichever SDK backs it. */
export interface ReaderSession {
start(): Promise<ConnectOutcome<ReaderConnectSnapshot>>;
destroy(): void;
getSnapshot(): ReaderConnectSnapshot;
subscribe(listener: () => void): () => void;
select(selectedCollectionId: string): ConnectOutcome<unknown>;
authorize(target: "choose" | "selected", popup?: boolean): Promise<ConnectOutcome<unknown>>;
applyCollectionSetup(): Promise<ConnectOutcome<ReaderConnectSnapshot>>;
connectedCollection(expectedCollectionId?: string): ReaderConnectedCollection | null;
}

export class ReaderApplicationSession implements ReaderSession {
readonly #session;

public constructor(options: ReaderApplicationSessionOptions) {
Expand Down
15 changes: 15 additions & 0 deletions packages/connect/src/next/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Which SDK backs Reader's session. The current Connect SDK stays the default until cutover.

export type ReaderSdkBackend = "connect" | "next";

/**
* Picks the backend the way Reader picks its server: a `?sdk=next` URL flag first,
* then `VITE_MDBASE_SDK=next` at build time. Anything else is the Connect SDK.
*/
export function readerSdkBackend(
urlFlag: string | null | undefined,
buildSetting?: string,
): ReaderSdkBackend {
const choice = (urlFlag ?? buildSetting ?? "").trim().toLowerCase();
return choice === "next" ? "next" : "connect";
}
Loading