@playfast/reform-query-browser 0.1.0 → 1.0.2

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@playfast/reform-query-browser",
3
3
  "playbook": "./playbook",
4
- "version": "0.1.0",
4
+ "version": "1.0.2",
5
5
  "type": "module",
6
6
  "description": "Browser host layers for Reform's AsyncCalc — window focus/online auto-invalidation and localStorage persistence. Keeps reform core DOM-free: implements the optional QueryEvents and QueryStore seams.",
7
7
  "keywords": [
package/src/index.ts CHANGED
@@ -1,8 +1,3 @@
1
- // `@playfast/reform-query-browser` — browser host layers for AsyncCalc's optional
2
- // seams. Reform core stays DOM-free; these layers implement `QueryEvents`
3
- // (window focus/online) and `QueryStore` (localStorage), plus the focus/reconnect
4
- // auto-invalidation managers built on them. Provide them alongside `Engine`.
5
-
6
1
  export { QueryEventsBrowser } from './queryEvents.browser'
7
2
  export { localStorageQueryStore } from './localStorage.store'
8
3
  export { BrowserQueryDefaults, RefetchOnFocus, RefetchOnReconnect } from './refetchManagers'
@@ -1,15 +1,10 @@
1
1
  import { QueryStore, type QueryStoreApi, noopQueryStore } from '@playfast/reform'
2
2
  import { Effect, Layer, Option, Schema } from 'effect'
3
3
 
4
- // `QueryStore` implemented over `localStorage` (JSON values under a namespace
5
- // prefix). The driver hands already-`output`-encoded values across this seam, so
6
- // JSON round-trips faithfully; a corrupt/absent entry reads as `None` and the
7
- // query just fetches fresh. Off the main thread it falls back to the no-op store.
8
-
9
4
  const PREFIX = 'reform-query:'
10
5
 
11
6
  const JsonValue = Schema.parseJson(Schema.Unknown)
12
- // A hand-edited or version-skewed entry fails to decode `None`: a cache miss, fetch fresh.
7
+ // Corrupt/version-skewed entry → None (cache miss); fetch fresh.
13
8
  const decodeJson = Schema.decodeUnknownOption(JsonValue)
14
9
  const encodeJson = Schema.encodeSync(JsonValue)
15
10
 
@@ -34,5 +29,4 @@ const makeApi = (): QueryStoreApi => {
34
29
  }
35
30
  }
36
31
 
37
- /** A `QueryStore` backed by `window.localStorage`. Provide it to enable `persist`. */
38
32
  export const localStorageQueryStore: Layer.Layer<QueryStore> = Layer.succeed(QueryStore, makeApi())
@@ -1,13 +1,6 @@
1
1
  import { QueryEvents, type QueryEventsApi, noopQueryEvents } from '@playfast/reform'
2
2
  import { Effect, Layer } from 'effect'
3
3
 
