@livestore/livestore 0.3.0-dev.4 → 0.3.0-dev.40

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