@livestore/livestore 0.0.21 → 0.0.23

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 (78) hide show
  1. package/README.md +14 -4
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/react/fixture.js +0 -2
  5. package/dist/__tests__/react/fixture.js.map +1 -1
  6. package/dist/__tests__/react/useQuery.test.js +1 -1
  7. package/dist/__tests__/react/useQuery.test.js.map +1 -1
  8. package/dist/__tests__/react/utils/stack-info.test.d.ts +2 -0
  9. package/dist/__tests__/react/utils/stack-info.test.d.ts.map +1 -0
  10. package/dist/__tests__/react/utils/stack-info.test.js +43 -0
  11. package/dist/__tests__/react/utils/stack-info.test.js.map +1 -0
  12. package/dist/__tests__/reactive.test.js +13 -1
  13. package/dist/__tests__/reactive.test.js.map +1 -1
  14. package/dist/__tests__/reactiveQueries/sql.test.js +3 -3
  15. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
  16. package/dist/inMemoryDatabase.d.ts +2 -1
  17. package/dist/inMemoryDatabase.d.ts.map +1 -1
  18. package/dist/inMemoryDatabase.js +3 -2
  19. package/dist/inMemoryDatabase.js.map +1 -1
  20. package/dist/react/index.d.ts +1 -0
  21. package/dist/react/index.d.ts.map +1 -1
  22. package/dist/react/index.js +1 -0
  23. package/dist/react/index.js.map +1 -1
  24. package/dist/react/useComponentState.d.ts.map +1 -1
  25. package/dist/react/useComponentState.js +19 -27
  26. package/dist/react/useComponentState.js.map +1 -1
  27. package/dist/react/useQuery.d.ts.map +1 -1
  28. package/dist/react/useQuery.js +46 -26
  29. package/dist/react/useQuery.js.map +1 -1
  30. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  31. package/dist/react/useTemporaryQuery.js +2 -0
  32. package/dist/react/useTemporaryQuery.js.map +1 -1
  33. package/dist/react/utils/stack-info.d.ts +11 -0
  34. package/dist/react/utils/stack-info.d.ts.map +1 -0
  35. package/dist/react/utils/stack-info.js +49 -0
  36. package/dist/react/utils/stack-info.js.map +1 -0
  37. package/dist/reactive.d.ts +33 -43
  38. package/dist/reactive.d.ts.map +1 -1
  39. package/dist/reactive.js +66 -255
  40. package/dist/reactive.js.map +1 -1
  41. package/dist/reactiveQueries/base-class.d.ts +15 -13
  42. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  43. package/dist/reactiveQueries/base-class.js +5 -8
  44. package/dist/reactiveQueries/base-class.js.map +1 -1
  45. package/dist/reactiveQueries/graphql.d.ts +4 -3
  46. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  47. package/dist/reactiveQueries/graphql.js +29 -34
  48. package/dist/reactiveQueries/graphql.js.map +1 -1
  49. package/dist/reactiveQueries/js.d.ts +2 -1
  50. package/dist/reactiveQueries/js.d.ts.map +1 -1
  51. package/dist/reactiveQueries/js.js +8 -9
  52. package/dist/reactiveQueries/js.js.map +1 -1
  53. package/dist/reactiveQueries/sql.d.ts +11 -5
  54. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  55. package/dist/reactiveQueries/sql.js +31 -34
  56. package/dist/reactiveQueries/sql.js.map +1 -1
  57. package/dist/store.d.ts +26 -12
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/store.js +41 -255
  60. package/dist/store.js.map +1 -1
  61. package/package.json +3 -3
  62. package/src/__tests__/react/fixture.tsx +0 -3
  63. package/src/__tests__/react/useQuery.test.tsx +1 -1
  64. package/src/__tests__/react/utils/{extractStackInfoFromStackTrace.test.ts → stack-info.test.ts} +25 -20
  65. package/src/__tests__/reactive.test.ts +20 -1
  66. package/src/__tests__/reactiveQueries/sql.test.ts +3 -3
  67. package/src/inMemoryDatabase.ts +9 -6
  68. package/src/react/index.ts +1 -0
  69. package/src/react/useComponentState.ts +25 -30
  70. package/src/react/useQuery.ts +66 -34
  71. package/src/react/useTemporaryQuery.ts +2 -0
  72. package/src/react/utils/{extractStackInfoFromStackTrace.ts → stack-info.ts} +21 -5
  73. package/src/reactive.ts +148 -339
  74. package/src/reactiveQueries/base-class.ts +23 -22
  75. package/src/reactiveQueries/graphql.ts +34 -36
  76. package/src/reactiveQueries/js.ts +14 -10
  77. package/src/reactiveQueries/sql.ts +55 -48
  78. package/src/store.ts +70 -305
