@livestore/livestore 0.3.0-dev.5 → 0.3.0-dev.51

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.
Files changed (170) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/QueryCache.d.ts.map +1 -1
  3. package/dist/SqliteDbWrapper.d.ts +60 -0
  4. package/dist/SqliteDbWrapper.d.ts.map +1 -0
  5. package/dist/{SynchronousDatabaseWrapper.js → SqliteDbWrapper.js} +69 -34
  6. package/dist/SqliteDbWrapper.js.map +1 -0
  7. package/dist/effect/LiveStore.d.ts +6 -34
  8. package/dist/effect/LiveStore.d.ts.map +1 -1
  9. package/dist/effect/LiveStore.js +10 -12
  10. package/dist/effect/LiveStore.js.map +1 -1
  11. package/dist/effect/mod.d.ts +3 -0
  12. package/dist/effect/mod.d.ts.map +1 -0
  13. package/dist/effect/mod.js +3 -0
  14. package/dist/effect/mod.js.map +1 -0
  15. package/dist/internal/mod.d.ts +3 -0
  16. package/dist/internal/mod.d.ts.map +1 -0
  17. package/dist/internal/mod.js +3 -0
  18. package/dist/internal/mod.js.map +1 -0
  19. package/dist/live-queries/base-class.d.ts +69 -29
  20. package/dist/live-queries/base-class.d.ts.map +1 -1
  21. package/dist/live-queries/base-class.js +60 -14
  22. package/dist/live-queries/base-class.js.map +1 -1
  23. package/dist/live-queries/client-document-get-query.d.ts +12 -0
  24. package/dist/live-queries/client-document-get-query.d.ts.map +1 -0
  25. package/dist/live-queries/client-document-get-query.js +18 -0
  26. package/dist/live-queries/client-document-get-query.js.map +1 -0
  27. package/dist/live-queries/computed.d.ts +13 -15
  28. package/dist/live-queries/computed.d.ts.map +1 -1
  29. package/dist/live-queries/computed.js +37 -15
  30. package/dist/live-queries/computed.js.map +1 -1
  31. package/dist/live-queries/db-query.d.ts +93 -0
  32. package/dist/live-queries/db-query.d.ts.map +1 -0
  33. package/dist/live-queries/{db.js → db-query.js} +113 -40
  34. package/dist/live-queries/db-query.js.map +1 -0
  35. package/dist/live-queries/db-query.test.d.ts +2 -0
  36. package/dist/live-queries/db-query.test.d.ts.map +1 -0
  37. package/dist/live-queries/db-query.test.js +133 -0
  38. package/dist/live-queries/db-query.test.js.map +1 -0
  39. package/dist/live-queries/mod.d.ts +5 -0
  40. package/dist/live-queries/mod.d.ts.map +1 -0
  41. package/dist/live-queries/mod.js +5 -0
  42. package/dist/live-queries/mod.js.map +1 -0
  43. package/dist/live-queries/signal.d.ts +25 -0
  44. package/dist/live-queries/signal.d.ts.map +1 -0
  45. package/dist/live-queries/signal.js +50 -0
  46. package/dist/live-queries/signal.js.map +1 -0
  47. package/dist/live-queries/signal.test.d.ts +2 -0
  48. package/dist/live-queries/signal.test.d.ts.map +1 -0
  49. package/dist/live-queries/signal.test.js +25 -0
  50. package/dist/live-queries/signal.test.js.map +1 -0
  51. package/dist/mod.d.ts +14 -0
  52. package/dist/mod.d.ts.map +1 -0
  53. package/dist/mod.js +13 -0
  54. package/dist/mod.js.map +1 -0
  55. package/dist/reactive.d.ts +23 -17
  56. package/dist/reactive.d.ts.map +1 -1
  57. package/dist/reactive.js +23 -19
  58. package/dist/reactive.js.map +1 -1
  59. package/dist/reactive.test.js +1 -1
  60. package/dist/reactive.test.js.map +1 -1
  61. package/dist/store/create-store.d.ts +70 -12
  62. package/dist/store/create-store.d.ts.map +1 -1
  63. package/dist/store/create-store.js +68 -19
  64. package/dist/store/create-store.js.map +1 -1
  65. package/dist/store/devtools.d.ts +5 -4
  66. package/dist/store/devtools.d.ts.map +1 -1
  67. package/dist/store/devtools.js +92 -40
  68. package/dist/store/devtools.js.map +1 -1
  69. package/dist/store/store-types.d.ts +54 -42
  70. package/dist/store/store-types.d.ts.map +1 -1
  71. package/dist/store/store-types.js +2 -5
  72. package/dist/store/store-types.js.map +1 -1
  73. package/dist/store/store.d.ts +141 -35
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +322 -154
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/data-structures.d.ts.map +1 -1
  78. package/dist/utils/dev.d.ts.map +1 -1
  79. package/dist/utils/dev.js +6 -1
  80. package/dist/utils/dev.js.map +1 -1
  81. package/dist/utils/function-string.d.ts +7 -0
  82. package/dist/utils/function-string.d.ts.map +1 -0
  83. package/dist/utils/function-string.js +9 -0
  84. package/dist/utils/function-string.js.map +1 -0
  85. package/dist/utils/stack-info.d.ts.map +1 -1
  86. package/dist/utils/stack-info.js +6 -1
  87. package/dist/utils/stack-info.js.map +1 -1
  88. package/dist/utils/stack-info.test.js +54 -1
  89. package/dist/utils/stack-info.test.js.map +1 -1
  90. package/dist/utils/tests/fixture.d.ts +59 -216
  91. package/dist/utils/tests/fixture.d.ts.map +1 -1
  92. package/dist/utils/tests/fixture.js +23 -18
  93. package/dist/utils/tests/fixture.js.map +1 -1
  94. package/dist/utils/tests/mod.d.ts +1 -0
  95. package/dist/utils/tests/mod.d.ts.map +1 -1
  96. package/dist/utils/tests/mod.js +1 -0
  97. package/dist/utils/tests/mod.js.map +1 -1
  98. package/dist/utils/tests/otel.d.ts.map +1 -1
  99. package/dist/utils/tests/otel.js +8 -3
  100. package/dist/utils/tests/otel.js.map +1 -1
  101. package/package.json +29 -26
  102. package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +92 -42
  103. package/src/effect/LiveStore.ts +27 -64
  104. package/src/effect/{index.ts → mod.ts} +2 -3
  105. package/src/internal/mod.ts +2 -0
  106. package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +241 -45
  107. package/src/live-queries/base-class.ts +170 -53
  108. package/src/live-queries/client-document-get-query.ts +52 -0
  109. package/src/live-queries/computed.ts +51 -33
  110. package/src/live-queries/db-query.test.ts +192 -0
  111. package/src/live-queries/{db.ts → db-query.ts} +171 -82
  112. package/src/live-queries/mod.ts +4 -0
  113. package/src/live-queries/signal.test.ts +40 -0
  114. package/src/live-queries/signal.ts +81 -0
  115. package/src/mod.ts +51 -0
  116. package/src/reactive.test.ts +1 -1
  117. package/src/reactive.ts +66 -43
  118. package/src/store/create-store.ts +188 -62
  119. package/src/store/devtools.ts +124 -46
  120. package/src/store/store-types.ts +54 -43
  121. package/src/store/store.ts +457 -237
  122. package/src/utils/dev.ts +6 -1
  123. package/src/utils/function-string.ts +12 -0
  124. package/src/utils/stack-info.test.ts +58 -1
  125. package/src/utils/stack-info.ts +6 -1
  126. package/src/utils/tests/fixture.ts +22 -31
  127. package/src/utils/tests/mod.ts +1 -0
  128. package/src/utils/tests/otel.ts +10 -3
  129. package/dist/SynchronousDatabaseWrapper.d.ts +0 -41
  130. package/dist/SynchronousDatabaseWrapper.d.ts.map +0 -1
  131. package/dist/SynchronousDatabaseWrapper.js.map +0 -1
  132. package/dist/effect/index.d.ts +0 -2
  133. package/dist/effect/index.d.ts.map +0 -1
  134. package/dist/effect/index.js +0 -2
  135. package/dist/effect/index.js.map +0 -1
  136. package/dist/global-state.d.ts +0 -14
  137. package/dist/global-state.d.ts.map +0 -1
  138. package/dist/global-state.js +0 -16
  139. package/dist/global-state.js.map +0 -1
  140. package/dist/index.d.ts +0 -20
  141. package/dist/index.d.ts.map +0 -1
  142. package/dist/index.js +0 -16
  143. package/dist/index.js.map +0 -1
  144. package/dist/live-queries/db.d.ts +0 -66
  145. package/dist/live-queries/db.d.ts.map +0 -1
  146. package/dist/live-queries/db.js.map +0 -1
  147. package/dist/live-queries/db.test.d.ts +0 -2
  148. package/dist/live-queries/db.test.d.ts.map +0 -1
  149. package/dist/live-queries/db.test.js +0 -118
  150. package/dist/live-queries/db.test.js.map +0 -1
  151. package/dist/live-queries/graphql.d.ts +0 -49
  152. package/dist/live-queries/graphql.d.ts.map +0 -1
  153. package/dist/live-queries/graphql.js +0 -122
  154. package/dist/live-queries/graphql.js.map +0 -1
  155. package/dist/row-query-utils.d.ts +0 -17
  156. package/dist/row-query-utils.d.ts.map +0 -1
  157. package/dist/row-query-utils.js +0 -30
  158. package/dist/row-query-utils.js.map +0 -1
  159. package/dist/utils/otel.d.ts +0 -4
  160. package/dist/utils/otel.d.ts.map +0 -1
  161. package/dist/utils/otel.js +0 -6
  162. package/dist/utils/otel.js.map +0 -1
  163. package/src/global-state.ts +0 -20
  164. package/src/index.ts +0 -66
  165. package/src/live-queries/db.test.ts +0 -154
  166. package/src/live-queries/graphql.ts +0 -219
  167. package/src/row-query-utils.ts +0 -65
  168. package/src/utils/otel.ts +0 -9
  169. package/tsconfig.json +0 -18
  170. package/vitest.config.js +0 -9
