@livestore/common 0.4.0-dev.12 → 0.4.0-dev.14
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/devtools/devtools-messages-client-session.d.ts +21 -21
- package/dist/devtools/devtools-messages-common.d.ts +6 -6
- package/dist/devtools/devtools-messages-leader.d.ts +24 -24
- package/dist/devtools/devtools-sessioninfo.d.ts +12 -0
- package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -1
- package/dist/devtools/devtools-sessioninfo.js +6 -0
- package/dist/devtools/devtools-sessioninfo.js.map +1 -1
- package/dist/devtools/mod.d.ts +13 -2
- package/dist/devtools/mod.d.ts.map +1 -1
- package/dist/devtools/mod.js +10 -3
- package/dist/devtools/mod.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +2 -2
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/eventlog.js +2 -1
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/make-client-session.d.ts +3 -1
- package/dist/make-client-session.d.ts.map +1 -1
- package/dist/make-client-session.js +5 -2
- package/dist/make-client-session.js.map +1 -1
- package/dist/schema/EventSequenceNumber.d.ts +10 -4
- package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber.js +10 -6
- package/dist/schema/EventSequenceNumber.js.map +1 -1
- package/dist/schema/EventSequenceNumber.test.js +7 -7
- package/dist/schema/LiveStoreEvent.d.ts +4 -4
- package/dist/schema/LiveStoreEvent.js +4 -4
- package/dist/schema/mod.d.ts +1 -1
- package/dist/schema/mod.d.ts.map +1 -1
- package/dist/schema/mod.js +1 -1
- package/dist/schema/mod.js.map +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.js +9 -0
- package/dist/schema/state/sqlite/column-def.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.test.js +10 -0
- package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/mod.js +1 -1
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.d.ts +547 -0
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.d.ts.map +1 -0
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.js +54 -0
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.js.map +1 -0
- package/dist/schema/state/sqlite/system-tables/mod.d.ts +3 -0
- package/dist/schema/state/sqlite/system-tables/mod.d.ts.map +1 -0
- package/dist/schema/state/sqlite/system-tables/mod.js +3 -0
- package/dist/schema/state/sqlite/system-tables/mod.js.map +1 -0
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts +456 -0
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts.map +1 -0
- package/dist/schema/state/sqlite/system-tables/state-tables.js +55 -0
- package/dist/schema/state/sqlite/system-tables/state-tables.js.map +1 -0
- package/dist/schema-management/migrations.d.ts +30 -0
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +31 -1
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/sync/syncstate.d.ts +2 -2
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +9 -4
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +36 -0
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/version.d.ts +15 -5
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +15 -5
- package/dist/version.js.map +1 -1
- package/package.json +4 -4
- package/src/devtools/devtools-sessioninfo.ts +6 -0
- package/src/devtools/mod.ts +11 -2
- package/src/index.ts +1 -1
- package/src/leader-thread/LeaderSyncProcessor.ts +2 -2
- package/src/leader-thread/eventlog.ts +2 -2
- package/src/make-client-session.ts +7 -1
- package/src/schema/EventSequenceNumber.test.ts +7 -7
- package/src/schema/EventSequenceNumber.ts +13 -7
- package/src/schema/LiveStoreEvent.ts +4 -4
- package/src/schema/mod.ts +1 -1
- package/src/schema/schema.ts +1 -1
- package/src/schema/state/sqlite/column-def.test.ts +13 -0
- package/src/schema/state/sqlite/column-def.ts +16 -0
- package/src/schema/state/sqlite/mod.ts +1 -1
- package/src/schema/state/sqlite/system-tables/eventlog-tables.ts +64 -0
- package/src/schema/state/sqlite/system-tables/mod.ts +2 -0
- package/src/schema/state/sqlite/system-tables/state-tables.ts +69 -0
- package/src/schema-management/migrations.ts +33 -2
- package/src/sync/syncstate.test.ts +42 -0
- package/src/sync/syncstate.ts +11 -4
- package/src/version.ts +15 -5
- package/src/schema/state/sqlite/system-tables.ts +0 -106
|
@@ -381,6 +381,19 @@ describe('getColumnDefForSchema', () => {
|
|
|
381
381
|
expect((table.rowSchema as any).fields.count.toString()).toBe('Int | null')
|
|
382
382
|
})
|
|
383
383
|
|
|
384
|
+
it('should treat unions of string literals as text columns without JSON parsing', () => {
|
|
385
|
+
const schema = Schema.Struct({
|
|
386
|
+
id: Schema.String,
|
|
387
|
+
status: Schema.Literal('idle', 'running', 'stopped'),
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const table = State.SQLite.table({ name: 'timers', schema })
|
|
391
|
+
|
|
392
|
+
expect(table.sqliteDef.columns.status.columnType).toBe('text')
|
|
393
|
+
expect(table.sqliteDef.columns.status.schema.toString()).toBe('"idle" | "running" | "stopped"')
|
|
394
|
+
expect((table.rowSchema as any).fields.status.toString()).toBe('"idle" | "running" | "stopped"')
|
|
395
|
+
})
|
|
396
|
+
|
|
384
397
|
it('should handle Schema.NullOr with complex types', () => {
|
|
385
398
|
const schema = Schema.Struct({
|
|
386
399
|
data: Schema.NullOr(Schema.Struct({ value: Schema.Number })),
|
|
@@ -185,6 +185,10 @@ const getColumnForSchema = (schema: Schema.Schema.AnyNoContext, nullable = false
|
|
|
185
185
|
if (typeof value === 'boolean') return SqliteDsl.boolean({ nullable })
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
if (isLiteralUnionOf(coreAst, (value): value is string => typeof value === 'string')) {
|
|
189
|
+
return SqliteDsl.text({ schema: coreSchema, nullable })
|
|
190
|
+
}
|
|
191
|
+
|
|
188
192
|
// Literals based on their encoded type
|
|
189
193
|
if (SchemaAST.isLiteral(encodedAst)) {
|
|
190
194
|
const value = encodedAst.literal
|
|
@@ -199,6 +203,10 @@ const getColumnForSchema = (schema: Schema.Schema.AnyNoContext, nullable = false
|
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
|
|
206
|
+
if (isLiteralUnionOf(encodedAst, (value): value is string => typeof value === 'string')) {
|
|
207
|
+
return SqliteDsl.text({ schema: coreSchema, nullable })
|
|
208
|
+
}
|
|
209
|
+
|
|
202
210
|
// Everything else needs JSON encoding
|
|
203
211
|
return SqliteDsl.json({ schema: coreSchema, nullable })
|
|
204
212
|
}
|
|
@@ -221,3 +229,11 @@ const stripNullable = (ast: SchemaAST.AST): SchemaAST.AST => {
|
|
|
221
229
|
|
|
222
230
|
return SchemaAST.Union.make(coreTypes, ast.annotations)
|
|
223
231
|
}
|
|
232
|
+
|
|
233
|
+
const isLiteralUnionOf = <T extends SchemaAST.LiteralValue>(
|
|
234
|
+
ast: SchemaAST.AST,
|
|
235
|
+
predicate: (value: SchemaAST.LiteralValue) => value is T,
|
|
236
|
+
): ast is SchemaAST.Union & { types: ReadonlyArray<SchemaAST.Literal & { literal: T }> } =>
|
|
237
|
+
SchemaAST.isUnion(ast) &&
|
|
238
|
+
ast.types.length > 0 &&
|
|
239
|
+
ast.types.every((type) => SchemaAST.isLiteral(type) && predicate(type.literal))
|
|
@@ -5,7 +5,7 @@ import type { Materializer } from '../../EventDef.ts'
|
|
|
5
5
|
import type { InternalState } from '../../schema.ts'
|
|
6
6
|
import { ClientDocumentTableDefSymbol, tableIsClientDocumentTable } from './client-document-def.ts'
|
|
7
7
|
import { SqliteAst } from './db-schema/mod.ts'
|
|
8
|
-
import { stateSystemTables } from './system-tables.ts'
|
|
8
|
+
import { stateSystemTables } from './system-tables/state-tables.ts'
|
|
9
9
|
import type { TableDef, TableDefBase } from './table-def.ts'
|
|
10
10
|
|
|
11
11
|
export * from '../../EventDef.ts'
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Schema } from '@livestore/utils/effect'
|
|
2
|
+
|
|
3
|
+
import * as EventSequenceNumber from '../../../EventSequenceNumber.ts'
|
|
4
|
+
import { SqliteDsl } from '../db-schema/mod.ts'
|
|
5
|
+
import { table } from '../table-def.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* EVENTLOG DATABASE SYSTEM TABLES
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ CRITICAL: NEVER modify eventlog schemas without bumping `liveStoreStorageFormatVersion`!
|
|
11
|
+
* Eventlog is the source of truth - schema changes cause permanent data loss.
|
|
12
|
+
*
|
|
13
|
+
* TODO: Implement proper eventlog versioning system to prevent accidental data loss
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const EVENTLOG_META_TABLE = 'eventlog'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Main client-side event log storing all events (global and local/rebased).
|
|
20
|
+
*/
|
|
21
|
+
export const eventlogMetaTable = table({
|
|
22
|
+
name: EVENTLOG_META_TABLE,
|
|
23
|
+
columns: {
|
|
24
|
+
// TODO Adjust modeling so a global event never needs a client id component
|
|
25
|
+
seqNumGlobal: SqliteDsl.integer({ primaryKey: true, schema: EventSequenceNumber.GlobalEventSequenceNumber }),
|
|
26
|
+
seqNumClient: SqliteDsl.integer({ primaryKey: true, schema: EventSequenceNumber.ClientEventSequenceNumber }),
|
|
27
|
+
seqNumRebaseGeneration: SqliteDsl.integer({ primaryKey: true }),
|
|
28
|
+
parentSeqNumGlobal: SqliteDsl.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
|
|
29
|
+
parentSeqNumClient: SqliteDsl.integer({ schema: EventSequenceNumber.ClientEventSequenceNumber }),
|
|
30
|
+
parentSeqNumRebaseGeneration: SqliteDsl.integer({}),
|
|
31
|
+
/** Event definition name */
|
|
32
|
+
name: SqliteDsl.text({}),
|
|
33
|
+
argsJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Any) }),
|
|
34
|
+
clientId: SqliteDsl.text({}),
|
|
35
|
+
sessionId: SqliteDsl.text({}),
|
|
36
|
+
schemaHash: SqliteDsl.integer({}),
|
|
37
|
+
syncMetadataJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Option(Schema.JsonValue)) }),
|
|
38
|
+
},
|
|
39
|
+
indexes: [
|
|
40
|
+
{ columns: ['seqNumGlobal'], name: 'idx_eventlog_seqNumGlobal' },
|
|
41
|
+
{ columns: ['seqNumGlobal', 'seqNumClient', 'seqNumRebaseGeneration'], name: 'idx_eventlog_seqNum' },
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export type EventlogMetaRow = typeof eventlogMetaTable.Type
|
|
46
|
+
|
|
47
|
+
export const SYNC_STATUS_TABLE = '__livestore_sync_status'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tracks sync status including the remote head position and backend identity.
|
|
51
|
+
*/
|
|
52
|
+
// TODO support sync backend identity (to detect if sync backend changes)
|
|
53
|
+
export const syncStatusTable = table({
|
|
54
|
+
name: SYNC_STATUS_TABLE,
|
|
55
|
+
columns: {
|
|
56
|
+
head: SqliteDsl.integer({ primaryKey: true }),
|
|
57
|
+
// Null means the sync backend is not yet connected and we haven't yet seen a backend ID
|
|
58
|
+
backendId: SqliteDsl.text({ nullable: true }),
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export type SyncStatusRow = typeof syncStatusTable.Type
|
|
63
|
+
|
|
64
|
+
export const eventlogSystemTables = [eventlogMetaTable, syncStatusTable] as const
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as EventSequenceNumber from '../../../EventSequenceNumber.ts'
|
|
2
|
+
import { SqliteDsl } from '../db-schema/mod.ts'
|
|
3
|
+
import { table } from '../table-def.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* STATE DATABASE SYSTEM TABLES
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ SAFE TO CHANGE: State tables are automatically rebuilt from eventlog when schema changes.
|
|
9
|
+
* No need to bump `liveStoreStorageFormatVersion` (uses hash-based migration via SqliteAst.hash()).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const SCHEMA_META_TABLE = '__livestore_schema'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tracks schema hashes for user-defined tables to detect schema changes.
|
|
16
|
+
*/
|
|
17
|
+
export const schemaMetaTable = table({
|
|
18
|
+
name: SCHEMA_META_TABLE,
|
|
19
|
+
columns: {
|
|
20
|
+
tableName: SqliteDsl.text({ primaryKey: true }),
|
|
21
|
+
schemaHash: SqliteDsl.integer({ nullable: false }),
|
|
22
|
+
/** ISO date format */
|
|
23
|
+
updatedAt: SqliteDsl.text({ nullable: false }),
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export type SchemaMetaRow = typeof schemaMetaTable.Type
|
|
28
|
+
|
|
29
|
+
export const SCHEMA_EVENT_DEFS_META_TABLE = '__livestore_schema_event_defs'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tracks schema hashes for event definitions to detect event schema changes.
|
|
33
|
+
*/
|
|
34
|
+
export const schemaEventDefsMetaTable = table({
|
|
35
|
+
name: SCHEMA_EVENT_DEFS_META_TABLE,
|
|
36
|
+
columns: {
|
|
37
|
+
eventName: SqliteDsl.text({ primaryKey: true }),
|
|
38
|
+
schemaHash: SqliteDsl.integer({ nullable: false }),
|
|
39
|
+
/** ISO date format */
|
|
40
|
+
updatedAt: SqliteDsl.text({ nullable: false }),
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export type SchemaEventDefsMetaRow = typeof schemaEventDefsMetaTable.Type
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Table which stores SQLite changeset blobs which is used for rolling back
|
|
48
|
+
* read-model state during rebasing.
|
|
49
|
+
*/
|
|
50
|
+
export const SESSION_CHANGESET_META_TABLE = '__livestore_session_changeset'
|
|
51
|
+
|
|
52
|
+
export const sessionChangesetMetaTable = table({
|
|
53
|
+
name: SESSION_CHANGESET_META_TABLE,
|
|
54
|
+
columns: {
|
|
55
|
+
// TODO bring back primary key
|
|
56
|
+
seqNumGlobal: SqliteDsl.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
|
|
57
|
+
seqNumClient: SqliteDsl.integer({ schema: EventSequenceNumber.ClientEventSequenceNumber }),
|
|
58
|
+
seqNumRebaseGeneration: SqliteDsl.integer({}),
|
|
59
|
+
changeset: SqliteDsl.blob({ nullable: true }),
|
|
60
|
+
debug: SqliteDsl.json({ nullable: true }),
|
|
61
|
+
},
|
|
62
|
+
indexes: [{ columns: ['seqNumGlobal', 'seqNumClient'], name: 'idx_session_changeset_id' }],
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export type SessionChangesetMetaRow = typeof sessionChangesetMetaTable.Type
|
|
66
|
+
|
|
67
|
+
export const stateSystemTables = [schemaMetaTable, schemaEventDefsMetaTable, sessionChangesetMetaTable] as const
|
|
68
|
+
|
|
69
|
+
export const isStateSystemTable = (tableName: string) => stateSystemTables.some((_) => _.sqliteDef.name === tableName)
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTOMATIC HASH-BASED SCHEMA MIGRATIONS
|
|
3
|
+
*
|
|
4
|
+
* This module implements automatic schema versioning using hash-based change detection.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ CRITICAL DISTINCTION:
|
|
7
|
+
* - STATE TABLES (safe to modify): Changes trigger rematerialization from eventlog
|
|
8
|
+
* - EVENTLOG TABLES (NEVER modify): Changes cause data loss - need manual versioning!
|
|
9
|
+
*
|
|
10
|
+
* How it works:
|
|
11
|
+
* 1. Each table's schema is hashed using SqliteAst.hash()
|
|
12
|
+
* 2. Hashes are stored in SCHEMA_META_TABLE after successful migrations
|
|
13
|
+
* 3. On app start, current schema hashes are compared with stored hashes
|
|
14
|
+
* 4. Mismatches trigger migrations:
|
|
15
|
+
* - State tables: Recreated and repopulated from eventlog (safe, no data loss)
|
|
16
|
+
* - Eventlog tables: Uses 'create-if-not-exists' (UNSAFE - causes data loss!)
|
|
17
|
+
*
|
|
18
|
+
* State Table Changes (SAFE):
|
|
19
|
+
* - User-defined tables are rebuilt from eventlog
|
|
20
|
+
* - System tables (schemaMetaTable, etc.) are recreated
|
|
21
|
+
* - Data preserved through rematerializeFromEventlog()
|
|
22
|
+
*
|
|
23
|
+
* Eventlog Table Changes (UNSAFE):
|
|
24
|
+
* - eventlogMetaTable, syncStatusTable changes cause "soft reset"
|
|
25
|
+
* - Old table becomes inaccessible (but remains in DB)
|
|
26
|
+
* - No automatic migration - effectively data loss
|
|
27
|
+
* - TODO: Implement proper EVENTLOG_PERSISTENCE_FORMAT_VERSION system
|
|
28
|
+
*
|
|
29
|
+
* See system-tables/state-tables.ts and system-tables/eventlog-tables.ts for detailed documentation on each table type.
|
|
30
|
+
*/
|
|
31
|
+
|
|
1
32
|
import { memoizeByStringifyArgs } from '@livestore/utils'
|
|
2
33
|
import { Effect } from '@livestore/utils/effect'
|
|
3
34
|
|
|
@@ -7,14 +38,14 @@ import type { UnexpectedError } from '../errors.ts'
|
|
|
7
38
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
|
8
39
|
import { makeColumnSpec } from '../schema/state/sqlite/column-spec.ts'
|
|
9
40
|
import { SqliteAst } from '../schema/state/sqlite/db-schema/mod.ts'
|
|
10
|
-
import type { SchemaEventDefsMetaRow, SchemaMetaRow } from '../schema/state/sqlite/system-tables.ts'
|
|
41
|
+
import type { SchemaEventDefsMetaRow, SchemaMetaRow } from '../schema/state/sqlite/system-tables/state-tables.ts'
|
|
11
42
|
import {
|
|
12
43
|
isStateSystemTable,
|
|
13
44
|
SCHEMA_EVENT_DEFS_META_TABLE,
|
|
14
45
|
SCHEMA_META_TABLE,
|
|
15
46
|
schemaEventDefsMetaTable,
|
|
16
47
|
stateSystemTables,
|
|
17
|
-
} from '../schema/state/sqlite/system-tables.ts'
|
|
48
|
+
} from '../schema/state/sqlite/system-tables/state-tables.ts'
|
|
18
49
|
import { sql } from '../util.ts'
|
|
19
50
|
import type { SchemaManager } from './common.ts'
|
|
20
51
|
import { dbExecute, dbSelect } from './common.ts'
|
|
@@ -441,6 +441,48 @@ describe('syncstate', () => {
|
|
|
441
441
|
expectEventArraysEqual(result.newEvents, [e1_1, e1_2, e1_3])
|
|
442
442
|
expectEventArraysEqual(result.confirmedEvents, [])
|
|
443
443
|
})
|
|
444
|
+
|
|
445
|
+
// Leaders can choose to ignore client-only events while still returning them for broadcast.
|
|
446
|
+
// Ensure pending/local head only reflects events that must be pushed upstream.
|
|
447
|
+
it('keeps pending empty when pushing only client-only events that are being ignored', () => {
|
|
448
|
+
const syncState = new SyncState.SyncState({
|
|
449
|
+
pending: [],
|
|
450
|
+
upstreamHead: EventSequenceNumber.ROOT,
|
|
451
|
+
localHead: EventSequenceNumber.ROOT,
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const result = merge({
|
|
455
|
+
syncState,
|
|
456
|
+
payload: SyncState.PayloadLocalPush.make({ newEvents: [e0_1] }),
|
|
457
|
+
ignoreClientEvents: true,
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
expectAdvance(result)
|
|
461
|
+
expectEventArraysEqual(result.newSyncState.pending, [])
|
|
462
|
+
expect(result.newSyncState.upstreamHead).toMatchObject(EventSequenceNumber.ROOT)
|
|
463
|
+
expect(result.newSyncState.localHead).toMatchObject(EventSequenceNumber.ROOT)
|
|
464
|
+
expectEventArraysEqual(result.newEvents, [e0_1])
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('appends only upstream-bound events to pending when ignoring client-only pushes', () => {
|
|
468
|
+
const syncState = new SyncState.SyncState({
|
|
469
|
+
pending: [],
|
|
470
|
+
upstreamHead: EventSequenceNumber.ROOT,
|
|
471
|
+
localHead: EventSequenceNumber.ROOT,
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
const result = merge({
|
|
475
|
+
syncState,
|
|
476
|
+
payload: SyncState.PayloadLocalPush.make({ newEvents: [e0_1, e1_0] }),
|
|
477
|
+
ignoreClientEvents: true,
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
expectAdvance(result)
|
|
481
|
+
expectEventArraysEqual(result.newSyncState.pending, [e1_0])
|
|
482
|
+
expect(result.newSyncState.upstreamHead).toMatchObject(EventSequenceNumber.ROOT)
|
|
483
|
+
expect(result.newSyncState.localHead).toMatchObject(e1_0.seqNum)
|
|
484
|
+
expectEventArraysEqual(result.newEvents, [e0_1, e1_0])
|
|
485
|
+
})
|
|
444
486
|
})
|
|
445
487
|
|
|
446
488
|
describe('reject', () => {
|
package/src/sync/syncstate.ts
CHANGED
|
@@ -15,7 +15,7 @@ import * as LiveStoreEvent from '../schema/LiveStoreEvent.ts'
|
|
|
15
15
|
* +------------------------+
|
|
16
16
|
* ▼ ▼
|
|
17
17
|
* Upstream Head Local Head
|
|
18
|
-
*
|
|
18
|
+
* e1 e1.1, e1.2, e2
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
21
|
* **Pending Events**: Events awaiting acknowledgment from the upstream.
|
|
@@ -31,7 +31,7 @@ import * as LiveStoreEvent from '../schema/LiveStoreEvent.ts'
|
|
|
31
31
|
* Invariants:
|
|
32
32
|
* 1. **Chain Continuity**: Each event must reference its immediate parent.
|
|
33
33
|
* 2. **Head Ordering**: Upstream Head ≤ Local Head.
|
|
34
|
-
* 3. **Event number sequence**: Must follow the pattern
|
|
34
|
+
* 3. **Event number sequence**: Must follow the pattern e1→e1.1→e1.2→e2.
|
|
35
35
|
*
|
|
36
36
|
* A few further notes to help form an intuition:
|
|
37
37
|
* - The goal is to keep the pending events as small as possible (i.e. to have synced with the next upstream node)
|
|
@@ -407,13 +407,20 @@ export const merge = ({
|
|
|
407
407
|
}),
|
|
408
408
|
)
|
|
409
409
|
} else {
|
|
410
|
+
const nonClientEvents = ignoreClientEvents
|
|
411
|
+
? payload.newEvents.filter((event) => !isClientEvent(event))
|
|
412
|
+
: payload.newEvents
|
|
413
|
+
const newPending = [...syncState.pending, ...nonClientEvents]
|
|
414
|
+
const newLocalHead =
|
|
415
|
+
newPending.at(-1)?.seqNum ?? EventSequenceNumber.max(syncState.localHead, syncState.upstreamHead)
|
|
416
|
+
|
|
410
417
|
return validateMergeResult(
|
|
411
418
|
MergeResultAdvance.make({
|
|
412
419
|
_tag: 'advance',
|
|
413
420
|
newSyncState: new SyncState({
|
|
414
|
-
pending:
|
|
421
|
+
pending: newPending,
|
|
415
422
|
upstreamHead: syncState.upstreamHead,
|
|
416
|
-
localHead:
|
|
423
|
+
localHead: newLocalHead,
|
|
417
424
|
}),
|
|
418
425
|
newEvents: payload.newEvents,
|
|
419
426
|
confirmedEvents: [],
|
package/src/version.ts
CHANGED
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
// import packageJson from '../package.json' with { type: 'json' }
|
|
3
3
|
// export const liveStoreVersion = packageJson.version
|
|
4
4
|
|
|
5
|
-
export const liveStoreVersion = '0.4.0-dev.
|
|
5
|
+
export const liveStoreVersion = '0.4.0-dev.14' as const
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* Whenever this version changes, LiveStore will start with fresh database files. Old database files are not deleted.
|
|
8
|
+
* CRITICAL: Increment this version whenever you modify client-side EVENTLOG table schemas.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Used to generate database file names (e.g., `eventlog@6.db`, `state@6.db`) across all client adapters.
|
|
11
|
+
*
|
|
12
|
+
* Bump required when:
|
|
13
|
+
* - Modifying eventlog system tables (eventlogMetaTable, syncStatusTable) in schema/state/sqlite/system-tables/eventlog-tables.ts
|
|
14
|
+
* - Changing columns, types, constraints, or indexes in eventlog tables
|
|
15
|
+
*
|
|
16
|
+
* Bump NOT required when:
|
|
17
|
+
* - Modifying STATE table schemas (auto-migrated via hash-based detection and rebuilt from eventlog)
|
|
18
|
+
* - Changing query patterns or client-side implementation details
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ CRITICAL: Eventlog changes without bumping this version cause permanent data loss!
|
|
21
|
+
*
|
|
22
|
+
* Impact: Version changes trigger a "soft reset" - old data becomes inaccessible but remains on disk.
|
|
13
23
|
*/
|
|
14
24
|
export const liveStoreStorageFormatVersion = 6
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { Schema } from '@livestore/utils/effect'
|
|
2
|
-
|
|
3
|
-
import * as EventSequenceNumber from '../../EventSequenceNumber.ts'
|
|
4
|
-
import { SqliteDsl } from './db-schema/mod.ts'
|
|
5
|
-
import { table } from './table-def.ts'
|
|
6
|
-
|
|
7
|
-
/// State DB
|
|
8
|
-
|
|
9
|
-
export const SCHEMA_META_TABLE = '__livestore_schema'
|
|
10
|
-
|
|
11
|
-
export const schemaMetaTable = table({
|
|
12
|
-
name: SCHEMA_META_TABLE,
|
|
13
|
-
columns: {
|
|
14
|
-
tableName: SqliteDsl.text({ primaryKey: true }),
|
|
15
|
-
schemaHash: SqliteDsl.integer({ nullable: false }),
|
|
16
|
-
/** ISO date format */
|
|
17
|
-
updatedAt: SqliteDsl.text({ nullable: false }),
|
|
18
|
-
},
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
export type SchemaMetaRow = typeof schemaMetaTable.Type
|
|
22
|
-
|
|
23
|
-
export const SCHEMA_EVENT_DEFS_META_TABLE = '__livestore_schema_event_defs'
|
|
24
|
-
|
|
25
|
-
export const schemaEventDefsMetaTable = table({
|
|
26
|
-
name: SCHEMA_EVENT_DEFS_META_TABLE,
|
|
27
|
-
columns: {
|
|
28
|
-
eventName: SqliteDsl.text({ primaryKey: true }),
|
|
29
|
-
schemaHash: SqliteDsl.integer({ nullable: false }),
|
|
30
|
-
/** ISO date format */
|
|
31
|
-
updatedAt: SqliteDsl.text({ nullable: false }),
|
|
32
|
-
},
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
export type SchemaEventDefsMetaRow = typeof schemaEventDefsMetaTable.Type
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Table which stores SQLite changeset blobs which is used for rolling back
|
|
39
|
-
* read-model state during rebasing.
|
|
40
|
-
*/
|
|
41
|
-
export const SESSION_CHANGESET_META_TABLE = '__livestore_session_changeset'
|
|
42
|
-
|
|
43
|
-
export const sessionChangesetMetaTable = table({
|
|
44
|
-
name: SESSION_CHANGESET_META_TABLE,
|
|
45
|
-
columns: {
|
|
46
|
-
// TODO bring back primary key
|
|
47
|
-
seqNumGlobal: SqliteDsl.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
|
|
48
|
-
seqNumClient: SqliteDsl.integer({ schema: EventSequenceNumber.ClientEventSequenceNumber }),
|
|
49
|
-
seqNumRebaseGeneration: SqliteDsl.integer({}),
|
|
50
|
-
changeset: SqliteDsl.blob({ nullable: true }),
|
|
51
|
-
debug: SqliteDsl.json({ nullable: true }),
|
|
52
|
-
},
|
|
53
|
-
indexes: [{ columns: ['seqNumGlobal', 'seqNumClient'], name: 'idx_session_changeset_id' }],
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
export type SessionChangesetMetaRow = typeof sessionChangesetMetaTable.Type
|
|
57
|
-
|
|
58
|
-
export const stateSystemTables = [schemaMetaTable, schemaEventDefsMetaTable, sessionChangesetMetaTable] as const
|
|
59
|
-
|
|
60
|
-
export const isStateSystemTable = (tableName: string) => stateSystemTables.some((_) => _.sqliteDef.name === tableName)
|
|
61
|
-
|
|
62
|
-
/// Eventlog DB
|
|
63
|
-
|
|
64
|
-
export const EVENTLOG_META_TABLE = 'eventlog'
|
|
65
|
-
|
|
66
|
-
export const eventlogMetaTable = table({
|
|
67
|
-
name: EVENTLOG_META_TABLE,
|
|
68
|
-
columns: {
|
|
69
|
-
// TODO Adjust modeling so a global event never needs a client id component
|
|
70
|
-
seqNumGlobal: SqliteDsl.integer({ primaryKey: true, schema: EventSequenceNumber.GlobalEventSequenceNumber }),
|
|
71
|
-
seqNumClient: SqliteDsl.integer({ primaryKey: true, schema: EventSequenceNumber.ClientEventSequenceNumber }),
|
|
72
|
-
seqNumRebaseGeneration: SqliteDsl.integer({ primaryKey: true }),
|
|
73
|
-
parentSeqNumGlobal: SqliteDsl.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
|
|
74
|
-
parentSeqNumClient: SqliteDsl.integer({ schema: EventSequenceNumber.ClientEventSequenceNumber }),
|
|
75
|
-
parentSeqNumRebaseGeneration: SqliteDsl.integer({}),
|
|
76
|
-
/** Event definition name */
|
|
77
|
-
name: SqliteDsl.text({}),
|
|
78
|
-
argsJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Any) }),
|
|
79
|
-
clientId: SqliteDsl.text({}),
|
|
80
|
-
sessionId: SqliteDsl.text({}),
|
|
81
|
-
schemaHash: SqliteDsl.integer({}),
|
|
82
|
-
syncMetadataJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Option(Schema.JsonValue)) }),
|
|
83
|
-
},
|
|
84
|
-
indexes: [
|
|
85
|
-
{ columns: ['seqNumGlobal'], name: 'idx_eventlog_seqNumGlobal' },
|
|
86
|
-
{ columns: ['seqNumGlobal', 'seqNumClient', 'seqNumRebaseGeneration'], name: 'idx_eventlog_seqNum' },
|
|
87
|
-
],
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
export type EventlogMetaRow = typeof eventlogMetaTable.Type
|
|
91
|
-
|
|
92
|
-
export const SYNC_STATUS_TABLE = '__livestore_sync_status'
|
|
93
|
-
|
|
94
|
-
// TODO support sync backend identity (to detect if sync backend changes)
|
|
95
|
-
export const syncStatusTable = table({
|
|
96
|
-
name: SYNC_STATUS_TABLE,
|
|
97
|
-
columns: {
|
|
98
|
-
head: SqliteDsl.integer({ primaryKey: true }),
|
|
99
|
-
// Null means the sync backend is not yet connected and we haven't yet seen a backend ID
|
|
100
|
-
backendId: SqliteDsl.text({ nullable: true }),
|
|
101
|
-
},
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
export type SyncStatusRow = typeof syncStatusTable.Type
|
|
105
|
-
|
|
106
|
-
export const eventlogSystemTables = [eventlogMetaTable, syncStatusTable] as const
|