4
- // `QueryEvents` implemented over the browser: window focus/visibility drives the
5
- // focus signal, `online` drives the reconnect signal. This is the only place the
6
- // DOM is touched — reform core stays renderer-neutral. Off the main thread (SSR,
7
- // a worker, a test with no `window`), it falls back to the inert no-op source, so
8
- // the same app layer is safe everywhere.
9
-
10
- /** A browser-backed `QueryEvents`, scoped so the window listeners are removed with the layer. */
11
4
  export const QueryEventsBrowser: Layer.Layer<QueryEvents> = Layer.scoped(
12
5
  QueryEvents,
13
6
  Effect.gen(function* () {
@@ -20,8 +13,7 @@ export const QueryEventsBrowser: Layer.Layer<QueryEvents> = Layer.scoped(
20
13
  const fire = (listeners: ReadonlySet<() => void>) => {
21
14
  listeners.forEach((listener) => listener())
22
15
  }
23
- // Refocus and tab-becomes-visible both count as "focus"; ignore the
24
- // visibility event that fires on hide.
16
+ // Focus + tab-visible both fire; ignore hide visibilitychange.
25
17
  const onFocus = () => {
26
18
  if (document.visibilityState === 'visible') {
27
19
  fire(focusListeners)
@@ -29,7 +21,6 @@ export const QueryEventsBrowser: Layer.Layer<QueryEvents> = Layer.scoped(
29
21
  }
30
22
  const onOnline = () => fire(onlineListeners)
31
23
 
32
- // `visibilitychange` targets `document`; `focus`/`online` target `window`.
33
24
  document.addEventListener('visibilitychange', onFocus)
34
25
  window.addEventListener('focus', onFocus)
35
26
  window.addEventListener('online', onOnline)
@@ -2,12 +2,6 @@ import { QueryEvents, type QueryEventsApi, Queries } from '@playfast/reform'
2
2
  import { Effect, Layer } from 'effect'
3
3
  import { QueryEventsBrowser } from './queryEvents.browser'
4
4
 
5
- // Auto-invalidation managers: a window signal (focus / reconnect) invalidates
6
- // every live query, so stale + actively-read queries refetch (React-Query's
7
- // `refetchOnWindowFocus` / `refetchOnReconnect`). They are pure routers — the
8
- // signal comes from `QueryEvents`, the act is `handle.invalidate()` on the
9
- // `Queries` registry — so nothing here touches the DOM directly.
10
-
11
5
  const onSignal = (
12
6
  pick: (events: QueryEventsApi) => (listener: () => void) => () => void,
13
7
  ): Layer.Layer<never, never, QueryEvents | Queries> =>
@@ -23,21 +17,14 @@ const onSignal = (
23
17
  }),
24
18
  )
25
19
 
26
- /** Invalidate every live query when the window regains focus. Requires `QueryEvents`. */
27
20
  export const RefetchOnFocus: Layer.Layer<never, never, QueryEvents | Queries> = onSignal(
28
21
  (events) => events.subscribeFocus,
29
22
  )
30
23
 
31
- /** Invalidate every live query when the network reconnects. Requires `QueryEvents`. */
32
24
  export const RefetchOnReconnect: Layer.Layer<never, never, QueryEvents | Queries> = onSignal(
33
25
  (events) => events.subscribeOnline,
34
26
  )
35
27
 
36
- /**
37
- * The common browser default: focus + reconnect auto-invalidation wired over the
38
- * browser `QueryEvents`, so the app only needs `Queries` (part of `Engine`) in
39
- * context. Merge it alongside `Engine`; add `localStorageQueryStore` to persist.
40
- */
41
28
  export const BrowserQueryDefaults: Layer.Layer<never, never, Queries> = Layer.mergeAll(
42
29
  RefetchOnFocus,
43
30
  RefetchOnReconnect,
@@ -1,10 +1,5 @@
1
1
  /**
2
2
  * @vitest-environment happy-dom
3
- *
4
- * The browser host layers, end to end over a real (happy-dom) window: window
5
- * focus invalidates live queries (so an actively-read query refetches), and
6
- * `persist` hydrates from / writes through `localStorage` — with reform core
7
- * never touching the DOM.
8
3
  */
9
4
  import { expect, it } from '@effect/vitest'
10
5
  import { AsyncCalc, Engine, State, StateGroup } from '@playfast/reform'
@@ -13,7 +8,11 @@ import { localStorageQueryStore } from './localStorage.store'
13
8
  import { QueryEventsBrowser } from './queryEvents.browser'
14
9
  import { RefetchOnFocus } from './refetchManagers'
15
10
 
16
- const tick = (ms = 10) => Effect.sleep(Duration.millis(ms))
11
+ const waitUntil = (predicate: () => boolean) =>
12
+ Effect.sleep(Duration.millis(5)).pipe(
13
+ Effect.repeat({ until: predicate }),
14
+ Effect.timeout(Duration.seconds(2)),
15
+ )
17
16
 
18
17
  it.live('window focus invalidates a live query, so an active reader refetches', () => {
19
18
  class Count extends State.make('count', S.Number) {}
@@ -40,12 +39,11 @@ it.live('window focus invalidates a live query, so an active reader refetches',
40
39
  return Effect.gen(function* () {
41
40
  const store = yield* Q.store
42
41
  store.subscribe(() => {}) // an active reader (a mounted view)
43
- yield* tick(30)
42
+ yield* waitUntil(() => runs.n === 1)
44
43
  expect(runs.n).toBe(1)
45
44
 
46
- // A focus event invalidates the query; stale + active ⇒ refetch.
47
45
  window.dispatchEvent(new Event('focus'))
48
- yield* tick(30)
46
+ yield* waitUntil(() => runs.n === 2)
49
47
  expect(runs.n).toBe(2)
50
48
  }).pipe(Effect.provide(App))
51
49
  })
@@ -73,7 +71,7 @@ it.live('persist hydrates from localStorage (stale) then writes the settled valu
73
71
  const view = yield* Q.store
74
72
  expect(view.get()).toMatchObject({ _tag: 'Success', value: 99, refetching: true })
75
73
 
76
- yield* tick(40)
74
+ yield* waitUntil(() => localStorage.getItem('reform-query:P') === '10')
77
75
  expect(view.get()).toMatchObject({ _tag: 'Success', value: 10, refetching: false })
78
76
  expect(localStorage.getItem('reform-query:P')).toBe('10')
79
77
  }).pipe(Effect.provide(App))