@@ -1,72 +1,73 @@
1
- import { Devtools, getExecArgsFromMutation, getResultSchema, IntentionalShutdownCause, isQueryBuilder, liveStoreVersion, makeClientSessionSyncProcessor, prepareBindValues, QueryBuilderAstSymbol, replaceSessionIdSymbol, } from '@livestore/common';
2
- import { MutationEvent, SCHEMA_META_TABLE, SCHEMA_MUTATIONS_META_TABLE, SESSION_CHANGESET_META_TABLE, } from '@livestore/common/schema';
3
- import { assertNever, isDevEnv } from '@livestore/utils';
4
- import { Cause, Data, Effect, Inspectable, MutableHashMap, Runtime, Schema } from '@livestore/utils/effect';
1
+ import { Devtools, getDurationMsFromSpan, getExecArgsFromEvent, getResultSchema, IntentionalShutdownCause, isQueryBuilder, liveStoreVersion, makeClientSessionSyncProcessor, prepareBindValues, QueryBuilderAstSymbol, replaceSessionIdSymbol, } from '@livestore/common';
2
+ import { getEventDef, LiveStoreEvent, SystemTables } from '@livestore/common/schema';
3
+ import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils';
4
+ import { Cause, Effect, Fiber, Inspectable, OtelTracer, Runtime, Schema, Stream } from '@livestore/utils/effect';
5
5
  import { nanoid } from '@livestore/utils/nanoid';
