@livestore/common 0.3.1 → 0.3.2-dev.0
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 +35 -0
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -0
- package/dist/ClientSessionLeaderThreadProxy.js +6 -0
- package/dist/ClientSessionLeaderThreadProxy.js.map +1 -0
- package/dist/adapter-types.d.ts +10 -161
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +5 -49
- package/dist/adapter-types.js.map +1 -1
- package/dist/defs.d.ts +20 -0
- package/dist/defs.d.ts.map +1 -0
- package/dist/defs.js +12 -0
- package/dist/defs.js.map +1 -0
- package/dist/devtools/devtools-messages-client-session.d.ts +23 -21
- package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.d.ts +6 -6
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +26 -24
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +36 -0
- package/dist/errors.js.map +1 -0
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +6 -7
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +112 -122
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +17 -6
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +32 -17
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +1 -2
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +37 -7
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +7 -1
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +1 -0
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +1 -0
- package/dist/leader-thread/mod.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +13 -6
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +1 -3
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/types.d.ts +5 -7
- package/dist/leader-thread/types.d.ts.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 +1 -1
- package/dist/make-client-session.js.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +1 -1
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.js +10 -2
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +2 -2
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js +2 -2
- package/dist/schema/EventDef.js.map +1 -1
- package/dist/schema/EventSequenceNumber.d.ts +20 -2
- package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber.js +71 -19
- package/dist/schema/EventSequenceNumber.js.map +1 -1
- package/dist/schema/EventSequenceNumber.test.js +88 -3
- package/dist/schema/EventSequenceNumber.test.js.map +1 -1
- package/dist/schema/LiveStoreEvent.d.ts +25 -11
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +12 -4
- package/dist/schema/LiveStoreEvent.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -2
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/hash.js +3 -1
- package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -1
- package/dist/schema/state/sqlite/mod.d.ts +1 -1
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts +35 -8
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.js +16 -11
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +1 -81
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +34 -20
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +67 -62
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables.js +8 -17
- package/dist/schema/state/sqlite/system-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-management/migrations.d.ts +3 -1
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +2 -0
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sqlite-types.d.ts +72 -0
- package/dist/sqlite-types.d.ts.map +1 -0
- package/dist/sqlite-types.js +5 -0
- package/dist/sqlite-types.js.map +1 -0
- package/dist/sync/ClientSessionSyncProcessor.d.ts +6 -2
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +16 -13
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/next/graphology.d.ts.map +1 -1
- package/dist/sync/next/graphology.js +0 -6
- package/dist/sync/next/graphology.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +1 -0
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/compact-events.test.js +1 -1
- package/dist/sync/next/test/compact-events.test.js.map +1 -1
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/event-fixtures.js +12 -3
- package/dist/sync/next/test/event-fixtures.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -0
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +3 -0
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +13 -4
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +23 -10
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +17 -17
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +7 -6
- package/src/ClientSessionLeaderThreadProxy.ts +40 -0
- package/src/adapter-types.ts +19 -166
- package/src/defs.ts +17 -0
- package/src/errors.ts +49 -0
- package/src/leader-thread/LeaderSyncProcessor.ts +141 -180
- package/src/leader-thread/eventlog.ts +78 -56
- package/src/leader-thread/leader-worker-devtools.ts +1 -2
- package/src/leader-thread/make-leader-thread-layer.ts +52 -8
- package/src/leader-thread/materialize-event.ts +8 -1
- package/src/leader-thread/mod.ts +1 -0
- package/src/leader-thread/recreate-db.ts +99 -91
- package/src/leader-thread/types.ts +6 -11
- package/src/make-client-session.ts +2 -2
- package/src/rematerialize-from-eventlog.ts +10 -2
- package/src/schema/EventDef.ts +5 -3
- package/src/schema/EventSequenceNumber.test.ts +120 -3
- package/src/schema/EventSequenceNumber.ts +95 -23
- package/src/schema/LiveStoreEvent.ts +20 -4
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +2 -2
- package/src/schema/state/sqlite/db-schema/hash.ts +3 -3
- package/src/schema/state/sqlite/mod.ts +1 -1
- package/src/schema/state/sqlite/query-builder/api.ts +38 -8
- package/src/schema/state/sqlite/query-builder/impl.test.ts +60 -20
- package/src/schema/state/sqlite/query-builder/impl.ts +15 -12
- package/src/schema/state/sqlite/system-tables.ts +9 -22
- package/src/schema/state/sqlite/table-def.ts +1 -1
- package/src/schema-management/migrations.ts +3 -1
- package/src/sql-queries/sql-queries.ts +2 -0
- package/src/sqlite-types.ts +76 -0
- package/src/sync/ClientSessionSyncProcessor.ts +17 -20
- package/src/sync/next/graphology.ts +0 -6
- package/src/sync/next/rebase-events.ts +1 -0
- package/src/sync/next/test/compact-events.test.ts +1 -1
- package/src/sync/next/test/event-fixtures.ts +12 -3
- package/src/sync/sync.ts +3 -0
- package/src/sync/syncstate.test.ts +17 -17
- package/src/sync/syncstate.ts +31 -10
- package/src/version.ts +1 -1
@@ -4,9 +4,126 @@ import { expect } from 'vitest'
|
|
4
4
|
import { EventSequenceNumber } from './mod.js'
|
5
5
|
|
6
6
|
Vitest.describe('EventSequenceNumber', () => {
|
7
|
-
Vitest.test('nextPair', () => {
|
7
|
+
Vitest.test('nextPair (no rebase)', () => {
|
8
8
|
const e_0_0 = EventSequenceNumber.make({ global: 0, client: 0 })
|
9
|
-
expect(EventSequenceNumber.nextPair(e_0_0, false).seqNum).toStrictEqual({
|
10
|
-
|
9
|
+
expect(EventSequenceNumber.nextPair({ seqNum: e_0_0, isClient: false }).seqNum).toStrictEqual({
|
10
|
+
global: 1,
|
11
|
+
client: 0,
|
12
|
+
rebaseGeneration: 0,
|
13
|
+
})
|
14
|
+
expect(EventSequenceNumber.nextPair({ seqNum: e_0_0, isClient: true }).seqNum).toStrictEqual({
|
15
|
+
global: 0,
|
16
|
+
client: 1,
|
17
|
+
rebaseGeneration: 0,
|
18
|
+
})
|
19
|
+
})
|
20
|
+
|
21
|
+
Vitest.test('nextPair (rebase)', () => {
|
22
|
+
const e_0_0 = EventSequenceNumber.make({ global: 0, client: 0 })
|
23
|
+
expect(EventSequenceNumber.nextPair({ seqNum: e_0_0, isClient: false, rebaseGeneration: 1 }).seqNum).toStrictEqual({
|
24
|
+
global: 1,
|
25
|
+
client: 0,
|
26
|
+
rebaseGeneration: 1,
|
27
|
+
})
|
28
|
+
expect(EventSequenceNumber.nextPair({ seqNum: e_0_0, isClient: true, rebaseGeneration: 1 }).seqNum).toStrictEqual({
|
29
|
+
global: 0,
|
30
|
+
client: 1,
|
31
|
+
rebaseGeneration: 1,
|
32
|
+
})
|
33
|
+
|
34
|
+
const e_0_0_g1 = EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 2 })
|
35
|
+
expect(EventSequenceNumber.nextPair({ seqNum: e_0_0_g1, isClient: false }).seqNum).toStrictEqual({
|
36
|
+
global: 1,
|
37
|
+
client: 0,
|
38
|
+
rebaseGeneration: 2,
|
39
|
+
})
|
40
|
+
})
|
41
|
+
|
42
|
+
Vitest.test('toString', () => {
|
43
|
+
expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 0, client: 0 }))).toBe('e0')
|
44
|
+
expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 1 }))).toBe(
|
45
|
+
'e0r1',
|
46
|
+
)
|
47
|
+
expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 0, client: 1 }))).toBe('e0+1')
|
48
|
+
expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 0, client: 1, rebaseGeneration: 1 }))).toBe(
|
49
|
+
'e0+1r1',
|
50
|
+
)
|
51
|
+
expect(EventSequenceNumber.toString(EventSequenceNumber.make({ global: 5, client: 3, rebaseGeneration: 2 }))).toBe(
|
52
|
+
'e5+3r2',
|
53
|
+
)
|
54
|
+
})
|
55
|
+
|
56
|
+
Vitest.test('fromString', () => {
|
57
|
+
// Basic cases
|
58
|
+
expect(EventSequenceNumber.fromString('e0')).toStrictEqual(EventSequenceNumber.make({ global: 0, client: 0 }))
|
59
|
+
expect(EventSequenceNumber.fromString('e0r1')).toStrictEqual(
|
60
|
+
EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 1 }),
|
61
|
+
)
|
62
|
+
expect(EventSequenceNumber.fromString('e0+1')).toStrictEqual(EventSequenceNumber.make({ global: 0, client: 1 }))
|
63
|
+
expect(EventSequenceNumber.fromString('e0+1r1')).toStrictEqual(
|
64
|
+
EventSequenceNumber.make({ global: 0, client: 1, rebaseGeneration: 1 }),
|
65
|
+
)
|
66
|
+
expect(EventSequenceNumber.fromString('e5+3r2')).toStrictEqual(
|
67
|
+
EventSequenceNumber.make({ global: 5, client: 3, rebaseGeneration: 2 }),
|
68
|
+
)
|
69
|
+
|
70
|
+
// Error cases
|
71
|
+
expect(() => EventSequenceNumber.fromString('0')).toThrow(
|
72
|
+
'Invalid event sequence number string: must start with "e"',
|
73
|
+
)
|
74
|
+
expect(() => EventSequenceNumber.fromString('eabc')).toThrow(
|
75
|
+
'Invalid event sequence number string: invalid number format',
|
76
|
+
)
|
77
|
+
expect(() => EventSequenceNumber.fromString('e0+abc')).toThrow(
|
78
|
+
'Invalid event sequence number string: invalid number format',
|
79
|
+
)
|
80
|
+
expect(() => EventSequenceNumber.fromString('e0rabc')).toThrow(
|
81
|
+
'Invalid event sequence number string: invalid number format',
|
82
|
+
)
|
83
|
+
})
|
84
|
+
|
85
|
+
Vitest.test('toString/fromString roundtrip', () => {
|
86
|
+
const testCases = [
|
87
|
+
{ global: 0, client: 0, rebaseGeneration: 0 },
|
88
|
+
{ global: 0, client: 0, rebaseGeneration: 1 },
|
89
|
+
{ global: 0, client: 1, rebaseGeneration: 0 },
|
90
|
+
{ global: 0, client: 1, rebaseGeneration: 1 },
|
91
|
+
{ global: 5, client: 3, rebaseGeneration: 2 },
|
92
|
+
{ global: 100, client: 50, rebaseGeneration: 10 },
|
93
|
+
]
|
94
|
+
|
95
|
+
for (const testCase of testCases) {
|
96
|
+
const original = EventSequenceNumber.make(testCase)
|
97
|
+
const str = EventSequenceNumber.toString(original)
|
98
|
+
const parsed = EventSequenceNumber.fromString(str)
|
99
|
+
expect(parsed).toStrictEqual(original)
|
100
|
+
}
|
101
|
+
})
|
102
|
+
|
103
|
+
Vitest.test('compare', () => {
|
104
|
+
const e_0_0_r0 = EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 0 })
|
105
|
+
const e_0_0_r1 = EventSequenceNumber.make({ global: 0, client: 0, rebaseGeneration: 1 })
|
106
|
+
const e_0_1_r0 = EventSequenceNumber.make({ global: 0, client: 1, rebaseGeneration: 0 })
|
107
|
+
const e_0_1_r1 = EventSequenceNumber.make({ global: 0, client: 1, rebaseGeneration: 1 })
|
108
|
+
const e_1_0_r0 = EventSequenceNumber.make({ global: 1, client: 0, rebaseGeneration: 0 })
|
109
|
+
const e_1_1_r0 = EventSequenceNumber.make({ global: 1, client: 1, rebaseGeneration: 0 })
|
110
|
+
|
111
|
+
// Global comparison (strongest level)
|
112
|
+
expect(EventSequenceNumber.compare(e_0_0_r0, e_1_0_r0)).toBeLessThan(0)
|
113
|
+
expect(EventSequenceNumber.compare(e_1_0_r0, e_0_0_r0)).toBeGreaterThan(0)
|
114
|
+
expect(EventSequenceNumber.compare(e_0_1_r1, e_1_0_r0)).toBeLessThan(0) // global overrides client and rebase
|
115
|
+
|
116
|
+
// Client comparison (second level)
|
117
|
+
expect(EventSequenceNumber.compare(e_0_0_r0, e_0_1_r0)).toBeLessThan(0)
|
118
|
+
expect(EventSequenceNumber.compare(e_0_1_r0, e_0_0_r0)).toBeGreaterThan(0)
|
119
|
+
expect(EventSequenceNumber.compare(e_0_0_r1, e_0_1_r0)).toBeLessThan(0) // client overrides rebase
|
120
|
+
|
121
|
+
// Rebase generation comparison (weakest level)
|
122
|
+
expect(EventSequenceNumber.compare(e_0_0_r0, e_0_0_r1)).toBeLessThan(0)
|
123
|
+
expect(EventSequenceNumber.compare(e_0_0_r1, e_0_0_r0)).toBeGreaterThan(0)
|
124
|
+
|
125
|
+
// Equal comparison
|
126
|
+
expect(EventSequenceNumber.compare(e_0_0_r0, e_0_0_r0)).toBe(0)
|
127
|
+
expect(EventSequenceNumber.compare(e_1_1_r0, e_1_1_r0)).toBe(0)
|
11
128
|
})
|
12
129
|
})
|
@@ -10,6 +10,8 @@ export const GlobalEventSequenceNumber = Schema.fromBrand(globalEventSequenceNum
|
|
10
10
|
|
11
11
|
export const clientDefault = 0 as any as ClientEventSequenceNumber
|
12
12
|
|
13
|
+
export const rebaseGenerationDefault = 0
|
14
|
+
|
13
15
|
/**
|
14
16
|
* LiveStore event sequence number value consisting of a globally unique event sequence number
|
15
17
|
* and a client sequence number.
|
@@ -20,16 +22,17 @@ export type EventSequenceNumber = {
|
|
20
22
|
global: GlobalEventSequenceNumber
|
21
23
|
client: ClientEventSequenceNumber
|
22
24
|
/**
|
23
|
-
*
|
25
|
+
* Generation integer that is incremented whenever the client rebased.
|
26
|
+
* Starts from and resets to 0 for each global sequence number.
|
24
27
|
*/
|
25
|
-
|
28
|
+
rebaseGeneration: number
|
26
29
|
}
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
// export const GlobalEventSequenceNumber = Schema.Struct({})
|
31
|
+
export type EventSequenceNumberInput =
|
32
|
+
| EventSequenceNumber
|
33
|
+
| (Omit<typeof EventSequenceNumber.Encoded, 'rebaseGeneration'> & { rebaseGeneration?: number })
|
32
34
|
|
35
|
+
// TODO adjust name to `ClientEventSequenceNumber`
|
33
36
|
/**
|
34
37
|
* NOTE: Client mutation events with a non-0 client id, won't be synced to the sync backend.
|
35
38
|
*/
|
@@ -40,36 +43,83 @@ export const EventSequenceNumber = Schema.Struct({
|
|
40
43
|
|
41
44
|
// TODO also provide a way to see "confirmation level" of event (e.g. confirmed by leader/sync backend)
|
42
45
|
|
43
|
-
// TODO: actually add this field
|
44
46
|
// Client only
|
45
|
-
|
46
|
-
}).annotations({
|
47
|
+
rebaseGeneration: Schema.Int,
|
48
|
+
}).annotations({
|
49
|
+
title: 'LiveStore.EventSequenceNumber',
|
50
|
+
pretty: () => (seqNum) => toString(seqNum),
|
51
|
+
})
|
47
52
|
|
48
53
|
/**
|
49
54
|
* Compare two event sequence numbers i.e. checks if the first event sequence number is less than the second.
|
55
|
+
* Comparison hierarchy: global > client > rebaseGeneration
|
50
56
|
*/
|
51
57
|
export const compare = (a: EventSequenceNumber, b: EventSequenceNumber) => {
|
52
58
|
if (a.global !== b.global) {
|
53
59
|
return a.global - b.global
|
54
60
|
}
|
55
|
-
|
61
|
+
if (a.client !== b.client) {
|
62
|
+
return a.client - b.client
|
63
|
+
}
|
64
|
+
return a.rebaseGeneration - b.rebaseGeneration
|
56
65
|
}
|
57
66
|
|
58
67
|
/**
|
59
68
|
* Convert an event sequence number to a string representation.
|
60
69
|
*/
|
61
|
-
export const toString = (seqNum: EventSequenceNumber) =>
|
62
|
-
seqNum.
|
70
|
+
export const toString = (seqNum: EventSequenceNumber) => {
|
71
|
+
const rebaseGenerationStr = seqNum.rebaseGeneration > 0 ? `r${seqNum.rebaseGeneration}` : ''
|
72
|
+
return seqNum.client === 0
|
73
|
+
? `e${seqNum.global}${rebaseGenerationStr}`
|
74
|
+
: `e${seqNum.global}+${seqNum.client}${rebaseGenerationStr}`
|
75
|
+
}
|
63
76
|
|
64
77
|
/**
|
65
78
|
* Convert a string representation of an event sequence number to an event sequence number.
|
79
|
+
* Parses strings in the format: e{global}[+{client}][r{rebaseGeneration}]
|
80
|
+
* Examples: "e0", "e0r1", "e0+1", "e0+1r1"
|
66
81
|
*/
|
67
82
|
export const fromString = (str: string): EventSequenceNumber => {
|
68
|
-
|
69
|
-
|
70
|
-
|
83
|
+
if (!str.startsWith('e')) {
|
84
|
+
throw new Error('Invalid event sequence number string: must start with "e"')
|
85
|
+
}
|
86
|
+
|
87
|
+
// Remove the 'e' prefix
|
88
|
+
const remaining = str.slice(1)
|
89
|
+
|
90
|
+
// Parse rebase generation if present
|
91
|
+
let rebaseGeneration = rebaseGenerationDefault
|
92
|
+
let withoutRebase = remaining
|
93
|
+
const rebaseMatch = remaining.match(/r(\d+)$/)
|
94
|
+
if (rebaseMatch !== null) {
|
95
|
+
rebaseGeneration = Number.parseInt(rebaseMatch[1]!, 10)
|
96
|
+
withoutRebase = remaining.slice(0, -rebaseMatch[0].length)
|
97
|
+
}
|
98
|
+
|
99
|
+
// Parse global and client parts
|
100
|
+
const parts = withoutRebase.split('+')
|
101
|
+
|
102
|
+
// Validate that parts contain only digits (and possibly empty for client)
|
103
|
+
if (parts[0] === '' || !/^\d+$/.test(parts[0]!)) {
|
104
|
+
throw new Error('Invalid event sequence number string: invalid number format')
|
105
|
+
}
|
106
|
+
|
107
|
+
if (parts.length > 1 && parts[1] !== undefined && (parts[1] === '' || !/^\d+$/.test(parts[1]))) {
|
108
|
+
throw new Error('Invalid event sequence number string: invalid number format')
|
109
|
+
}
|
110
|
+
|
111
|
+
const global = Number.parseInt(parts[0]!, 10)
|
112
|
+
const client = parts.length > 1 && parts[1] !== undefined ? Number.parseInt(parts[1], 10) : 0
|
113
|
+
|
114
|
+
if (Number.isNaN(global) || Number.isNaN(client) || Number.isNaN(rebaseGeneration)) {
|
115
|
+
throw new TypeError('Invalid event sequence number string: invalid number format')
|
116
|
+
}
|
117
|
+
|
118
|
+
return {
|
119
|
+
global: global as any as GlobalEventSequenceNumber,
|
120
|
+
client: client as any as ClientEventSequenceNumber,
|
121
|
+
rebaseGeneration,
|
71
122
|
}
|
72
|
-
return { global, client } as EventSequenceNumber
|
73
123
|
}
|
74
124
|
|
75
125
|
export const isEqual = (a: EventSequenceNumber, b: EventSequenceNumber) =>
|
@@ -80,6 +130,7 @@ export type EventSequenceNumberPair = { seqNum: EventSequenceNumber; parentSeqNu
|
|
80
130
|
export const ROOT = {
|
81
131
|
global: 0 as any as GlobalEventSequenceNumber,
|
82
132
|
client: clientDefault,
|
133
|
+
rebaseGeneration: rebaseGenerationDefault,
|
83
134
|
} satisfies EventSequenceNumber
|
84
135
|
|
85
136
|
export const isGreaterThan = (a: EventSequenceNumber, b: EventSequenceNumber) => {
|
@@ -101,21 +152,42 @@ export const diff = (a: EventSequenceNumber, b: EventSequenceNumber) => {
|
|
101
152
|
}
|
102
153
|
}
|
103
154
|
|
104
|
-
export const make = (seqNum:
|
105
|
-
return Schema.is(EventSequenceNumber)(seqNum)
|
155
|
+
export const make = (seqNum: EventSequenceNumberInput): EventSequenceNumber => {
|
156
|
+
return Schema.is(EventSequenceNumber)(seqNum)
|
157
|
+
? seqNum
|
158
|
+
: Schema.decodeSync(EventSequenceNumber)({
|
159
|
+
...seqNum,
|
160
|
+
rebaseGeneration: seqNum.rebaseGeneration ?? rebaseGenerationDefault,
|
161
|
+
})
|
106
162
|
}
|
107
163
|
|
108
|
-
export const nextPair = (
|
109
|
-
|
164
|
+
export const nextPair = ({
|
165
|
+
seqNum,
|
166
|
+
isClient,
|
167
|
+
rebaseGeneration,
|
168
|
+
}: {
|
169
|
+
seqNum: EventSequenceNumber
|
170
|
+
isClient: boolean
|
171
|
+
rebaseGeneration?: number
|
172
|
+
}): EventSequenceNumberPair => {
|
173
|
+
if (isClient) {
|
110
174
|
return {
|
111
|
-
seqNum: {
|
175
|
+
seqNum: {
|
176
|
+
global: seqNum.global,
|
177
|
+
client: (seqNum.client + 1) as any as ClientEventSequenceNumber,
|
178
|
+
rebaseGeneration: rebaseGeneration ?? seqNum.rebaseGeneration,
|
179
|
+
},
|
112
180
|
parentSeqNum: seqNum,
|
113
181
|
}
|
114
182
|
}
|
115
183
|
|
116
184
|
return {
|
117
|
-
seqNum: {
|
185
|
+
seqNum: {
|
186
|
+
global: (seqNum.global + 1) as any as GlobalEventSequenceNumber,
|
187
|
+
client: clientDefault,
|
188
|
+
rebaseGeneration: rebaseGeneration ?? seqNum.rebaseGeneration,
|
189
|
+
},
|
118
190
|
// NOTE we always point to `client: 0` for non-clientOnly events
|
119
|
-
parentSeqNum: { global: seqNum.global, client: clientDefault },
|
191
|
+
parentSeqNum: { global: seqNum.global, client: clientDefault, rebaseGeneration: seqNum.rebaseGeneration },
|
120
192
|
}
|
121
193
|
}
|
@@ -221,10 +221,18 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('LiveStoreEve
|
|
221
221
|
* +---- global number
|
222
222
|
* Client num is ommitted for global events
|
223
223
|
*/
|
224
|
-
rebase = (
|
224
|
+
rebase = ({
|
225
|
+
parentSeqNum,
|
226
|
+
isClient,
|
227
|
+
rebaseGeneration,
|
228
|
+
}: {
|
229
|
+
parentSeqNum: EventSequenceNumber.EventSequenceNumber
|
230
|
+
isClient: boolean
|
231
|
+
rebaseGeneration: number
|
232
|
+
}) =>
|
225
233
|
new EncodedWithMeta({
|
226
234
|
...this,
|
227
|
-
...EventSequenceNumber.nextPair(parentSeqNum, isClient),
|
235
|
+
...EventSequenceNumber.nextPair({ seqNum: parentSeqNum, isClient, rebaseGeneration }),
|
228
236
|
})
|
229
237
|
|
230
238
|
static fromGlobal = (
|
@@ -237,8 +245,16 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('LiveStoreEve
|
|
237
245
|
) =>
|
238
246
|
new EncodedWithMeta({
|
239
247
|
...event,
|
240
|
-
seqNum: {
|
241
|
-
|
248
|
+
seqNum: {
|
249
|
+
global: event.seqNum,
|
250
|
+
client: EventSequenceNumber.clientDefault,
|
251
|
+
rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
|
252
|
+
},
|
253
|
+
parentSeqNum: {
|
254
|
+
global: event.parentSeqNum,
|
255
|
+
client: EventSequenceNumber.clientDefault,
|
256
|
+
rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
|
257
|
+
},
|
242
258
|
meta: {
|
243
259
|
sessionChangeset: { _tag: 'unset' as const },
|
244
260
|
syncMetadata: meta.syncMetadata,
|
@@ -17,7 +17,7 @@ export const isColumnDefinition = (value: unknown): value is ColumnDefinition<an
|
|
17
17
|
typeof value === 'object' &&
|
18
18
|
value !== null &&
|
19
19
|
'columnType' in value &&
|
20
|
-
validColumnTypes.includes(value
|
20
|
+
validColumnTypes.includes(value.columnType as any)
|
21
21
|
)
|
22
22
|
}
|
23
23
|
|
@@ -36,7 +36,7 @@ export type SqlDefaultValue = {
|
|
36
36
|
}
|
37
37
|
|
38
38
|
export const isSqlDefaultValue = (value: unknown): value is SqlDefaultValue => {
|
39
|
-
return typeof value === 'object' && value !== null && 'sql' in value && typeof value
|
39
|
+
return typeof value === 'object' && value !== null && 'sql' in value && typeof value.sql === 'string'
|
40
40
|
}
|
41
41
|
|
42
42
|
export type ColDefFn<TColumnType extends FieldColumnType> = {
|
@@ -1,8 +1,8 @@
|
|
1
1
|
// Based on https://stackoverflow.com/a/7616484
|
2
2
|
export const hashCode = (str: string) => {
|
3
|
-
let hash = 0
|
4
|
-
|
5
|
-
|
3
|
+
let hash = 0
|
4
|
+
let i: number
|
5
|
+
let chr: number
|
6
6
|
if (str.length === 0) return hash
|
7
7
|
for (i = 0; i < str.length; i++) {
|
8
8
|
// eslint-disable-next-line unicorn/prefer-code-point
|
@@ -6,7 +6,7 @@ import type { InternalState } from '../../schema.js'
|
|
6
6
|
import { ClientDocumentTableDefSymbol, tableIsClientDocumentTable } from './client-document-def.js'
|
7
7
|
import { SqliteAst } from './db-schema/mod.js'
|
8
8
|
import { stateSystemTables } from './system-tables.js'
|
9
|
-
import {
|
9
|
+
import type { TableDef, TableDefBase } from './table-def.js'
|
10
10
|
|
11
11
|
export * from './table-def.js'
|
12
12
|
export {
|
@@ -19,7 +19,11 @@ export namespace QueryBuilderAst {
|
|
19
19
|
export interface SelectQuery {
|
20
20
|
readonly _tag: 'SelectQuery'
|
21
21
|
readonly columns: string[]
|
22
|
-
readonly pickFirst:
|
22
|
+
readonly pickFirst:
|
23
|
+
| { _tag: 'disabled' }
|
24
|
+
| { _tag: 'enabled'; behaviour: 'undefined' }
|
25
|
+
| { _tag: 'enabled'; behaviour: 'error' }
|
26
|
+
| { _tag: 'enabled'; behaviour: 'fallback'; fallback: () => any }
|
23
27
|
readonly select: {
|
24
28
|
columns: ReadonlyArray<string>
|
25
29
|
}
|
@@ -167,6 +171,21 @@ export namespace QueryBuilder {
|
|
167
171
|
direction: 'asc' | 'desc'
|
168
172
|
}>
|
169
173
|
|
174
|
+
export type FirstQueryBehaviour<TResult, TFallback> =
|
175
|
+
| {
|
176
|
+
/** Will error if no matching row was found */
|
177
|
+
behaviour: 'error'
|
178
|
+
}
|
179
|
+
| {
|
180
|
+
/** Will return `undefined` if no matching row was found */
|
181
|
+
behaviour: 'undefined'
|
182
|
+
}
|
183
|
+
| {
|
184
|
+
/** Will return a fallback value if no matching row was found */
|
185
|
+
behaviour: 'fallback'
|
186
|
+
fallback: () => TResult | TFallback
|
187
|
+
}
|
188
|
+
|
170
189
|
export type ApiFull<TResult, TTableDef extends TableDefBase, TWithout extends ApiFeature> = {
|
171
190
|
/**
|
172
191
|
* `SELECT *` is the default
|
@@ -285,16 +304,27 @@ export namespace QueryBuilder {
|
|
285
304
|
* Example:
|
286
305
|
* ```ts
|
287
306
|
* db.todos.first()
|
288
|
-
* db.todos.where('id', '123').first()
|
307
|
+
* db.todos.where('id', '123').first() // will return `undefined` if no rows are returned
|
308
|
+
* db.todos.where('id', '123').first({ behaviour: 'error' }) // will throw if no rows are returned
|
309
|
+
* db.todos.first({ behaviour: 'fallback', fallback: () => ({ id: '123', text: 'Buy milk', status: 'active' }) })
|
289
310
|
* ```
|
290
311
|
*
|
291
|
-
*
|
312
|
+
* Behaviour:
|
313
|
+
* - `undefined`: Will return `undefined` if no rows are returned (default behaviour)
|
314
|
+
* - `error`: Will throw if no rows are returned
|
315
|
+
* - `fallback`: Will return a fallback value if no rows are returned
|
292
316
|
*/
|
293
|
-
readonly first: <
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
317
|
+
readonly first: <
|
318
|
+
TBehaviour extends QueryBuilder.FirstQueryBehaviour<GetSingle<TResult>, TFallback>,
|
319
|
+
TFallback = never,
|
320
|
+
>(
|
321
|
+
behaviour?: QueryBuilder.FirstQueryBehaviour<GetSingle<TResult>, TFallback> & TBehaviour,
|
322
|
+
) => QueryBuilder<
|
323
|
+
TBehaviour extends { behaviour: 'fallback' }
|
324
|
+
? ReturnType<TBehaviour['fallback']> | GetSingle<TResult>
|
325
|
+
: TBehaviour extends { behaviour: 'undefined' }
|
326
|
+
? undefined | GetSingle<TResult>
|
327
|
+
: GetSingle<TResult>,
|
298
328
|
TTableDef,
|
299
329
|
TWithout | 'row' | 'first' | 'orderBy' | 'select' | 'limit' | 'offset' | 'where' | 'returning' | 'onConflict'
|
300
330
|
>
|
@@ -55,7 +55,7 @@ const UiStateWithDefaultId = State.SQLite.clientDocument({
|
|
55
55
|
},
|
56
56
|
})
|
57
57
|
|
58
|
-
|
58
|
+
const issue = State.SQLite.table({
|
59
59
|
name: 'issue',
|
60
60
|
columns: {
|
61
61
|
id: State.SQLite.integer({ primaryKey: true }),
|
@@ -120,7 +120,19 @@ describe('query builder', () => {
|
|
120
120
|
}
|
121
121
|
`)
|
122
122
|
|
123
|
-
expect(dump(db.todos.select('id', 'text').first({
|
123
|
+
expect(dump(db.todos.select('id', 'text').first({ behaviour: 'error' }))).toMatchInlineSnapshot(`
|
124
|
+
{
|
125
|
+
"bindValues": [
|
126
|
+
1,
|
127
|
+
],
|
128
|
+
"query": "SELECT id, text FROM 'todos' LIMIT ?",
|
129
|
+
"schema": "(ReadonlyArray<{ readonly id: string; readonly text: string }> <-> { readonly id: string; readonly text: string })",
|
130
|
+
}
|
131
|
+
`)
|
132
|
+
|
133
|
+
expect(
|
134
|
+
dump(db.todos.select('id', 'text').first({ behaviour: 'fallback', fallback: () => undefined })),
|
135
|
+
).toMatchInlineSnapshot(`
|
124
136
|
{
|
125
137
|
"bindValues": [
|
126
138
|
1,
|
@@ -166,8 +178,9 @@ describe('query builder', () => {
|
|
166
178
|
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
167
179
|
}
|
168
180
|
`)
|
169
|
-
expect(
|
170
|
-
.
|
181
|
+
expect(
|
182
|
+
dump(db.todos.select('id', 'text').where({ deletedAt: { op: '<=', value: new Date('2024-01-01') } })),
|
183
|
+
).toMatchInlineSnapshot(`
|
171
184
|
{
|
172
185
|
"bindValues": [
|
173
186
|
"2024-01-01T00:00:00.000Z",
|
@@ -176,8 +189,9 @@ describe('query builder', () => {
|
|
176
189
|
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
177
190
|
}
|
178
191
|
`)
|
179
|
-
expect(
|
180
|
-
.
|
192
|
+
expect(
|
193
|
+
dump(db.todos.select('id', 'text').where({ status: { op: 'IN', value: ['active'] } })),
|
194
|
+
).toMatchInlineSnapshot(`
|
181
195
|
{
|
182
196
|
"bindValues": [
|
183
197
|
"active",
|
@@ -186,8 +200,9 @@ describe('query builder', () => {
|
|
186
200
|
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
187
201
|
}
|
188
202
|
`)
|
189
|
-
expect(
|
190
|
-
.
|
203
|
+
expect(
|
204
|
+
dump(db.todos.select('id', 'text').where({ status: { op: 'NOT IN', value: ['active', 'completed'] } })),
|
205
|
+
).toMatchInlineSnapshot(`
|
191
206
|
{
|
192
207
|
"bindValues": [
|
193
208
|
"active",
|
@@ -197,6 +212,25 @@ describe('query builder', () => {
|
|
197
212
|
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
198
213
|
}
|
199
214
|
`)
|
215
|
+
|
216
|
+
expect(
|
217
|
+
dump(
|
218
|
+
db.todos
|
219
|
+
.select('id', 'text')
|
220
|
+
.where({ completed: false })
|
221
|
+
.where({ status: { op: 'IN', value: ['active'] } })
|
222
|
+
.where({ deletedAt: undefined }),
|
223
|
+
),
|
224
|
+
).toMatchInlineSnapshot(`
|
225
|
+
{
|
226
|
+
"bindValues": [
|
227
|
+
0,
|
228
|
+
"active",
|
229
|
+
],
|
230
|
+
"query": "SELECT id, text FROM 'todos' WHERE completed = ? AND status IN (?)",
|
231
|
+
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
232
|
+
}
|
233
|
+
`)
|
200
234
|
})
|
201
235
|
|
202
236
|
it('should handle OFFSET and LIMIT clauses', () => {
|
@@ -375,8 +409,9 @@ describe('query builder', () => {
|
|
375
409
|
})
|
376
410
|
|
377
411
|
it('should handle INSERT queries with undefined values', () => {
|
378
|
-
expect(
|
379
|
-
.
|
412
|
+
expect(
|
413
|
+
dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active', completed: undefined })),
|
414
|
+
).toMatchInlineSnapshot(`
|
380
415
|
{
|
381
416
|
"bindValues": [
|
382
417
|
"123",
|
@@ -443,8 +478,9 @@ describe('query builder', () => {
|
|
443
478
|
})
|
444
479
|
|
445
480
|
it('should handle UPDATE queries with undefined values', () => {
|
446
|
-
expect(
|
447
|
-
.
|
481
|
+
expect(
|
482
|
+
dump(db.todos.update({ status: undefined, text: 'some text' }).where({ id: '123' })),
|
483
|
+
).toMatchInlineSnapshot(`
|
448
484
|
{
|
449
485
|
"bindValues": [
|
450
486
|
"some text",
|
@@ -483,8 +519,9 @@ describe('query builder', () => {
|
|
483
519
|
})
|
484
520
|
|
485
521
|
it('should handle INSERT with ON CONFLICT', () => {
|
486
|
-
expect(
|
487
|
-
.
|
522
|
+
expect(
|
523
|
+
dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'ignore')),
|
524
|
+
).toMatchInlineSnapshot(`
|
488
525
|
{
|
489
526
|
"bindValues": [
|
490
527
|
"123",
|
@@ -516,8 +553,9 @@ describe('query builder', () => {
|
|
516
553
|
}
|
517
554
|
`)
|
518
555
|
|
519
|
-
expect(
|
520
|
-
.
|
556
|
+
expect(
|
557
|
+
dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'replace')),
|
558
|
+
).toMatchInlineSnapshot(`
|
521
559
|
{
|
522
560
|
"bindValues": [
|
523
561
|
"123",
|
@@ -547,8 +585,9 @@ describe('query builder', () => {
|
|
547
585
|
})
|
548
586
|
|
549
587
|
it('should handle RETURNING clause', () => {
|
550
|
-
expect(
|
551
|
-
.
|
588
|
+
expect(
|
589
|
+
dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).returning('id')),
|
590
|
+
).toMatchInlineSnapshot(`
|
552
591
|
{
|
553
592
|
"bindValues": [
|
554
593
|
"123",
|
@@ -560,8 +599,9 @@ describe('query builder', () => {
|
|
560
599
|
}
|
561
600
|
`)
|
562
601
|
|
563
|
-
expect(
|
564
|
-
.
|
602
|
+
expect(
|
603
|
+
dump(db.todos.update({ status: 'completed' }).where({ id: '123' }).returning('id')),
|
604
|
+
).toMatchInlineSnapshot(`
|
565
605
|
{
|
566
606
|
"bindValues": [
|
567
607
|
"completed",
|