@typed/navigation 0.17.0 → 0.18.1

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 (115) hide show
  1. package/.nvmrc +1 -0
  2. package/biome.json +39 -0
  3. package/dist/Blocking.d.ts +23 -0
  4. package/dist/Blocking.js +41 -0
  5. package/dist/Blocking.js.map +1 -0
  6. package/dist/Destination.d.ts +11 -0
  7. package/dist/Destination.js +10 -0
  8. package/dist/Destination.js.map +1 -0
  9. package/dist/Error.d.ts +33 -0
  10. package/dist/Error.js +22 -0
  11. package/dist/Error.js.map +1 -0
  12. package/dist/Event.d.ts +45 -0
  13. package/dist/Event.js +17 -0
  14. package/dist/Event.js.map +1 -0
  15. package/dist/Forms.d.ts +79 -0
  16. package/dist/Forms.js +111 -0
  17. package/dist/Forms.js.map +1 -0
  18. package/dist/Handler.d.ts +6 -0
  19. package/dist/Handler.js +2 -0
  20. package/dist/Handler.js.map +1 -0
  21. package/dist/{dts/Layer.d.ts → Layer.d.ts} +11 -8
  22. package/dist/{esm/Layer.js → Layer.js} +2 -2
  23. package/dist/Layer.js.map +1 -0
  24. package/dist/NavigateOptions.d.ts +7 -0
  25. package/dist/NavigateOptions.js +7 -0
  26. package/dist/NavigateOptions.js.map +1 -0
  27. package/dist/Navigation.d.ts +79 -0
  28. package/dist/Navigation.js +49 -0
  29. package/dist/Navigation.js.map +1 -0
  30. package/dist/NavigationType.d.ts +3 -0
  31. package/dist/NavigationType.js +3 -0
  32. package/dist/NavigationType.js.map +1 -0
  33. package/dist/ProposedDestination.d.ts +13 -0
  34. package/dist/ProposedDestination.js +4 -0
  35. package/dist/ProposedDestination.js.map +1 -0
  36. package/dist/Url.d.ts +13 -0
  37. package/dist/Url.js +72 -0
  38. package/dist/Url.js.map +1 -0
  39. package/dist/index.d.ts +12 -0
  40. package/dist/index.js +13 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/internal/fromWindow.d.ts +4 -0
  43. package/dist/internal/fromWindow.js +358 -0
  44. package/dist/internal/fromWindow.js.map +1 -0
  45. package/dist/internal/memory.d.ts +6 -0
  46. package/dist/internal/memory.js +59 -0
  47. package/dist/internal/memory.js.map +1 -0
  48. package/dist/internal/shared.d.ts +109 -0
  49. package/dist/{esm/internal → internal}/shared.js +134 -165
  50. package/dist/internal/shared.js.map +1 -0
  51. package/package.json +35 -52
  52. package/readme.md +243 -0
  53. package/src/Blocking.ts +65 -65
  54. package/src/Destination.ts +14 -0
  55. package/src/Error.ts +28 -0
  56. package/src/Event.ts +26 -0
  57. package/src/Forms.ts +216 -0
  58. package/src/Handler.ts +16 -0
  59. package/src/Layer.ts +20 -9
  60. package/src/NavigateOptions.ts +9 -0
  61. package/src/Navigation.test.ts +697 -0
  62. package/src/Navigation.ts +133 -468
  63. package/src/NavigationType.ts +5 -0
  64. package/src/ProposedDestination.ts +8 -0
  65. package/src/Url.ts +106 -0
  66. package/src/index.ts +12 -17
  67. package/src/internal/fromWindow.ts +250 -180
  68. package/src/internal/memory.ts +62 -49
  69. package/src/internal/shared.ts +238 -305
  70. package/tsconfig.json +30 -0
  71. package/Blocking/package.json +0 -6
  72. package/LICENSE +0 -21
  73. package/Layer/package.json +0 -6
  74. package/Navigation/package.json +0 -6
  75. package/README.md +0 -5
  76. package/dist/cjs/Blocking.js +0 -58
  77. package/dist/cjs/Blocking.js.map +0 -1
  78. package/dist/cjs/Layer.js +0 -27
  79. package/dist/cjs/Layer.js.map +0 -1
  80. package/dist/cjs/Navigation.js +0 -275
  81. package/dist/cjs/Navigation.js.map +0 -1
  82. package/dist/cjs/index.js +0 -39
  83. package/dist/cjs/index.js.map +0 -1
  84. package/dist/cjs/internal/fromWindow.js +0 -421
  85. package/dist/cjs/internal/fromWindow.js.map +0 -1
  86. package/dist/cjs/internal/memory.js +0 -72
  87. package/dist/cjs/internal/memory.js.map +0 -1
  88. package/dist/cjs/internal/shared.js +0 -522
  89. package/dist/cjs/internal/shared.js.map +0 -1
  90. package/dist/dts/Blocking.d.ts +0 -34
  91. package/dist/dts/Blocking.d.ts.map +0 -1
  92. package/dist/dts/Layer.d.ts.map +0 -1
  93. package/dist/dts/Navigation.d.ts +0 -462
  94. package/dist/dts/Navigation.d.ts.map +0 -1
  95. package/dist/dts/index.d.ts +0 -17
  96. package/dist/dts/index.d.ts.map +0 -1
  97. package/dist/dts/internal/fromWindow.d.ts +0 -12
  98. package/dist/dts/internal/fromWindow.d.ts.map +0 -1
  99. package/dist/dts/internal/memory.d.ts +0 -6
  100. package/dist/dts/internal/memory.d.ts.map +0 -1
  101. package/dist/dts/internal/shared.d.ts +0 -114
  102. package/dist/dts/internal/shared.d.ts.map +0 -1
  103. package/dist/esm/Blocking.js +0 -46
  104. package/dist/esm/Blocking.js.map +0 -1
  105. package/dist/esm/Layer.js.map +0 -1
  106. package/dist/esm/Navigation.js +0 -237
  107. package/dist/esm/Navigation.js.map +0 -1
  108. package/dist/esm/index.js +0 -17
  109. package/dist/esm/index.js.map +0 -1
  110. package/dist/esm/internal/fromWindow.js +0 -310
  111. package/dist/esm/internal/fromWindow.js.map +0 -1
  112. package/dist/esm/internal/memory.js +0 -56
  113. package/dist/esm/internal/memory.js.map +0 -1
  114. package/dist/esm/internal/shared.js.map +0 -1
  115. package/dist/esm/package.json +0 -4
