@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.
- package/.eslintrc.cjs +6 -0
- package/README.md +12 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/common/connection.d.ts +7 -0
- package/dist/common/connection.d.ts.map +1 -0
- package/dist/common/connection.js +25 -0
- package/dist/common/connection.js.map +1 -0
- package/dist/devtools-bridge/background-browser-channel.d.ts +9 -0
- package/dist/devtools-bridge/background-browser-channel.d.ts.map +1 -0
- package/dist/devtools-bridge/background-browser-channel.js +31 -0
- package/dist/devtools-bridge/background-browser-channel.js.map +1 -0
- package/dist/devtools-bridge/background-message.d.ts +75 -0
- package/dist/devtools-bridge/background-message.d.ts.map +1 -0
- package/dist/devtools-bridge/background-message.js +53 -0
- package/dist/devtools-bridge/background-message.js.map +1 -0
- package/dist/devtools-bridge/bridge-shared.d.ts +14 -0
- package/dist/devtools-bridge/bridge-shared.d.ts.map +1 -0
- package/dist/devtools-bridge/bridge-shared.js +67 -0
- package/dist/devtools-bridge/bridge-shared.js.map +1 -0
- package/dist/devtools-bridge/browser-extension-bridge.d.ts +3 -0
- package/dist/devtools-bridge/browser-extension-bridge.d.ts.map +1 -0
- package/dist/devtools-bridge/browser-extension-bridge.js +59 -0
- package/dist/devtools-bridge/browser-extension-bridge.js.map +1 -0
- package/dist/devtools-bridge/iframe-message.d.ts +16 -0
- package/dist/devtools-bridge/iframe-message.d.ts.map +1 -0
- package/dist/devtools-bridge/iframe-message.js +11 -0
- package/dist/devtools-bridge/iframe-message.js.map +1 -0
- package/dist/devtools-bridge/index.d.ts +6 -0
- package/dist/devtools-bridge/index.d.ts.map +1 -0
- package/dist/devtools-bridge/index.js +5 -0
- package/dist/devtools-bridge/index.js.map +1 -0
- package/dist/devtools-bridge/web-bridge.d.ts +31 -0
- package/dist/devtools-bridge/web-bridge.d.ts.map +1 -0
- package/dist/devtools-bridge/web-bridge.js +131 -0
- package/dist/devtools-bridge/web-bridge.js.map +1 -0
- package/dist/in-memory/index.d.ts +4 -0
- package/dist/in-memory/index.d.ts.map +1 -0
- package/dist/in-memory/index.js +50 -0
- package/dist/in-memory/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/opfs-utils.d.ts +5 -0
- package/dist/opfs-utils.d.ts.map +1 -0
- package/dist/opfs-utils.js +43 -0
- package/dist/opfs-utils.js.map +1 -0
- package/dist/web-worker/client-session/client-session-devtools.d.ts +7 -0
- package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -0
- package/dist/web-worker/client-session/client-session-devtools.js +107 -0
- package/dist/web-worker/client-session/client-session-devtools.js.map +1 -0
- package/dist/web-worker/client-session/index.d.ts +41 -0
- package/dist/web-worker/client-session/index.d.ts.map +1 -0
- package/dist/web-worker/client-session/index.js +299 -0
- package/dist/web-worker/client-session/index.js.map +1 -0
- package/dist/web-worker/client-session/trim-batch.d.ts +4 -0
- package/dist/web-worker/client-session/trim-batch.d.ts.map +1 -0
- package/dist/web-worker/client-session/trim-batch.js +13 -0
- package/dist/web-worker/client-session/trim-batch.js.map +1 -0
- package/dist/web-worker/client-session/trim-batch.test.d.ts +2 -0
- package/dist/web-worker/client-session/trim-batch.test.d.ts.map +1 -0
- package/dist/web-worker/client-session/trim-batch.test.js +38 -0
- package/dist/web-worker/client-session/trim-batch.test.js.map +1 -0
- package/dist/web-worker/common/persisted-sqlite.d.ts +23 -0
- package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -0
- package/dist/web-worker/common/persisted-sqlite.js +92 -0
- package/dist/web-worker/common/persisted-sqlite.js.map +1 -0
- package/dist/web-worker/common/shutdown-channel.d.ts +7 -0
- package/dist/web-worker/common/shutdown-channel.d.ts.map +1 -0
- package/dist/web-worker/common/shutdown-channel.js +7 -0
- package/dist/web-worker/common/shutdown-channel.js.map +1 -0
- package/dist/web-worker/common/worker-schema.d.ts +226 -0
- package/dist/web-worker/common/worker-schema.d.ts.map +1 -0
- package/dist/web-worker/common/worker-schema.js +176 -0
- package/dist/web-worker/common/worker-schema.js.map +1 -0
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts +15 -0
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -0
- package/dist/web-worker/leader-worker/make-leader-worker.js +144 -0
- package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -0
- package/dist/web-worker/shared-worker/make-shared-worker.d.ts +2 -0
- package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -0
- package/dist/web-worker/shared-worker/make-shared-worker.js +160 -0
- package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -0
- package/dist/web-worker/vite-dev-polyfill.d.ts +2 -0
- package/dist/web-worker/vite-dev-polyfill.d.ts.map +1 -0
- package/dist/web-worker/vite-dev-polyfill.js +37 -0
- package/dist/web-worker/vite-dev-polyfill.js.map +1 -0
- package/package.json +78 -0
- package/src/common/connection.ts +32 -0
- package/src/devtools-bridge/background-browser-channel.ts +57 -0
- package/src/devtools-bridge/background-message.ts +42 -0
- package/src/devtools-bridge/bridge-shared.ts +97 -0
- package/src/devtools-bridge/browser-extension-bridge.ts +64 -0
- package/src/devtools-bridge/iframe-message.ts +9 -0
- package/src/devtools-bridge/index.ts +9 -0
- package/src/devtools-bridge/web-bridge.ts +169 -0
- package/src/in-memory/index.ts +66 -0
- package/src/index.ts +3 -0
- package/src/opfs-utils.ts +61 -0
- package/src/web-worker/ambient.d.ts +37 -0
- package/src/web-worker/client-session/client-session-devtools.ts +167 -0
- package/src/web-worker/client-session/index.ts +537 -0
- package/src/web-worker/client-session/trim-batch.test.ts +48 -0
- package/src/web-worker/client-session/trim-batch.ts +15 -0
- package/src/web-worker/common/persisted-sqlite.ts +136 -0
- package/src/web-worker/common/shutdown-channel.ts +8 -0
- package/src/web-worker/common/worker-schema.ts +206 -0
- package/src/web-worker/leader-worker/make-leader-worker.ts +276 -0
- package/src/web-worker/shared-worker/make-shared-worker.ts +300 -0
- package/src/web-worker/vite-dev-polyfill.ts +36 -0
- 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,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
|
+
})
|