6
6
  import * as otel from '@opentelemetry/api';
7
- import { makeExecBeforeFirstRun } from '../row-query-utils.js';
8
- import { SynchronousDatabaseWrapper } from '../SynchronousDatabaseWrapper.js';
7
+ import { makeReactivityGraph } from '../live-queries/base-class.js';
8
+ import { makeExecBeforeFirstRun } from '../live-queries/client-document-get-query.js';
9
+ import { SqliteDbWrapper } from '../SqliteDbWrapper.js';
9
10
  import { ReferenceCountedSet } from '../utils/data-structures.js';
10
11
  import { downloadBlob, exposeDebugUtils } from '../utils/dev.js';
11
- import { getDurationMsFromSpan } from '../utils/otel.js';
12
12
  if (isDevEnv()) {
13
13
  exposeDebugUtils();
14
14
  }
15
15
  export class Store extends Inspectable.Class {
16
16
  storeId;
17
17
  reactivityGraph;
18
- syncDbWrapper;
18
+ sqliteDbWrapper;
19
19
  clientSession;
20
20
  schema;
21
- graphQLSchema;
22
- graphQLContext;
21
+ context;
23
22
  otel;
24
23
  /**
25
24
  * Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
26
25
  * This only works in combination with `equal: () => false` which will always trigger a refresh.
27
26
  */
28
27
  tableRefs;
29
- runtime;
28
+ effectContext;
30
29
  /** RC-based set to see which queries are currently subscribed to */
31
30
  activeQueries;
32
- // NOTE this is currently exposed for the Devtools databrowser to emit mutation events
33
- __mutationEventSchema;
34
- unsyncedMutationEvents;
31
+ // NOTE this is currently exposed for the Devtools databrowser to commit events
32
+ __eventSchema;
35
33
  syncProcessor;
36
- lifetimeScope;
34
+ boot;
37
35
  // #region constructor
38
- constructor({ clientSession, schema, graphQLOptions, reactivityGraph, otelOptions, disableDevtools, batchUpdates, unsyncedMutationEvents, storeId, lifetimeScope, runtime, }) {
36
+ constructor({ clientSession, schema, otelOptions, context, batchUpdates, storeId, effectContext, params, confirmUnsavedChanges, __runningInDevtools, }) {
39
37
  super();
40
38
  this.storeId = storeId;
41
- this.unsyncedMutationEvents = unsyncedMutationEvents;
42
- this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: clientSession.syncDb });
39
+ this.sqliteDbWrapper = new SqliteDbWrapper({ otel: otelOptions, db: clientSession.sqliteDb });
43
40
  this.clientSession = clientSession;
44
41
  this.schema = schema;
45
- this.lifetimeScope = lifetimeScope;
46
- this.runtime = runtime;
42
+ this.context = context;
43
+ this.effectContext = effectContext;
44
+ const reactivityGraph = makeReactivityGraph();
47
45
  const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext);