package/src/store.ts CHANGED
@@ -11,6 +11,7 @@ import type { LiveStoreEvent } from './events.js'
11
11
  import { InMemoryDatabase } from './inMemoryDatabase.js'
12
12
  import { migrateDb } from './migrations.js'
13
13
  import { getDurationMsFromSpan } from './otel.js'
14
+ import type { StackInfo } from './react/utils/stack-info.js'
14
15
  import type { ReactiveGraph, Ref } from './reactive.js'
15
16
  import type { ILiveStoreQuery } from './reactiveQueries/base-class.js'
16
17
  import { type DbContext, dbGraph } from './reactiveQueries/graph.js'
@@ -42,8 +43,6 @@ export type QueryResult<TQuery> = TQuery extends LiveStoreSQLQuery<infer R>
42
43
  ? Readonly<Result>
43
44
  : never
44
45
 
45
- export const globalComponentKey: ComponentKey = { _tag: 'singleton', componentName: '__global', id: 'singleton' }
46
-
47
46
  export type GraphQLOptions<TContext> = {
48
47
  schema: GraphQLSchema
49
48
  makeContext: (db: InMemoryDatabase, tracer: otel.Tracer) => TContext
@@ -87,9 +86,21 @@ export type RefreshReason =
87
86
  _tag: 'makeThunk'
88
87
  label?: string
89
88
  }
89
+ | {
90
+ _tag: 'react'
91
+ api: string
92
+ label?: string
93
+ stackInfo?: StackInfo
94
+ }
95
+ | { _tag: 'manual'; label?: string }
90
96
  | { _tag: 'unknown' }
91
97
 
92
- export type QueryDebugInfo = { _tag: 'graphql' | 'sql' | 'js' | 'unknown'; label: string; query: string }
98
+ export type QueryDebugInfo = {
99
+ _tag: 'graphql' | 'sql' | 'js' | 'unknown'
100
+ label: string
101
+ query: string
102
+ durationMs: number
103
+ }
93
104
 
94
105
  export type StoreOtel = {
95
106
  tracer: otel.Tracer
@@ -110,10 +121,11 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
110
121
  * Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
111
122
  * This only works in combination with `equal: () => false` which will always trigger a refresh.
112
123
  */
113
- tableRefs: { [key: string]: Ref<null> }
114
- activeQueries: Set<LiveStoreQuery>
124
+ tableRefs: { [key: string]: Ref<null, DbContext, RefreshReason> }
125
+
126
+ /** RC-based set to see which queries are currently subscribed to */
127
+ activeQueries: ReferenceCountedSet<LiveStoreQuery>
115
128
  storage?: Storage
116
- temporaryQueries: Set<LiveStoreQuery> | undefined
117
129
 
118
130
  private constructor({
119
131
  db,
@@ -126,16 +138,10 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
126
138
  }: StoreOptions<TGraphQLContext>) {
127
139
  this.inMemoryDB = db
128
140
  this._proxyDb = dbProxy
129
- // this.graph = new ReactiveGraph({
130
- // // TODO move this into React module
131
- // // Do all our updates inside a single React setState batch to avoid multiple UI re-renders
132
- // effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
133
- // otelTracer,
134
- // })
135
141
  this.schema = schema
136
142
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
137
143
  this.tableRefs = {}
138
- this.activeQueries = new Set()
144
+ this.activeQueries = new ReferenceCountedSet()
139
145
  this.storage = storage
140
146
 
141
147
  const applyEventsSpan = otelTracer.startSpan('LiveStore:applyEvents', {}, otelRootSpanContext)
@@ -144,6 +150,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
144
150
  const queriesSpan = otelTracer.startSpan('LiveStore:queries', {}, otelRootSpanContext)
145
151
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
146
152
 
153
+ // TODO allow passing in a custom graph
147
154
  this.graph = dbGraph
148
155
  this.graph.context = { store: this, otelTracer, rootOtelContext: otelQueriesSpanContext }
149
156
 
@@ -186,287 +193,6 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
186
193
  })
187
194
  }
188
195
 
