@livestore/adapter-web 0.4.0-dev.2 → 0.4.0-dev.20
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/dist/.tsbuildinfo +1 -1
- package/dist/in-memory/in-memory-adapter.d.ts +15 -5
- package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
- package/dist/in-memory/in-memory-adapter.js +29 -15
- package/dist/in-memory/in-memory-adapter.js.map +1 -1
- package/dist/web-worker/client-session/client-session-devtools.d.ts +1 -1
- package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -1
- package/dist/web-worker/client-session/client-session-devtools.js +14 -3
- package/dist/web-worker/client-session/client-session-devtools.js.map +1 -1
- package/dist/web-worker/client-session/persisted-adapter.d.ts +15 -0
- package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
- package/dist/web-worker/client-session/persisted-adapter.js +67 -46
- package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
- package/dist/web-worker/client-session/sqlite-loader.d.ts +2 -0
- package/dist/web-worker/client-session/sqlite-loader.d.ts.map +1 -0
- package/dist/web-worker/client-session/sqlite-loader.js +16 -0
- package/dist/web-worker/client-session/sqlite-loader.js.map +1 -0
- package/dist/web-worker/common/persisted-sqlite.d.ts +23 -7
- package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
- package/dist/web-worker/common/persisted-sqlite.js +114 -76
- package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
- package/dist/web-worker/common/shutdown-channel.d.ts +3 -2
- package/dist/web-worker/common/shutdown-channel.d.ts.map +1 -1
- package/dist/web-worker/common/shutdown-channel.js +2 -2
- package/dist/web-worker/common/shutdown-channel.js.map +1 -1
- package/dist/web-worker/common/worker-disconnect-channel.d.ts +2 -6
- package/dist/web-worker/common/worker-disconnect-channel.d.ts.map +1 -1
- package/dist/web-worker/common/worker-disconnect-channel.js +3 -2
- package/dist/web-worker/common/worker-disconnect-channel.js.map +1 -1
- package/dist/web-worker/common/worker-schema.d.ts +103 -58
- package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
- package/dist/web-worker/common/worker-schema.js +48 -36
- package/dist/web-worker/common/worker-schema.js.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts +4 -2
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.js +47 -21
- package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
- package/dist/web-worker/shared-worker/make-shared-worker.d.ts +2 -1
- package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -1
- package/dist/web-worker/shared-worker/make-shared-worker.js +65 -49
- package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
- package/dist/web-worker/vite-dev-polyfill.js +1 -0
- package/dist/web-worker/vite-dev-polyfill.js.map +1 -1
- package/package.json +8 -9
- package/src/in-memory/in-memory-adapter.ts +36 -20
- package/src/web-worker/ambient.d.ts +7 -24
- package/src/web-worker/client-session/client-session-devtools.ts +18 -3
- package/src/web-worker/client-session/persisted-adapter.ts +112 -59
- package/src/web-worker/client-session/sqlite-loader.ts +19 -0
- package/src/web-worker/common/persisted-sqlite.ts +219 -113
- package/src/web-worker/common/shutdown-channel.ts +10 -3
- package/src/web-worker/common/worker-disconnect-channel.ts +10 -3
- package/src/web-worker/common/worker-schema.ts +62 -35
- package/src/web-worker/leader-worker/make-leader-worker.ts +58 -33
- package/src/web-worker/shared-worker/make-shared-worker.ts +95 -75
- package/src/web-worker/vite-dev-polyfill.ts +1 -0
- package/dist/opfs-utils.d.ts +0 -5
- package/dist/opfs-utils.d.ts.map +0 -1
- package/dist/opfs-utils.js +0 -43
- package/dist/opfs-utils.js.map +0 -1
- package/src/opfs-utils.ts +0 -61
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SqliteDb, SyncOptions } from '@livestore/common'
|
|
2
|
-
import { Devtools,
|
|
2
|
+
import { Devtools, LogConfig, UnknownError } from '@livestore/common'
|
|
3
3
|
import type { DevtoolsOptions } from '@livestore/common/leader-thread'
|
|
4
4
|
import { configureConnection, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
|
|
5
5
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
@@ -10,37 +10,36 @@ import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
|
10
10
|
import { isDevEnv, LS_DEV } from '@livestore/utils'
|
|
11
11
|
import type { HttpClient, Scope, WorkerError } from '@livestore/utils/effect'
|
|
12
12
|
import {
|
|
13
|
-
BrowserWorkerRunner,
|
|
14
13
|
Effect,
|
|
15
14
|
FetchHttpClient,
|
|
16
15
|
identity,
|
|
17
16
|
Layer,
|
|
18
|
-
Logger,
|
|
19
|
-
LogLevel,
|
|
20
17
|
OtelTracer,
|
|
21
18
|
Scheduler,
|
|
19
|
+
type Schema,
|
|
22
20
|
Stream,
|
|
23
21
|
TaskTracing,
|
|
24
22
|
WorkerRunner,
|
|
25
23
|
} from '@livestore/utils/effect'
|
|
24
|
+
import { BrowserWorkerRunner, Opfs } from '@livestore/utils/effect/browser'
|
|
26
25
|
import type * as otel from '@opentelemetry/api'
|
|
27
26
|
|
|
28
|
-
import
|
|
29
|
-
import { getStateDbFileName, sanitizeOpfsDir } from '../common/persisted-sqlite.ts'
|
|
27
|
+
import { cleanupOldStateDbFiles, getStateDbFileName, sanitizeOpfsDir } from '../common/persisted-sqlite.ts'
|
|
30
28
|
import { makeShutdownChannel } from '../common/shutdown-channel.ts'
|
|
31
29
|
import * as WorkerSchema from '../common/worker-schema.ts'
|
|
32
30
|
|
|
33
31
|
export type WorkerOptions = {
|
|
34
32
|
schema: LiveStoreSchema
|
|
35
33
|
sync?: SyncOptions
|
|
34
|
+
syncPayloadSchema?: Schema.Schema<any>
|
|
36
35
|
otelOptions?: {
|
|
37
36
|
tracer?: otel.Tracer
|
|
38
37
|
}
|
|
39
|
-
}
|
|
38
|
+
} & LogConfig.WithLoggerOptions
|
|
40
39
|
|
|
41
40
|
if (isDevEnv()) {
|
|
42
41
|
globalThis.__debugLiveStoreUtils = {
|
|
43
|
-
opfs:
|
|
42
|
+
opfs: Opfs.debugUtils,
|
|
44
43
|
blobUrl: (buffer: Uint8Array<ArrayBuffer>) =>
|
|
45
44
|
URL.createObjectURL(new Blob([buffer], { type: 'application/octet-stream' })),
|
|
46
45
|
runSync: (effect: Effect.Effect<any, any, never>) => Effect.runSync(effect),
|
|
@@ -59,23 +58,23 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
|
|
|
59
58
|
)
|
|
60
59
|
: undefined
|
|
61
60
|
|
|
61
|
+
const runtimeLayer = Layer.mergeAll(FetchHttpClient.layer, TracingLive ?? Layer.empty)
|
|
62
|
+
|
|
62
63
|
return makeWorkerRunnerOuter(options).pipe(
|
|
63
64
|
Layer.provide(BrowserWorkerRunner.layer),
|
|
64
65
|
WorkerRunner.launch,
|
|
65
66
|
Effect.scoped,
|
|
66
67
|
Effect.tapCauseLogPretty,
|
|
67
68
|
Effect.annotateLogs({ thread: self.name }),
|
|
68
|
-
Effect.provide(
|
|
69
|
-
Effect.provide(FetchHttpClient.layer),
|
|
69
|
+
Effect.provide(runtimeLayer),
|
|
70
70
|
LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
|
|
71
|
-
TracingLive ? Effect.provide(TracingLive) : identity,
|
|
72
71
|
// We're using this custom scheduler to improve op batching behaviour and reduce the overhead
|
|
73
72
|
// of the Effect fiber runtime given we have different tradeoffs on a worker thread.
|
|
74
73
|
// Despite the "message channel" name, is has nothing to do with the `incomingRequestsPort` above.
|
|
75
74
|
Effect.withScheduler(Scheduler.messageChannel()),
|
|
76
75
|
// We're increasing the Effect ops limit here to allow for larger chunks of operations at a time
|
|
77
76
|
Effect.withMaxOpsBeforeYield(4096),
|
|
78
|
-
|
|
77
|
+
LogConfig.withLoggerConfig({ logger: options.logger, logLevel: options.logLevel }, { threadName: self.name }),
|
|
79
78
|
)
|
|
80
79
|
}
|
|
81
80
|
|
|
@@ -93,7 +92,12 @@ const makeWorkerRunnerOuter = (
|
|
|
93
92
|
Effect.withSpan('@livestore/adapter-web:worker:wrapper:InitialMessage:innerFiber'),
|
|
94
93
|
Effect.tapCauseLogPretty,
|
|
95
94
|
Effect.provide(
|
|
96
|
-
|
|
95
|
+
Layer.mergeAll(
|
|
96
|
+
Opfs.Opfs.Default,
|
|
97
|
+
WebmeshWorker.CacheService.layer({
|
|
98
|
+
nodeName: Devtools.makeNodeName.client.leader({ storeId, clientId }),
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
97
101
|
),
|
|
98
102
|
Effect.forkScoped,
|
|
99
103
|
)
|
|
@@ -102,18 +106,19 @@ const makeWorkerRunnerOuter = (
|
|
|
102
106
|
}).pipe(Effect.withSpan('@livestore/adapter-web:worker:wrapper:InitialMessage'), Layer.unwrapScoped),
|
|
103
107
|
})
|
|
104
108
|
|
|
105
|
-
const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
|
|
109
|
+
const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }: WorkerOptions) =>
|
|
106
110
|
WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInnerRequest, {
|
|
107
|
-
InitialMessage: ({ storageOptions, storeId, clientId, devtoolsEnabled, debugInstanceId,
|
|
111
|
+
InitialMessage: ({ storageOptions, storeId, clientId, devtoolsEnabled, debugInstanceId, syncPayloadEncoded }) =>
|
|
108
112
|
Effect.gen(function* () {
|
|
109
113
|
const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
|
|
110
114
|
const makeSqliteDb = sqliteDbFactory({ sqlite3 })
|
|
115
|
+
const opfsDirectory = yield* sanitizeOpfsDir(storageOptions.directory, storeId)
|
|
111
116
|
const runtime = yield* Effect.runtime<never>()
|
|
112
117
|
|
|
113
118
|
const makeDb = (kind: 'state' | 'eventlog') =>
|
|
114
119
|
makeSqliteDb({
|
|
115
120
|
_tag: 'opfs',
|
|
116
|
-
opfsDirectory
|
|
121
|
+
opfsDirectory,
|
|
117
122
|
fileName: kind === 'state' ? getStateDbFileName(schema) : 'eventlog.db',
|
|
118
123
|
configureDb: (db) =>
|
|
119
124
|
configureConnection(db, {
|
|
@@ -131,6 +136,16 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
|
|
|
131
136
|
concurrency: 2,
|
|
132
137
|
})
|
|
133
138
|
|
|
139
|
+
// Clean up old state database files after successful database creation
|
|
140
|
+
// This prevents OPFS file pool capacity exhaustion from accumulated state db files after schema changes/migrations
|
|
141
|
+
if (dbState.metadata._tag === 'opfs') {
|
|
142
|
+
yield* cleanupOldStateDbFiles({
|
|
143
|
+
vfs: dbState.metadata.vfs,
|
|
144
|
+
currentSchema: schema,
|
|
145
|
+
opfsDirectory: dbState.metadata.persistenceInfo.opfsDirectory,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
134
149
|
const devtoolsOptions = yield* makeDevtoolsOptions({ devtoolsEnabled, dbState, dbEventlog })
|
|
135
150
|
const shutdownChannel = yield* makeShutdownChannel(storeId)
|
|
136
151
|
|
|
@@ -144,11 +159,12 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
|
|
|
144
159
|
dbEventlog,
|
|
145
160
|
devtoolsOptions,
|
|
146
161
|
shutdownChannel,
|
|
147
|
-
|
|
162
|
+
syncPayloadEncoded,
|
|
163
|
+
syncPayloadSchema,
|
|
148
164
|
})
|
|
149
165
|
}).pipe(
|
|
150
166
|
Effect.tapCauseLogPretty,
|
|
151
|
-
|
|
167
|
+
UnknownError.mapToUnknownError,
|
|
152
168
|
Effect.withPerformanceMeasure('@livestore/adapter-web:worker:InitialMessage'),
|
|
153
169
|
Effect.withSpan('@livestore/adapter-web:worker:InitialMessage'),
|
|
154
170
|
Effect.annotateSpans({ debugInstanceId }),
|
|
@@ -166,10 +182,7 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
|
|
|
166
182
|
|
|
167
183
|
const snapshot = workerCtx.dbState.export()
|
|
168
184
|
return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
|
|
169
|
-
}).pipe(
|
|
170
|
-
UnexpectedError.mapToUnexpectedError,
|
|
171
|
-
Effect.withSpan('@livestore/adapter-web:worker:GetRecreateSnapshot'),
|
|
172
|
-
),
|
|
185
|
+
}).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetRecreateSnapshot')),
|
|
173
186
|
PullStream: ({ cursor }) =>
|
|
174
187
|
Effect.gen(function* () {
|
|
175
188
|
const { syncProcessor } = yield* LeaderThreadCtx
|
|
@@ -182,19 +195,19 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
|
|
|
182
195
|
PushToLeader: ({ batch }) =>
|
|
183
196
|
Effect.andThen(LeaderThreadCtx, ({ syncProcessor }) =>
|
|
184
197
|
syncProcessor.push(
|
|
185
|
-
batch.map((event) => new LiveStoreEvent.EncodedWithMeta(event)),
|
|
198
|
+
batch.map((event) => new LiveStoreEvent.Client.EncodedWithMeta(event)),
|
|
186
199
|
// We'll wait in order to keep back pressure on the client session
|
|
187
200
|
{ waitForProcessing: true },
|
|
188
201
|
),
|
|
189
202
|
).pipe(Effect.uninterruptible, Effect.withSpan('@livestore/adapter-web:worker:PushToLeader')),
|
|
190
203
|
Export: () =>
|
|
191
204
|
Effect.andThen(LeaderThreadCtx, (_) => _.dbState.export()).pipe(
|
|
192
|
-
|
|
205
|
+
UnknownError.mapToUnknownError,
|
|
193
206
|
Effect.withSpan('@livestore/adapter-web:worker:Export'),
|
|
194
207
|
),
|
|
195
208
|
ExportEventlog: () =>
|
|
196
209
|
Effect.andThen(LeaderThreadCtx, (_) => _.dbEventlog.export()).pipe(
|
|
197
|
-
|
|
210
|
+
UnknownError.mapToUnknownError,
|
|
198
211
|
Effect.withSpan('@livestore/adapter-web:worker:ExportEventlog'),
|
|
199
212
|
),
|
|
200
213
|
BootStatusStream: () =>
|
|
@@ -203,15 +216,27 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
|
|
|
203
216
|
Effect.gen(function* () {
|
|
204
217
|
const workerCtx = yield* LeaderThreadCtx
|
|
205
218
|
return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
|
|
206
|
-
}).pipe(
|
|
219
|
+
}).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderHead')),
|
|
207
220
|
GetLeaderSyncState: () =>
|
|
208
221
|
Effect.gen(function* () {
|
|
209
222
|
const workerCtx = yield* LeaderThreadCtx
|
|
210
223
|
return yield* workerCtx.syncProcessor.syncState
|
|
211
|
-
}).pipe(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
224
|
+
}).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderSyncState')),
|
|
225
|
+
SyncStateStream: () =>
|
|
226
|
+
Effect.gen(function* () {
|
|
227
|
+
const workerCtx = yield* LeaderThreadCtx
|
|
228
|
+
return workerCtx.syncProcessor.syncState.changes
|
|
229
|
+
}).pipe(Stream.unwrapScoped),
|
|
230
|
+
GetNetworkStatus: () =>
|
|
231
|
+
Effect.gen(function* () {
|
|
232
|
+
const workerCtx = yield* LeaderThreadCtx
|
|
233
|
+
return yield* workerCtx.networkStatus
|
|
234
|
+
}).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetNetworkStatus')),
|
|
235
|
+
NetworkStatusStream: () =>
|
|
236
|
+
Effect.gen(function* () {
|
|
237
|
+
const workerCtx = yield* LeaderThreadCtx
|
|
238
|
+
return workerCtx.networkStatus.changes
|
|
239
|
+
}).pipe(Stream.unwrapScoped),
|
|
215
240
|
Shutdown: () =>
|
|
216
241
|
Effect.gen(function* () {
|
|
217
242
|
yield* Effect.logDebug('[@livestore/adapter-web:worker] Shutdown')
|
|
@@ -219,10 +244,10 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
|
|
|
219
244
|
// Buy some time for Otel to flush
|
|
220
245
|
// TODO find a cleaner way to do this
|
|
221
246
|
yield* Effect.sleep(300)
|
|
222
|
-
}).pipe(
|
|
247
|
+
}).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:Shutdown')),
|
|
223
248
|
ExtraDevtoolsMessage: ({ message }) =>
|
|
224
249
|
Effect.andThen(LeaderThreadCtx, (_) => _.extraIncomingMessagesQueue.offer(message)).pipe(
|
|
225
|
-
|
|
250
|
+
UnknownError.mapToUnknownError,
|
|
226
251
|
Effect.withSpan('@livestore/adapter-web:worker:ExtraDevtoolsMessage'),
|
|
227
252
|
),
|
|
228
253
|
'DevtoolsWebCommon.CreateConnection': WebmeshWorker.CreateConnection,
|
|
@@ -236,7 +261,7 @@ const makeDevtoolsOptions = ({
|
|
|
236
261
|
devtoolsEnabled: boolean
|
|
237
262
|
dbState: SqliteDb
|
|
238
263
|
dbEventlog: SqliteDb
|
|
239
|
-
}): Effect.Effect<DevtoolsOptions,
|
|
264
|
+
}): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope | WebmeshWorker.CacheService> =>
|
|
240
265
|
Effect.gen(function* () {
|
|
241
266
|
if (devtoolsEnabled === false) {
|
|
242
267
|
return { enabled: false }
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import { Devtools,
|
|
1
|
+
import { Devtools, LogConfig, liveStoreVersion, UnknownError } from '@livestore/common'
|
|
2
2
|
import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
|
|
3
3
|
import * as WebmeshWorker from '@livestore/devtools-web-common/worker'
|
|
4
4
|
import { isDevEnv, isNotUndefined, LS_DEV } from '@livestore/utils'
|
|
5
5
|
import {
|
|
6
|
-
BrowserWorker,
|
|
7
|
-
BrowserWorkerRunner,
|
|
8
6
|
Deferred,
|
|
9
7
|
Effect,
|
|
10
8
|
Exit,
|
|
11
9
|
FetchHttpClient,
|
|
12
10
|
identity,
|
|
13
11
|
Layer,
|
|
14
|
-
Logger,
|
|
15
|
-
LogLevel,
|
|
16
12
|
ParseResult,
|
|
17
13
|
Ref,
|
|
18
14
|
Schema,
|
|
@@ -24,10 +20,25 @@ import {
|
|
|
24
20
|
WorkerError,
|
|
25
21
|
WorkerRunner,
|
|
26
22
|
} from '@livestore/utils/effect'
|
|
23
|
+
import { BrowserWorker, BrowserWorkerRunner } from '@livestore/utils/effect/browser'
|
|
27
24
|
|
|
28
25
|
import { makeShutdownChannel } from '../common/shutdown-channel.ts'
|
|
29
26
|
import * as WorkerSchema from '../common/worker-schema.ts'
|
|
30
27
|
|
|
28
|
+
// Extract from `livestore-shared-worker-${storeId}`
|
|
29
|
+
const storeId = self.name.replace('livestore-shared-worker-', '')
|
|
30
|
+
|
|
31
|
+
// We acquire a lock that is held as long as this shared worker is alive.
|
|
32
|
+
// This way, when the shared worker is terminated (e.g. by the browser when the page is closed),
|
|
33
|
+
// the lock is released and any thread waiting for the lock can be notified.
|
|
34
|
+
const LIVESTORE_SHARED_WORKER_TERMINATION_LOCK = `livestore-shared-worker-termination-lock-${storeId}`
|
|
35
|
+
navigator.locks.request(
|
|
36
|
+
LIVESTORE_SHARED_WORKER_TERMINATION_LOCK,
|
|
37
|
+
{ steal: true },
|
|
38
|
+
// We use a never-resolving promise to hold the lock
|
|
39
|
+
async () => new Promise(() => {}),
|
|
40
|
+
)
|
|
41
|
+
|
|
31
42
|
if (isDevEnv()) {
|
|
32
43
|
globalThis.__debugLiveStoreUtils = {
|
|
33
44
|
blobUrl: (buffer: Uint8Array<ArrayBuffer>) =>
|
|
@@ -46,22 +57,21 @@ const makeWorkerRunner = Effect.gen(function* () {
|
|
|
46
57
|
| undefined
|
|
47
58
|
>(undefined)
|
|
48
59
|
|
|
49
|
-
const initialMessagePayloadDeferredRef = yield* Deferred.make<
|
|
50
|
-
typeof WorkerSchema.SharedWorkerInitialMessagePayloadFromClientSession.Type
|
|
51
|
-
>().pipe(Effect.andThen(Ref.make))
|
|
52
|
-
|
|
53
60
|
const waitForWorker = SubscriptionRef.waitUntil(leaderWorkerContextSubRef, isNotUndefined).pipe(
|
|
54
61
|
Effect.map((_) => _.worker),
|
|
55
62
|
)
|
|
56
63
|
|
|
57
64
|
const forwardRequest = <TReq extends WorkerSchema.LeaderWorkerInnerRequest>(
|
|
58
65
|
req: TReq,
|
|
59
|
-
):
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
): Effect.Effect<
|
|
67
|
+
Schema.WithResult.Success<TReq>,
|
|
68
|
+
UnknownError | Schema.WithResult.Failure<TReq>,
|
|
69
|
+
Schema.WithResult.Context<TReq>
|
|
70
|
+
> =>
|
|
71
|
+
// Forward the request to the active worker and normalize platform errors into UnknownError.
|
|
62
72
|
waitForWorker.pipe(
|
|
63
73
|
// Effect.logBefore(`forwardRequest: ${req._tag}`),
|
|
64
|
-
Effect.andThen((worker) => worker.executeEffect(req) as Effect.Effect<unknown, unknown,
|
|
74
|
+
Effect.andThen((worker) => worker.executeEffect(req) as Effect.Effect<unknown, unknown, unknown>),
|
|
65
75
|
// Effect.tap((_) => Effect.log(`forwardRequest: ${req._tag}`, _)),
|
|
66
76
|
// Effect.tapError((cause) => Effect.logError(`forwardRequest err: ${req._tag}`, cause)),
|
|
67
77
|
Effect.interruptible,
|
|
@@ -70,25 +80,31 @@ const makeWorkerRunner = Effect.gen(function* () {
|
|
|
70
80
|
duration: 500,
|
|
71
81
|
}),
|
|
72
82
|
Effect.mapError((cause) =>
|
|
73
|
-
Schema.is(
|
|
83
|
+
Schema.is(UnknownError)(cause)
|
|
74
84
|
? cause
|
|
75
85
|
: ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
|
|
76
|
-
? new
|
|
86
|
+
? new UnknownError({ cause })
|
|
77
87
|
: cause,
|
|
78
88
|
),
|
|
79
|
-
Effect.catchAllDefect((cause) => new
|
|
89
|
+
Effect.catchAllDefect((cause) => new UnknownError({ cause })),
|
|
80
90
|
Effect.tapCauseLogPretty,
|
|
81
|
-
) as
|
|
91
|
+
) as Effect.Effect<
|
|
92
|
+
Schema.WithResult.Success<TReq>,
|
|
93
|
+
UnknownError | Schema.WithResult.Failure<TReq>,
|
|
94
|
+
Schema.WithResult.Context<TReq>
|
|
95
|
+
>
|
|
82
96
|
|
|
83
97
|
const forwardRequestStream = <TReq extends WorkerSchema.LeaderWorkerInnerRequest>(
|
|
84
98
|
req: TReq,
|
|
85
|
-
):
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
): Stream.Stream<
|
|
100
|
+
Schema.WithResult.Success<TReq>,
|
|
101
|
+
UnknownError | Schema.WithResult.Failure<TReq>,
|
|
102
|
+
Schema.WithResult.Context<TReq>
|
|
103
|
+
> =>
|
|
88
104
|
Effect.gen(function* () {
|
|
89
105
|
yield* Effect.logDebug(`forwardRequestStream: ${req._tag}`)
|
|
90
106
|
const { worker, scope } = yield* SubscriptionRef.waitUntil(leaderWorkerContextSubRef, isNotUndefined)
|
|
91
|
-
const stream = worker.execute(req) as Stream.Stream<unknown, unknown,
|
|
107
|
+
const stream = worker.execute(req) as Stream.Stream<unknown, unknown, unknown>
|
|
92
108
|
|
|
93
109
|
// It seems the request stream is not automatically interrupted when the scope shuts down
|
|
94
110
|
// so we need to manually interrupt it when the scope shuts down
|
|
@@ -104,12 +120,16 @@ const makeWorkerRunner = Effect.gen(function* () {
|
|
|
104
120
|
return Stream.merge(stream, scopeShutdownStream, { haltStrategy: 'either' })
|
|
105
121
|
}).pipe(
|
|
106
122
|
Effect.interruptible,
|
|
107
|
-
|
|
123
|
+
UnknownError.mapToUnknownError,
|
|
108
124
|
Effect.tapCauseLogPretty,
|
|
109
125
|
Stream.unwrap,
|
|
110
126
|
Stream.ensuring(Effect.logDebug(`shutting down stream for ${req._tag}`)),
|
|
111
|
-
|
|
112
|
-
) as
|
|
127
|
+
UnknownError.mapToUnknownErrorStream,
|
|
128
|
+
) as Stream.Stream<
|
|
129
|
+
Schema.WithResult.Success<TReq>,
|
|
130
|
+
UnknownError | Schema.WithResult.Failure<TReq>,
|
|
131
|
+
Schema.WithResult.Context<TReq>
|
|
132
|
+
>
|
|
113
133
|
|
|
114
134
|
const resetCurrentWorkerCtx = Effect.gen(function* () {
|
|
115
135
|
const prevWorker = yield* SubscriptionRef.get(leaderWorkerContextSubRef)
|
|
@@ -128,62 +148,59 @@ const makeWorkerRunner = Effect.gen(function* () {
|
|
|
128
148
|
}
|
|
129
149
|
}).pipe(Effect.withSpan('@livestore/adapter-web:shared-worker:resetCurrentWorkerCtx'))
|
|
130
150
|
|
|
131
|
-
// const devtoolsWebBridge = yield* makeDevtoolsWebBridge
|
|
132
|
-
|
|
133
151
|
const reset = Effect.gen(function* () {
|
|
134
152
|
yield* Effect.logDebug('reset')
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
yield* Ref.set(initialMessagePayloadDeferredRef, initialMessagePayloadDeferred)
|
|
139
|
-
|
|
153
|
+
// Clear cached invariants so a fresh configuration can be accepted after shutdown
|
|
154
|
+
yield* Ref.set(invariantsRef, undefined)
|
|
155
|
+
// Tear down current leader worker context
|
|
140
156
|
yield* resetCurrentWorkerCtx
|
|
141
|
-
// yield* devtoolsWebBridge.reset
|
|
142
157
|
})
|
|
143
158
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const messageSchema = WorkerSchema.LeaderWorkerInnerInitialMessage.pipe(
|
|
156
|
-
Schema.omit('devtoolsEnabled', 'debugInstanceId'),
|
|
157
|
-
)
|
|
158
|
-
const isEqual = Schema.equivalence(messageSchema)
|
|
159
|
-
if (isEqual(initialMessage, previousInitialMessage.initialMessage) === false) {
|
|
160
|
-
const diff = Schema.debugDiff(messageSchema)(previousInitialMessage.initialMessage, initialMessage)
|
|
159
|
+
// Cache first-applied invariants to enforce stability across leader transitions
|
|
160
|
+
const InvariantsSchema = Schema.Struct({
|
|
161
|
+
storeId: Schema.String,
|
|
162
|
+
storageOptions: WorkerSchema.StorageType,
|
|
163
|
+
syncPayloadEncoded: Schema.UndefinedOr(Schema.JsonValue),
|
|
164
|
+
liveStoreVersion: Schema.Literal(liveStoreVersion),
|
|
165
|
+
devtoolsEnabled: Schema.Boolean,
|
|
166
|
+
})
|
|
167
|
+
type Invariants = typeof InvariantsSchema.Type
|
|
168
|
+
const invariantsRef = yield* Ref.make<Invariants | undefined>(undefined)
|
|
169
|
+
const sameInvariants = Schema.equivalence(InvariantsSchema)
|
|
161
170
|
|
|
162
|
-
|
|
163
|
-
cause: 'Initial message already sent and was different now',
|
|
164
|
-
payload: {
|
|
165
|
-
diff,
|
|
166
|
-
previousInitialMessage: previousInitialMessage.initialMessage,
|
|
167
|
-
newInitialMessage: initialMessage,
|
|
168
|
-
},
|
|
169
|
-
})
|
|
170
|
-
}
|
|
171
|
-
} else {
|
|
172
|
-
yield* Deferred.succeed(initialMessagePayloadDeferred, message.payload)
|
|
173
|
-
}
|
|
174
|
-
}),
|
|
171
|
+
return WorkerRunner.layerSerialized(WorkerSchema.SharedWorkerRequest, {
|
|
175
172
|
// Whenever the client session leader changes (and thus creates a new leader thread), the new client session leader
|
|
176
173
|
// sends a new MessagePort to the shared worker which proxies messages to the new leader thread.
|
|
177
|
-
UpdateMessagePort: ({ port }) =>
|
|
174
|
+
UpdateMessagePort: ({ port, initial, liveStoreVersion: clientLiveStoreVersion }) =>
|
|
178
175
|
Effect.gen(function* () {
|
|
179
|
-
|
|
176
|
+
// Enforce invariants: storeId, storageOptions, syncPayloadEncoded, liveStoreVersion must remain stable
|
|
177
|
+
const invariants: Invariants = {
|
|
178
|
+
storeId: initial.storeId,
|
|
179
|
+
storageOptions: initial.storageOptions,
|
|
180
|
+
syncPayloadEncoded: initial.syncPayloadEncoded,
|
|
181
|
+
liveStoreVersion: clientLiveStoreVersion,
|
|
182
|
+
devtoolsEnabled: initial.devtoolsEnabled,
|
|
183
|
+
}
|
|
184
|
+
const prev = yield* Ref.get(invariantsRef)
|
|
185
|
+
// Early return on mismatch to keep happy path linear
|
|
186
|
+
if (prev !== undefined && !sameInvariants(prev, invariants)) {
|
|
187
|
+
const diff = Schema.debugDiff(InvariantsSchema)(prev, invariants)
|
|
188
|
+
return yield* new UnknownError({
|
|
189
|
+
cause: 'Store invariants changed across leader transitions',
|
|
190
|
+
payload: { diff, previous: prev, next: invariants },
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
// First writer records invariants
|
|
194
|
+
if (prev === undefined) {
|
|
195
|
+
yield* Ref.set(invariantsRef, invariants)
|
|
196
|
+
}
|
|
180
197
|
|
|
181
198
|
yield* resetCurrentWorkerCtx
|
|
182
199
|
|
|
183
200
|
const scope = yield* Scope.make()
|
|
184
201
|
|
|
185
202
|
yield* Effect.gen(function* () {
|
|
186
|
-
const shutdownChannel = yield* makeShutdownChannel(
|
|
203
|
+
const shutdownChannel = yield* makeShutdownChannel(initial.storeId)
|
|
187
204
|
|
|
188
205
|
yield* shutdownChannel.listen.pipe(
|
|
189
206
|
Stream.flatten(),
|
|
@@ -198,7 +215,7 @@ const makeWorkerRunner = Effect.gen(function* () {
|
|
|
198
215
|
const worker = yield* Worker.makePoolSerialized<WorkerSchema.LeaderWorkerInnerRequest>({
|
|
199
216
|
size: 1,
|
|
200
217
|
concurrency: 100,
|
|
201
|
-
initialMessage: () =>
|
|
218
|
+
initialMessage: () => initial,
|
|
202
219
|
}).pipe(
|
|
203
220
|
Effect.provide(workerLayer),
|
|
204
221
|
Effect.withSpan('@livestore/adapter-web:shared-worker:makeWorkerProxyFromPort'),
|
|
@@ -206,7 +223,7 @@ const makeWorkerRunner = Effect.gen(function* () {
|
|
|
206
223
|
|
|
207
224
|
// Prepare the web mesh connection for leader worker to be able to connect to the devtools
|
|
208
225
|
const { node } = yield* WebmeshWorker.CacheService
|
|
209
|
-
const { storeId, clientId } =
|
|
226
|
+
const { storeId, clientId } = initial
|
|
210
227
|
|
|
211
228
|
yield* DevtoolsWeb.connectViaWorker({
|
|
212
229
|
node,
|
|
@@ -218,7 +235,7 @@ const makeWorkerRunner = Effect.gen(function* () {
|
|
|
218
235
|
}).pipe(Effect.tapCauseLogPretty, Scope.extend(scope), Effect.forkIn(scope))
|
|
219
236
|
}).pipe(
|
|
220
237
|
Effect.withSpan('@livestore/adapter-web:shared-worker:updateMessagePort'),
|
|
221
|
-
|
|
238
|
+
UnknownError.mapToUnknownError,
|
|
222
239
|
Effect.tapCauseLogPretty,
|
|
223
240
|
),
|
|
224
241
|
|
|
@@ -231,7 +248,10 @@ const makeWorkerRunner = Effect.gen(function* () {
|
|
|
231
248
|
ExportEventlog: forwardRequest,
|
|
232
249
|
Setup: forwardRequest,
|
|
233
250
|
GetLeaderSyncState: forwardRequest,
|
|
251
|
+
SyncStateStream: forwardRequestStream,
|
|
234
252
|
GetLeaderHead: forwardRequest,
|
|
253
|
+
GetNetworkStatus: forwardRequest,
|
|
254
|
+
NetworkStatusStream: forwardRequestStream,
|
|
235
255
|
Shutdown: forwardRequest,
|
|
236
256
|
ExtraDevtoolsMessage: forwardRequest,
|
|
237
257
|
|
|
@@ -240,9 +260,11 @@ const makeWorkerRunner = Effect.gen(function* () {
|
|
|
240
260
|
})
|
|
241
261
|
}).pipe(Layer.unwrapScoped)
|
|
242
262
|
|
|
243
|
-
export const makeWorker = () => {
|
|
244
|
-
|
|
245
|
-
|
|
263
|
+
export const makeWorker = (options?: LogConfig.WithLoggerOptions): void => {
|
|
264
|
+
const runtimeLayer = Layer.mergeAll(
|
|
265
|
+
FetchHttpClient.layer,
|
|
266
|
+
WebmeshWorker.CacheService.layer({ nodeName: DevtoolsWeb.makeNodeName.sharedWorker({ storeId }) }),
|
|
267
|
+
)
|
|
246
268
|
|
|
247
269
|
makeWorkerRunner.pipe(
|
|
248
270
|
Layer.provide(BrowserWorkerRunner.layer),
|
|
@@ -251,13 +273,11 @@ export const makeWorker = () => {
|
|
|
251
273
|
Effect.scoped,
|
|
252
274
|
Effect.tapCauseLogPretty,
|
|
253
275
|
Effect.annotateLogs({ thread: self.name }),
|
|
254
|
-
Effect.provide(
|
|
255
|
-
Effect.provide(FetchHttpClient.layer),
|
|
256
|
-
Effect.provide(WebmeshWorker.CacheService.layer({ nodeName: DevtoolsWeb.makeNodeName.sharedWorker({ storeId }) })),
|
|
276
|
+
Effect.provide(runtimeLayer),
|
|
257
277
|
LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
|
|
258
278
|
// TODO remove type-cast (currently needed to silence a tsc bug)
|
|
259
279
|
(_) => _ as any as Effect.Effect<void, any>,
|
|
260
|
-
|
|
280
|
+
LogConfig.withLoggerConfig(options, { threadName: self.name }),
|
|
261
281
|
Effect.runFork,
|
|
262
282
|
)
|
|
263
283
|
}
|
|
@@ -5,6 +5,7 @@ globalThis.$RefreshReg$ = () => {}
|
|
|
5
5
|
// @ts-expect-error TODO remove when Vite does proper treeshaking during dev
|
|
6
6
|
globalThis.$RefreshSig$ = () => (type: any) => type
|
|
7
7
|
|
|
8
|
+
// biome-ignore lint/suspicious/noTsIgnore: sometimes @types/node is there, sometimes not.
|
|
8
9
|
// @ts-ignore
|
|
9
10
|
globalThis.process = globalThis.process ?? { env: {} }
|
|
10
11
|
|
package/dist/opfs-utils.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export declare const rootHandlePromise: Promise<FileSystemDirectoryHandle>;
|
|
2
|
-
export declare const getDirHandle: (absDirPath: string | undefined) => Promise<FileSystemDirectoryHandle>;
|
|
3
|
-
export declare const printTree: (directoryHandle_?: FileSystemDirectoryHandle | Promise<FileSystemDirectoryHandle>, depth?: number, prefix?: string) => Promise<void>;
|
|
4
|
-
export declare const deleteAll: (directoryHandle: FileSystemDirectoryHandle) => Promise<void>;
|
|
5
|
-
//# sourceMappingURL=opfs-utils.d.ts.map
|
package/dist/opfs-utils.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"opfs-utils.d.ts","sourceRoot":"","sources":["../src/opfs-utils.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,iBAAiB,oCAYQ,CAAA;AAEtC,eAAO,MAAM,YAAY,GAAU,YAAY,MAAM,GAAG,SAAS,uCAWhE,CAAA;AAED,eAAO,MAAM,SAAS,GACpB,mBAAkB,yBAAyB,GAAG,OAAO,CAAC,yBAAyB,CAAqB,EACpG,QAAO,MAAiC,EACxC,eAAW,KACV,OAAO,CAAC,IAAI,CAgBd,CAAA;AAED,eAAO,MAAM,SAAS,GAAU,iBAAiB,yBAAyB,kBAMzE,CAAA"}
|
package/dist/opfs-utils.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// NOTE we're already firing off this promise call here since we'll need it anyway and need it cached
|
|
2
|
-
import { prettyBytes } from '@livestore/utils';
|
|
3
|
-
// To improve LiveStore compatibility with e.g. Node.js we're guarding for `navigator` / `navigator.storage` to be defined.
|
|
4
|
-
export const rootHandlePromise = typeof navigator === 'undefined' || navigator.storage === undefined
|
|
5
|
-
? // We're using a proxy here to make the promise reject lazy
|
|
6
|
-
new Proxy({}, {
|
|
7
|
-
get: () => Promise.reject(new Error(`Can't get OPFS root handle in this environment as navigator.storage is undefined`)),
|
|
8
|
-
})
|
|
9
|
-
: navigator.storage.getDirectory();
|
|
10
|
-
export const getDirHandle = async (absDirPath) => {
|
|
11
|
-
const rootHandle = await rootHandlePromise;
|
|
12
|
-
if (absDirPath === undefined)
|
|
13
|
-
return rootHandle;
|
|
14
|
-
let dirHandle = rootHandle;
|
|
15
|
-
const directoryStack = absDirPath?.split('/').filter(Boolean);
|
|
16
|
-
while (directoryStack.length > 0) {
|
|
17
|
-
dirHandle = await dirHandle.getDirectoryHandle(directoryStack.shift());
|
|
18
|
-
}
|
|
19
|
-
return dirHandle;
|
|
20
|
-
};
|
|
21
|
-
export const printTree = async (directoryHandle_ = rootHandlePromise, depth = Number.POSITIVE_INFINITY, prefix = '') => {
|
|
22
|
-
if (depth < 0)
|
|
23
|
-
return;
|
|
24
|
-
const directoryHandle = await directoryHandle_;
|
|
25
|
-
const entries = directoryHandle.values();
|
|
26
|
-
for await (const entry of entries) {
|
|
27
|
-
const isDirectory = entry.kind === 'directory';
|
|
28
|
-
const size = entry.kind === 'file' ? await entry.getFile().then((file) => prettyBytes(file.size)) : undefined;
|
|
29
|
-
console.log(`${prefix}${isDirectory ? '📁' : '📄'} ${entry.name} ${size ? `(${size})` : ''}`);
|
|
30
|
-
if (isDirectory) {
|
|
31
|
-
const nestedDirectoryHandle = await directoryHandle.getDirectoryHandle(entry.name);
|
|
32
|
-
await printTree(nestedDirectoryHandle, depth - 1, `${prefix} `);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
export const deleteAll = async (directoryHandle) => {
|
|
37
|
-
if (directoryHandle.kind !== 'directory')
|
|
38
|
-
return;
|
|
39
|
-
for await (const entryName of directoryHandle.keys()) {
|
|
40
|
-
await directoryHandle.removeEntry(entryName, { recursive: true });
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
//# sourceMappingURL=opfs-utils.js.map
|
package/dist/opfs-utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"opfs-utils.js","sourceRoot":"","sources":["../src/opfs-utils.ts"],"names":[],"mappings":"AAAA,qGAAqG;AAErG,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,2HAA2H;AAC3H,MAAM,CAAC,MAAM,iBAAiB,GAC5B,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,CAAC,OAAO,KAAK,SAAS;IACjE,CAAC,CAAC,2DAA2D;QAC1D,IAAI,KAAK,CACR,EAAE,EACF;YACE,GAAG,EAAE,GAAG,EAAE,CACR,OAAO,CAAC,MAAM,CACZ,IAAI,KAAK,CAAC,kFAAkF,CAAC,CAC9F;SACJ,CACQ;IACb,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,CAAA;AAEtC,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,UAA8B,EAAE,EAAE;IACnE,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAA;IAC1C,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAA;IAE/C,IAAI,SAAS,GAAG,UAAU,CAAA;IAC1B,MAAM,cAAc,GAAG,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC7D,OAAO,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,SAAS,GAAG,MAAM,SAAS,CAAC,kBAAkB,CAAC,cAAc,CAAC,KAAK,EAAG,CAAC,CAAA;IACzE,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAC5B,mBAAmF,iBAAiB,EACpG,QAAgB,MAAM,CAAC,iBAAiB,EACxC,MAAM,GAAG,EAAE,EACI,EAAE;IACjB,IAAI,KAAK,GAAG,CAAC;QAAE,OAAM;IAErB,MAAM,eAAe,GAAG,MAAM,gBAAgB,CAAA;IAC9C,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,EAAE,CAAA;IAExC,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAClC,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,KAAK,WAAW,CAAA;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC7G,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;QAE7F,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,qBAAqB,GAAG,MAAM,eAAe,CAAC,kBAAkB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YAClF,MAAM,SAAS,CAAC,qBAAqB,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,MAAM,IAAI,CAAC,CAAA;QAClE,CAAC;IACH,CAAC;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,eAA0C,EAAE,EAAE;IAC5E,IAAI,eAAe,CAAC,IAAI,KAAK,WAAW;QAAE,OAAM;IAEhD,IAAI,KAAK,EAAE,MAAM,SAAS,IAAI,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC;QACrD,MAAM,eAAe,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACnE,CAAC;AACH,CAAC,CAAA"}
|