@livestore/adapter-web 0.0.0-snapshot-a953343ad2d7468c6573bcb5e26f0eab4302078f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.eslintrc.cjs +6 -0
  2. package/README.md +12 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/common/connection.d.ts +7 -0
  5. package/dist/common/connection.d.ts.map +1 -0
  6. package/dist/common/connection.js +25 -0
  7. package/dist/common/connection.js.map +1 -0
  8. package/dist/devtools-bridge/background-browser-channel.d.ts +9 -0
  9. package/dist/devtools-bridge/background-browser-channel.d.ts.map +1 -0
  10. package/dist/devtools-bridge/background-browser-channel.js +31 -0
  11. package/dist/devtools-bridge/background-browser-channel.js.map +1 -0
  12. package/dist/devtools-bridge/background-message.d.ts +75 -0
  13. package/dist/devtools-bridge/background-message.d.ts.map +1 -0
  14. package/dist/devtools-bridge/background-message.js +53 -0
  15. package/dist/devtools-bridge/background-message.js.map +1 -0
  16. package/dist/devtools-bridge/bridge-shared.d.ts +14 -0
  17. package/dist/devtools-bridge/bridge-shared.d.ts.map +1 -0
  18. package/dist/devtools-bridge/bridge-shared.js +67 -0
  19. package/dist/devtools-bridge/bridge-shared.js.map +1 -0
  20. package/dist/devtools-bridge/browser-extension-bridge.d.ts +3 -0
  21. package/dist/devtools-bridge/browser-extension-bridge.d.ts.map +1 -0
  22. package/dist/devtools-bridge/browser-extension-bridge.js +59 -0
  23. package/dist/devtools-bridge/browser-extension-bridge.js.map +1 -0
  24. package/dist/devtools-bridge/iframe-message.d.ts +16 -0
  25. package/dist/devtools-bridge/iframe-message.d.ts.map +1 -0
  26. package/dist/devtools-bridge/iframe-message.js +11 -0
  27. package/dist/devtools-bridge/iframe-message.js.map +1 -0
  28. package/dist/devtools-bridge/index.d.ts +6 -0
  29. package/dist/devtools-bridge/index.d.ts.map +1 -0
  30. package/dist/devtools-bridge/index.js +5 -0
  31. package/dist/devtools-bridge/index.js.map +1 -0
  32. package/dist/devtools-bridge/web-bridge.d.ts +31 -0
  33. package/dist/devtools-bridge/web-bridge.d.ts.map +1 -0
  34. package/dist/devtools-bridge/web-bridge.js +131 -0
  35. package/dist/devtools-bridge/web-bridge.js.map +1 -0
  36. package/dist/in-memory/index.d.ts +4 -0
  37. package/dist/in-memory/index.d.ts.map +1 -0
  38. package/dist/in-memory/index.js +50 -0
  39. package/dist/in-memory/index.js.map +1 -0
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +4 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/opfs-utils.d.ts +5 -0
  45. package/dist/opfs-utils.d.ts.map +1 -0
  46. package/dist/opfs-utils.js +43 -0
  47. package/dist/opfs-utils.js.map +1 -0
  48. package/dist/web-worker/client-session/client-session-devtools.d.ts +7 -0
  49. package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -0
  50. package/dist/web-worker/client-session/client-session-devtools.js +107 -0
  51. package/dist/web-worker/client-session/client-session-devtools.js.map +1 -0
  52. package/dist/web-worker/client-session/index.d.ts +41 -0
  53. package/dist/web-worker/client-session/index.d.ts.map +1 -0
  54. package/dist/web-worker/client-session/index.js +299 -0
  55. package/dist/web-worker/client-session/index.js.map +1 -0
  56. package/dist/web-worker/client-session/trim-batch.d.ts +4 -0
  57. package/dist/web-worker/client-session/trim-batch.d.ts.map +1 -0
  58. package/dist/web-worker/client-session/trim-batch.js +13 -0
  59. package/dist/web-worker/client-session/trim-batch.js.map +1 -0
  60. package/dist/web-worker/client-session/trim-batch.test.d.ts +2 -0
  61. package/dist/web-worker/client-session/trim-batch.test.d.ts.map +1 -0
  62. package/dist/web-worker/client-session/trim-batch.test.js +38 -0
  63. package/dist/web-worker/client-session/trim-batch.test.js.map +1 -0
  64. package/dist/web-worker/common/persisted-sqlite.d.ts +23 -0
  65. package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -0
  66. package/dist/web-worker/common/persisted-sqlite.js +92 -0
  67. package/dist/web-worker/common/persisted-sqlite.js.map +1 -0
  68. package/dist/web-worker/common/shutdown-channel.d.ts +7 -0
  69. package/dist/web-worker/common/shutdown-channel.d.ts.map +1 -0
  70. package/dist/web-worker/common/shutdown-channel.js +7 -0
  71. package/dist/web-worker/common/shutdown-channel.js.map +1 -0
  72. package/dist/web-worker/common/worker-schema.d.ts +226 -0
  73. package/dist/web-worker/common/worker-schema.d.ts.map +1 -0
  74. package/dist/web-worker/common/worker-schema.js +176 -0
  75. package/dist/web-worker/common/worker-schema.js.map +1 -0
  76. package/dist/web-worker/leader-worker/make-leader-worker.d.ts +15 -0
  77. package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -0
  78. package/dist/web-worker/leader-worker/make-leader-worker.js +144 -0
  79. package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -0
  80. package/dist/web-worker/shared-worker/make-shared-worker.d.ts +2 -0
  81. package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -0
  82. package/dist/web-worker/shared-worker/make-shared-worker.js +160 -0
  83. package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -0
  84. package/dist/web-worker/vite-dev-polyfill.d.ts +2 -0
  85. package/dist/web-worker/vite-dev-polyfill.d.ts.map +1 -0
  86. package/dist/web-worker/vite-dev-polyfill.js +37 -0
  87. package/dist/web-worker/vite-dev-polyfill.js.map +1 -0
  88. package/package.json +78 -0
  89. package/src/common/connection.ts +32 -0
  90. package/src/devtools-bridge/background-browser-channel.ts +57 -0
  91. package/src/devtools-bridge/background-message.ts +42 -0
  92. package/src/devtools-bridge/bridge-shared.ts +97 -0
  93. package/src/devtools-bridge/browser-extension-bridge.ts +64 -0
  94. package/src/devtools-bridge/iframe-message.ts +9 -0
  95. package/src/devtools-bridge/index.ts +9 -0
  96. package/src/devtools-bridge/web-bridge.ts +169 -0
  97. package/src/in-memory/index.ts +66 -0
  98. package/src/index.ts +3 -0
  99. package/src/opfs-utils.ts +61 -0
  100. package/src/web-worker/ambient.d.ts +37 -0
  101. package/src/web-worker/client-session/client-session-devtools.ts +167 -0
  102. package/src/web-worker/client-session/index.ts +537 -0
  103. package/src/web-worker/client-session/trim-batch.test.ts +48 -0
  104. package/src/web-worker/client-session/trim-batch.ts +15 -0
  105. package/src/web-worker/common/persisted-sqlite.ts +136 -0
  106. package/src/web-worker/common/shutdown-channel.ts +8 -0
  107. package/src/web-worker/common/worker-schema.ts +206 -0
  108. package/src/web-worker/leader-worker/make-leader-worker.ts +276 -0
  109. package/src/web-worker/shared-worker/make-shared-worker.ts +300 -0
  110. package/src/web-worker/vite-dev-polyfill.ts +36 -0
  111. package/tsconfig.json +17 -0
