import { Devtools, liveStoreVersion } from '@livestore/common' import type { LiveStoreSchema } from '@livestore/common/schema' import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel' import { isDevEnv } from '@livestore/utils' import type { Worker } from '@livestore/utils/effect' import { Effect, Stream } from '@livestore/utils/effect' import { WebChannelBrowser } from '@livestore/utils/effect/browser' import * as Webmesh from '@livestore/webmesh' export const logDevtoolsUrl = Effect.fn('@livestore/adapter-web:client-session:devtools:logDevtoolsUrl')(function* ({ schema, storeId, clientId, sessionId, }: { schema: LiveStoreSchema storeId: string clientId: string sessionId: string }) { if (isDevEnv() === true) { const devtoolsPath = (globalThis as unknown as { LIVESTORE_DEVTOOLS_PATH?: string }).LIVESTORE_DEVTOOLS_PATH ?? `/_livestore` const devtoolsBaseUrl = `${location.origin}${devtoolsPath}` // Check whether devtools are available and then log the URL const response = yield* Effect.promise(() => fetch(devtoolsBaseUrl)) if (response.ok === true) { const text = yield* Effect.promise(() => response.text()) if (text.includes('') === true) { const url = `${devtoolsBaseUrl}/web/${storeId}/${clientId}/${sessionId}/${schema.devtools.alias}` yield* Effect.log(`[@livestore/adapter-web] Devtools ready on ${url}`) } // Check for DevTools Chrome extension presence via iframe container the extension injects const hasExt = document.querySelector('[id^="livestore-devtools-iframe-"]') !== null if (hasExt === false) { const g = globalThis as { __livestoreDevtoolsChromeNoticeShown?: boolean } if (g.__livestoreDevtoolsChromeNoticeShown !== true) { g.__livestoreDevtoolsChromeNoticeShown = true const urlToLog = `https://github.com/livestorejs/livestore/releases/download/v${liveStoreVersion}/livestore-devtools-chrome-${liveStoreVersion}.zip` yield* Effect.log( `[@livestore/adapter-web] LiveStore DevTools Chrome extension not detected. Install v${liveStoreVersion}: ${urlToLog}`, ) } } } } }) export const connectWebmeshNodeClientSession = Effect.fn(function* ({ webmeshNode, sessionInfo, sharedWorker, devtoolsEnabled, schema, }: { webmeshNode: Webmesh.MeshNode sessionInfo: Devtools.SessionInfo.SessionInfo sharedWorker: Worker.SerializedWorkerPool devtoolsEnabled: boolean schema: LiveStoreSchema }) { if (devtoolsEnabled === true) { const { clientId, sessionId, storeId } = sessionInfo yield* logDevtoolsUrl({ clientId, sessionId, schema, storeId }) // This additional sessioninfo broadcast channel is needed since we can't use the shared worker // as it's currently storeId-specific yield* Devtools.SessionInfo.provideSessionInfo({ webChannel: yield* DevtoolsWeb.makeSessionInfoBroadcastChannel, sessionInfo, }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped) yield* Effect.gen(function* () { const clientSessionStaticChannel = yield* DevtoolsWeb.makeStaticClientSessionChannel.clientSession yield* clientSessionStaticChannel.send( DevtoolsWeb.ClientSessionContentscriptMainReq.make({ clientId, sessionId, storeId }), ) const { tabId } = yield* clientSessionStaticChannel.listen.pipe(Stream.flatten(), Stream.runHead, Effect.flatten) const contentscriptMainNodeName = DevtoolsWeb.makeNodeName.browserExtension.contentscriptMain(tabId) const contentscriptMainChannel = yield* WebChannelBrowser.windowChannel({ listenWindow: window, sendWindow: window, schema: Webmesh.WebmeshSchema.Packet, ids: { own: webmeshNode.nodeName, other: contentscriptMainNodeName }, }) yield* webmeshNode.addEdge({ target: contentscriptMainNodeName, edgeChannel: contentscriptMainChannel }) }).pipe( Effect.withSpan('@livestore/adapter-web:client-session:devtools:browser-extension'), Effect.tapCauseLogPretty, Effect.forkScoped, ) yield* DevtoolsWeb.connectViaWorker({ node: webmeshNode, target: DevtoolsWeb.makeNodeName.sharedWorker({ storeId }), worker: sharedWorker, }) } })