48
46
  this.syncProcessor = makeClientSessionSyncProcessor({
49
47
  schema,
50
- initialLeaderHead: clientSession.leaderThread.mutations.initialMutationEventId,
51
- // rebaseBehaviour: 'auto-rebase',
52
- pushToLeader: (batch) => clientSession.leaderThread.mutations.push(batch).pipe(
53
- // NOTE we don't want to shutdown in case of an invalid push error, since it will be retried
54
- Effect.catchTag('InvalidPushError', Effect.ignoreLogged), this.runEffectFork),
55
- pullFromLeader: clientSession.leaderThread.mutations.pull,
56
- applyMutation: (mutationEventDecoded, { otelContext, withChangeset }) => {
57
- const mutationDef = schema.mutations.get(mutationEventDecoded.mutation);
58
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded });
48
+ clientSession,
49
+ runtime: effectContext.runtime,
50
+ materializeEvent: (eventDecoded, { otelContext, withChangeset }) => {
51
+ const { eventDef, materializer } = getEventDef(schema, eventDecoded.name);
52
+ const execArgsArr = getExecArgsFromEvent({
53
+ eventDef,
54
+ materializer,
55
+ db: this.sqliteDbWrapper,
56
+ event: { decoded: eventDecoded, encoded: undefined },
57
+ });
59
58
  const writeTablesForEvent = new Set();
60
59
  const exec = () => {
61
- for (const { statementSql, bindValues, writeTables = this.syncDbWrapper.getTablesUsed(statementSql), } of execArgsArr) {
62
- this.syncDbWrapper.execute(statementSql, bindValues, writeTables, { otelContext });
60
+ for (const { statementSql, bindValues, writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql), } of execArgsArr) {
61
+ this.sqliteDbWrapper.execute(statementSql, bindValues, { otelContext, writeTables });
63
62
  // durationMsTotal += durationMs
64
- writeTables.forEach((table) => writeTablesForEvent.add(table));
63
+ for (const table of writeTables) {
64
+ writeTablesForEvent.add(table);
65
+ }
65
66
  }
66
67
  };
67
- let sessionChangeset;
68
+ let sessionChangeset = { _tag: 'unset' };
68
69
  if (withChangeset === true) {
69
- sessionChangeset = this.syncDbWrapper.withChangeset(exec).changeset;
70
+ sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset;
70
71
  }
71
72
  else {
72
73
  exec();
@@ -74,7 +75,7 @@ export class Store extends Inspectable.Class {
74
75
  return { writeTables: writeTablesForEvent, sessionChangeset };
75
76
  },
76
77
  rollback: (changeset) => {
77
- this.syncDbWrapper.rollback(changeset);
78
+ this.sqliteDbWrapper.rollback(changeset);
78
79
  },
79
80
  refreshTables: (tables) => {
80
81
  const tablesToUpdate = [];
@@ -83,51 +84,57 @@ export class Store extends Inspectable.Class {
83
84
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`);
84
85
  tablesToUpdate.push([tableRef, null]);
85
86
  }
86
- this.reactivityGraph.setRefs(tablesToUpdate);
87
+ reactivityGraph.setRefs(tablesToUpdate);
87
88
  },
88
89
  span: syncSpan,
90
+ params: {
91
+ leaderPushBatchSize: params.leaderPushBatchSize,
92
+ },
93
+ confirmUnsavedChanges,
89
94
  });
90
- this.__mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema);
95
+ this.__eventSchema = LiveStoreEvent.makeEventDefSchemaMemo(schema);
91
96
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
92
97
  this.tableRefs = {};
93
98
  this.activeQueries = new ReferenceCountedSet();
94
- const mutationsSpan = otelOptions.tracer.startSpan('LiveStore:mutations', {}, otelOptions.rootSpanContext);
95
- const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), mutationsSpan);
99
+ const commitsSpan = otelOptions.tracer.startSpan('LiveStore:commits', {}, otelOptions.rootSpanContext);
100
+ const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), commitsSpan);
96
101
  const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext);
97
102
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan);
98
103
  this.reactivityGraph = reactivityGraph;
99
104
  this.reactivityGraph.context = {
100
105
  store: this,
106
+ defRcMap: new Map(),
107
+ reactivityGraph: new WeakRef(reactivityGraph),
101
108
  otelTracer: otelOptions.tracer,
102
109
  rootOtelContext: otelQueriesSpanContext,
103
110
  effectsWrapper: batchUpdates,
104
111
  };
105
112
  this.otel = {
106
113
  tracer: otelOptions.tracer,
107
- mutationsSpanContext: otelMuationsSpanContext,
114
+ rootSpanContext: otelOptions.rootSpanContext,
115
+ commitsSpanContext: otelMuationsSpanContext,
108
116
  queriesSpanContext: otelQueriesSpanContext,
109
117
  };
110
- // TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
111
- // But for now this is a good enough approximation with little downsides
112
- const isRunningInDevtools = disableDevtools === true;
113
118
  // Need a set here since `schema.tables` might contain duplicates and some componentStateTables
114
119
  const allTableNames = new Set(
115
- // NOTE we're excluding the LiveStore schema and mutations tables as they are not user-facing
120
+ // NOTE we're excluding the LiveStore schema and events tables as they are not user-facing
116
121
  // unless LiveStore is running in the devtools
117
- isRunningInDevtools
118
- ? this.schema.tables.keys()
119
- : Array.from(this.schema.tables.keys()).filter((_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE && _ !== SESSION_CHANGESET_META_TABLE));
122
+ __runningInDevtools
123
+ ? this.schema.state.sqlite.tables.keys()
124
+ : Array.from(this.schema.state.sqlite.tables.keys()).filter((_) => !SystemTables.isStateSystemTable(_)));
120
125
  const existingTableRefs = new Map(Array.from(this.reactivityGraph.atoms.values())
121
126
  .filter((_) => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
122
127
  .map((_) => [_.label.slice('tableRef:'.length), _]));
123
128
  for (const tableName of allTableNames) {
124
- this.tableRefs[tableName] = existingTableRefs.get(tableName) ?? this.makeTableRef(tableName);
129
+ this.tableRefs[tableName] =
130
+ existingTableRefs.get(tableName) ??
131
+ this.reactivityGraph.makeRef(null, {
132
+ equal: () => false,
133
+ label: `tableRef:${tableName}`,
134
+ meta: { liveStoreRefType: 'table' },
135
+ });
125
136
  }
126
- if (graphQLOptions) {
127
- this.graphQLSchema = graphQLOptions.schema;
128
- this.graphQLContext = graphQLOptions.makeContext(this.syncDbWrapper, this.otel.tracer, clientSession.sessionId);
129
- }
130
- Effect.gen(this, function* () {
137
+ this.boot = Effect.gen(this, function* () {
131
138
  yield* Effect.addFinalizer(() => Effect.sync(() => {
132
139
  // Remove all table refs from the reactivity graph
133
140
  for (const tableRef of Object.values(this.tableRefs)) {
@@ -137,47 +144,59 @@ export class Store extends Inspectable.Class {
137
144
  }
138
145
  // End the otel spans
139
146
  syncSpan.end();
140
- mutationsSpan.end();
147
+ commitsSpan.end();
141
148
  queriesSpan.end();
142
149
  }));
143
150
  yield* this.syncProcessor.boot;
144
- }).pipe(this.runEffectFork);
151
+ });
145
152
  }
146
153
  // #endregion constructor
147
- static createStore = (storeOptions, parentSpan) => {
148
- const ctx = otel.trace.setSpan(otel.context.active(), parentSpan);
149
- return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:createStore', {}, ctx, (span) => {
150
- try {
151
- return new Store(storeOptions);
152
- }
153
- finally {
154
- span.end();
155
- }
156
- });
157
- };
158
154
  get sessionId() {
159
155
  return this.clientSession.sessionId;
160
156
  }
157
+ get clientId() {
158
+ return this.clientSession.clientId;
159
+ }
161
160
  /**
162
161
  * Subscribe to the results of a query
163
162
  * Returns a function to cancel the subscription.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const unsubscribe = store.subscribe(query$, { onUpdate: (result) => console.log(result) })
167
+ * ```
164
168
  */
165
- subscribe = (query$, onNewValue, onUnsubsubscribe, options) => this.otel.tracer.startActiveSpan(`LiveStore.subscribe`, { attributes: { label: options?.label, queryLabel: query$.label } }, options?.otelContext ?? this.otel.queriesSpanContext, (span) => {
169
+ subscribe = (query, options) => this.otel.tracer.startActiveSpan(`LiveStore.subscribe`, { attributes: { label: options?.label, queryLabel: query.label } }, options?.otelContext ?? this.otel.queriesSpanContext, (span) => {
166
170
  // console.debug('store sub', query$.id, query$.label)
167
171
  const otelContext = otel.trace.setSpan(otel.context.active(), span);
172
+ const queryRcRef = query._tag === 'def' || query._tag === 'signal-def'
173
+ ? query.make(this.reactivityGraph.context)
174
+ : {
175
+ value: query,
176
+ deref: () => { },
177
+ };
178
+ const query$ = queryRcRef.value;
168
179
  const label = `subscribe:${options?.label}`;
169
- const effect = this.reactivityGraph.makeEffect((get) => onNewValue(get(query$.results$)), { label });
180
+ const effect = this.reactivityGraph.makeEffect((get, _otelContext, debugRefreshReason) => options.onUpdate(get(query$.results$, otelContext, debugRefreshReason)), { label });
181
+ if (options?.stackInfo) {
182
+ query$.activeSubscriptions.add(options.stackInfo);
183
+ }
184
+ options?.onSubscribe?.(query$);
170
185
  this.activeQueries.add(query$);
171
186
  // Running effect right away to get initial value (unless `skipInitialRun` is set)
172
- if (options?.skipInitialRun !== true) {
173
- effect.doEffect(otelContext);
187
+ if (options?.skipInitialRun !== true && !query$.isDestroyed) {
188
+ effect.doEffect(otelContext, { _tag: 'subscribe.initial', label: `subscribe-initial-run:${options?.label}` });
174
189
  }
175
190
  const unsubscribe = () => {
176
191
  // console.debug('store unsub', query$.id, query$.label)
177
192
  try {
178
193
  this.reactivityGraph.destroyNode(effect);
179
194
  this.activeQueries.remove(query$);
180
- onUnsubsubscribe?.();
195
+ if (options?.stackInfo) {
196
+ query$.activeSubscriptions.delete(options.stackInfo);
197
+ }
198
+ queryRcRef.deref();
199
+ options?.onUnsubsubscribe?.();
181
200
  }
182
201
  finally {
183
202
  span.end();
@@ -185,6 +204,15 @@ export class Store extends Inspectable.Class {
185
204
  };
186
205
  return unsubscribe;
187
206
  });
207
+ subscribeStream = (query$, options) => Stream.asyncPush((emit) => Effect.gen(this, function* () {
208
+ const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)));
209
+ const otelContext = otelSpan ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active();
210
+ yield* Effect.acquireRelease(Effect.sync(() => this.subscribe(query$, {
211
+ onUpdate: (result) => emit.single(result),
212
+ otelContext,
213
+ label: options?.label,
214
+ })), (unsub) => Effect.sync(() => unsub()));
215
+ }));
188
216
  /**
189
217
  * Synchronously queries the database without creating a LiveQuery.
190
218
  * This is useful for queries that don't need to be reactive.
@@ -201,8 +229,7 @@ export class Store extends Inspectable.Class {
201
229
  */
202
230
  query = (query, options) => {
203
231
  if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
204
- return this.syncDbWrapper.select(query.query, {
205
- bindValues: prepareBindValues(query.bindValues, query.query),
232
+ return this.sqliteDbWrapper.select(query.query, prepareBindValues(query.bindValues, query.query), {
206
233
  otelContext: options?.otelContext,
207
234
  });
208
235
  }
@@ -212,53 +239,145 @@ export class Store extends Inspectable.Class {
212
239
  makeExecBeforeFirstRun({
213
240
  table: ast.tableDef,
214
241
  id: ast.id,
215
- insertValues: ast.insertValues,
242
+ explicitDefaultValues: ast.explicitDefaultValues,
216
243
  otelContext: options?.otelContext,
217
244
  })(this.reactivityGraph.context);
218
245
  }
219
246
  const sqlRes = query.asSql();
220
247
  const schema = getResultSchema(query);
221
- const rawRes = this.syncDbWrapper.select(sqlRes.query, {
222
- bindValues: sqlRes.bindValues,
248
+ const rawRes = this.sqliteDbWrapper.select(sqlRes.query, sqlRes.bindValues, {
223
249
  otelContext: options?.otelContext,
224
250
  queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
225
251
  });
226
252
  return Schema.decodeSync(schema)(rawRes);
227
253
  }
254
+ else if (query._tag === 'def') {
255
+ const query$ = query.make(this.reactivityGraph.context);
256
+ const result = this.query(query$.value, options);
257
+ query$.deref();
258
+ return result;
259
+ }
260
+ else if (query._tag === 'signal-def') {
261
+ const signal$ = query.make(this.reactivityGraph.context);
262
+ return signal$.value.get();
263
+ }
228
264
  else {
229
- return query.run(options?.otelContext);
265
+ return query.run({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason });
230
266
  }
231
267
  };
232
- // #region mutate
233
- mutate = (firstMutationOrTxnFnOrOptions, ...restMutations) => {
234
- const { mutationsEvents, options } = this.getMutateArgs(firstMutationOrTxnFnOrOptions, restMutations);
235
- for (const mutationEvent of mutationsEvents) {
236
- replaceSessionIdSymbol(mutationEvent.args, this.clientSession.sessionId);
268
+ /**
269
+ * Set the value of a signal
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * const count$ = signal(0, { label: 'count$' })
274
+ * store.setSignal(count$, 2)
275
+ * ```
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * const count$ = signal(0, { label: 'count$' })
280
+ * store.setSignal(count$, (prev) => prev + 1)
281
+ * ```
282
+ */
283
+ setSignal = (signalDef, value) => {
284
+ const signalRef = signalDef.make(this.reactivityGraph.context);
285
+ const newValue = typeof value === 'function' ? value(signalRef.value.get()) : value;
286
+ signalRef.value.set(newValue);
287
+ // The current implementation of signals i.e. the separation into `signal-def` and `signal`
288
+ // can lead to a situation where a reffed signal is immediately de-reffed when calling `store.setSignal`,
289
+ // in case there is nothing else holding a reference to the signal which leads to the set value being "lost".
290
+ // To avoid this, we don't deref the signal here if this set call is the only reference to the signal.
291
+ // Hopefully this won't lead to any issues in the future. 🤞
292
+ if (signalRef.rc > 1) {
293
+ signalRef.deref();
237
294
  }
238
- if (mutationsEvents.length === 0)
295
+ };
296
+ // #region commit
297
+ /**
298
+ * Commit a list of events to the store which will immediately update the local database
299
+ * and sync the events across other clients (similar to a `git commit`).
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * store.commit(events.todoCreated({ id: nanoid(), text: 'Make coffee' }))
304
+ * ```
305
+ *
306
+ * You can call `commit` with multiple events to apply them in a single database transaction.
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * const todoId = nanoid()
311
+ * store.commit(
312
+ * events.todoCreated({ id: todoId, text: 'Make coffee' }),
313
+ * events.todoCompleted({ id: todoId }))
314
+ * ```
315
+ *
316
+ * For more advanced transaction scenarios, you can pass a synchronous function to `commit` which will receive a callback
317
+ * to which you can pass multiple events to be committed in the same database transaction.
318
+ * Under the hood this will simply collect all events and apply them in a single database transaction.
319
+ *
320
+ * @example
321
+ * ```ts
322
+ * store.commit((commit) => {
323
+ * const todoId = nanoid()
324
+ * if (Math.random() > 0.5) {
325
+ * commit(events.todoCreated({ id: todoId, text: 'Make coffee' }))
326
+ * } else {
327
+ * commit(events.todoCompleted({ id: todoId }))
328
+ * }
329
+ * })
330
+ * ```
331
+ *
332
+ * When committing a large batch of events, you can also skip the database refresh to improve performance
333
+ * and call `store.manualRefresh()` after all events have been committed.
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * const todos = [
338
+ * { id: nanoid(), text: 'Make coffee' },
339
+ * { id: nanoid(), text: 'Buy groceries' },
340
+ * // ... 1000 more todos
341
+ * ]
342
+ * for (const todo of todos) {
343
+ * store.commit({ skipRefresh: true }, events.todoCreated({ id: todo.id, text: todo.text }))
344
+ * }
345
+ * store.manualRefresh()
346
+ * ```
347
+ */
348
+ commit = (firstEventOrTxnFnOrOptions, ...restEvents) => {
349
+ const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents);
350
+ for (const event of events) {
351
+ replaceSessionIdSymbol(event.args, this.clientSession.sessionId);
352
+ }
353
+ if (events.length === 0)
239
354
  return;
240
- const label = options?.label ?? 'mutate';
241
355
  const skipRefresh = options?.skipRefresh ?? false;
242
- const mutationsSpan = otel.trace.getSpan(this.otel.mutationsSpanContext);
243
- mutationsSpan.addEvent('mutate');
244
- // console.group('LiveStore.mutate', { skipRefresh, wasSyncMessage, label })
245
- // mutationsEvents.forEach((_) => console.debug(_.mutation, _.id, _.args))
356
+ const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext);
357
+ commitsSpan.addEvent('commit');
358
+ // console.group('LiveStore.commit', { skipRefresh })
359
+ // events.forEach((_) => console.debug(_.name, _.args))
246
360
  // console.groupEnd()
247
361
  let durationMs;
248
- return this.otel.tracer.startActiveSpan('LiveStore:mutate', { attributes: { 'livestore.mutateLabel': label }, links: options?.spanLinks }, options?.otelContext ?? this.otel.mutationsSpanContext, (span) => {
362
+ return this.otel.tracer.startActiveSpan('LiveStore:commit', {
363
+ attributes: {
364
+ 'livestore.eventsCount': events.length,
365
+ 'livestore.eventTags': events.map((_) => _.name),
366
+ 'livestore.commitLabel': options?.label,
367
+ },
368
+ links: options?.spanLinks,
369
+ }, options?.otelContext ?? this.otel.commitsSpanContext, (span) => {
249
370
  const otelContext = otel.trace.setSpan(otel.context.active(), span);
250
371
  try {
251
- const { writeTables } = this.otel.tracer.startActiveSpan('LiveStore:mutate:applyMutations', { attributes: { 'livestore.mutateLabel': label } }, otel.trace.setSpan(otel.context.active(), span), (span) => {
372
+ const { writeTables } = (() => {
252
373
  try {
253
- const otelContext = otel.trace.setSpan(otel.context.active(), span);
254
- // 5
255
- const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext });
256
- if (mutationsEvents.length > 1) {
374
+ const materializeEvents = () => this.syncProcessor.push(events, { otelContext });
375
+ if (events.length > 1) {
257
376
  // TODO: what to do about leader transaction here?
258
- return this.syncDbWrapper.txn(applyMutations);
377
+ return this.sqliteDbWrapper.txn(materializeEvents);
259
378
  }
260
379
  else {
261
- return applyMutations();
380
+ return materializeEvents();
262
381
  }
263
382
  }
264
383
  catch (e) {
@@ -269,7 +388,7 @@ export class Store extends Inspectable.Class {
269
388
  finally {
270
389
  span.end();
271
390
  }
272
- });
391
+ })();
273
392
  const tablesToUpdate = [];
274
393
  for (const tableName of writeTables) {
275
394
  const tableRef = this.tableRefs[tableName];
@@ -277,8 +396,8 @@ export class Store extends Inspectable.Class {
277
396
  tablesToUpdate.push([tableRef, null]);
278
397
  }
279
398
  const debugRefreshReason = {
280
- _tag: 'mutate',
281
- mutations: mutationsEvents,
399
+ _tag: 'commit',
400
+ events,
282
401
  writeTables: Array.from(writeTables),
283
402
  };
284
403
  // Update all table refs together in a batch, to only trigger one reactive update
@@ -296,85 +415,134 @@ export class Store extends Inspectable.Class {
296
415
  return { durationMs };
297
416
  });
298
417
  };
299
- // #endregion mutate
418
+ // #endregion commit
300
419
  /**
301
- * This can be used in combination with `skipRefresh` when applying mutations.
420
+ * Returns an async iterable of events.
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * for await (const event of store.events()) {
425
+ * console.log(event)
426
+ * }
427
+ * ```
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * // Get all events from the beginning of time
432
+ * for await (const event of store.events({ cursor: EventSequenceNumber.ROOT })) {
433
+ * console.log(event)
434
+ * }
435
+ * ```
436
+ */
437
+ events = (_options) => {
438
+ return notYetImplemented(`store.events() is not yet implemented but planned soon`);
439
+ };
440
+ eventsStream = (_options) => {
441
+ return notYetImplemented(`store.eventsStream() is not yet implemented but planned soon`);
442
+ };
443
+ /**
444
+ * This can be used in combination with `skipRefresh` when committing events.
302
445
  * We might need a better solution for this. Let's see.
303
446
  */
304
447
  manualRefresh = (options) => {
305
448
  const { label } = options ?? {};
306
- this.otel.tracer.startActiveSpan('LiveStore:manualRefresh', { attributes: { 'livestore.manualRefreshLabel': label } }, this.otel.mutationsSpanContext, (span) => {
449
+ this.otel.tracer.startActiveSpan('LiveStore:manualRefresh', { attributes: { 'livestore.manualRefreshLabel': label } }, this.otel.commitsSpanContext, (span) => {
307
450
  const otelContext = otel.trace.setSpan(otel.context.active(), span);
308
451
  this.reactivityGraph.runDeferredEffects({ otelContext });
309
452
  span.end();
310
453
  });
311
454
  };
312
- makeTableRef = (tableName) => this.reactivityGraph.makeRef(null, {
313
- equal: () => false,
314
- label: `tableRef:${tableName}`,
315
- meta: { liveStoreRefType: 'table' },
316
- });
317
- __devDownloadDb = (source = 'local') => {
318
- Effect.gen(this, function* () {
319
- const data = source === 'local' ? this.syncDbWrapper.export() : yield* this.clientSession.leaderThread.export;
320
- downloadBlob(data, `livestore-${Date.now()}.db`);
321
- }).pipe(this.runEffectFork);
322
- };
323
- __devDownloadMutationLogDb = () => {
324
- Effect.gen(this, function* () {
325
- const data = yield* this.clientSession.leaderThread.getMutationLogData;
326
- downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`);
327
- }).pipe(this.runEffectFork);
328
- };
329
- __devHardReset = (mode = 'all-data') => {
330
- Effect.gen(this, function* () {
331
- yield* this.clientSession.leaderThread.sendDevtoolsMessage(Devtools.ResetAllDataReq.make({ liveStoreVersion, mode, requestId: nanoid() }));
332
- }).pipe(this.runEffectFork);
333
- };
334
- __devSyncStates = () => {
335
- Effect.gen(this, function* () {
336
- const session = this.syncProcessor.syncStateRef.current;
337
- console.log('Session sync state:', session);
338
- const leader = yield* this.clientSession.leaderThread.getSyncState;
339
- console.log('Leader sync state:', leader);
340
- }).pipe(this.runEffectFork);
341
- };
342
- __devShutdown = (cause) => {
343
- this.clientSession
455
+ /**
456
+ * Shuts down the store and closes the client session.
457
+ *
458
+ * This is called automatically when the store was created using the React or Effect API.
459
+ */
460
+ shutdown = async (cause) => {
461
+ await this.clientSession
344
462
  .shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
345
- .pipe(Effect.tapCauseLogPretty, Effect.provide(this.runtime), Effect.runFork);
463
+ .pipe(this.runEffectFork, Fiber.join, Effect.runPromise);
464
+ };
465
+ /**
466
+ * Helper methods useful during development
467
+ *
468
+ * @internal
469
+ */
470
+ _dev = {
471
+ downloadDb: (source = 'local') => {
472
+ Effect.gen(this, function* () {
473
+ const data = source === 'local' ? this.sqliteDbWrapper.export() : yield* this.clientSession.leaderThread.export;
474
+ downloadBlob(data, `livestore-${Date.now()}.db`);
475
+ }).pipe(this.runEffectFork);
476
+ },
477
+ downloadEventlogDb: () => {
478
+ Effect.gen(this, function* () {
479
+ const data = yield* this.clientSession.leaderThread.getEventlogData;
480
+ downloadBlob(data, `livestore-eventlog-${Date.now()}.db`);
481
+ }).pipe(this.runEffectFork);
482
+ },
483
+ hardReset: (mode = 'all-data') => {
484
+ Effect.gen(this, function* () {
485
+ const clientId = this.clientSession.clientId;
486
+ yield* this.clientSession.leaderThread.sendDevtoolsMessage(Devtools.Leader.ResetAllData.Request.make({ liveStoreVersion, mode, requestId: nanoid(), clientId }));
487
+ }).pipe(this.runEffectFork);
488
+ },
489
+ overrideNetworkStatus: (status) => {
490
+ const clientId = this.clientSession.clientId;
491
+ this.clientSession.leaderThread
492
+ .sendDevtoolsMessage(Devtools.Leader.SetSyncLatch.Request.make({
493
+ clientId,
494
+ closeLatch: status === 'offline',
495
+ liveStoreVersion,
496
+ requestId: nanoid(),
497
+ }))
498
+ .pipe(this.runEffectFork);
499
+ },
500
+ syncStates: () => {
501
+ Effect.gen(this, function* () {
502
+ const session = yield* this.syncProcessor.syncState;
503
+ console.log('Session sync state:', session.toJSON());
504
+ const leader = yield* this.clientSession.leaderThread.getSyncState;
505
+ console.log('Leader sync state:', leader.toJSON());
506
+ }).pipe(this.runEffectFork);
507
+ },
508
+ version: liveStoreVersion,
509
+ otel: {
510
+ rootSpanContext: () => otel.trace.getSpan(this.otel.rootSpanContext)?.spanContext(),
511
+ },
346
512
  };
347
513
  // NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
348
514
  toJSON = () => ({
349
515
  _tag: 'livestore.Store',
350
516
  reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
351
517
  });
352
- runEffectFork = (effect) => effect.pipe(Effect.forkIn(this.lifetimeScope), Effect.tapCauseLogPretty, Runtime.runFork(this.runtime));
353
- getMutateArgs = (firstMutationOrTxnFnOrOptions, restMutations) => {
354
- let mutationsEvents;
518
+ runEffectFork = (effect) => effect.pipe(Effect.forkIn(this.effectContext.lifetimeScope), Effect.tapCauseLogPretty, Runtime.runFork(this.effectContext.runtime));
519
+ getCommitArgs = (firstEventOrTxnFnOrOptions, restEvents) => {
520
+ let events;
355
521
  let options;
356
- if (typeof firstMutationOrTxnFnOrOptions === 'function') {
522
+ if (typeof firstEventOrTxnFnOrOptions === 'function') {
357
523
  // TODO ensure that function is synchronous and isn't called in a async way (also write tests for this)
358
- mutationsEvents = firstMutationOrTxnFnOrOptions((arg) => mutationsEvents.push(arg));
524
+ events = firstEventOrTxnFnOrOptions((arg) => events.push(arg));
359
525
  }
360
- else if (firstMutationOrTxnFnOrOptions?.label !== undefined ||
361
- firstMutationOrTxnFnOrOptions?.skipRefresh !== undefined ||
362
- firstMutationOrTxnFnOrOptions?.otelContext !== undefined ||
363
- firstMutationOrTxnFnOrOptions?.spanLinks !== undefined) {
364
- options = firstMutationOrTxnFnOrOptions;
365
- mutationsEvents = restMutations;
526
+ else if (firstEventOrTxnFnOrOptions?.label !== undefined ||
527
+ firstEventOrTxnFnOrOptions?.skipRefresh !== undefined ||
528
+ firstEventOrTxnFnOrOptions?.otelContext !== undefined ||
529
+ firstEventOrTxnFnOrOptions?.spanLinks !== undefined) {
530
+ options = firstEventOrTxnFnOrOptions;
531
+ events = restEvents;
366
532
  }
367
- else if (firstMutationOrTxnFnOrOptions === undefined) {
368
- // When `mutate` is called with no arguments (which sometimes happens when dynamically filtering mutations)
369
- mutationsEvents = [];
533
+ else if (firstEventOrTxnFnOrOptions === undefined) {
534
+ // When `commit` is called with no arguments (which sometimes happens when dynamically filtering events)
535
+ events = [];
370
536
  }
371
537
  else {
372
- mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations];
538
+ events = [firstEventOrTxnFnOrOptions, ...restEvents];
373
539
  }
374
- mutationsEvents = mutationsEvents.filter(
375
- // @ts-expect-error TODO
376
- (_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)));
377
- return { mutationsEvents, options };
540
+ // for (const event of events) {
541
+ // if (event.args.id === SessionIdSymbol) {
542
+ // event.args.id = this.clientSession.sessionId
543
+ // }
544
+ // }
545
+ return { events, options };
378
546
  };
379
547
  }
380
548
  //# sourceMappingURL=store.js.map