@@ -0,0 +1,169 @@
1
+ import type { WebAdapterOptions } from '@livestore/adapter-web'
2
+ import { WorkerSchema } from '@livestore/adapter-web'
3
+ import { Devtools, liveStoreVersion } from '@livestore/common'
4
+ import type { LiveStoreSchema } from '@livestore/common/schema'
5
+ import { tryAsFunctionAndNew } from '@livestore/utils'
6
+ import type { Deferred, HashSet, Scope, SubscriptionRef } from '@livestore/utils/effect'
7
+ import { BrowserWorker, Effect, Equal, FiberSet, Hash, PubSub, Schema, Stream, Worker } from '@livestore/utils/effect'
8
+ import { nanoid } from '@livestore/utils/nanoid'
9
+
10
+ import { makeShared } from './bridge-shared.js'
11
+
12
+ export class WebBridgeInfo extends Schema.Class<WebBridgeInfo>('WebBridgeChannelInfo')({
13
+ appHostId: Schema.String,
14
+ storeId: Schema.String,
15
+ webBridgeId: Schema.String,
16
+ isLeader: Schema.Boolean,
17
+ }) {
18
+ // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
19
+ [Hash.symbol](): number {
20
+ return Hash.string(this.appHostId)
21
+ }
22
+
23
+ // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
24
+ [Equal.symbol](that: Equal.Equal): boolean {
25
+ return this.appHostId === (that as WebBridgeInfo).appHostId
26
+ }
27
+ }
28
+
29
+ export type WebBridgeOptions = {
30
+ selectedChannelInfoDeferred: Deferred.Deferred<WebBridgeInfo>
31
+ bridgeInfos: SubscriptionRef.SubscriptionRef<HashSet.HashSet<WebBridgeInfo>>
32
+ }
33
+
34
+ // const prepareWebDevtoolsBridge = (
35
+ // options: {
36
+ // sharedWorker: WebAdapterOptions['sharedWorker']
37
+ // appSchema: LiveStoreSchema
38
+ // } & WebBridgeOptions,
39
+ // ): Effect.Effect<Devtools.PrepareDevtoolsBridge, never, Scope.Scope> =>
40
+ // Effect.gen(function* () {
41
+ // const responsePubSub = yield* PubSub.unbounded<
42
+ // Devtools.MessageFromApp | Devtools.MessageFromApp
43
+ // >().pipe(Effect.acquireRelease(PubSub.shutdown))
44
+
45
+ // const devtoolsId = nanoid()
46
+
47
+ // const portForDevtoolsDeferred = yield* Deferred.make<MessagePort>()
48
+
49
+ // const webBridgeBroadcastChannel = yield* Devtools.WebBridge.makeBroadcastChannel()
50
+
51
+ // yield* webBridgeBroadcastChannel.listen.pipe(
52
+ // Stream.flatten(),
53
+ // Stream.filter(Schema.is(Devtools.WebBridge.AppHostReady)),
54
+ // Stream.tap(() => webBridgeBroadcastChannel.send(Devtools.WebBridge.DevtoolsReady.make({ devtoolsId }))),
55
+ // Stream.runDrain,
56
+ // Effect.withSpan(`@livestore/adapter-web:devtools:webBridgeChannel:listen`),
57
+ // Effect.ignoreLogged,
58
+ // Effect.forkScoped,
59
+ // )
60
+
61
+ // yield* webBridgeBroadcastChannel.send(Devtools.WebBridge.DevtoolsReady.make({ devtoolsId }))
62
+
63
+ // const connectionFiberSet = yield* FiberSet.make()
64
+
65
+ // yield* webBridgeBroadcastChannel.listen.pipe(
66
+ // Stream.flatten(),
67
+ // Stream.filter(Schema.is(Devtools.WebBridge.ConnectToDevtools)),
68
+ // Stream.tap((msg) =>
69
+ // Effect.gen(function* () {
70
+ // if (devtoolsId !== msg.devtoolsId) return
71
+
72
+ // const bridgeInfo = new WebBridgeInfo({
73
+ // appHostId: msg.appHostId,
74
+ // webBridgeId: msg.webBridgeId,
75
+ // isLeader: msg.isLeader,
76
+ // storeId: msg.storeId,
77
+ // })
78
+
79
+ // // Propagate disconnect event while connecting.
80
+ // // There's another disconnect handler below after the connection is established.
81
+ // yield* webBridgeBroadcastChannel.listen.pipe(
82
+ // Stream.flatten(),
83
+ // Stream.filter(Schema.is(Devtools.WebBridge.AppHostWillDisconnect)),
84
+ // Stream.filter((msg) => msg.appHostId === bridgeInfo.appHostId),
85
+ // Stream.tap(() => SubscriptionRef.getAndUpdate(options.bridgeInfos, HashSet.remove(bridgeInfo))),
86
+ // Stream.runDrain,
87
+ // Effect.withSpan(`@livestore/adapter-web:devtools:webBridgeChannel:listenForAppHostWillDisconnect`),
88
+ // Effect.tapCauseLogPretty,
89
+ // FiberSet.run(connectionFiberSet),
90
+ // )
91
+
92
+ // yield* SubscriptionRef.getAndUpdate(options.bridgeInfos, HashSet.add(bridgeInfo))
93
+ // }),
94
+ // ),
95
+ // Stream.runDrain,
96
+ // Effect.withSpan(`@livestore/adapter-web:devtools:webBridgeChannel:listen`),
97
+ // Effect.tapCauseLogPretty,
98
+ // FiberSet.run(connectionFiberSet),
99
+ // )
100
+
101
+ // const selectedChannelInfo = yield* Deferred.await(options.selectedChannelInfoDeferred)
102
+
103
+ // const sharedWorker = tryAsFunctionAndNew(options.sharedWorker, {
104
+ // name: `livestore-shared-worker-${selectedChannelInfo.storeId}`,
105
+ // })
106
+
107
+ // const sharedWorkerDeferred = yield* Worker.makeSerialized<typeof WorkerSchema.SharedWorker.Request.Type>({
108
+ // initialMessage: () => new WorkerSchema.SharedWorker.InitialMessage({ payload: { _tag: 'FromWebBridge' } }),
109
+ // }).pipe(
110
+ // Effect.provide(BrowserWorker.layer(() => sharedWorker)),
111
+ // Effect.tapCauseLogPretty,
112
+ // Effect.withSpan('@livestore/adapter-web:coordinator:setupSharedWorker'),
113
+ // Effect.toForkedDeferred,
114
+ // )
115
+
116
+ // yield* Effect.gen(function* () {
117
+ // const mc = new MessageChannel()
118
+
119
+ // const worker = yield* Deferred.await(sharedWorkerDeferred)
120
+ // yield* worker.executeEffect(
121
+ // new WorkerSchema.SharedWorker.DevtoolsWebBridgeOfferPort({
122
+ // port: mc.port1,
123
+ // webBridgeId: selectedChannelInfo.webBridgeId,
124
+ // }),
125
+ // )
126
+
127
+ // yield* Deferred.succeed(portForDevtoolsDeferred, mc.port2)
128
+
129
+ // // Stop listening for new connections and close `AppHostWillDisconnect` listeners
130
+ // yield* FiberSet.clear(connectionFiberSet)
131
+ // }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
132
+
133
+ // const { sendToAppHost, appHostId, isLeader } = yield* makeShared({ portForDevtoolsDeferred, responsePubSub })
134
+
135
+ // // NOTE we need a second listener here since we depend on the `appHostId` to be set
136
+ // yield* webBridgeBroadcastChannel.listen.pipe(
137
+ // Stream.flatten(),
138
+ // Stream.filter(Schema.is(Devtools.WebBridge.AppHostWillDisconnect)),
139
+ // Stream.filter((msg) => msg.appHostId === appHostId),
140
+ // Stream.tap(() => SubscriptionRef.getAndUpdate(options.bridgeInfos, HashSet.remove(selectedChannelInfo))),
141
+ // Stream.tap(() => PubSub.publish(responsePubSub, Devtools.Disconnect.make({ appHostId, liveStoreVersion }))),
142
+ // Stream.runDrain,
143
+ // Effect.withSpan(`@livestore/adapter-web:devtools:webBridgeChannel:listenForAppHostWillDisconnect`),
144
+ // Effect.ignoreLogged,
145
+ // Effect.forkScoped,
146
+ // )
147
+
148
+ // // NOTE this is not guaranteed to "go through" to the app host but at least we try 🤷
149
+ // yield* Stream.fromEventListener(window, 'beforeunload').pipe(
150
+ // Stream.tap(() => sendToAppHost(Devtools.Disconnect.make({ appHostId, liveStoreVersion }))),
151
+ // Stream.runDrain,
152
+ // Effect.ignoreLogged,
153
+ // Effect.forkScoped,
154
+ // )
155
+
156
+ // const copyToClipboard = (text: string) =>
157
+ // Effect.sync(() => {
158
+ // navigator.clipboard.writeText(text)
159
+ // })
160
+
161
+ // return {
162
+ // responsePubSub,
163
+ // sendToAppHost,
164
+ // appHostId,
165
+ // copyToClipboard,
166
+ // sendEscapeKey: Effect.void,
167
+ // isLeader,
168
+ // } satisfies Devtools.PrepareDevtoolsBridge
169
+ // }).pipe(Effect.orDie)
@@ -0,0 +1,66 @@
1
+ import type { Adapter, ClientSession, LockStatus, MigrationsReport } from '@livestore/common'
2
+ import { initializeSingletonTables, migrateDb, UnexpectedError } from '@livestore/common'
3
+ import { configureConnection } from '@livestore/common/leader-thread'
4
+ import { EventId } from '@livestore/common/schema'
5
+ import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
6
+ import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
7
+ import { Effect, Stream, SubscriptionRef } from '@livestore/utils/effect'
8
+ import { nanoid } from '@livestore/utils/nanoid'
9
+
10
+ // TODO unify in-memory adapter with other in-memory adapter implementations
11
+
12
+ // NOTE we're starting to initialize the sqlite wasm binary here to speed things up
13
+ const sqlite3Promise = loadSqlite3Wasm()
14
+
15
+ /** NOTE: This adapter is currently only used for testing */
16
+ export const makeInMemoryAdapter =
17
+ (initialData?: Uint8Array): Adapter =>
18
+ ({
19
+ schema,
20
+ // devtoolsEnabled, bootStatusQueue, shutdown, connectDevtoolsToStore
21
+ }) =>
22
+ Effect.gen(function* () {
23
+ const sqlite3 = yield* Effect.promise(() => sqlite3Promise)
24
+
25
+ const sqliteDb = yield* sqliteDbFactory({ sqlite3 })({ _tag: 'in-memory' })
26
+ let migrationsReport: MigrationsReport = { migrations: [] }
27
+
28
+ if (initialData === undefined) {
29
+ yield* configureConnection(sqliteDb, { foreignKeys: true })
30
+
31
+ migrationsReport = yield* migrateDb({ db: sqliteDb, schema })
32
+
33
+ initializeSingletonTables(schema, sqliteDb)
34
+ } else {
35
+ sqliteDb.import(initialData)
36
+
37
+ yield* configureConnection(sqliteDb, { foreignKeys: true })
38
+ }
39
+
40
+ const lockStatus = SubscriptionRef.make<LockStatus>('has-lock').pipe(Effect.runSync)
41
+
42
+ const clientSession = {
43
+ sqliteDb,
44
+ devtools: { enabled: false },
45
+ clientId: 'in-memory',
46
+ sessionId: nanoid(6),
47
+ leaderThread: {
48
+ mutations: {
49
+ pull: Stream.never,
50
+ push: () => Effect.void,
51
+ },
52
+ initialState: { leaderHead: EventId.ROOT, migrationsReport },
53
+ export: Effect.dieMessage('Not implemented'),
54
+ getMutationLogData: Effect.succeed(new Uint8Array()),
55
+ getSyncState: Effect.dieMessage('Not implemented'),
56
+ networkStatus: SubscriptionRef.make({ isConnected: false, timestampMs: Date.now(), latchClosed: false }).pipe(
57
+ Effect.runSync,
58
+ ),
59
+ sendDevtoolsMessage: () => Effect.dieMessage('Not implemented'),
60
+ },
61
+ lockStatus,
62
+ shutdown: () => Effect.dieMessage('TODO implement shutdown'),
63
+ } satisfies ClientSession
64
+
65
+ return clientSession
66
+ }).pipe(UnexpectedError.mapToUnexpectedError)
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { makeAdapter, type WebAdapterOptions } from './web-worker/client-session/index.js'
2
+ export { makeInMemoryAdapter } from './in-memory/index.js'
3
+ export * as WorkerSchema from './web-worker/common/worker-schema.js'
@@ -0,0 +1,61 @@
1
+ // NOTE we're already firing off this promise call here since we'll need it anyway and need it cached
2
+
3
+ import { prettyBytes } from '@livestore/utils'
4
+
5
+ // To improve LiveStore compatibility with e.g. Node.js we're guarding for `navigator` / `navigator.storage` to be defined.
6
+ export const rootHandlePromise =
7
+ typeof navigator === 'undefined' || navigator.storage === undefined
8
+ ? // We're using a proxy here to make the promise reject lazy
9
+ (new Proxy(
10
+ {},
11
+ {
12
+ get: () =>
13
+ Promise.reject(
14
+ new Error(`Can't get OPFS root handle in this environment as navigator.storage is undefined`),
15
+ ),
16
+ },
17
+ ) as never)
18
+ : navigator.storage.getDirectory()
19
+
20
+ export const getDirHandle = async (absDirPath: string | undefined) => {
21
+ const rootHandle = await rootHandlePromise
22
+ if (absDirPath === undefined) return rootHandle
23
+
24
+ let dirHandle = rootHandle
25
+ const directoryStack = absDirPath?.split('/').filter(Boolean)
26
+ while (directoryStack.length > 0) {
27
+ dirHandle = await dirHandle.getDirectoryHandle(directoryStack.shift()!)
28
+ }
29
+
30
+ return dirHandle
31
+ }
32
+
33
+ export const printTree = async (
34
+ directoryHandle_: FileSystemDirectoryHandle | Promise<FileSystemDirectoryHandle> = rootHandlePromise,
35
+ depth: number = Number.POSITIVE_INFINITY,
36
+ prefix: string = '',
37
+ ): Promise<void> => {
38
+ if (depth < 0) return
39
+
40
+ const directoryHandle = await directoryHandle_
41
+ const entries = directoryHandle.values()
42
+
43
+ for await (const entry of entries) {
44
+ const isDirectory = entry.kind === 'directory'
45
+ const size = entry.kind === 'file' ? await entry.getFile().then((file) => prettyBytes(file.size)) : undefined
46
+ console.log(`${prefix}${isDirectory ? '📁' : '📄'} ${entry.name} ${size ? `(${size})` : ''}`)
47
+
48
+ if (isDirectory) {
49
+ const nestedDirectoryHandle = await directoryHandle.getDirectoryHandle(entry.name)
50
+ await printTree(nestedDirectoryHandle, depth - 1, `${prefix} `)
51
+ }
52
+ }
53
+ }
54
+
55
+ export const deleteAll = async (directoryHandle: FileSystemDirectoryHandle) => {
56
+ if (directoryHandle.kind !== 'directory') return
57
+
58
+ for await (const entryName of directoryHandle.keys()) {
59
+ await directoryHandle.removeEntry(entryName, { recursive: true })
60
+ }
61
+ }
@@ -0,0 +1,37 @@
1
+ // TODO remove OPFS stuff
2
+ // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle
3
+ interface FileSystemSyncAccessHandle {
4
+ close: () => void
5
+ flush: () => Promise<void>
6
+ getSize: () => number
7
+ read: (buffer: ArrayBuffer, options?: FileSystemReadWriteOptions) => number
8
+ truncate: (newSize: number) => void
9
+ write: (buffer: ArrayBuffer, options?: FileSystemReadWriteOptions) => number
10
+ seek: (offset: number) => void
11
+ }
12
+
13
+ interface FileSystemReadWriteOptions {
14
+ at?: number
15
+ }
16
+
17
+ interface FileSystemFileHandle {
18
+ createSyncAccessHandle: () => Promise<FileSystemSyncAccessHandle>
19
+ }
20
+
21
+ // TODO bring back when Vite limitation is resolved https://github.com/vitejs/vite/issues/8427
22
+ // declare module '*?sharedworker' {
23
+ // const sharedWorkerConstructor: {
24
+ // new (options?: { name?: string }): SharedWorker
25
+ // }
26
+ // export default sharedWorkerConstructor
27
+ // }
28
+
29
+ interface ImportMeta {
30
+ env: {
31
+ DEV: boolean | undefined
32
+ VITE_LIVESTORE_EXPERIMENTAL_SYNC_NEXT: boolean | undefined
33
+ }
34
+ }
35
+
36
+ // eslint-disable-next-line no-var
37
+ declare var __debugLiveStoreUtils: any
@@ -0,0 +1,167 @@
1
+ import type { ClientSession, UnexpectedError } from '@livestore/common'
2
+ import { Devtools } from '@livestore/common'
3
+ import { ShutdownChannel } from '@livestore/common/leader-thread'
4
+ import { isDevEnv } from '@livestore/utils'
5
+ import type { Scope } from '@livestore/utils/effect'
6
+ import { Effect, FiberHandle, Runtime, Schema, Stream, WebChannel } from '@livestore/utils/effect'
7
+ import { nanoid } from '@livestore/utils/nanoid'
8
+
9
+ import { makeShutdownChannel } from '../common/shutdown-channel.js'
10
+
11
+ export const bootDevtools = ({
12
+ clientSession,
13
+ storeId,
14
+ // waitForDevtoolsWebBridgePort,
15
+ // connectToDevtools,
16
+ }: {
17
+ clientSession: ClientSession
18
+ storeId: string
19
+ // waitForDevtoolsWebBridgePort: (_: { webBridgeId: string }) => Effect.Effect<MessagePort, UnexpectedError>
20
+ // connectToDevtools: (coordinatorMessagePort: MessagePort) => Effect.Effect<void, UnexpectedError, Scope.Scope>
21
+ }) =>
22
+ Effect.gen(function* () {
23
+ // const webBridgeFiberHandle = yield* FiberHandle.make()
24
+
25
+ // // NOTE we're not using the existing coordinator `shutdownChannel` as we won't be able to listen to messages emitted by the same coordinator
26
+ // const shutdownChannel = yield* makeShutdownChannel(storeId)
27
+
28
+ // const connectWebBridge = FiberHandle.run(
29
+ // webBridgeFiberHandle,
30
+ // listenToWebBridge({ coordinator, waitForDevtoolsWebBridgePort, connectToDevtools, storeId }),
31
+ // )
32
+
33
+ // yield* connectWebBridge
34
+
35
+ // // TODO Given we're listening to our own messages and given the leader will emit an initial
36
+ // // `DedicatedWorkerDisconnectBroadcast`, this will re-run and we should avoid it
37
+ // yield* shutdownChannel.listen.pipe(
38
+ // Stream.flatten(),
39
+ // Stream.filter(Schema.is(ShutdownChannel.DedicatedWorkerDisconnectBroadcast)),
40
+ // Stream.tap(() => connectWebBridge),
41
+ // Stream.runDrain,
42
+ // Effect.ignoreLogged,
43
+ // Effect.forkScoped,
44
+ // )
45
+
46
+ // yield* listenToBrowserExtensionBridge({ coordinator, connectToDevtools })
47
+
48
+ if (isDevEnv()) {
49
+ const searchParams = new URLSearchParams()
50
+ searchParams.set('clientId', clientSession.clientId)
51
+ searchParams.set('sessionId', clientSession.sessionId)
52
+ searchParams.set('storeId', storeId)
53
+ const url = `${location.origin}/_livestore?${searchParams.toString()}`
54
+
55
+ // Check whether devtools are available and then log the URL
56
+ const response = yield* Effect.promise(() => fetch(url))
57
+ if (response.ok) {
58
+ const text = yield* Effect.promise(() => response.text())
59
+ if (text.includes('<meta name="livestore-devtools" content="true" />')) {
60
+ // NOTE the trailing `&` is intentional to avoid Chrome opening the URL in the sources pane
61
+ // as the browser already fetched it
62
+ yield* Effect.log(`[@livestore/adapter-web] Devtools ready on ${url}&`)
63
+ }
64
+ }
65
+ }
66
+ }).pipe(Effect.withSpan('@livestore/adapter-web:coordinator:devtools:boot'))
67
+
68
+ const listenToWebBridge = ({
69
+ clientSession,
70
+ storeId,
71
+ waitForDevtoolsWebBridgePort,
72
+ connectToDevtools,
73
+ }: {
74
+ clientSession: ClientSession
75
+ storeId: string
76
+ waitForDevtoolsWebBridgePort: (_: { webBridgeId: string }) => Effect.Effect<MessagePort, UnexpectedError>
77
+ connectToDevtools: (coordinatorMessagePort: MessagePort) => Effect.Effect<void, UnexpectedError, Scope.Scope>
78
+ }) =>
79
+ Effect.gen(function* () {
80
+ // const appHostId = clientSession.devtools.appHostId
81
+ const webBridgeBroadcastChannel = yield* Devtools.WebBridge.makeBroadcastChannel()
82
+
83
+ // const isLeader = yield* clientSession.lockStatus.get.pipe(Effect.map((_) => _ === 'has-lock'))
84
+ // yield* webBridgeBroadcastChannel.send(Devtools.WebBridge.AppHostReady.make({ appHostId, isLeader }))
85
+
86
+ const runtime = yield* Effect.runtime()
87
+
88
+ // window.addEventListener('beforeunload', () =>
89
+ // webBridgeBroadcastChannel
90
+ // .send(Devtools.WebBridge.AppHostWillDisconnect.make({ appHostId }))
91
+ // .pipe(Runtime.runFork(runtime)),
92
+ // )
93
+
94
+ // yield* Effect.addFinalizer(() =>
95
+ // webBridgeBroadcastChannel
96
+ // .send(Devtools.WebBridge.AppHostWillDisconnect.make({ appHostId }))
97
+ // .pipe(Effect.ignoreLogged),
98
+ // )
99
+
100
+ // yield* webBridgeBroadcastChannel.listen.pipe(
101
+ // Stream.flatten(),
102
+ // Stream.filter(Schema.is(Devtools.WebBridge.DevtoolsReady)),
103
+ // Stream.tap(({ devtoolsId }) =>
104
+ // Effect.gen(function* () {
105
+ // const webBridgeId = nanoid()
106
+ // yield* waitForDevtoolsWebBridgePort({ webBridgeId }).pipe(
107
+ // Effect.andThen(connectToDevtools),
108
+ // Effect.tapCauseLogPretty,
109
+ // Effect.forkScoped,
110
+ // )
111
+
112
+ // const isLeader = yield* clientSession.lockStatus.get.pipe(Effect.map((_) => _ === 'has-lock'))
113
+ // yield* webBridgeBroadcastChannel.send(
114
+ // Devtools.WebBridge.ConnectToDevtools.make({ appHostId, isLeader, devtoolsId, webBridgeId, storeId }),
115
+ // )
116
+ // }),
117
+ // ),
118
+ // Stream.runDrain,
119
+ // Effect.ignoreLogged,
120
+ // Effect.forkScoped,
121
+ // )
122
+
123
+ yield* Effect.never
124
+ }).pipe(Effect.scoped)
125
+
126
+ const listenToBrowserExtensionBridge = ({
127
+ appHostId,
128
+ connectToDevtools,
129
+ }: {
130
+ appHostId: string
131
+ connectToDevtools: (coordinatorMessagePort: MessagePort) => Effect.Effect<void, UnexpectedError, Scope.Scope>
132
+ }) =>
133
+ Effect.gen(function* () {
134
+ const windowChannel = yield* WebChannel.windowChannel({
135
+ window,
136
+ schema: {
137
+ listen: Devtools.DevtoolsWindowMessage.MessageForStore,
138
+ send: Devtools.DevtoolsWindowMessage.MessageForContentscript,
139
+ },
140
+ })
141
+
142
+ yield* windowChannel.send(Devtools.DevtoolsWindowMessage.LoadIframe.make({}))
143
+
144
+ yield* windowChannel.listen.pipe(
145
+ Stream.flatten(),
146
+ Stream.tap((message) =>
147
+ Effect.gen(function* () {
148
+ if (message._tag === 'LSD.WindowMessage.ContentscriptListening') {
149
+ // Send message to contentscript via window (which the contentscript iframe is listening to)
150
+ yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ appHostId }))
151
+ return
152
+ }
153
+
154
+ if (message.appHostId !== appHostId) return
155
+
156
+ if (message._tag === 'LSD.WindowMessage.MessagePortReady') {
157
+ yield* connectToDevtools(message.port)
158
+ }
159
+ }).pipe(Effect.ignoreLogged, Effect.forkScoped),
160
+ ),
161
+ Stream.runDrain,
162
+ Effect.ignoreLogged,
163
+ Effect.forkScoped,
164
+ )
165
+
166
+ yield* windowChannel.send(Devtools.DevtoolsWindowMessage.StoreReady.make({ appHostId }))
167
+ })