@@ -1,22 +1,20 @@
1
- import * as Equivalence from "@effect/schema/Equivalence"
2
- import * as Context from "@typed/context"
3
- import { Window } from "@typed/dom/Window"
4
- import * as RefSubject from "@typed/fx/RefSubject"
5
- import { GetRandomValues, Uuid } from "@typed/id"
6
-
7
- import * as Effect from "effect/Effect"
8
- import type * as Fiber from "effect/Fiber"
9
- import * as Option from "effect/Option"
10
- import * as Runtime from "effect/Runtime"
11
- import * as Scope from "effect/Scope"
12
-
13
- import * as Schema from "@effect/schema/Schema"
14
- import * as Exit from "effect/Exit"
15
- import type * as Layer from "effect/Layer"
16
- import type { Commit } from "../Layer.js"
17
- import type { BeforeNavigationEvent, Destination, NavigationEvent, Transition } from "../Navigation.js"
18
- import { Navigation, NavigationError } from "../Navigation.js"
19
- import type { ModelAndIntent } from "./shared.js"
1
+ import { GetRandomValues, Uuid4 } from '@typed/id'
2
+ import * as LazyRef from '@typed/lazy-ref'
3
+ import { Schema } from 'effect'
4
+ import * as Context from 'effect/Context'
5
+ import * as Effect from 'effect/Effect'
6
+ import * as Exit from 'effect/Exit'
7
+ import type * as Fiber from 'effect/Fiber'
8
+ import * as Layer from 'effect/Layer'
9
+ import * as Option from 'effect/Option'
10
+ import * as Runtime from 'effect/Runtime'
11
+ import * as Scope from 'effect/Scope'
12
+ import type { Destination } from '../Destination.js'
13
+ import { NavigationError } from '../Error.js'
14
+ import type { NavigationEvent, TransitionEvent } from '../Event.js'
15
+ import type { Commit } from '../Layer.js'
16
+ import { Navigation } from '../Navigation.js'
17
+ import type { ModelAndIntent, PatchedState } from './shared.js'
20
18
  import {
21
19
  getOriginalState,
22
20
  getUrl,
@@ -24,138 +22,130 @@ import {
24
22
  makeDestination,
25
23
  makeHandlersState,
26
24
  NavigationState,
27
- setupFromModelAndIntent
28
- } from "./shared.js"
29
-
30
- /* eslint-disable @typescript-eslint/consistent-type-imports */
31
- type NativeNavigation = import("@virtualstate/navigation").Navigation
32
- type NativeEntry = import("@virtualstate/navigation").NavigationHistoryEntry
33
- type NativeEvent = import("@virtualstate/navigation").NavigationEventMap["navigate"]
34
- /* eslint-enable @typescript-eslint/consistent-type-imports */
35
-
36
- declare global {
37
- export interface Window {
38
- navigation?: NativeNavigation
39
- }
40
- }
25
+ setupFromModelAndIntent,
26
+ } from './shared.js'
41
27
 
