@livestore/common 0.3.1-dev.0 → 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 -156
- 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-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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- 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 +122 -123
- 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 +34 -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 +3 -3
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +27 -10
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +2 -0
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +2 -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 +6 -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/materializer-helper.d.ts +13 -2
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +25 -11
- package/dist/materializer-helper.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 +12 -4
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +8 -3
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js +5 -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 +56 -8
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +34 -8
- 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 +36 -9
- 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 -86
- 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 +380 -432
- 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 +2 -2
- 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-db-helper.d.ts +7 -0
- package/dist/sqlite-db-helper.d.ts.map +1 -0
- package/dist/sqlite-db-helper.js +29 -0
- package/dist/sqlite-db-helper.js.map +1 -0
- 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 +12 -3
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +37 -19
- 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.js +1 -1
- package/package.json +7 -6
- package/src/ClientSessionLeaderThreadProxy.ts +40 -0
- package/src/adapter-types.ts +19 -161
- package/src/defs.ts +17 -0
- package/src/errors.ts +49 -0
- package/src/index.ts +1 -0
- package/src/leader-thread/LeaderSyncProcessor.ts +157 -181
- package/src/leader-thread/eventlog.ts +78 -54
- 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 +33 -12
- package/src/leader-thread/mod.ts +2 -0
- package/src/leader-thread/recreate-db.ts +99 -91
- package/src/leader-thread/types.ts +10 -12
- package/src/make-client-session.ts +2 -2
- package/src/materializer-helper.ts +45 -19
- package/src/rematerialize-from-eventlog.ts +12 -4
- package/src/schema/EventDef.ts +16 -4
- package/src/schema/EventSequenceNumber.test.ts +120 -3
- package/src/schema/EventSequenceNumber.ts +95 -23
- package/src/schema/LiveStoreEvent.ts +49 -8
- 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 +39 -9
- 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 +2 -2
- package/src/schema-management/migrations.ts +3 -1
- package/src/sql-queries/sql-queries.ts +2 -0
- package/src/sqlite-db-helper.ts +41 -0
- package/src/sqlite-types.ts +76 -0
- package/src/sync/ClientSessionSyncProcessor.ts +51 -28
- 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
@@ -12,13 +12,13 @@ import type {
|
|
12
12
|
import { Context, Schema } from '@livestore/utils/effect'
|
13
13
|
import type { MeshNode } from '@livestore/webmesh'
|
14
14
|
|
15
|
-
import type {
|
15
|
+
import type { MigrationsReport } from '../defs.js'
|
16
|
+
import type { SqliteError } from '../errors.js'
|
16
17
|
import type {
|
17
18
|
BootStatus,
|
18
19
|
Devtools,
|
19
20
|
LeaderAheadError,
|
20
21
|
MakeSqliteDb,
|
21
|
-
MigrationsReport,
|
22
22
|
PersistenceInfo,
|
23
23
|
SqliteDb,
|
24
24
|
SyncBackend,
|
@@ -121,7 +121,10 @@ export type MaterializeEvent = (
|
|
121
121
|
skipEventlog?: boolean
|
122
122
|
},
|
123
123
|
) => Effect.Effect<
|
124
|
-
{
|
124
|
+
{
|
125
|
+
sessionChangeset: { _tag: 'sessionChangeset'; data: Uint8Array; debug: any } | { _tag: 'no-op' }
|
126
|
+
hash: Option.Option<number>
|
127
|
+
},
|
125
128
|
SqliteError | UnexpectedError
|
126
129
|
>
|
127
130
|
|
@@ -133,16 +136,12 @@ export type InitialBlockingSyncContext = {
|
|
133
136
|
export interface LeaderSyncProcessor {
|
134
137
|
/** Used by client sessions to subscribe to upstream sync state changes */
|
135
138
|
pull: (args: {
|
136
|
-
cursor:
|
137
|
-
}) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type
|
139
|
+
cursor: EventSequenceNumber.EventSequenceNumber
|
140
|
+
}) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type }, UnexpectedError>
|
138
141
|
/** The `pullQueue` API can be used instead of `pull` when more convenient */
|
139
142
|
pullQueue: (args: {
|
140
|
-
cursor:
|
141
|
-
}) => Effect.Effect<
|
142
|
-
Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
|
143
|
-
UnexpectedError,
|
144
|
-
Scope.Scope
|
145
|
-
>
|
143
|
+
cursor: EventSequenceNumber.EventSequenceNumber
|
144
|
+
}) => Effect.Effect<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>, UnexpectedError, Scope.Scope>
|
146
145
|
|
147
146
|
/** Used by client sessions to push events to the leader thread */
|
148
147
|
push: (
|
@@ -170,5 +169,4 @@ export interface LeaderSyncProcessor {
|
|
170
169
|
LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
|
171
170
|
>
|
172
171
|
syncState: Subscribable.Subscribable<SyncState.SyncState>
|
173
|
-
getMergeCounter: () => number
|
174
172
|
}
|
@@ -39,7 +39,7 @@ export const makeClientSession = <R>({
|
|
39
39
|
sessionId: string
|
40
40
|
isLeader: boolean
|
41
41
|
lockStatus: SubscriptionRef.SubscriptionRef<LockStatus>
|
42
|
-
leaderThread: ClientSessionLeaderThreadProxy
|
42
|
+
leaderThread: ClientSessionLeaderThreadProxy.ClientSessionLeaderThreadProxy
|
43
43
|
sqliteDb: SqliteDb
|
44
44
|
connectWebmeshNode: (args: {
|
45
45
|
webmeshNode: Webmesh.MeshNode
|
@@ -133,4 +133,4 @@ export const makeClientSession = <R>({
|
|
133
133
|
shutdown,
|
134
134
|
debugInstanceId,
|
135
135
|
} satisfies ClientSession
|
136
|
-
})
|
136
|
+
}).pipe(Effect.withSpan('@livestore/common:make-client-session'))
|
@@ -1,26 +1,27 @@
|
|
1
|
-
import { isNil, isReadonlyArray } from '@livestore/utils'
|
2
|
-
import { Schema } from '@livestore/utils/effect'
|
1
|
+
import { isDevEnv, isNil, isReadonlyArray } from '@livestore/utils'
|
2
|
+
import { Hash, Option, Schema } from '@livestore/utils/effect'
|
3
3
|
|
4
4
|
import type { SqliteDb } from './adapter-types.js'
|
5
5
|
import { SessionIdSymbol } from './adapter-types.js'
|
6
6
|
import type { EventDef, Materializer, MaterializerContextQuery, MaterializerResult } from './schema/EventDef.js'
|
7
7
|
import type * as LiveStoreEvent from './schema/LiveStoreEvent.js'
|
8
|
+
import { getEventDef, type LiveStoreSchema } from './schema/schema.js'
|
8
9
|
import type { QueryBuilder } from './schema/state/sqlite/query-builder/api.js'
|
9
10
|
import { isQueryBuilder } from './schema/state/sqlite/query-builder/api.js'
|
10
11
|
import { getResultSchema } from './schema/state/sqlite/query-builder/impl.js'
|
11
|
-
import {
|
12
|
+
import type { BindValues } from './sql-queries/sql-queries.js'
|
12
13
|
import type { ParamsObject, PreparedBindValues } from './util.js'
|
13
14
|
import { prepareBindValues } from './util.js'
|
14
15
|
|
15
|
-
export const
|
16
|
+
export const getExecStatementsFromMaterializer = ({
|
16
17
|
eventDef,
|
17
18
|
materializer,
|
18
|
-
|
19
|
+
dbState,
|
19
20
|
event,
|
20
21
|
}: {
|
21
22
|
eventDef: EventDef.AnyWithoutFn
|
22
23
|
materializer: Materializer
|
23
|
-
|
24
|
+
dbState: SqliteDb
|
24
25
|
/** Both encoded and decoded events are supported to reduce the number of times we need to decode/encode */
|
25
26
|
event:
|
26
27
|
| {
|
@@ -53,25 +54,25 @@ export const getExecArgsFromEvent = ({
|
|
53
54
|
) => {
|
54
55
|
if (isQueryBuilder(rawQueryOrQueryBuilder)) {
|
55
56
|
const { query, bindValues } = rawQueryOrQueryBuilder.asSql()
|
56
|
-
const rawResults =
|
57
|
+
const rawResults = dbState.select(query, prepareBindValues(bindValues, query))
|
57
58
|
const resultSchema = getResultSchema(rawQueryOrQueryBuilder)
|
58
59
|
return Schema.decodeSync(resultSchema)(rawResults)
|
59
60
|
} else {
|
60
61
|
const { query, bindValues } = rawQueryOrQueryBuilder
|
61
|
-
return
|
62
|
+
return dbState.select(query, prepareBindValues(bindValues, query))
|
62
63
|
}
|
63
64
|
}
|
64
65
|
|
65
|
-
const
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
66
|
+
const statementResults = fromMaterializerResult(
|
67
|
+
materializer(eventArgsDecoded, {
|
68
|
+
eventDef,
|
69
|
+
query,
|
70
|
+
// TODO properly implement this
|
71
|
+
currentFacts: new Map(),
|
72
|
+
}),
|
73
|
+
)
|
73
74
|
|
74
|
-
return
|
75
|
+
return statementResults.map((statementRes) => {
|
75
76
|
const statementSql = statementRes.sql
|
76
77
|
|
77
78
|
const bindValues = typeof statementRes === 'string' ? eventArgsEncoded : statementRes.bindValues
|
@@ -82,7 +83,32 @@ export const getExecArgsFromEvent = ({
|
|
82
83
|
})
|
83
84
|
}
|
84
85
|
|
85
|
-
const
|
86
|
+
export const makeMaterializerHash =
|
87
|
+
({ schema, dbState }: { schema: LiveStoreSchema; dbState: SqliteDb }) =>
|
88
|
+
(event: LiveStoreEvent.AnyEncodedGlobal): Option.Option<number> => {
|
89
|
+
if (isDevEnv()) {
|
90
|
+
const { eventDef, materializer } = getEventDef(schema, event.name)
|
91
|
+
const materializerResults = getExecStatementsFromMaterializer({
|
92
|
+
eventDef,
|
93
|
+
materializer,
|
94
|
+
dbState,
|
95
|
+
event: { decoded: undefined, encoded: event },
|
96
|
+
})
|
97
|
+
return Option.some(Hash.string(JSON.stringify(materializerResults)))
|
98
|
+
}
|
99
|
+
|
100
|
+
return Option.none()
|
101
|
+
}
|
102
|
+
|
103
|
+
export const hashMaterializerResults = (
|
104
|
+
materializerResults: ReadonlyArray<{
|
105
|
+
statementSql: string
|
106
|
+
bindValues: PreparedBindValues
|
107
|
+
writeTables: ReadonlySet<string> | undefined
|
108
|
+
}>,
|
109
|
+
) => Hash.string(JSON.stringify(materializerResults))
|
110
|
+
|
111
|
+
const fromMaterializerResult = (
|
86
112
|
materializerResult: MaterializerResult | ReadonlyArray<MaterializerResult>,
|
87
113
|
): ReadonlyArray<{
|
88
114
|
sql: string
|
@@ -90,7 +116,7 @@ const mapMaterializerResult = (
|
|
90
116
|
writeTables: ReadonlySet<string> | undefined
|
91
117
|
}> => {
|
92
118
|
if (isReadonlyArray(materializerResult)) {
|
93
|
-
return materializerResult.flatMap(
|
119
|
+
return materializerResult.flatMap(fromMaterializerResult)
|
94
120
|
}
|
95
121
|
if (isQueryBuilder(materializerResult)) {
|
96
122
|
const { query, bindValues } = materializerResult.asSql()
|
@@ -27,13 +27,13 @@ export const rematerializeFromEventlog = ({
|
|
27
27
|
`SELECT COUNT(*) AS count FROM ${SystemTables.EVENTLOG_META_TABLE}`,
|
28
28
|
)[0]!.count
|
29
29
|
|
30
|
-
const
|
30
|
+
const hashEventDef = memoizeByRef((event: EventDef.AnyWithoutFn) => Schema.hash(event.schema))
|
31
31
|
|
32
32
|
const processEvent = (row: SystemTables.EventlogMetaRow) =>
|
33
33
|
Effect.gen(function* () {
|
34
34
|
const eventDef = getEventDef(schema, row.name)
|
35
35
|
|
36
|
-
if (
|
36
|
+
if (hashEventDef(eventDef.eventDef) !== row.schemaHash) {
|
37
37
|
yield* Effect.logWarning(
|
38
38
|
`Schema hash mismatch for event definition ${row.name}. Trying to materialize event anyway.`,
|
39
39
|
)
|
@@ -56,8 +56,16 @@ This likely means the schema has changed in an incompatible way.
|
|
56
56
|
)
|
57
57
|
|
58
58
|
const eventEncoded = LiveStoreEvent.EncodedWithMeta.make({
|
59
|
-
seqNum: {
|
60
|
-
|
59
|
+
seqNum: {
|
60
|
+
global: row.seqNumGlobal,
|
61
|
+
client: row.seqNumClient,
|
62
|
+
rebaseGeneration: row.seqNumRebaseGeneration,
|
63
|
+
},
|
64
|
+
parentSeqNum: {
|
65
|
+
global: row.parentSeqNumGlobal,
|
66
|
+
client: row.parentSeqNumClient,
|
67
|
+
rebaseGeneration: row.parentSeqNumRebaseGeneration,
|
68
|
+
},
|
61
69
|
name: row.name,
|
62
70
|
args,
|
63
71
|
clientId: row.clientId,
|
package/src/schema/EventDef.ts
CHANGED
@@ -27,12 +27,20 @@ export type EventDef<TName extends string, TType, TEncoded = TType, TDerived ext
|
|
27
27
|
derived: TDerived
|
28
28
|
}
|
29
29
|
|
30
|
-
/** Helper function to construct a partial
|
31
|
-
(
|
30
|
+
/** Helper function to construct a partial event */
|
31
|
+
(
|
32
|
+
args: TType,
|
33
|
+
): {
|
32
34
|
name: TName
|
33
35
|
args: TType
|
34
36
|
}
|
35
37
|
|
38
|
+
/** Helper function to construct a partial encoded event */
|
39
|
+
encoded: (args: TEncoded) => {
|
40
|
+
name: TName
|
41
|
+
args: TEncoded
|
42
|
+
}
|
43
|
+
|
36
44
|
readonly Event: {
|
37
45
|
name: TName
|
38
46
|
args: TType
|
@@ -123,6 +131,10 @@ export const defineEvent = <TName extends string, TType, TEncoded = TType, TDeri
|
|
123
131
|
|
124
132
|
Object.defineProperty(makePartialEvent, 'name', { value: name })
|
125
133
|
Object.defineProperty(makePartialEvent, 'schema', { value: schema })
|
134
|
+
Object.defineProperty(makePartialEvent, 'encoded', {
|
135
|
+
value: (args: TEncoded) => ({ name: name, args }),
|
136
|
+
})
|
137
|
+
|
126
138
|
Object.defineProperty(makePartialEvent, 'options', {
|
127
139
|
value: {
|
128
140
|
clientOnly: options?.clientOnly ?? false,
|
@@ -184,14 +196,14 @@ export type Materializer<TEventDef extends EventDef.AnyWithoutFn = EventDef.AnyW
|
|
184
196
|
) => SingleOrReadonlyArray<MaterializerResult>
|
185
197
|
|
186
198
|
export const defineMaterializer = <TEventDef extends EventDef.AnyWithoutFn>(
|
187
|
-
|
199
|
+
_eventDef: TEventDef,
|
188
200
|
materializer: Materializer<TEventDef>,
|
189
201
|
): Materializer<TEventDef> => {
|
190
202
|
return materializer
|
191
203
|
}
|
192
204
|
|
193
205
|
export const materializers = <TInputRecord extends Record<string, EventDef.AnyWithoutFn>>(
|
194
|
-
|
206
|
+
_eventDefRecord: TInputRecord,
|
195
207
|
handlers: {
|
196
208
|
[TEventName in TInputRecord[keyof TInputRecord]['name'] as Extract<
|
197
209
|
TInputRecord[keyof TInputRecord],
|
@@ -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
|
}
|
@@ -171,12 +171,25 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('LiveStoreEve
|
|
171
171
|
Schema.TaggedStruct('unset', {}),
|
172
172
|
),
|
173
173
|
syncMetadata: Schema.Option(Schema.JsonValue),
|
174
|
+
/** Used to detect if the materializer is side effecting (during dev) */
|
175
|
+
materializerHashLeader: Schema.Option(Schema.Number),
|
176
|
+
materializerHashSession: Schema.Option(Schema.Number),
|
174
177
|
}).pipe(
|
175
178
|
Schema.mutable,
|
176
179
|
Schema.optional,
|
177
180
|
Schema.withDefaults({
|
178
|
-
constructor: () => ({
|
179
|
-
|
181
|
+
constructor: () => ({
|
182
|
+
sessionChangeset: { _tag: 'unset' as const },
|
183
|
+
syncMetadata: Option.none(),
|
184
|
+
materializerHashLeader: Option.none(),
|
185
|
+
materializerHashSession: Option.none(),
|
186
|
+
}),
|
187
|
+
decoding: () => ({
|
188
|
+
sessionChangeset: { _tag: 'unset' as const },
|
189
|
+
syncMetadata: Option.none(),
|
190
|
+
materializerHashLeader: Option.none(),
|
191
|
+
materializerHashSession: Option.none(),
|
192
|
+
}),
|
180
193
|
}),
|
181
194
|
),
|
182
195
|
}) {
|
@@ -208,18 +221,46 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('LiveStoreEve
|
|
208
221
|
* +---- global number
|
209
222
|
* Client num is ommitted for global events
|
210
223
|
*/
|
211
|
-
rebase = (
|
224
|
+
rebase = ({
|
225
|
+
parentSeqNum,
|
226
|
+
isClient,
|
227
|
+
rebaseGeneration,
|
228
|
+
}: {
|
229
|
+
parentSeqNum: EventSequenceNumber.EventSequenceNumber
|
230
|
+
isClient: boolean
|
231
|
+
rebaseGeneration: number
|
232
|
+
}) =>
|
212
233
|
new EncodedWithMeta({
|
213
234
|
...this,
|
214
|
-
...EventSequenceNumber.nextPair(parentSeqNum, isClient),
|
235
|
+
...EventSequenceNumber.nextPair({ seqNum: parentSeqNum, isClient, rebaseGeneration }),
|
215
236
|
})
|
216
237
|
|
217
|
-
static fromGlobal = (
|
238
|
+
static fromGlobal = (
|
239
|
+
event: AnyEncodedGlobal,
|
240
|
+
meta: {
|
241
|
+
syncMetadata: Option.Option<Schema.JsonValue>
|
242
|
+
materializerHashLeader: Option.Option<number>
|
243
|
+
materializerHashSession: Option.Option<number>
|
244
|
+
},
|
245
|
+
) =>
|
218
246
|
new EncodedWithMeta({
|
219
247
|
...event,
|
220
|
-
seqNum: {
|
221
|
-
|
222
|
-
|
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
|
+
},
|
258
|
+
meta: {
|
259
|
+
sessionChangeset: { _tag: 'unset' as const },
|
260
|
+
syncMetadata: meta.syncMetadata,
|
261
|
+
materializerHashLeader: meta.materializerHashLeader,
|
262
|
+
materializerHashSession: meta.materializerHashSession,
|
263
|
+
},
|
223
264
|
})
|
224
265
|
|
225
266
|
toGlobal = (): AnyEncodedGlobal => ({
|