@livestore/livestore 0.0.58-dev.6 → 0.0.58-dev.8

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 (44) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/effect/LiveStore.js +1 -1
  3. package/dist/effect/LiveStore.js.map +1 -1
  4. package/dist/react/LiveStoreProvider.js +2 -2
  5. package/dist/react/LiveStoreProvider.js.map +1 -1
  6. package/dist/react/useLocalId.d.ts.map +1 -1
  7. package/dist/react/useLocalId.js +1 -0
  8. package/dist/react/useLocalId.js.map +1 -1
  9. package/dist/react/useQuery.d.ts.map +1 -1
  10. package/dist/react/useQuery.js +1 -0
  11. package/dist/react/useQuery.js.map +1 -1
  12. package/dist/react/useRow.test.js +5 -359
  13. package/dist/react/useRow.test.js.map +1 -1
  14. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  15. package/dist/react/useTemporaryQuery.js +12 -7
  16. package/dist/react/useTemporaryQuery.js.map +1 -1
  17. package/dist/react/useTemporaryQuery.test.js +23 -1
  18. package/dist/react/useTemporaryQuery.test.js.map +1 -1
  19. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  20. package/dist/reactiveQueries/sql.js +2 -2
  21. package/dist/reactiveQueries/sql.js.map +1 -1
  22. package/dist/store-devtools.js +1 -1
  23. package/dist/store-devtools.js.map +1 -1
  24. package/dist/store.d.ts +17 -13
  25. package/dist/store.d.ts.map +1 -1
  26. package/dist/store.js +71 -38
  27. package/dist/store.js.map +1 -1
  28. package/dist/utils/dev.d.ts.map +1 -1
  29. package/dist/utils/dev.js +1 -0
  30. package/dist/utils/dev.js.map +1 -1
  31. package/package.json +14 -13
  32. package/src/ambient.d.ts +3 -0
  33. package/src/effect/LiveStore.ts +1 -1
  34. package/src/react/LiveStoreProvider.tsx +2 -2
  35. package/src/react/__snapshots__/useRow.test.tsx.snap +359 -0
  36. package/src/react/useLocalId.ts +1 -0
  37. package/src/react/useQuery.ts +1 -0
  38. package/src/react/useRow.test.tsx +5 -359
  39. package/src/react/useTemporaryQuery.test.tsx +44 -2
  40. package/src/react/useTemporaryQuery.ts +23 -13
  41. package/src/reactiveQueries/sql.ts +2 -2
  42. package/src/store-devtools.ts +1 -1
  43. package/src/store.ts +111 -49
  44. package/src/utils/dev.ts +1 -0
@@ -12,7 +12,7 @@ import * as LiveStoreReact from './index.js'
12
12
  import type { StackInfo } from './utils/stack-info.js'
13
13
 
14
14
  // NOTE running tests concurrently doesn't work with the default global db graph
