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

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