189
- /**
190
- * Creates a reactive LiveStore SQL query
191
- *
192
- * NOTE The query is actually running (even if no one has subscribed to it yet) and will be kept up to date.
193
- */
194
- // querySQL = <TResult>(
195
- // genQueryString: string | ((get: GetAtomResult) => string),
196
- // {
197
- // queriedTables,
198
- // bindValues,
199
- // componentKey,
200
- // label,
201
- // otelContext = otel.context.active(),
202
- // }: {
203
- // /**
204
- // * List of tables that are queried in this query;
205
- // * used to determine reactive dependencies.
206
- // *
207
- // * NOTE In the future we want to auto-generate this via parsing the query
208
- // */
209
- // queriedTables: string[]
210
- // bindValues?: Bindable | undefined
211
- // componentKey?: ComponentKey | undefined
212
- // label?: string | undefined
213
- // otelContext?: otel.Context
214
- // },
215
- // ): LiveStoreSQLQuery<TResult> =>
216
- // this.otel.tracer.startActiveSpan(
217
- // 'querySQL', // NOTE span name will be overridden further down
218
- // { attributes: { label } },
219
- // otelContext,
220
- // (span) => {
221
- // const otelContext = otel.trace.setSpan(otel.context.active(), span)
222
-
223
- // const queryString$ = this.graph.makeThunk(
224
- // (get, addDebugInfo) => {
225
- // if (typeof genQueryString === 'function') {
226
- // const queryString = genQueryString(makeGetAtomResult(get))
227
- // addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
228
- // return queryString
229
- // } else {
230
- // return genQueryString
231
- // }
232
- // },
233
- // { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
234
- // otelContext,
235
- // )
236
-
237
- // label = label ?? queryString$.result
238
- // span.updateName(`querySQL:${label}`)
239
-
240
- // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
241
-
242
- // const results$ = this.graph.makeThunk<ReadonlyArray<TResult>>(
243
- // (get, addDebugInfo) =>
244
- // this.otel.tracer.startActiveSpan(
245
- // 'sql:', // NOTE span name will be overridden further down
246
- // {},
247
- // otelContext,
248
- // (span) => {
249
- // try {
250
- // const otelContext = otel.trace.setSpan(otel.context.active(), span)
251
-
252
- // // Establish a reactive dependency on the tables used in the query
253
- // for (const tableName of queriedTables) {
254
- // const tableRef =
255
- // this.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
256
- // get(tableRef)
257
- // }
258
- // const sqlString = get(queryString$)
259
-
260
- // span.setAttribute('sql.query', sqlString)
261
- // span.updateName(`sql:${sqlString.slice(0, 50)}`)
262
-
263
- // const results = this.inMemoryDB.select<TResult>(sqlString, {
264
- // queriedTables,
265
- // bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
266
- // otelContext,
267
- // })
268
-
269
- // span.setAttribute('sql.rowsCount', results.length)
270
- // addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
271
-
272
- // return results
273
- // } finally {
274
- // span.end()
275
- // }
276
- // },
277
- // ),
278
- // { label: queryLabel },
279
- // otelContext,
280
- // )
281
-
282
- // const query = new LiveStoreSQLQuery<TResult>({
283
- // label,
284
- // queryString$,
285
- // results$,
286
- // componentKey: componentKey ?? globalComponentKey,
287
- // store: this,
288
- // otelContext,
289
- // })
290
-
291
- // this.activeQueries.add(query)
292
-
293
- // // TODO get rid of temporary query workaround
294
- // if (this.temporaryQueries !== undefined) {
295
- // this.temporaryQueries.add(query)
296
- // }
297
-
298
- // // NOTE we are not ending the span here but in the query `destroy` method
299
- // return query
300
- // },
301
- // )
302
-
303
- // queryJS = <TResult>(
304
- // genResults: (get: GetAtomResult) => TResult,
305
- // {
306
- // componentKey = globalComponentKey,
307
- // label = `js${uniqueId()}`,
308
- // otelContext = otel.context.active(),
309
- // }: { componentKey?: ComponentKey; label?: string; otelContext?: otel.Context },
310
- // ): LiveStoreJSQuery<TResult> =>
311
- // this.otel.tracer.startActiveSpan(`queryJS:${label}`, { attributes: { label } }, otelContext, (span) => {
312
- // const otelContext = otel.trace.setSpan(otel.context.active(), span)
313
- // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
314
- // const results$ = this.graph.makeThunk(
315
- // (get, addDebugInfo) => {
316
- // addDebugInfo({ _tag: 'js', label, query: genResults.toString() })
317
- // return genResults(makeGetAtomResult(get))
318
- // },
319
- // { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
320
- // otelContext,
321
- // )
322
-
323
- // // const query = new LiveStoreJSQuery<TResult>({
324
- // // label,
325
- // // results$,
326
- // // componentKey,
327
- // // store: this,
328
- // // otelContext,
329
- // // })
330
-
331
- // this.activeQueries.add(query)
332
-
333
- // // TODO get rid of temporary query workaround
334
- // if (this.temporaryQueries !== undefined) {
335
- // this.temporaryQueries.add(query)
336
- // }
337
-
338
- // // NOTE we are not ending the span here but in the query `destroy` method
339
- // return query
340
- // })
341
-
342
- // queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
343
- // document: DocumentNode<TResult, TVariableValues>,
344
- // genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
345
- // {
346
- // componentKey,
347
- // label,
348
- // otelContext = otel.context.active(),
349
- // }: {
350
- // componentKey: ComponentKey
351
- // label?: string
352
- // otelContext?: otel.Context
353
- // },
354
- // ): LiveStoreGraphQLQuery<TResult, TVariableValues, TGraphQLContext> =>
355
- // this.otel.tracer.startActiveSpan(
356
- // `queryGraphQL:`, // NOTE span name will be overridden further down
357
- // {},
358
- // otelContext,
359
- // (span) => {
360
- // const otelContext = otel.trace.setSpan(otel.context.active(), span)
361
-
362
- // if (this.graphQLContext === undefined) {
363
- // return shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
364
- // }
365
-
366
- // const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
367
-
368
- // span.updateName(`queryGraphQL:${labelWithDefault}`)
369
-
370
- // const variableValues$ = this.graph.makeThunk(
371
- // (get) => {
372
- // if (typeof genVariableValues === 'function') {
373
- // return genVariableValues(makeGetAtomResult(get))
374
- // } else {
375
- // return genVariableValues
376
- // }
377
- // },
378
- // { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
379
- // // otelContext,
380
- // )
381
-
382
- // const resultsLabel = `${labelWithDefault}:results` + (this.temporaryQueries ? ':temp' : '')
383
- // const results$ = this.graph.makeThunk<TResult>(
384
- // (get, addDebugInfo) => {
385
- // const variableValues = get(variableValues$)
386
- // const { result, queriedTables } = this.queryGraphQLOnce(document, variableValues, otelContext)
387
-
388
- // // Add dependencies on any tables that were used
389
- // for (const tableName of queriedTables) {
390
- // const tableRef = this.tableRefs[tableName]
391
- // assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
392
- // get(tableRef!)
393
- // }
394
-
395
- // addDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document) })
396
-
397
- // return result
398
- // },
399
- // { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
400
- // // otelContext,
401
- // )
402
-
403
- // const query = new LiveStoreGraphQLQuery({
404
- // document,
405
- // context: this.graphQLContext,
406
- // results$,
407
- // componentKey,
408
- // label: labelWithDefault,
409
- // store: this,
410
- // otelContext,
411
- // })
412
-
413
- // this.activeQueries.add(query)
414
-
415
- // // TODO get rid of temporary query workaround
416
- // if (this.temporaryQueries !== undefined) {
417
- // this.temporaryQueries.add(query)
418
- // }
419
-
420
- // // NOTE we are not ending the span here but in the query `destroy` method
421
- // return query
422
- // },
423
- // )
424
-
425
- // queryGraphQLOnce = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
426
- // document: DocumentNode<TResult, TVariableValues>,
427
- // variableValues: TVariableValues,
428
- // otelContext: otel.Context = this.otel.queriesSpanContext,
429
- // ): { result: TResult; queriedTables: string[] } => {
430
- // const schema =
431
- // this.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
432
- // const context =
433
- // this.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
434
- // const tracer = this.otel.tracer
435
-
436
- // const operationName = graphql.getOperationAST(document)?.name?.value
437
-
438
- // return tracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
439
- // try {
440
- // span.setAttribute('graphql.variables', JSON.stringify(variableValues))
441
- // span.setAttribute('graphql.query', graphql.print(document))
442
-
443
- // context.queriedTables.clear()
444
-
445
- // context.otelContext = otel.trace.setSpan(otel.context.active(), span)
446
-
447
- // const res = graphql.executeSync({
448
- // document,
449
- // contextValue: context,
450
- // schema: schema,
451
- // variableValues,
452
- // })
453
-
454
- // // TODO track number of nested SQL queries via Otel + debug info
455
-
456
- // if (res.errors) {
457
- // span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
458
- // span.setAttribute('graphql.error', res.errors.join('\n'))
459
- // span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
460
- // console.error(`graphql error (${operationName})`, res.errors)
461
- // }
462
-
463
- // return { result: res.data as unknown as TResult, queriedTables: Array.from(context.queriedTables.values()) }
464
- // } finally {
465
- // span.end()
466
- // }
467
- // })
468
- // }
469
-
470
196
  /**
471
197
  * Subscribe to the results of a query
472
198
  * Returns a function to cancel the subscription.
@@ -474,8 +200,8 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
474
200
  subscribe = <TResult>(
475
201
  query: ILiveStoreQuery<TResult>,
476
202
  onNewValue: (value: TResult) => void,
477
- onSubsubscribe?: () => void,
478
- options?: { label?: string; otelContext?: otel.Context } | undefined,
203
+ onUnsubsubscribe?: () => void,
204
+ options?: { label?: string; otelContext?: otel.Context; skipInitialRun?: boolean } | undefined,
479
205
  ): (() => void) =>
480
206
  this.otel.tracer.startActiveSpan(
481
207
  `LiveStore.subscribe`,
@@ -487,20 +213,23 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
487
213
  const label = `subscribe:${options?.label}`
488
214
  const effect = this.graph.makeEffect((get) => onNewValue(get(query.results$)), { label })
489
215
 
490
- effect.doEffect(otelContext)
216
+ this.activeQueries.add(query as LiveStoreQuery)
217
+
218
+ // Running effect right away to get initial value (unless `skipInitialRun` is set)
219
+ if (options?.skipInitialRun !== true) {
220
+ effect.doEffect(otelContext)
221
+ }
491
222
 
492
223
  const unsubscribe = () => {
493
224
  try {
494
225
  this.graph.destroy(effect)
495
- this.activeQueries.delete(query as LiveStoreQuery)
496
- onSubsubscribe?.()
226
+ this.activeQueries.remove(query as LiveStoreQuery)
227
+ onUnsubsubscribe?.()
497
228
  } finally {
498
229
  span.end()
499
230
  }
500
231
  }
501
232
 
502
- this.activeQueries.add(query as LiveStoreQuery)
503
-
504
233
  return unsubscribe
505
234
  },
506
235
  )
@@ -540,7 +269,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
540
269
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
541
270
  const writeTables = this.applyEventWithoutRefresh(eventType, args, otelContext).writeTables
542
271
 
543
- const tablesToUpdate = [] as [Ref<null>, null][]
272
+ const tablesToUpdate = [] as [Ref<null, DbContext, RefreshReason>, null][]
544
273
  for (const tableName of writeTables) {
545
274
  const tableRef = this.tableRefs[tableName]
546
275
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
@@ -641,7 +370,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
641
370
  },
642
371
  )
643
372
 
644
- const tablesToUpdate = [] as [Ref<null>, null][]
373
+ const tablesToUpdate = [] as [Ref<null, DbContext, RefreshReason>, null][]
645
374
  for (const tableName of writeTables) {
646
375
  const tableRef = this.tableRefs[tableName]
647
376
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
@@ -783,8 +512,8 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
783
512
  * This should only be used for framework-internal purposes;
784
513
  * all app writes should go through applyEvent.
785
514
  */