15
- describe.concurrent('useRow', () => {
15
+ describe('useRow', () => {
16
16
  it('should update the data based on component key', () =>
17
17
  Effect.gen(function* () {
18
18
  const { wrapper, AppComponentSchema, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvc({
@@ -184,7 +184,7 @@ describe.concurrent('useRow', () => {
184
184
 
185
185
  expect(appRouterRenderCount.val).toBe(2)
186
186
  expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
187
- `"{"completed":false,"id":"t1","text":"buy milk"}"`,
187
+ `"{"id":"t1","text":"buy milk","completed":false}"`,
188
188
  )
189
189
 
190
190
  expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
@@ -334,365 +334,11 @@ describe.concurrent('useRow', () => {
334
334
  })
335
335
  }
336
336
 
337
+ // TODO improve testing setup so "obsolete" warning is avoided
337
338
  if (strictMode) {
338
- expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchInlineSnapshot(`
339
- {
340
- "_name": "test",
341
- "children": [
342
- {
343
- "_name": "livestore.in-memory-db:execute",
344
- "attributes": {
345
- "sql.query": "
346
- PRAGMA page_size=32768;
347
- PRAGMA cache_size=10000;
348
- PRAGMA journal_mode='MEMORY'; -- we don't flush to disk before committing a write
349
- PRAGMA synchronous='OFF';
350
- PRAGMA temp_store='MEMORY';
351
- PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
352
- ",
353
- },
354
- },
355
- {
356
- "_name": "sql-in-memory-select",
357
- "attributes": {
358
- "sql.cached": false,
359
- "sql.query": "select 1 from UserInfo where id = 'u1'",
360
- "sql.rowsCount": 0,
361
- },
362
- },
363
- {
364
- "_name": "sql-in-memory-select",
365
- "attributes": {
366
- "sql.cached": false,
367
- "sql.query": "select 1 from UserInfo where id = 'u2'",
368
- "sql.rowsCount": 1,
369
- },
370
- },
371
- {
372
- "_name": "LiveStore:mutations",
373
- "children": [
374
- {
375
- "_name": "LiveStore:mutate",
376
- "attributes": {
377
- "livestore.mutateLabel": "mutate",
378
- },
379
- "children": [
380
- {
381
- "_name": "LiveStore:processWrites",
382
- "attributes": {
383
- "livestore.mutateLabel": "mutate",
384
- },
385
- "children": [
386
- {
387
- "_name": "LiveStore:mutateWithoutRefresh",
388
- "attributes": {
389
- "livestore.args": "{
390
- "sql": "INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')"
391
- }",
392
- "livestore.mutation": "livestore.RawSql",
393
- },
394
- "children": [
395
- {
396
- "_name": "livestore.in-memory-db:execute",
397
- "attributes": {
398
- "sql.query": "INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')",
399
- },
400
- },
401
- ],
402
- },
403
- ],
404
- },
405
- ],
406
- },
407
- ],
408
- },
409
- {
410
- "_name": "LiveStore:queries",
411
- "children": [
412
- {
413
- "_name": "sql:select * from UserInfo where id = 'u1' limit 1",
414
- "attributes": {
415
- "sql.query": "select * from UserInfo where id = 'u1' limit 1",
416
- "sql.rowsCount": 1,
417
- },
418
- "children": [
419
- {
420
- "_name": "sql-in-memory-select",
421
- "attributes": {
422
- "sql.cached": false,
423
- "sql.query": "select * from UserInfo where id = 'u1' limit 1",
424
- "sql.rowsCount": 1,
425
- },
426
- },
427
- ],
428
- },
429
- {
430
- "_name": "LiveStore:useRow:UserInfo:u1",
431
- "attributes": {
432
- "id": "u1",
433
- },
434
- "children": [
435
- {
436
- "_name": "LiveStore:mutateWithoutRefresh",
437
- "attributes": {
438
- "livestore.args": "{
439
- "id": "u1"
440
- }",
441
- "livestore.mutation": "_Derived_Create_UserInfo",
442
- },
443
- "children": [
444
- {
445
- "_name": "livestore.in-memory-db:execute",
446
- "attributes": {
447
- "sql.query": "INSERT INTO UserInfo (username, text, id) VALUES ($username, $text, $id)",
448
- },
449
- },
450
- ],
451
- },
452
- {
453
- "_name": "LiveStore:useQuery:sql(rowQuery:query:UserInfo:u1)",
454
- "attributes": {
455
- "label": "sql(rowQuery:query:UserInfo:u1)",
456
- "stackInfo": "{"frames":[{"name":"renderHook.wrapper","filePath":"__REPLACED_FOR_SNAPSHOT__"},{"name":"useRow","filePath":"__REPLACED_FOR_SNAPSHOT__"}]}",
457
- },
458
- "children": [
459
- {
460
- "_name": "sql:select * from UserInfo where id = 'u1' limit 1",
461
- "attributes": {
462
- "sql.query": "select * from UserInfo where id = 'u1' limit 1",
463
- "sql.rowsCount": 1,
464
- },
465
- "children": [
466
- {
467
- "_name": "sql-in-memory-select",
468
- "attributes": {
469
- "sql.cached": false,
470
- "sql.query": "select * from UserInfo where id = 'u1' limit 1",
471
- "sql.rowsCount": 1,
472
- },
473
- },
474
- ],
475
- },
476
- {
477
- "_name": "LiveStore.subscribe",
478
- "attributes": {
479
- "label": "sql(rowQuery:query:UserInfo:u1)",
480
- "queryLabel": "sql(rowQuery:query:UserInfo:u1)",
481
- },
482
- },
483
- {
484
- "_name": "LiveStore.subscribe",
485
- "attributes": {
486
- "label": "sql(rowQuery:query:UserInfo:u1)",
487
- "queryLabel": "sql(rowQuery:query:UserInfo:u1)",
488
- },
489
- },
490
- ],
491
- },
492
- ],
493
- },
494
- ],
495
- },
496
- ],
497
- }
498
- `)
499
- // Below: Strict mode disabled
339
+ expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=true')
500
340
  } else {
501
- expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchInlineSnapshot(`
502
- {
503
- "_name": "test",
504
- "children": [
505
- {
506
- "_name": "livestore.in-memory-db:execute",
507
- "attributes": {
508
- "sql.query": "
509
- PRAGMA page_size=32768;
510
- PRAGMA cache_size=10000;
511
- PRAGMA journal_mode='MEMORY'; -- we don't flush to disk before committing a write
512
- PRAGMA synchronous='OFF';
513
- PRAGMA temp_store='MEMORY';
514
- PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
515
- ",
516
- },
517
- },
518
- {
519
- "_name": "sql-in-memory-select",
520
- "attributes": {
521
- "sql.cached": false,
522
- "sql.query": "select 1 from UserInfo where id = 'u1'",
523
- "sql.rowsCount": 0,
524
- },
525
- },
526
- {
527
- "_name": "sql-in-memory-select",
528
- "attributes": {
529
- "sql.cached": false,
530
- "sql.query": "select 1 from UserInfo where id = 'u2'",
531
- "sql.rowsCount": 1,
532
- },
533
- },
534
- {
535
- "_name": "LiveStore:mutations",
536
- "children": [
537
- {
538
- "_name": "LiveStore:mutate",
539
- "attributes": {
540
- "livestore.mutateLabel": "mutate",
541
- },
542
- "children": [
543
- {
544
- "_name": "LiveStore:processWrites",
545
- "attributes": {
546
- "livestore.mutateLabel": "mutate",
547
- },
548
- "children": [
549
- {
550
- "_name": "LiveStore:mutateWithoutRefresh",
551
- "attributes": {
552
- "livestore.args": "{
553
- "sql": "INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')"
554
- }",
555
- "livestore.mutation": "livestore.RawSql",
556
- },
557
- "children": [
558
- {
559
- "_name": "livestore.in-memory-db:execute",
560
- "attributes": {
561
- "sql.query": "INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')",
562
- },
563
- },
564
- ],
565
- },
566
- ],
567
- },
568
- ],
569
- },
570
- ],
571
- },
572
- {
573
- "_name": "LiveStore:queries",
574
- "children": [
575
- {
576
- "_name": "sql:select * from UserInfo where id = 'u1' limit 1",
577
- "attributes": {
578
- "sql.query": "select * from UserInfo where id = 'u1' limit 1",
579
- "sql.rowsCount": 1,
580
- },
581
- "children": [
582
- {
583
- "_name": "sql-in-memory-select",
584
- "attributes": {
585
- "sql.cached": false,
586
- "sql.query": "select * from UserInfo where id = 'u1' limit 1",
587
- "sql.rowsCount": 1,
588
- },
589
- },
590
- ],
591
- },
592
- {
593
- "_name": "LiveStore:useRow:UserInfo:u1",
594
- "attributes": {
595
- "id": "u1",
596
- },
597
- "children": [
598
- {
599
- "_name": "LiveStore:mutateWithoutRefresh",
600
- "attributes": {
601
- "livestore.args": "{
602
- "id": "u1"
603
- }",
604
- "livestore.mutation": "_Derived_Create_UserInfo",
605
- },
606
- "children": [
607
- {
608
- "_name": "livestore.in-memory-db:execute",
609
- "attributes": {
610
- "sql.query": "INSERT INTO UserInfo (username, text, id) VALUES ($username, $text, $id)",
611
- },
612
- },
613
- ],
614
- },
615
- {
616
- "_name": "LiveStore:useQuery:sql(rowQuery:query:UserInfo:u1)",
617
- "attributes": {
618
- "label": "sql(rowQuery:query:UserInfo:u1)",
619
- "stackInfo": "{"frames":[{"name":"renderHook.wrapper","filePath":"__REPLACED_FOR_SNAPSHOT__"},{"name":"useRow","filePath":"__REPLACED_FOR_SNAPSHOT__"}]}",
620
- },
621
- "children": [
622
- {
623
- "_name": "sql:select * from UserInfo where id = 'u1' limit 1",
624
- "attributes": {
625
- "sql.query": "select * from UserInfo where id = 'u1' limit 1",
626
- "sql.rowsCount": 1,
627
- },
628
- "children": [
629
- {
630
- "_name": "sql-in-memory-select",
631
- "attributes": {
632
- "sql.cached": false,
633
- "sql.query": "select * from UserInfo where id = 'u1' limit 1",
634
- "sql.rowsCount": 1,
635
- },
636
- },
637
- ],
638
- },
639
- {
640
- "_name": "LiveStore.subscribe",
641
- "attributes": {
642
- "label": "sql(rowQuery:query:UserInfo:u1)",
643
- "queryLabel": "sql(rowQuery:query:UserInfo:u1)",
644
- },
645
- },
646
- ],
647
- },
648
- ],
649
- },
650
- {
651
- "_name": "LiveStore:useRow:UserInfo:u2",
652
- "attributes": {
653
- "id": "u2",
654
- },
655
- "children": [
656
- {
657
- "_name": "LiveStore:useQuery:sql(rowQuery:query:UserInfo:u2)",
658
- "attributes": {
659
- "label": "sql(rowQuery:query:UserInfo:u2)",
660
- "stackInfo": "{"frames":[{"name":"renderHook.wrapper","filePath":"__REPLACED_FOR_SNAPSHOT__"},{"name":"useRow","filePath":"__REPLACED_FOR_SNAPSHOT__"}]}",
661
- },
662
- "children": [
663
- {
664
- "_name": "sql:select * from UserInfo where id = 'u2' limit 1",
665
- "attributes": {
666
- "sql.query": "select * from UserInfo where id = 'u2' limit 1",
667
- "sql.rowsCount": 1,
668
- },
669
- "children": [
670
- {
671
- "_name": "sql-in-memory-select",
672
- "attributes": {
673
- "sql.cached": false,
674
- "sql.query": "select * from UserInfo where id = 'u2' limit 1",
675
- "sql.rowsCount": 1,
676
- },
677
- },
678
- ],
679
- },
680
- {
681
- "_name": "LiveStore.subscribe",
682
- "attributes": {
683
- "label": "sql(rowQuery:query:UserInfo:u2)",
684
- "queryLabel": "sql(rowQuery:query:UserInfo:u2)",
685
- },
686
- },
687
- ],
688
- },
689
- ],
690
- },
691
- ],
692
- },
693
- ],
694
- }
695
- `)
341
+ expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=false')
696
342
  }
697
343
  })
698
344
  })
@@ -1,9 +1,12 @@
1
1
  import { Effect, Schema } from '@livestore/utils/effect'
2
- import { renderHook } from '@testing-library/react'
2
+ import { render, renderHook } from '@testing-library/react'
3
+ import React from 'react'
4
+ // @ts-expect-error no types
5
+ import * as ReactWindow from 'react-window'
3
6
  import { describe, expect, it } from 'vitest'
4
7
 
5
8
  import { makeTodoMvc, tables, todos } from '../__tests__/react/fixture.js'
6
- import type * as LiveStore from '../index.js'
9
+ import * as LiveStore from '../index.js'
7
10
  import { querySQL } from '../reactiveQueries/sql.js'
8
11
  import * as LiveStoreReact from './index.js'
9
12
 
@@ -53,4 +56,43 @@ describe('useTemporaryQuery', () => {
53
56
 
54
57
  expect(queryMap.get('t2')!.runs).toBe(1)
55
58
  }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
59
+
60
+ // NOTE this test covers some special react lifecyle paths which I couldn't easily reproduce without react-window
61
+ // it basically causes a "query swap" in the `useMemo` and both a `useEffect` cleanup call.
62
+ // To handle this properly we introduced the `_tag: 'destroyed'` state in the `spanAlreadyStartedCache`.
63
+ it('should work for a list with react-window', () =>
64
+ Effect.gen(function* () {
65
+ const { wrapper } = yield* makeTodoMvc()
66
+
67
+ const ListWrapper: React.FC<{ numItems: number }> = ({ numItems }) => {
68
+ return (
69
+ <ReactWindow.FixedSizeList
70
+ height={100}
71
+ width={100}
72
+ itemSize={10}
73
+ itemCount={numItems}
74
+ itemData={Array.from({ length: numItems }, (_, i) => i).reverse()}
75
+ >
76
+ {ListItem}
77
+ </ReactWindow.FixedSizeList>
78
+ )
79
+ }
80
+
81
+ const ListItem: React.FC<{ data: ReadonlyArray<number>; index: number }> = ({ data: ids, index }) => {
82
+ const id = ids[index]!
83
+ const res = LiveStoreReact.useTemporaryQuery(
84
+ () => LiveStore.computed(() => id, { label: `ListItem.${id}` }),
85
+ id,
86
+ )
87
+ return <div role="listitem">{res}</div>
88
+ }
89
+
90
+ const renderResult = render(<ListWrapper numItems={1} />, { wrapper })
91
+
92
+ expect(renderResult.container.textContent).toBe('0')
93
+
94
+ renderResult.rerender(<ListWrapper numItems={2} />)
95
+
96
+ expect(renderResult.container.textContent).toBe('10')
97
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
56
98
  })
@@ -12,12 +12,16 @@ import { useQueryRef } from './useQuery.js'
12
12
  // Please definitely open an issue if you see or run into any problems with this approach!
13
13
  const cache = new Map<
14
14
  string,
15
- {
16
- rc: number
17
- query$: LiveQuery<any, any>
18
- span: otel.Span
19
- otelContext: otel.Context
20
- }
15
+ | {
16
+ _tag: 'active'
17
+ rc: number
18
+ query$: LiveQuery<any, any>
19
+ span: otel.Span
20
+ otelContext: otel.Context
21
+ }
22
+ | {
23
+ _tag: 'destroyed'
24
+ }
21
25
  >()
22
26
 
23
27
  export type DepKey = string | number | ReadonlyArray<string | number>
@@ -60,22 +64,24 @@ export const useMakeTemporaryQuery = <TResult, TQueryInfo extends QueryInfo>(
60
64
 
61
65
  const { query$, otelContext } = React.useMemo(() => {
62
66
  if (fullKeyRef.current !== undefined && fullKeyRef.current !== fullKey) {
63
- // console.debug('fullKey changed, destroying previous', fullKeyRef.current.split('-')[0]!, fullKey.split('-')[0]!)
67
+ // console.debug('fullKey changed', 'prev', fullKeyRef.current.split('-')[0]!, '-> new', fullKey.split('-')[0]!)
64
68
 
65
69
  const cachedItem = cache.get(fullKeyRef.current)
66
- if (cachedItem !== undefined) {
70
+ if (cachedItem !== undefined && cachedItem._tag === 'active') {
67
71
  cachedItem.rc--
68
72
 
69
73
  if (cachedItem.rc === 0) {
74
+ // console.debug('rc=0-changed', cachedItem.query$.id, cachedItem.query$.label)
70
75
  cachedItem.query$.destroy()
71
76
  cachedItem.span.end()
72
- cache.delete(fullKeyRef.current)
77
+ cache.set(fullKeyRef.current, { _tag: 'destroyed' })
73
78
  }
74
79
  }
75
80
  }
76
81
 
77
82
  const cachedItem = cache.get(fullKey)
78
- if (cachedItem !== undefined) {
83
+ if (cachedItem !== undefined && cachedItem._tag === 'active') {
84
+ // console.debug('rc++', cachedItem.query$.id, cachedItem.query$.label)
79
85
  cachedItem.rc++
80
86
 
81
87
  return cachedItem
@@ -93,7 +99,7 @@ export const useMakeTemporaryQuery = <TResult, TQueryInfo extends QueryInfo>(
93
99
 
94
100
  const query$ = makeQuery(otelContext)
95
101
 
96
- cache.set(fullKey, { rc: 1, query$, span, otelContext })
102
+ cache.set(fullKey, { _tag: 'active', rc: 1, query$, span, otelContext })
97
103
 
98
104
  return { query$, otelContext }
99
105
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -103,19 +109,23 @@ export const useMakeTemporaryQuery = <TResult, TQueryInfo extends QueryInfo>(
103
109
 
104
110
  React.useEffect(() => {
105
111
  return () => {
112
+ const fullKey = fullKeyRef.current!
106
113
  const cachedItem = cache.get(fullKey)
107
114
  // NOTE in case the fullKey changed then the query was already destroyed in the useMemo above
108
- if (cachedItem === undefined) return
115
+ if (cachedItem === undefined || cachedItem._tag === 'destroyed') return
116
+
117
+ // console.debug('rc--', cachedItem.query$.id, cachedItem.query$.label)
109
118
 
110
119
  cachedItem.rc--
111
120
 
112
121
  if (cachedItem.rc === 0) {
122
+ // console.debug('rc=0', cachedItem.query$.id, cachedItem.query$.label)
113
123
  cachedItem.query$.destroy()
114
124
  cachedItem.span.end()
115
125
  cache.delete(fullKey)
116
126
  }
117
127
  }
118
- }, [fullKey])
128
+ }, [])
119
129
 
120
130
  return { query$, otelContext }
121
131
  }
@@ -1,6 +1,6 @@
1
1
  import { type Bindable, prepareBindValues, type QueryInfo, type QueryInfoNone } from '@livestore/common'
2
2
  import { shouldNeverHappen } from '@livestore/utils'
3
- import { Schema, SchemaEquivalence, TreeFormatter } from '@livestore/utils/effect'
3
+ import { Schema, TreeFormatter } from '@livestore/utils/effect'
4
4
  import * as otel from '@opentelemetry/api'
5
5
 
6
6
  import { globalReactivityGraph } from '../global-state.js'
@@ -124,7 +124,7 @@ export class LiveStoreSQLQuery<
124
124
 
125
125
  const queriedTablesRef = { current: queriedTables }
126
126
 
127
- const schemaEqual = SchemaEquivalence.make(schema)
127
+ const schemaEqual = Schema.equivalence(schema)
128
128
  // TODO also support derived equality for `map` (probably will depend on having an easy way to transform a schema without an `encode` step)
129
129
  // This would mean dropping the `map` option
130
130
  const equal =
@@ -56,7 +56,7 @@ export const connectDevtoolsToStore = ({
56
56
 
57
57
  const requestId = decodedMessage.requestId
58
58
 
59
- const requestIdleCallback = window.requestIdleCallback ?? ((cb: Function) => cb())
59
+ const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
60
60
 
61
61
  switch (decodedMessage._tag) {
62
62
  case 'LSD.ReactivityGraphSubscribe': {