42
- export const fromWindow: Layer.Layer<Navigation, never, Window> = Navigation.scoped(
43
- Window.withEffect((window) => {
44
- const getRandomValues = (length: number) => Effect.sync(() => window.crypto.getRandomValues(new Uint8Array(length)))
45
- return Effect.gen(function*() {
28
+ export const fromWindow: (window: Window) => Layer.Layer<Navigation, never, GetRandomValues> = (
29
+ window: Window,
30
+ ) =>
31
+ Layer.scoped(
32
+ Navigation,
33
+ Effect.gen(function* () {
34
+ const getRandomValues = yield* GetRandomValues
46
35
  const { run, runPromise } = yield* scopedRuntime<never>()
47
36
  const hasNativeNavigation = !!window.navigation
37
+ const base = getBaseHref(window)
48
38
  const modelAndIntent = yield* hasNativeNavigation
49
- ? setupWithNavigation(window.navigation!, runPromise)
50
- : setupWithHistory(window, (event) => run(handleHistoryEvent(event)))
39
+ ? setupWithNavigation(window.navigation, runPromise)
40
+ : setupWithHistory(window, base, (event) => run(handleHistoryEvent(event)))
51
41
 
52
42
  const navigation = setupFromModelAndIntent(
53
43
  modelAndIntent,
54
44
  window.location.origin,
55
- getBaseHref(window),
45
+ base,
56
46
  getRandomValues,
57
- hasNativeNavigation ? () => getNavigationState(window.navigation!) : undefined
47
+ hasNativeNavigation ? () => getNavigationState(window.navigation) : undefined,
58
48
  )
59
49
 
60
50
  return navigation
61
51
 
62
52
  function handleHistoryEvent(event: HistoryEvent) {
63
- return Effect.gen(function*() {
64
- if (event._tag === "PushState") {
53
+ return Effect.gen(function* () {
54
+ if (event._tag === 'PushState') {
65
55
  return yield* navigation.navigate(event.url, {}, event.skipCommit)
66
- } else if (event._tag === "ReplaceState") {
56
+ } else if (event._tag === 'ReplaceState') {
67
57
  if (Option.isSome(event.url)) {
68
58
  return yield* navigation.navigate(
69
59
  event.url.value,
70
- { history: "replace", state: event.state },
71
- event.skipCommit
60
+ { history: 'replace', state: event.state },
61
+ event.skipCommit,
72
62
  )
73
63
  } else {
74
64
  return yield* navigation.updateCurrentEntry(event)
75
65
  }
76
- } else if (event._tag === "Traverse") {
66
+ } else if (event._tag === 'Traverse') {
77
67
  const { entries, index } = yield* modelAndIntent.state
78
68
  const toIndex = Math.min(Math.max(0, index + event.delta), entries.length - 1)
79
69
  const to = entries[toIndex]
80
70
 
81
- return yield* navigation.traverseTo(to.key, {}, event.skipCommit)
71
+ const result = yield* navigation.traverseTo(to.key, {}, event.skipCommit)
72
+
73
+ return result
82
74
  } else {
83
75
  yield* navigation.traverseTo(event.key, {}, event.skipCommit)
84
- return yield* navigation.updateCurrentEntry({ state: event.state })
76
+ return yield* navigation.updateCurrentEntry({
77
+ state: event.state,
78
+ })
85
79
  }
86
80
  })
87
81
  }
88
- }).pipe(
89
- GetRandomValues.provide(getRandomValues)
90
- )
91
- })
92
- )
82
+ }),
83
+ )
93
84
 
94
85
  function getBaseHref(window: Window) {
95
- const base = window.document.querySelector("base")
96
- return base ? base.href : "/"
86
+ const base = window.document.querySelector('base')
87
+ return base ? base.href : '/'
97
88
  }
98
89
 
99
- const getNavigationState = (navigation: NativeNavigation): NavigationState => {
90
+ const getNavigationState = (navigation: globalThis.Navigation): NavigationState => {
100
91
  const entries = navigation.entries().map(nativeEntryToDestination)
101
- const { index } = navigation.currentEntry
92
+ // biome-ignore lint/style/noNonNullAssertion: <explanation>
93
+ const { index } = navigation.currentEntry!
102
94
 
103
95
  return {
104
96
  entries,
105
97
  index,
106
- transition: Option.none<Transition>()
98
+ transition: Option.none<TransitionEvent>(),
107
99
  }
108
100
  }
109
101
 
110
102
  function setupWithNavigation(
111
- navigation: NativeNavigation,
112
- runPromise: <E, A>(effect: Effect.Effect<A, E, Scope.Scope>) => Promise<A>
103
+ navigation: globalThis.Navigation,
104
+ runPromise: <E, A>(effect: Effect.Effect<A, E, Scope.Scope>) => Promise<A>,
113
105
  ): Effect.Effect<ModelAndIntent, never, Scope.Scope | GetRandomValues> {
114
- return Effect.gen(function*() {
115
- const state = yield* RefSubject.fromEffect(
106
+ return Effect.gen(function* () {
107
+ const state = yield* LazyRef.fromEffect(
116
108
  Effect.sync((): NavigationState => getNavigationState(navigation)),
117
- { eq: Equivalence.make(Schema.typeSchema(Schema.typeSchema(NavigationState))) }
109
+ {
110
+ eq: Schema.equivalence(Schema.typeSchema(NavigationState)),
111
+ },
118
112
  )
119
- const canGoBack = RefSubject.map(state, (s) => s.index > 0)
120
- const canGoForward = RefSubject.map(state, (s) => s.index < s.entries.length - 1)
121
- const { beforeHandlers, formDataHandlers, handlers } = yield* makeHandlersState()
122
- const commit: Commit = (to: Destination, event: BeforeNavigationEvent) =>
123
- Effect.gen(function*(_) {
113
+ const { beforeHandlers, handlers } = yield* makeHandlersState
114
+ const commit: Commit = (to: Destination, event: TransitionEvent) =>
115
+ Effect.gen(function* () {
124
116
  const { key, state, url } = to
125
117
  const { info, type } = event
126
118
 
127
- if (type === "push" || type === "replace") {
128
- yield* _(
129
- Effect.promise(() => navigation.navigate(url.toString(), { history: type, state, info }).committed),
130
- Effect.catchAllDefect((error) => Effect.fail(new NavigationError({ error })))
131
- )
132
- } else if (event.type === "reload") {
133
- yield* _(
134
- Effect.promise(() => navigation.reload({ state, info }).committed),
135
- Effect.catchAllDefect((error) => Effect.fail(new NavigationError({ error })))
119
+ if (type === 'push' || type === 'replace') {
120
+ yield* Effect.promise(
121
+ () =>
122
+ navigation.navigate(url.toString(), {
123
+ history: type,
124
+ state,
125
+ info,
126
+ }).committed,
136
127
  )
128
+ } else if (event.type === 'reload') {
129
+ yield* Effect.promise(() => navigation.reload({ state, info }).committed)
137
130
  } else {
138
- yield* _(
139
- Effect.promise(() => navigation.traverseTo(key, { info }).committed),
140
- Effect.catchAllDefect((error) => Effect.fail(new NavigationError({ error })))
141
- )
131
+ yield* Effect.promise(() => navigation.traverseTo(key, { info }).committed)
142
132
  }
143
- })
133
+ }).pipe(Effect.catchAllDefect((cause) => new NavigationError({ cause })))
144
134
 
145
- const runHandlers = (native: NativeEvent) =>
146
- Effect.gen(function*() {
135
+ const runHandlers = (native: globalThis.NavigationEventMap['navigate']) =>
136
+ Effect.gen(function* () {
147
137
  const eventHandlers = yield* handlers
148
138
  const matches: Array<Effect.Effect<unknown>> = []
149
-
150
139
  const event: NavigationEvent = {
151
140
  type: native.navigationType,
152
- destination: nativeEntryToDestination(navigation.currentEntry),
153
- info: native.info
141
+ // biome-ignore lint/style/noNonNullAssertion: <explanation>
142
+ destination: nativeEntryToDestination(navigation.currentEntry!),
143
+ info: native.info,
154
144
  }
155
145
 
156
146
  for (const [handler, ctx] of eventHandlers) {
157
147
  const match = yield* Effect.provide(handler(event), ctx)
158
- if (Option.isSome(match)) {
148
+ if (match !== undefined && Option.isSome(match)) {
159
149
  matches.push(Effect.provide(match.value, ctx))
160
150
  }
161
151
  }
@@ -165,39 +155,40 @@ function setupWithNavigation(
165
155
  }
166
156
  })
167
157
 
168
- navigation.addEventListener("navigate", (ev) => {
158
+ navigation.addEventListener('navigate', (ev) => {
169
159
  if (shouldNotIntercept(ev)) return
170
160
 
171
161
  ev.intercept({
172
- handler: () => runPromise(runHandlers(ev))
162
+ handler: () => runPromise(runHandlers(ev)),
173
163
  })
174
164
  })
175
165
 
176
166
  return {
177
167
  state,
178
- canGoBack,
179
- canGoForward,
180
168
  beforeHandlers,
181
169
  handlers,
182
- formDataHandlers,
183
- commit
170
+ commit,
184
171
  } as const
185
172
  })
186
173
  }
187
174
 
188
175
  function nativeEntryToDestination(
189
- entry: Pick<NativeEntry, "id" | "key" | "url" | "getState" | "sameDocument">
176
+ entry: Pick<
177
+ globalThis.NavigationHistoryEntry,
178
+ 'id' | 'key' | 'url' | 'getState' | 'sameDocument'
179
+ >,
190
180
  ): Destination {
191
181
  return {
192
- id: Uuid(entry.id),
193
- key: Uuid(entry.key),
182
+ id: Uuid4.make(entry.id),
183
+ key: Uuid4.make(entry.key),
184
+ // biome-ignore lint/style/noNonNullAssertion: <explanation>
194
185
  url: new URL(entry.url!),
195
186
  state: entry.getState(),
196
- sameDocument: entry.sameDocument
187
+ sameDocument: entry.sameDocument,
197
188
  }
198
189
  }
199
190
 
200
- function shouldNotIntercept(navigationEvent: NativeEvent): boolean {
191
+ function shouldNotIntercept(navigationEvent: globalThis.NavigationEventMap['navigate']): boolean {
201
192
  return (
202
193
  !navigationEvent.canIntercept ||
203
194
  // If this is just a hashChange,
@@ -214,76 +205,117 @@ function shouldNotIntercept(navigationEvent: NativeEvent): boolean {
214
205
 
215
206
  function setupWithHistory(
216
207
  window: Window,
217
- onEvent: (event: HistoryEvent) => void
208
+ base: string,
209
+ onEvent: (event: HistoryEvent) => void,
218
210
  ): Effect.Effect<ModelAndIntent, never, GetRandomValues | Scope.Scope> {
219
- return Effect.gen(function*() {
211
+ return Effect.gen(function* () {
220
212
  const { location } = window
221
- const { original: history, unpatch } = patchHistory(window, onEvent)
213
+ const { getHistoryState, original: history, unpatch } = patchHistory(window, onEvent, base)
222
214
 
223
215
  yield* Effect.addFinalizer(() => unpatch)
224
216
 
225
- const state = yield* RefSubject.fromEffect(
217
+ const state = yield* LazyRef.fromEffect(
226
218
  Effect.suspend(() =>
227
219
  Effect.map(
228
- makeDestination(
229
- new URL(location.href),
230
- history.state,
231
- location.origin
232
- ),
233
- (destination): NavigationState => ({ entries: [destination], index: 0, transition: Option.none() })
234
- )
220
+ makeDestination(new URL(location.href), getHistoryState(), location.origin),
221
+ (destination): NavigationState => ({
222
+ entries: [destination],
223
+ index: 0,
224
+ transition: Option.none(),
225
+ }),
226
+ ),
235
227
  ),
236
- { eq: Equivalence.make(Schema.typeSchema(NavigationState)) }
228
+ { eq: Schema.equivalence(Schema.typeSchema(NavigationState)) },
237
229
  )
238
- const canGoBack = RefSubject.map(state, (s) => s.index > 0)
239
- const canGoForward = RefSubject.map(state, (s) => s.index < s.entries.length - 1)
240
- const { beforeHandlers, formDataHandlers, handlers } = yield* makeHandlersState()
241
- const commit: Commit = ({ id, key, state, url }: Destination, event: BeforeNavigationEvent) =>
230
+ const { beforeHandlers, handlers } = yield* makeHandlersState
231
+ const commit: Commit = ({ id, key, state, url }: Destination, event: TransitionEvent) =>
242
232
  Effect.sync(() => {
243
233
  const { type } = event
244
234
 
245
- if (type === "push") {
246
- history.pushState({ id, key, originalHistoryState: state }, "", url)
247
- } else if (type === "replace") {
248
- history.replaceState({ id, key, originalHistoryState: state }, "", url)
249
- } else if (event.type === "reload") {
235
+ if (type === 'push') {
236
+ history.pushState(
237
+ {
238
+ __typed__navigation__id__: id,
239
+ __typed__navigation__key__: key,
240
+ __typed__navigation__state__: state,
241
+ },
242
+ '',
243
+ url,
244
+ )
245
+ } else if (type === 'replace') {
246
+ history.replaceState(
247
+ {
248
+ __typed__navigation__id__: id,
249
+ __typed__navigation__key__: key,
250
+ __typed__navigation__state__: state,
251
+ },
252
+ '',
253
+ url,
254
+ )
255
+ } else if (event.type === 'reload') {
250
256
  location.reload()
251
257
  } else {
252
258
  history.go(event.delta)
259
+
260
+ history.replaceState(
261
+ {
262
+ __typed__navigation__id__: id,
263
+ __typed__navigation__key__: key,
264
+ __typed__navigation__state__: state,
265
+ },
266
+ '',
267
+ window.location.href,
268
+ )
253
269
  }
254
270
  })
255
271
 
256
272
  return {
257
273
  state,
258
- canGoBack,
259
- canGoForward,
260
274
  beforeHandlers,
261
275
  handlers,
262
- formDataHandlers,
263
- commit
276
+ commit,
264
277
  } satisfies ModelAndIntent
265
278
  })
266
279
  }
267
280
 
268
281
  type HistoryEvent = PushStateEvent | ReplaceStateEvent | TraverseEvent | TraverseToEvent
269
282
 
270
- type PushStateEvent = { _tag: "PushState"; state: unknown; url: URL; skipCommit: boolean }
271
- type ReplaceStateEvent = { _tag: "ReplaceState"; state: unknown; url: Option.Option<URL>; skipCommit: boolean }
272
- type TraverseEvent = { _tag: "Traverse"; delta: number; skipCommit: boolean }
273
- type TraverseToEvent = { _tag: "TraverseTo"; key: Uuid; state: unknown; skipCommit: boolean }
283
+ type PushStateEvent = {
284
+ _tag: 'PushState'
285
+ state: unknown
286
+ url: URL
287
+ skipCommit: boolean
288
+ }
289
+ type ReplaceStateEvent = {
290
+ _tag: 'ReplaceState'
291
+ state: unknown
292
+ url: Option.Option<URL>
293
+ skipCommit: boolean
294
+ }
295
+ type TraverseEvent = { _tag: 'Traverse'; delta: number; skipCommit: boolean }
296
+ type TraverseToEvent = {
297
+ _tag: 'TraverseTo'
298
+ key: Uuid4
299
+ state: unknown
300
+ skipCommit: boolean
301
+ }
274
302
 
275
- function patchHistory(window: Window, onEvent: (event: HistoryEvent) => void) {
303
+ function patchHistory(window: Window, onEvent: (event: HistoryEvent) => void, base: string) {
276
304
  const { history, location } = window
277
- const stateDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(history), "state")
305
+ const stateDescriptor =
306
+ Object.getOwnPropertyDescriptor(Object.getPrototypeOf(history), 'state') ||
307
+ Object.getOwnPropertyDescriptor(history, 'state')
278
308
 
279
309
  const methods = {
280
310
  pushState: history.pushState.bind(history),
281
311
  replaceState: history.replaceState.bind(history),
282
312
  go: history.go.bind(history),
283
313
  back: history.back.bind(history),
284
- forward: history.forward.bind(history)
314
+ forward: history.forward.bind(history),
285
315
  }
286
- const getState = stateDescriptor?.get?.bind(history)
316
+ const getStateDescriptor = stateDescriptor?.get?.bind(history)
317
+
318
+ const getHistoryState = () => getStateDescriptor?.()
287
319
 
288
320
  const original: History = {
289
321
  get length() {
@@ -296,80 +328,84 @@ function patchHistory(window: Window, onEvent: (event: HistoryEvent) => void) {
296
328
  history.scrollRestoration = mode
297
329
  },
298
330
  get state() {
299
- return getState?.() ?? history.state
331
+ return getHistoryState()
300
332
  },
301
333
  ...methods,
302
334
  pushState(data, _, url) {
303
- if (!stateDescriptor) {
304
- ;(history as any).state = data
305
- }
306
-
307
- return methods.pushState(data, _, url)
335
+ return methods.pushState(data, _, url?.toString())
308
336
  },
309
337
  replaceState(data, _, url) {
310
- if (!stateDescriptor) {
311
- ;(history as any).state = data
312
- }
313
-
314
- return methods.replaceState(data, _, url)
315
- }
338
+ return methods.replaceState(data, _, url?.toString())
339
+ },
316
340
  }
317
341
 
318
342
  history.pushState = (state, _, url) => {
319
343
  if (url) {
320
- onEvent({ _tag: "PushState", state, url: getUrl(location.origin, url), skipCommit: false })
344
+ onEvent({
345
+ _tag: 'PushState',
346
+ state,
347
+ url: getUrl(location.origin, url, base),
348
+ skipCommit: false,
349
+ })
321
350
  } else {
322
- onEvent({ _tag: "ReplaceState", state, url: Option.none(), skipCommit: false })
351
+ onEvent({
352
+ _tag: 'ReplaceState',
353
+ state,
354
+ url: Option.none(),
355
+ skipCommit: false,
356
+ })
323
357
  }
324
358
  }
325
359
  history.replaceState = (state, _, url) => {
326
360
  onEvent({
327
- _tag: "ReplaceState",
361
+ _tag: 'ReplaceState',
328
362
  state,
329
- url: url ? Option.some(getUrl(location.origin, url)) : Option.none(),
330
- skipCommit: false
363
+ url: url ? Option.some(getUrl(location.origin, url, base)) : Option.none(),
364
+ skipCommit: false,
331
365
  })
332
366
  }
333
367
  history.go = (delta) => {
334
368
  if (delta && delta !== 0) {
335
- onEvent({ _tag: "Traverse", delta, skipCommit: false })
369
+ onEvent({ _tag: 'Traverse', delta, skipCommit: false })
336
370
  }
337
371
  }
338
372
  history.back = () => {
339
- onEvent({ _tag: "Traverse", delta: -1, skipCommit: false })
373
+ onEvent({ _tag: 'Traverse', delta: -1, skipCommit: false })
340
374
  }
341
375
  history.forward = () => {
342
- onEvent({ _tag: "Traverse", delta: 1, skipCommit: false })
343
- }
344
-
345
- // In a proper browser this will allow patching to hide the id/key's associated with the state
346
- if (stateDescriptor) {
347
- try {
348
- Object.defineProperty(history, "state", {
349
- get() {
350
- return getOriginalState(stateDescriptor.get!.call(history))
351
- }
352
- })
353
- } catch {
354
- // We tried, but it didn't work
355
- }
376
+ onEvent({ _tag: 'Traverse', delta: 1, skipCommit: false })
356
377
  }
357
378
 
358
379
  const onHashChange = (ev: HashChangeEvent) => {
359
- onEvent({ _tag: "ReplaceState", state: history.state, url: Option.some(new URL(ev.newURL)), skipCommit: false })
380
+ onEvent({
381
+ _tag: 'ReplaceState',
382
+ state: history.state,
383
+ url: Option.some(new URL(ev.newURL)),
384
+ skipCommit: false,
385
+ })
360
386
  }
361
387
 
362
- window.addEventListener("hashchange", onHashChange, { capture: true })
388
+ window.addEventListener('hashchange', onHashChange, { capture: true })
363
389
 
364
390
  const onPopState = (ev: PopStateEvent) => {
365
391
  if (isPatchedState(ev.state)) {
366
- onEvent({ _tag: "TraverseTo", key: ev.state.key, state: ev.state.originalHistoryState, skipCommit: true })
392
+ onEvent({
393
+ _tag: 'TraverseTo',
394
+ key: ev.state.__typed__navigation__key__,
395
+ state: ev.state.__typed__navigation__state__,
396
+ skipCommit: true,
397
+ })
367
398
  } else {
368
- onEvent({ _tag: "ReplaceState", state: ev.state, url: Option.some(new URL(location.href)), skipCommit: true })
399
+ onEvent({
400
+ _tag: 'ReplaceState',
401
+ state: ev.state,
402
+ url: Option.some(new URL(location.href)),
403
+ skipCommit: true,
404
+ })
369
405
  }
370
406
  }
371
407
 
372
- window.addEventListener("popstate", onPopState, { capture: true })
408
+ window.addEventListener('popstate', onPopState, { capture: true })
373
409
 
374
410
  const unpatch = Effect.sync(() => {
375
411
  history.pushState = original.pushState
@@ -380,20 +416,52 @@ function patchHistory(window: Window, onEvent: (event: HistoryEvent) => void) {
380
416
 
381
417
  if (stateDescriptor) {
382
418
  try {
383
- Object.defineProperty(history, "state", stateDescriptor)
419
+ Object.defineProperty(history, 'state', stateDescriptor)
384
420
  } catch {
385
421
  // We tried, but it didn't work
386
422
  }
387
423
  }
388
424
 
389
- window.removeEventListener("hashchange", onHashChange)
390
- window.removeEventListener("popstate", onPopState)
425
+ window.removeEventListener('hashchange', onHashChange)
426
+ window.removeEventListener('popstate', onPopState)
427
+ })
428
+
429
+ Object.defineProperty(history, 'state', {
430
+ get() {
431
+ return getOriginalState(getStateDescriptor?.() ?? history.state)
432
+ },
433
+ set(value) {
434
+ const { __typed__navigation__id__, __typed__navigation__key__ } =
435
+ getStateDescriptor?.() ?? original.state
436
+
437
+ if (isPatchedState(value)) {
438
+ // The setter is not actually modifying the history.state
439
+ // We need to call the original replaceState to update the actual state
440
+ original.replaceState.call(history, value, '', location.href)
441
+ } else {
442
+ // The setter is not actually modifying the history.state
443
+ // We need to call the original replaceState to update the actual state
444
+ original.replaceState.call(
445
+ history,
446
+ {
447
+ __typed__navigation__id__,
448
+ __typed__navigation__key__,
449
+ __typed__navigation__state__: value,
450
+ } satisfies PatchedState,
451
+ '',
452
+ location.href,
453
+ )
454
+ }
455
+
456
+ return value
457
+ },
391
458
  })
392
459
 
393
460
  return {
461
+ getHistoryState,
394
462
  original,
395
463
  patched: history,
396
- unpatch
464
+ unpatch,
397
465
  } as const
398
466
  }
399
467
 
@@ -411,17 +479,19 @@ function scopedRuntime<R>(): Effect.Effect<ScopedRuntime<R>, never, R | Scope.Sc
411
479
  const runPromise = <E, A>(effect: Effect.Effect<A, E, R | Scope.Scope>): Promise<A> =>
412
480
  new Promise((resolve, reject) => {
413
481
  const fiber = runFork(effect, { scope })
414
- fiber.addObserver(Exit.match({
415
- onFailure: (cause) => reject(Runtime.makeFiberFailure(cause)),
416
- onSuccess: resolve
417
- }))
482
+ fiber.addObserver(
483
+ Exit.match({
484
+ onFailure: (cause) => reject(Runtime.makeFiberFailure(cause)),
485
+ onSuccess: resolve,
486
+ }),
487
+ )
418
488
  })
419
489
 
420
490
  return {
421
491
  runtime,
422
492
  scope: Context.unsafeGet(runtime.context, Scope.Scope),
423
493
  run: (eff) => runFork(eff, { scope }),
424
- runPromise
494
+ runPromise,
425
495
  } as const
426
496
  })
427
497
  }