786
- execute = (query: string, params: ParamsObject = {}, writeTables?: string[]) => {
787
- this.inMemoryDB.execute(query, prepareBindValues(params, query), writeTables)
515
+ execute = (query: string, params: ParamsObject = {}, writeTables?: string[], otelContext?: otel.Context) => {
516
+ this.inMemoryDB.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
788
517
 
789
518
  if (this.storage !== undefined) {
790
519
  const parentSpan = otel.trace.getSpan(otel.context.active())
@@ -907,3 +636,39 @@ const eventToSql = (
907
636
 
908
637
  return { statement, bindValues }
909
638
  }
639
+
640
+ class ReferenceCountedSet<T> {
641
+ private map: Map<T, number>
642
+
643
+ constructor() {
644
+ this.map = new Map<T, number>()
645
+ }
646
+
647
+ add = (key: T) => {
648
+ const count = this.map.get(key) ?? 0
649
+ this.map.set(key, count + 1)
650
+ }
651
+
652
+ remove = (key: T) => {
653
+ const count = this.map.get(key) ?? 0
654
+ if (count === 1) {
655
+ this.map.delete(key)
656
+ } else {
657
+ this.map.set(key, count - 1)
658
+ }
659
+ }
660
+
661
+ has = (key: T) => {
662
+ return this.map.has(key)
663
+ }
664
+
665
+ get size() {
666
+ return this.map.size
667
+ }
668
+
669
+ *[Symbol.iterator]() {
670
+ for (const key of this.map.keys()) {
671
+ yield key
672
+ }
673
+ }
674
+ }