@livestore/common 0.5.0-dev.0 → 0.5.0-dev.1
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/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
- package/dist/MaterializationJournal.d.ts +2 -2
- package/dist/MaterializationJournal.d.ts.map +1 -1
- package/dist/MaterializationJournal.js +7 -7
- package/dist/StateHead.d.ts +1 -1
- package/dist/StateHead.d.ts.map +1 -1
- package/dist/StateHead.js +4 -4
- package/dist/WorkerTransportError.d.ts.map +1 -1
- package/dist/__tests__/fixture.d.ts +7 -7
- package/dist/__tests__/fixture.js +2 -2
- package/dist/adapter-types.d.ts +4 -0
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +4 -4
- package/dist/bounded-collections.d.ts.map +1 -1
- package/dist/bounded-collections.js.map +1 -1
- package/dist/debug-info.d.ts.map +1 -1
- package/dist/debug-info.js +9 -8
- package/dist/debug-info.js.map +1 -1
- package/dist/defs.d.ts +4 -4
- package/dist/defs.js +2 -2
- package/dist/devtools/devtools-compatibility.test.js +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +4 -4
- package/dist/devtools/devtools-messages-client-session.js +4 -4
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.js +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +9 -9
- package/dist/devtools/devtools-messages-leader.js +4 -4
- package/dist/devtools/devtools-messages.js +3 -3
- package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -1
- package/dist/devtools/mod.d.ts.map +1 -1
- package/dist/devtools/mod.js +3 -3
- package/dist/errors.d.ts +4 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.js +22 -22
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +27 -6
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +65 -55
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/RejectedPushError.d.ts +9 -9
- package/dist/leader-thread/RejectedPushError.d.ts.map +1 -1
- package/dist/leader-thread/RejectedPushError.js +1 -1
- package/dist/leader-thread/connection.d.ts.map +1 -1
- package/dist/leader-thread/connection.js +2 -2
- package/dist/leader-thread/eventlog.d.ts +11 -1
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +13 -13
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +4 -4
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +6 -0
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +43 -29
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.test.js +2 -2
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +9 -9
- package/dist/leader-thread/mod.js +9 -9
- package/dist/leader-thread/recreate-db.d.ts +5 -1
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +21 -25
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +1 -1
- package/dist/leader-thread/stream-events.d.ts.map +1 -1
- package/dist/leader-thread/stream-events.js +3 -5
- package/dist/leader-thread/stream-events.js.map +1 -1
- package/dist/leader-thread/types.d.ts +6 -4
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js +5 -1
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/make-client-session.d.ts +1 -1
- package/dist/make-client-session.d.ts.map +1 -1
- package/dist/make-client-session.js +2 -2
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +4 -4
- package/dist/materializer-helper.js.map +1 -1
- package/dist/otel.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +3 -1
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.js +16 -9
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef/define.d.ts +2 -2
- package/dist/schema/EventDef/define.d.ts.map +1 -1
- package/dist/schema/EventDef/define.test.js +1 -1
- package/dist/schema/EventDef/deprecated.d.ts +1 -1
- package/dist/schema/EventDef/deprecated.d.ts.map +1 -1
- package/dist/schema/EventDef/deprecated.test.js +2 -2
- package/dist/schema/EventDef/facts.d.ts.map +1 -1
- package/dist/schema/EventDef/materializer.d.ts +2 -2
- package/dist/schema/EventDef/materializer.d.ts.map +1 -1
- package/dist/schema/EventDef/mod.js +5 -5
- package/dist/schema/EventSequenceNumber/client.d.ts +3 -8
- package/dist/schema/EventSequenceNumber/client.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber/client.js +12 -15
- package/dist/schema/EventSequenceNumber/client.js.map +1 -1
- package/dist/schema/EventSequenceNumber/mod.js +2 -2
- package/dist/schema/EventSequenceNumber.test.js +1 -1
- package/dist/schema/LiveStoreEvent/client.d.ts +8 -8
- package/dist/schema/LiveStoreEvent/client.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/client.js +1 -1
- package/dist/schema/LiveStoreEvent/client.js.map +1 -1
- package/dist/schema/LiveStoreEvent/client.test.js +2 -2
- package/dist/schema/LiveStoreEvent/global.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/global.js +1 -1
- package/dist/schema/LiveStoreEvent/input.d.ts +1 -1
- package/dist/schema/LiveStoreEvent/input.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/mod.js +4 -4
- package/dist/schema/events.js +1 -1
- package/dist/schema/mod.js +11 -11
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +3 -3
- package/dist/schema/state/mod.js +2 -2
- package/dist/schema/state/sqlite/client-document-def.d.ts +1 -1
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js +8 -8
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +24 -10
- package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.test.js +1 -1
- package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-def.js +17 -3
- package/dist/schema/state/sqlite/column-def.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.test.js +60 -5
- package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-spec.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-spec.js +1 -1
- package/dist/schema/state/sqlite/column-spec.test.js +2 -2
- package/dist/schema/state/sqlite/column-spec.test.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts +3 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js +5 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts +5 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js +183 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts +2 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js +142 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +1 -6
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +1 -82
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js +1 -1
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts +3 -0
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js +6 -0
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/mod.js +2 -2
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/mod.js +10 -9
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/persistence.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/astToSql.js +2 -7
- package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts +1 -3
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.js +2 -2
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +2 -2
- package/dist/schema/state/sqlite/query-builder/mod.js +2 -2
- package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -1
- package/dist/schema/state/sqlite/schema-helpers.js +1 -1
- package/dist/schema/state/sqlite/schema-helpers.test.js +2 -2
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.js +3 -3
- package/dist/schema/state/sqlite/system-tables/mod.js +2 -2
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts +60 -17
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables/state-tables.js +14 -5
- package/dist/schema/state/sqlite/system-tables/state-tables.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.js +24 -8
- package/dist/schema/state/sqlite/table-def.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.test.js +54 -1
- package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
- package/dist/schema/unknown-events.d.ts.map +1 -1
- package/dist/schema/unknown-events.js +1 -1
- package/dist/schema/unknown-events.js.map +1 -1
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js +26 -4
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js.map +1 -1
- package/dist/schema-management/common.d.ts.map +1 -1
- package/dist/schema-management/common.js +2 -2
- package/dist/schema-management/migrations.d.ts +4 -4
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +9 -9
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/schema-management/validate-schema.d.ts.map +1 -1
- package/dist/schema-management/validate-schema.js +1 -1
- package/dist/session-id-symbol.d.ts.map +1 -1
- package/dist/sql-queries/index.js +3 -3
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +3 -3
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sql-queries/sql-query-builder.js +1 -1
- package/dist/sql-queries/types.d.ts.map +1 -1
- package/dist/sqlite-db-helper.d.ts +2 -2
- package/dist/sqlite-db-helper.d.ts.map +1 -1
- package/dist/sqlite-db-helper.js +2 -2
- package/dist/sync/ClientSessionSyncProcessor.d.ts +3 -3
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +7 -7
- package/dist/sync/errors.js +1 -1
- package/dist/sync/index.js +6 -6
- package/dist/sync/mock-sync-backend.d.ts.map +1 -1
- package/dist/sync/mock-sync-backend.js +4 -4
- package/dist/sync/mock-sync-backend.test.js +2 -2
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/compact-events.js +3 -3
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +1 -1
- package/dist/sync/next/history-dag-common.d.ts +1 -1
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +1 -1
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/history-dag.js +3 -3
- package/dist/sync/next/mod.js +5 -5
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +2 -2
- package/dist/sync/next/test/compact-events.calculator.test.js +4 -4
- package/dist/sync/next/test/compact-events.test.js +4 -4
- package/dist/sync/next/test/event-fixtures.d.ts +1 -1
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/event-fixtures.js +5 -5
- package/dist/sync/next/test/history-dag-common.test.js +2 -2
- package/dist/sync/next/test/mod.js +1 -1
- package/dist/sync/sync-backend-kv.js +1 -1
- package/dist/sync/sync-backend.d.ts.map +1 -1
- package/dist/sync/sync-backend.js +1 -1
- package/dist/sync/sync.js +2 -2
- package/dist/sync/syncstate.d.ts +3 -3
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +2 -2
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +3 -3
- package/dist/sync/transport-chunking.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +2 -2
- package/dist/testing/event-factory.d.ts.map +1 -1
- package/dist/testing/event-factory.js +2 -2
- package/dist/testing/event-factory.js.map +1 -1
- package/dist/testing/mod.js +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +1 -0
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +40 -39
- package/src/adapter-types.ts +4 -0
- package/src/debug-info.ts +11 -8
- package/src/defs.ts +2 -2
- package/src/leader-thread/LeaderSyncProcessor.ts +121 -97
- package/src/leader-thread/eventlog.ts +13 -5
- package/src/leader-thread/leader-worker-devtools.ts +1 -1
- package/src/leader-thread/make-leader-thread-layer.ts +49 -23
- package/src/leader-thread/recreate-db.ts +26 -25
- package/src/leader-thread/stream-events.ts +1 -3
- package/src/leader-thread/types.ts +5 -0
- package/src/materializer-helper.ts +1 -1
- package/src/rematerialize-from-eventlog.ts +28 -13
- package/src/schema/EventSequenceNumber/client.ts +12 -15
- package/src/schema/schema.ts +1 -1
- package/src/schema/state/sqlite/client-document-def.test.ts +23 -7
- package/src/schema/state/sqlite/client-document-def.ts +2 -2
- package/src/schema/state/sqlite/column-def.test.ts +84 -3
- package/src/schema/state/sqlite/column-def.ts +16 -1
- package/src/schema/state/sqlite/db-schema/ast/fingerprint-digest.ts +6 -0
- package/src/schema/state/sqlite/db-schema/ast/fingerprint.test.ts +209 -0
- package/src/schema/state/sqlite/db-schema/ast/fingerprint.ts +225 -0
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +1 -90
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +1 -0
- package/src/schema/state/sqlite/db-schema/has-json-string-encoding.ts +9 -0
- package/src/schema/state/sqlite/mod.ts +2 -1
- package/src/schema/state/sqlite/query-builder/astToSql.ts +1 -8
- package/src/schema/state/sqlite/system-tables/state-tables.ts +13 -2
- package/src/schema/state/sqlite/table-def.test.ts +66 -0
- package/src/schema/state/sqlite/table-def.ts +21 -5
- package/src/schema-management/__tests__/migrations-autoincrement-quoting.test.ts +33 -2
- package/src/schema-management/migrations.ts +7 -6
- package/src/sql-queries/sql-queries.ts +1 -1
- package/src/util.ts +1 -0
- package/dist/schema/state/sqlite/db-schema/hash.d.ts +0 -2
- package/dist/schema/state/sqlite/db-schema/hash.d.ts.map +0 -1
- package/dist/schema/state/sqlite/db-schema/hash.js +0 -15
- package/dist/schema/state/sqlite/db-schema/hash.js.map +0 -1
- package/src/schema/state/sqlite/db-schema/hash.ts +0 -13
|
@@ -11,8 +11,22 @@ import {
|
|
|
11
11
|
UnknownError,
|
|
12
12
|
} from '../index.ts'
|
|
13
13
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
|
14
|
-
import {
|
|
14
|
+
import { SystemTables } from '../schema/mod.ts'
|
|
15
|
+
import { configureConnection, execSql } from './connection.ts'
|
|
15
16
|
import type { MaterializeEvent } from './types.ts'
|
|
17
|
+
import { STATE_REBUILD_BATCH_SIZE_DEFAULT } from './types.ts'
|
|
18
|
+
|
|
19
|
+
export const hasCompletedState = (db: SqliteDb): boolean => {
|
|
20
|
+
const tableNames = new Set(db.select<{ name: string }>('SELECT name FROM sqlite_master').map((_) => _.name))
|
|
21
|
+
return (
|
|
22
|
+
SystemTables.stateSystemTables.every((table) => tableNames.has(table.sqliteDef.name)) &&
|
|
23
|
+
db.select(`SELECT id FROM ${SystemTables.REBUILD_META_TABLE} WHERE id = 1`).length === 1
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Marks a successfully prepared state database as safe to use without rebuilding it. */
|
|
28
|
+
export const markStateAsCompleted = (db: SqliteDb): Effect.Effect<void, SqliteError> =>
|
|
29
|
+
execSql(db, `INSERT OR IGNORE INTO ${SystemTables.REBUILD_META_TABLE} (id) VALUES (1)`, {})
|
|
16
30
|
|
|
17
31
|
export const recreateDb = ({
|
|
18
32
|
dbState,
|
|
@@ -20,12 +34,14 @@ export const recreateDb = ({
|
|
|
20
34
|
schema,
|
|
21
35
|
bootStatusQueue,
|
|
22
36
|
materializeEvent,
|
|
37
|
+
stateRebuildBatchSize = STATE_REBUILD_BATCH_SIZE_DEFAULT,
|
|
23
38
|
}: {
|
|
24
39
|
dbState: SqliteDb
|
|
25
40
|
dbEventlog: SqliteDb
|
|
26
41
|
schema: LiveStoreSchema
|
|
27
42
|
bootStatusQueue: Queue.Queue<BootStatus>
|
|
28
43
|
materializeEvent: MaterializeEvent
|
|
44
|
+
stateRebuildBatchSize?: number
|
|
29
45
|
}): Effect.Effect<{ migrationsReport: MigrationsReport }, UnknownError | MaterializeError | SqliteError> =>
|
|
30
46
|
Effect.gen(function* () {
|
|
31
47
|
const hooks = schema.state.sqlite.migrations.hooks
|
|
@@ -36,50 +52,35 @@ export const recreateDb = ({
|
|
|
36
52
|
}),
|
|
37
53
|
)
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
// and later we'll overwrite the persisted database with the new data
|
|
41
|
-
// TODO bring back this optimization
|
|
42
|
-
// const tmpDb = yield* makeSqliteDb({ _tag: 'in-memory' })
|
|
43
|
-
const tmpDb = dbState
|
|
44
|
-
yield* configureConnection(tmpDb, { foreignKeys: true })
|
|
55
|
+
yield* configureConnection(dbState, { foreignKeys: true })
|
|
45
56
|
|
|
46
57
|
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- user hook errors are immediately normalized to LiveStore UnknownError
|
|
47
|
-
yield* Effect.trySyncOrPromiseOrEffect(() => hooks?.init?.(
|
|
58
|
+
yield* Effect.trySyncOrPromiseOrEffect(() => hooks?.init?.(dbState)).pipe(UnknownError.mapToUnknownError)
|
|
48
59
|
|
|
49
60
|
const migrationsReport = yield* migrateDb({
|
|
50
|
-
db:
|
|
61
|
+
db: dbState,
|
|
51
62
|
schema,
|
|
52
63
|
onProgress: ({ done, total }) => Queue.offer(bootStatusQueue, { stage: 'migrating', progress: { done, total } }),
|
|
53
64
|
})
|
|
54
65
|
|
|
55
66
|
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- user hook errors are immediately normalized to LiveStore UnknownError
|
|
56
|
-
yield* Effect.trySyncOrPromiseOrEffect(() => hooks?.pre?.(
|
|
67
|
+
yield* Effect.trySyncOrPromiseOrEffect(() => hooks?.pre?.(dbState)).pipe(UnknownError.mapToUnknownError)
|
|
57
68
|
|
|
58
69
|
yield* rematerializeFromEventlog({
|
|
59
|
-
// db: tmpDb,
|
|
60
70
|
dbEventlog,
|
|
71
|
+
dbState,
|
|
61
72
|
schema,
|
|
62
73
|
materializeEvent,
|
|
74
|
+
batchSize: stateRebuildBatchSize,
|
|
63
75
|
onProgress: ({ done, total }) =>
|
|
64
76
|
Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
|
|
65
77
|
})
|
|
66
78
|
|
|
67
79
|
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- user hook errors are immediately normalized to LiveStore UnknownError
|
|
68
|
-
yield* Effect.trySyncOrPromiseOrEffect(() => hooks?.post?.(
|
|
69
|
-
|
|
70
|
-
// TODO bring back
|
|
71
|
-
// Import the temporary in-memory database into the persistent database
|
|
72
|
-
// yield* Effect.sync(() => db.import(tmpDb)).pipe(
|
|
73
|
-
// Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
|
|
74
|
-
// )
|
|
75
|
-
|
|
76
|
-
// TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
|
|
77
|
-
// We've disabled this for now as it made the code too complex, as we often run syncing right after
|
|
78
|
-
// so the snapshot is no longer up to date
|
|
79
|
-
// const snapshotFromTmpDb = tmpDb.export()
|
|
80
|
+
yield* Effect.trySyncOrPromiseOrEffect(() => hooks?.post?.(dbState)).pipe(UnknownError.mapToUnknownError)
|
|
80
81
|
|
|
81
|
-
//
|
|
82
|
-
|
|
82
|
+
// Keep this out of finalizers, which also run on failure and interruption.
|
|
83
|
+
yield* markStateAsCompleted(dbState)
|
|
83
84
|
|
|
84
85
|
return { migrationsReport }
|
|
85
86
|
}).pipe(
|
|
@@ -151,9 +151,7 @@ export const streamEventsWithSyncState = ({
|
|
|
151
151
|
*/
|
|
152
152
|
const waitForHead = EventSequenceNumber.Client.isGreaterThanOrEqual(cursor, head)
|
|
153
153
|
const maybeHead =
|
|
154
|
-
waitForHead === true
|
|
155
|
-
? yield* Queue.take(headQueue).pipe(Effect.map(Option.some))
|
|
156
|
-
: yield* Queue.poll(headQueue)
|
|
154
|
+
waitForHead === true ? yield* Queue.take(headQueue).pipe(Effect.asSome) : yield* Queue.poll(headQueue)
|
|
157
155
|
const nextHead = Option.getOrElse(maybeHead, () => head)
|
|
158
156
|
const hardStop = options.until?.global ?? Number.POSITIVE_INFINITY
|
|
159
157
|
const target = EventSequenceNumber.Client.Composite.make({
|
|
@@ -139,6 +139,11 @@ export type InitialBlockingSyncContext = {
|
|
|
139
139
|
export const STREAM_EVENTS_BATCH_SIZE_DEFAULT = 100
|
|
140
140
|
export const STREAM_EVENTS_BATCH_SIZE_MAX = 1_000
|
|
141
141
|
|
|
142
|
+
export const STATE_REBUILD_BATCH_SIZE_DEFAULT = 100
|
|
143
|
+
export const StateRebuildBatchSizeSchema = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({
|
|
144
|
+
title: 'stateRebuildBatchSize',
|
|
145
|
+
})
|
|
146
|
+
|
|
142
147
|
export const StreamEventsOptionsFields = {
|
|
143
148
|
since: Schema.optional(EventSequenceNumber.Client.Composite),
|
|
144
149
|
until: Schema.optional(EventSequenceNumber.Client.Composite),
|
|
@@ -55,7 +55,7 @@ export const getExecStatementsFromMaterializer = ({
|
|
|
55
55
|
const { query, bindValues } = rawQueryOrQueryBuilder.asSql()
|
|
56
56
|
const rawResults = dbState.select(query, prepareBindValues(bindValues, query))
|
|
57
57
|
const resultSchema = getResultSchema(rawQueryOrQueryBuilder)
|
|
58
|
-
return Schema.
|
|
58
|
+
return Schema.decodeSync(resultSchema)(rawResults)
|
|
59
59
|
} else {
|
|
60
60
|
const { query, bindValues } = rawQueryOrQueryBuilder
|
|
61
61
|
return dbState.select(query, prepareBindValues(bindValues, query))
|
|
@@ -3,8 +3,10 @@ import { Effect, Option, ReadonlyArray as EffectArray, Schema, Stream } from '@l
|
|
|
3
3
|
|
|
4
4
|
import { type SqliteDb, UnknownError } from './adapter-types.ts'
|
|
5
5
|
import type { MaterializeEvent } from './leader-thread/mod.ts'
|
|
6
|
+
import { STATE_REBUILD_BATCH_SIZE_DEFAULT, StateRebuildBatchSizeSchema } from './leader-thread/types.ts'
|
|
6
7
|
import type { EventDef, LiveStoreSchema } from './schema/mod.ts'
|
|
7
8
|
import { EventSequenceNumber, LiveStoreEvent, SystemTables } from './schema/mod.ts'
|
|
9
|
+
import { withSavepoint } from './sqlite-db-helper.ts'
|
|
8
10
|
import type { PreparedBindValues } from './util.ts'
|
|
9
11
|
import { sql } from './util.ts'
|
|
10
12
|
|
|
@@ -14,17 +16,26 @@ const jsonParse = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)
|
|
|
14
16
|
export const rematerializeFromEventlog = Effect.fn('@livestore/common:rematerializeFromEventlog')(function* ({
|
|
15
17
|
dbEventlog,
|
|
16
18
|
// TODO re-use this db when bringing back the boot in-memory db implementation
|
|
17
|
-
|
|
19
|
+
dbState,
|
|
18
20
|
schema,
|
|
19
21
|
onProgress,
|
|
20
22
|
materializeEvent,
|
|
23
|
+
batchSize = STATE_REBUILD_BATCH_SIZE_DEFAULT,
|
|
21
24
|
}: {
|
|
22
25
|
dbEventlog: SqliteDb
|
|
23
|
-
|
|
26
|
+
dbState: SqliteDb
|
|
24
27
|
schema: LiveStoreSchema
|
|
25
28
|
onProgress: (_: { done: number; total: number }) => Effect.Effect<void>
|
|
26
29
|
materializeEvent: MaterializeEvent
|
|
30
|
+
batchSize?: number
|
|
27
31
|
}) {
|
|
32
|
+
if (Schema.is(StateRebuildBatchSizeSchema)(batchSize) === false) {
|
|
33
|
+
return yield* UnknownError.make({
|
|
34
|
+
cause: `Invalid stateRebuildBatchSize: ${String(batchSize)}. Expected a positive integer.`,
|
|
35
|
+
payload: { batchSize },
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
const eventsCount = dbEventlog.select<{ count: number }>(
|
|
29
40
|
`SELECT COUNT(*) AS count FROM ${SystemTables.EVENTLOG_META_TABLE}`,
|
|
30
41
|
)[0]!.count
|
|
@@ -85,13 +96,11 @@ This likely means the schema has changed in an incompatible way.
|
|
|
85
96
|
yield* materializeEvent(eventEncoded, { skipEventlog: true })
|
|
86
97
|
})
|
|
87
98
|
|
|
88
|
-
const CHUNK_SIZE = 100
|
|
89
|
-
|
|
90
99
|
const stmt = dbEventlog.prepare(sql`\
|
|
91
100
|
SELECT * FROM ${SystemTables.EVENTLOG_META_TABLE}
|
|
92
101
|
WHERE seqNumGlobal > $seqNumGlobal OR (seqNumGlobal = $seqNumGlobal AND seqNumClient > $seqNumClient)
|
|
93
102
|
ORDER BY seqNumGlobal ASC, seqNumClient ASC
|
|
94
|
-
LIMIT $
|
|
103
|
+
LIMIT $batchSize
|
|
95
104
|
`)
|
|
96
105
|
|
|
97
106
|
let processedEvents = 0
|
|
@@ -101,6 +110,7 @@ LIMIT ${CHUNK_SIZE}
|
|
|
101
110
|
const rows = stmt.select<SystemTables.EventlogMetaRow>({
|
|
102
111
|
$seqNumGlobal: lastId.global,
|
|
103
112
|
$seqNumClient: lastId.client,
|
|
113
|
+
$batchSize: batchSize,
|
|
104
114
|
} as any as PreparedBindValues)
|
|
105
115
|
|
|
106
116
|
if (EffectArray.isReadonlyArrayNonEmpty(rows) === false) {
|
|
@@ -125,14 +135,19 @@ LIMIT ${CHUNK_SIZE}
|
|
|
125
135
|
}),
|
|
126
136
|
).pipe(
|
|
127
137
|
Stream.bufferArray({ capacity: 2 }),
|
|
128
|
-
Stream.
|
|
129
|
-
Effect.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
Stream.runForEachArray((rows) =>
|
|
139
|
+
Effect.forEach(
|
|
140
|
+
rows,
|
|
141
|
+
(row) =>
|
|
142
|
+
Effect.gen(function* () {
|
|
143
|
+
yield* processEvent(row)
|
|
144
|
+
|
|
145
|
+
processedEvents++
|
|
146
|
+
yield* onProgress({ done: processedEvents, total: eventsCount })
|
|
147
|
+
}),
|
|
148
|
+
{ discard: true },
|
|
149
|
+
).pipe(withSavepoint(dbState)),
|
|
135
150
|
),
|
|
136
|
-
|
|
151
|
+
Effect.ensuring(Effect.sync(() => stmt.finalize())),
|
|
137
152
|
)
|
|
138
153
|
}, Effect.withPerformanceMeasure('@livestore/common:rematerializeFromEventlog'))
|
|
@@ -181,22 +181,9 @@ const CompositeSchema = S.Struct({
|
|
|
181
181
|
.annotate({ title: 'EventSequenceNumber.Composite' })
|
|
182
182
|
.pipe(S.overrideToFormatter(() => (seqNum) => toString(seqNum)))
|
|
183
183
|
|
|
184
|
-
/**
|
|
185
|
-
* Creates a validated Composite sequence number from input.
|
|
186
|
-
* If rebaseGeneration is omitted, defaults to REBASE_GENERATION_DEFAULT (0).
|
|
187
|
-
*/
|
|
188
|
-
const makeComposite = (seqNum: CompositeInput): Composite => {
|
|
189
|
-
return S.is(CompositeSchema)(seqNum) === true
|
|
190
|
-
? seqNum
|
|
191
|
-
: S.decodeSync(CompositeSchema)({
|
|
192
|
-
...seqNum,
|
|
193
|
-
rebaseGeneration: seqNum.rebaseGeneration ?? REBASE_GENERATION_DEFAULT,
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
184
|
/**
|
|
198
185
|
* Effect Schema for the composite event sequence number (global + client + rebaseGeneration).
|
|
199
|
-
*
|
|
186
|
+
* Its `make` validates input and defaults an omitted rebaseGeneration to REBASE_GENERATION_DEFAULT (0).
|
|
200
187
|
*
|
|
201
188
|
* @example
|
|
202
189
|
* ```ts
|
|
@@ -209,7 +196,17 @@ const makeComposite = (seqNum: CompositeInput): Composite => {
|
|
|
209
196
|
* const validated = EventSequenceNumber.Client.Composite.make({ global: 5, client: 0, rebaseGeneration: 0 })
|
|
210
197
|
* ```
|
|
211
198
|
*/
|
|
212
|
-
export const Composite
|
|
199
|
+
export const Composite: typeof CompositeSchema & { make(seqNum: CompositeInput): Composite } =
|
|
200
|
+
class extends CompositeSchema {
|
|
201
|
+
static override make(seqNum: CompositeInput): Composite {
|
|
202
|
+
return S.is(CompositeSchema)(seqNum) === true
|
|
203
|
+
? seqNum
|
|
204
|
+
: S.decodeSync(CompositeSchema)({
|
|
205
|
+
...seqNum,
|
|
206
|
+
rebaseGeneration: seqNum.rebaseGeneration ?? REBASE_GENERATION_DEFAULT,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
213
210
|
|
|
214
211
|
/** The root sequence number (global=0, client=0, rebaseGeneration=0). Parent of the first event. */
|
|
215
212
|
export const ROOT = {
|
package/src/schema/schema.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface InternalState {
|
|
|
73
73
|
readonly tables: Map<string, TableDef.Any>
|
|
74
74
|
readonly migrations: MigrationOptions
|
|
75
75
|
/** Compound hash of all table defs etc */
|
|
76
|
-
readonly hash:
|
|
76
|
+
readonly hash: string
|
|
77
77
|
}
|
|
78
78
|
readonly materializers: Map<string, Materializer>
|
|
79
79
|
}
|
|
@@ -10,9 +10,25 @@ import {
|
|
|
10
10
|
createOptimisticEventSchema,
|
|
11
11
|
mergeDefaultValues,
|
|
12
12
|
} from './client-document-def.ts'
|
|
13
|
+
import { fingerprint } from './db-schema/ast/fingerprint.ts'
|
|
13
14
|
import { getResultSchema } from './query-builder/impl.ts'
|
|
14
15
|
|
|
15
16
|
describe('client document table', () => {
|
|
17
|
+
test('tracks application value-schema changes in the storage fingerprint', () => {
|
|
18
|
+
const first = clientDocument({
|
|
19
|
+
name: 'documents',
|
|
20
|
+
schema: Schema.Struct({ title: Schema.String.check(Schema.isMinLength(1)) }),
|
|
21
|
+
default: { id: 'default', value: { title: 'valid' } },
|
|
22
|
+
})
|
|
23
|
+
const second = clientDocument({
|
|
24
|
+
name: 'documents',
|
|
25
|
+
schema: Schema.Struct({ title: Schema.String.check(Schema.isMinLength(2)) }),
|
|
26
|
+
default: { id: 'default', value: { title: 'valid' } },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(fingerprint(first.sqliteDef.ast)).not.toBe(fingerprint(second.sqliteDef.ast))
|
|
30
|
+
})
|
|
31
|
+
|
|
16
32
|
test('set event', () => {
|
|
17
33
|
expect(patchId(tables.UiState.set({ showSidebar: false }, 'session-1'))).toMatchInlineSnapshot(`
|
|
18
34
|
{
|
|
@@ -302,7 +318,7 @@ describe('client document table', () => {
|
|
|
302
318
|
})
|
|
303
319
|
const rowSchema = Schema.fromJsonString(optimisticSchema)
|
|
304
320
|
|
|
305
|
-
expect(Schema.
|
|
321
|
+
expect(Schema.decodeSync(rowSchema)(JSON.stringify(value))).toEqual(defaultValue)
|
|
306
322
|
})
|
|
307
323
|
|
|
308
324
|
test('decodes valid persisted JSON (encoded shape)', () => {
|
|
@@ -313,7 +329,7 @@ describe('client document table', () => {
|
|
|
313
329
|
})
|
|
314
330
|
const rowSchema = Schema.fromJsonString(optimisticSchema)
|
|
315
331
|
|
|
316
|
-
expect(Schema.
|
|
332
|
+
expect(Schema.decodeSync(rowSchema)(JSON.stringify(validPayload))).toEqual({
|
|
317
333
|
createdAt: new Date(42),
|
|
318
334
|
avatar: new Uint8Array([1, 2]),
|
|
319
335
|
})
|
|
@@ -327,7 +343,7 @@ describe('client document table', () => {
|
|
|
327
343
|
})
|
|
328
344
|
const rowSchema = Schema.fromJsonString(optimisticSchema)
|
|
329
345
|
|
|
330
|
-
expect(Schema.
|
|
346
|
+
expect(Schema.decodeSync(rowSchema)(JSON.stringify(extraFieldsPayload))).toEqual({
|
|
331
347
|
createdAt: new Date(42),
|
|
332
348
|
avatar: new Uint8Array([1, 2]),
|
|
333
349
|
})
|
|
@@ -345,7 +361,7 @@ describe('client document table', () => {
|
|
|
345
361
|
value: JSON.stringify(value),
|
|
346
362
|
}
|
|
347
363
|
|
|
348
|
-
expect(Schema.
|
|
364
|
+
expect(Schema.decodeSync(Doc.rowSchema)(row)).toEqual({ id: 'row-1', value: defaultValue })
|
|
349
365
|
})
|
|
350
366
|
|
|
351
367
|
test('decodes clientDocument rowSchema with valid encoded JSON', () => {
|
|
@@ -360,7 +376,7 @@ describe('client document table', () => {
|
|
|
360
376
|
value: JSON.stringify(validPayload),
|
|
361
377
|
}
|
|
362
378
|
|
|
363
|
-
expect(Schema.
|
|
379
|
+
expect(Schema.decodeSync(Doc.rowSchema)(row)).toEqual({
|
|
364
380
|
id: 'row-1',
|
|
365
381
|
value: { createdAt: new Date(42), avatar: new Uint8Array([1, 2]) },
|
|
366
382
|
})
|
|
@@ -382,7 +398,7 @@ describe('client document table', () => {
|
|
|
382
398
|
},
|
|
383
399
|
]
|
|
384
400
|
|
|
385
|
-
expect(Schema.
|
|
401
|
+
expect(Schema.decodeSync(resultSchema)(rawDbResults)).toEqual(defaultValue)
|
|
386
402
|
})
|
|
387
403
|
|
|
388
404
|
test('decodes RowQuery result schema with valid encoded JSON', () => {
|
|
@@ -401,7 +417,7 @@ describe('client document table', () => {
|
|
|
401
417
|
},
|
|
402
418
|
]
|
|
403
419
|
|
|
404
|
-
expect(Schema.
|
|
420
|
+
expect(Schema.decodeSync(resultSchema)(rawDbResults)).toEqual({
|
|
405
421
|
createdAt: new Date(42),
|
|
406
422
|
avatar: new Uint8Array([1, 2]),
|
|
407
423
|
})
|
|
@@ -241,7 +241,7 @@ export const createOptimisticEventSchema = ({
|
|
|
241
241
|
|
|
242
242
|
if (hasValidFields === true) {
|
|
243
243
|
try {
|
|
244
|
-
const decoded = Schema.
|
|
244
|
+
const decoded = Schema.decodeSync(targetSchema)(partialResult)
|
|
245
245
|
return encodeTarget(decoded)
|
|
246
246
|
} catch {
|
|
247
247
|
// Even filtered fields don't match schema
|
|
@@ -264,7 +264,7 @@ export const createOptimisticEventSchema = ({
|
|
|
264
264
|
|
|
265
265
|
// Try to decode the merged value
|
|
266
266
|
try {
|
|
267
|
-
const decoded = Schema.
|
|
267
|
+
const decoded = Schema.decodeSync(valueSchema)(merged)
|
|
268
268
|
return encodeTarget(decoded)
|
|
269
269
|
} catch {
|
|
270
270
|
// Merged value still doesn't match (e.g., type changes)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TestSchema } from 'effect/testing'
|
|
2
2
|
import { assert, describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import { Schema, SchemaAST, Struct } from '@livestore/utils/effect'
|
|
4
|
+
import { Schema, SchemaAST, SchemaTransformation, Struct } from '@livestore/utils/effect'
|
|
5
5
|
|
|
6
6
|
import * as State from '../mod.ts'
|
|
7
7
|
import { withAutoIncrement, withColumnType, withDefault, withPrimaryKey, withUnique } from './column-annotations.ts'
|
|
@@ -26,6 +26,9 @@ describe('getColumnDefForSchema', () => {
|
|
|
26
26
|
it('should map Schema.Date to text column', () => {
|
|
27
27
|
const columnDef = State.SQLite.getColumnDefForSchema(Schema.Date)
|
|
28
28
|
expect(columnDef.columnType).toBe('text')
|
|
29
|
+
const date = new Date('2026-01-02T03:04:05.678Z')
|
|
30
|
+
expect(Schema.encodeUnknownSync(columnDef.schema)(date)).toBe(date.toISOString())
|
|
31
|
+
expect(Schema.decodeSync(columnDef.schema)(date.toISOString())).toEqual(date)
|
|
29
32
|
expect(Schema.toEncoded(columnDef.schema).ast._tag).toBe('String')
|
|
30
33
|
expect(
|
|
31
34
|
SchemaAST.resolveAt<{ readonly id: string }>('representation')(Schema.toType(columnDef.schema).ast)?.id,
|
|
@@ -47,6 +50,84 @@ describe('getColumnDefForSchema', () => {
|
|
|
47
50
|
expect(State.SQLite.getColumnDefForSchema(schema).columnType).toBe('integer')
|
|
48
51
|
})
|
|
49
52
|
|
|
53
|
+
it('should preserve bare Date checks on encoding and decoding', () => {
|
|
54
|
+
const schema = Schema.Date.check(Schema.isGreaterThanDate(new Date(0)))
|
|
55
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
|
56
|
+
const date = new Date('2026-01-02T03:04:05.678Z')
|
|
57
|
+
|
|
58
|
+
expect(Schema.encodeUnknownSync(columnDef.schema)(date)).toBe(date.toISOString())
|
|
59
|
+
expect(Schema.decodeSync(columnDef.schema)(date.toISOString())).toEqual(date)
|
|
60
|
+
expect(() => Schema.encodeUnknownSync(columnDef.schema)(new Date(0))).toThrow()
|
|
61
|
+
expect(() => Schema.decodeSync(columnDef.schema)(new Date(0).toISOString())).toThrow()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should round-trip transformations with an encoded Date', () => {
|
|
65
|
+
const schema = Schema.Date.pipe(
|
|
66
|
+
Schema.decodeTo(
|
|
67
|
+
Schema.String,
|
|
68
|
+
SchemaTransformation.transform({
|
|
69
|
+
decode: (date) => date.toISOString(),
|
|
70
|
+
encode: (value) => new Date(value),
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
const columnDef = State.SQLite.getColumnDefForSchema(schema)
|
|
75
|
+
const value = '2026-01-02T03:04:05.678Z'
|
|
76
|
+
|
|
77
|
+
expect(columnDef.columnType).toBe('text')
|
|
78
|
+
expect(Schema.encodeUnknownSync(columnDef.schema)(value)).toBe(value)
|
|
79
|
+
expect(Schema.decodeSync(columnDef.schema)(value)).toBe(value)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it.each(['nullable', 'optional'] as const)(
|
|
83
|
+
'should round-trip %s transformations with an encoded Date',
|
|
84
|
+
(wrapper) => {
|
|
85
|
+
const schema = Schema.Date.pipe(
|
|
86
|
+
Schema.decodeTo(
|
|
87
|
+
Schema.String,
|
|
88
|
+
SchemaTransformation.transform({
|
|
89
|
+
decode: (date) => date.toISOString(),
|
|
90
|
+
encode: (value) => new Date(value),
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
const field = wrapper === 'nullable' ? Schema.NullOr(schema) : Schema.optional(schema)
|
|
95
|
+
const { columns } = State.SQLite.table({ name: 'stamps', schema: Schema.Struct({ value: field }) }).sqliteDef
|
|
96
|
+
const columnDef = columns.value
|
|
97
|
+
const value = '2026-01-02T03:04:05.678Z'
|
|
98
|
+
|
|
99
|
+
expect(columnDef.nullable).toBe(true)
|
|
100
|
+
expect(columnDef.columnType).toBe('text')
|
|
101
|
+
const encoded = Schema.encodeUnknownSync(columnDef.schema)(value)
|
|
102
|
+
expect(Schema.decodeSync(columnDef.schema)(encoded)).toBe(value)
|
|
103
|
+
expect(encoded).toBe(value)
|
|
104
|
+
expect(Schema.encodeUnknownSync(columnDef.schema)(null)).toBeNull()
|
|
105
|
+
expect(Schema.decodeSync(columnDef.schema)(null)).toBeNull()
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
it('should round-trip transformations with an encoded nullable Date', () => {
|
|
110
|
+
const schema = Schema.NullOr(Schema.Date).pipe(
|
|
111
|
+
Schema.decodeTo(
|
|
112
|
+
Schema.String,
|
|
113
|
+
SchemaTransformation.transform({
|
|
114
|
+
decode: (date) => (date === null ? 'none' : date.toISOString()),
|
|
115
|
+
encode: (value) => (value === 'none' ? null : new Date(value)),
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
const { columns } = State.SQLite.table({ name: 'stamps', schema: Schema.Struct({ value: schema }) }).sqliteDef
|
|
120
|
+
const columnDef = columns.value
|
|
121
|
+
const value = '2026-01-02T03:04:05.678Z'
|
|
122
|
+
const encoded = Schema.encodeUnknownSync(columnDef.schema)(value)
|
|
123
|
+
|
|
124
|
+
expect(Schema.decodeSync(columnDef.schema)(encoded)).toBe(value)
|
|
125
|
+
expect(encoded).toBe(value)
|
|
126
|
+
expect(columnDef.nullable).toBe(true)
|
|
127
|
+
expect(Schema.encodeUnknownSync(columnDef.schema)('none')).toBeNull()
|
|
128
|
+
expect(Schema.decodeSync(columnDef.schema)(null)).toBe('none')
|
|
129
|
+
})
|
|
130
|
+
|
|
50
131
|
it('should map Schema.BigInt to text column', () => {
|
|
51
132
|
const columnDef = State.SQLite.getColumnDefForSchema(Schema.BigInt)
|
|
52
133
|
expect(columnDef.columnType).toBe('text')
|
|
@@ -322,8 +403,8 @@ describe('getColumnDefForSchema', () => {
|
|
|
322
403
|
|
|
323
404
|
expect(columnDef.columnType).toBe('blob')
|
|
324
405
|
expect(columnDef.schema).toBe(schema)
|
|
325
|
-
expect(Schema.
|
|
326
|
-
expect(() => Schema.
|
|
406
|
+
expect(Schema.decodeSync(columnDef.schema)(new Uint8Array([1]))).toEqual(new Uint8Array([1]))
|
|
407
|
+
expect(() => Schema.decodeSync(columnDef.schema)(new Uint8Array())).toThrow()
|
|
327
408
|
})
|
|
328
409
|
})
|
|
329
410
|
|
|
@@ -167,6 +167,21 @@ const getColumnForSchema = (schema: Schema.Top, nullable = false): SqliteDsl.Col
|
|
|
167
167
|
// Get the encoded AST - what actually gets stored in SQLite
|
|
168
168
|
const encodedAst = Schema.toEncoded(coreSchema).ast
|
|
169
169
|
|
|
170
|
+
// Adapt Date values on the encoded side to ISO text, then run the original
|
|
171
|
+
// codec so its checks and transformations are preserved in both directions.
|
|
172
|
+
if (hasDateRepresentation(stripNullable(encodedAst)) === true) {
|
|
173
|
+
const encodedNullable = hasNull(encodedAst)
|
|
174
|
+
const dateStorage = encodedNullable === true ? Schema.NullOr(Schema.DateFromString) : Schema.DateFromString
|
|
175
|
+
const adaptedSchema = dateStorage.pipe(Schema.decodeTo(coreSchema))
|
|
176
|
+
return {
|
|
177
|
+
...SqliteDsl.text(),
|
|
178
|
+
schema: nullable === true ? Schema.NullOr(adaptedSchema) : adaptedSchema,
|
|
179
|
+
// Encoded nulls may decode to non-null values, so allow SQL NULL without
|
|
180
|
+
// bypassing the original codec's null transformation.
|
|
181
|
+
nullable: nullable === true || encodedNullable === true,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
170
185
|
// Check if the encoded type matches SQLite native types
|
|
171
186
|
if (SchemaAST.isString(encodedAst) === true) {
|
|
172
187
|
return SqliteDsl.text({ schema: coreSchema, nullable })
|
|
@@ -210,7 +225,7 @@ const stripNullable = (ast: SchemaAST.AST): SchemaAST.AST => {
|
|
|
210
225
|
return coreTypes[0]!
|
|
211
226
|
}
|
|
212
227
|
|
|
213
|
-
return new SchemaAST.Union(coreTypes, ast.
|
|
228
|
+
return new SchemaAST.Union(coreTypes, ast.options, ast.annotations)
|
|
214
229
|
}
|
|
215
230
|
|
|
216
231
|
const getLiteralColumnDefinition = (
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha2.js'
|
|
2
|
+
|
|
3
|
+
import { Base64Url } from '@livestore/utils/effect'
|
|
4
|
+
|
|
5
|
+
/** Preserve every digest bit while staying inside the 56-character SQLite VFS filename limit. */
|
|
6
|
+
export const digestToFingerprint = (bytes: Uint8Array): string => Base64Url.encode(sha256(bytes))
|