@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,58 +1,85 @@
1
- import * as HttpClient from "@effect/platform/HttpClient"
2
-
3
- import { Schema } from "@effect/schema"
4
- import type * as Context from "@typed/context"
5
- import * as RefSubject from "@typed/fx/RefSubject"
6
- import type { Uuid } from "@typed/id"
7
- import { GetRandomValues, makeUuid } from "@typed/id"
8
- import * as Effect from "effect/Effect"
9
- import * as Either from "effect/Either"
10
- import * as Option from "effect/Option"
11
- import type * as Scope from "effect/Scope"
12
- import type { Commit } from "../Layer.js"
13
- import type {
14
- BeforeNavigationEvent,
15
- BeforeNavigationHandler,
16
- CancelNavigation,
17
- FormDataEvent,
18
- FormDataHandler,
19
- FormInputFrom,
20
- NavigateOptions,
21
- Navigation,
22
- NavigationError,
23
- NavigationEvent,
24
- NavigationHandler,
25
- ProposedDestination,
26
- RedirectError
27
- } from "../Navigation.js"
28
- import { Destination, Transition } from "../Navigation.js"
1
+ import * as Headers from '@effect/platform/Headers'
2
+ import * as HttpClient from '@effect/platform/HttpClient'
3
+ import { GetRandomValues, makeUuid4, type Uuid4 } from '@typed/id'
4
+ import * as LazyRef from '@typed/lazy-ref'
5
+ import { Cause, Schema } from 'effect'
6
+ import type * as Context from 'effect/Context'
7
+ import * as Effect from 'effect/Effect'
8
+ import * as Either from 'effect/Either'
9
+ import * as Option from 'effect/Option'
10
+ import type * as Scope from 'effect/Scope'
11
+ import { Destination } from '../Destination.js'
12
+ import {
13
+ FormSubmitError,
14
+ type CancelNavigation,
15
+ type NavigationError,
16
+ type RedirectError,
17
+ } from '../Error.js'
18
+ import { TransitionEvent, type NavigationEvent } from '../Event.js'
19
+ import type { FormSubmit } from '../Forms.js'
20
+ import type { BeforeNavigationHandler, NavigationHandler } from '../Handler.js'
21
+ import type { Commit } from '../Layer.js'
22
+ import type { NavigateOptions } from '../NavigateOptions.js'
23
+ import type { Navigation } from '../Navigation.js'
24
+ import type { ProposedDestination } from '../ProposedDestination.js'
29
25
 
30
26
  export type NavigationState = {
31
27
  readonly entries: ReadonlyArray<Destination>
32
28
  readonly index: number
33
- readonly transition: Option.Option<Transition>
29
+ readonly transition: Option.Option<TransitionEvent>
34
30
  }
35
31
 
36
32
  export const NavigationState = Schema.Struct({
37
33
  entries: Schema.Array(Destination),
38
34
  index: Schema.Number,
39
- transition: Schema.OptionFromNullishOr(Transition, null)
35
+ transition: Schema.OptionFromNullishOr(TransitionEvent, null),
40
36
  })
41
37
 
42
- export const getUrl = (origin: string, urlOrPath: string | URL): URL => {
43
- return typeof urlOrPath === "string" ? new URL(urlOrPath, origin) : urlOrPath
38
+ export function getUrl(origin: string, urlOrPath: string | URL, base: string): URL {
39
+ if (typeof urlOrPath === 'string') {
40
+ const url = new URL(urlOrPath, origin)
41
+
42
+ // If the URL is relative, join it with the base
43
+ if (isRelativeUrl(urlOrPath)) {
44
+ url.pathname = joinPath(base, url.pathname)
45
+ }
46
+
47
+ return url
48
+ }
49
+
50
+ return urlOrPath
51
+ }
52
+
53
+ function isRelativeUrl(url: string) {
54
+ if (url.includes('://')) return false
55
+ return true
56
+ }
57
+
58
+ function joinPath(a: string, b: string) {
59
+ const aEndsWithSlash = a[a.length - 1] === '/'
60
+ const bStartsWithSlash = b[0] === '/'
61
+
62
+ if (aEndsWithSlash && bStartsWithSlash) {
63
+ return a + b.slice(1)
64
+ }
65
+
66
+ if (!aEndsWithSlash && !bStartsWithSlash) {
67
+ return `${a}/${b}`
68
+ }
69
+
70
+ return a + b
44
71
  }
45
72
 
46
73
  export type ModelAndIntent = {
47
- readonly state: RefSubject.RefSubject<NavigationState>
48
- readonly canGoBack: RefSubject.Computed<boolean>
49
- readonly canGoForward: RefSubject.Computed<boolean>
50
- readonly beforeHandlers: RefSubject.RefSubject<
74
+ readonly state: LazyRef.LazyRef<NavigationState>
75
+
76
+ readonly beforeHandlers: LazyRef.LazyRef<
51
77
  Set<readonly [BeforeNavigationHandler<any, any>, Context.Context<any>]>
52
78
  >
53
- readonly handlers: RefSubject.RefSubject<Set<readonly [NavigationHandler<any, any>, Context.Context<any>]>>
54
79
 
55
- readonly formDataHandlers: RefSubject.RefSubject<Set<readonly [FormDataHandler<any, any>, Context.Context<any>]>>
80
+ readonly handlers: LazyRef.LazyRef<
81
+ Set<readonly [NavigationHandler<any, any>, Context.Context<any>]>
82
+ >
56
83
 
57
84
  readonly commit: Commit
58
85
  }
@@ -63,16 +90,18 @@ export function setupFromModelAndIntent(
63
90
  modelAndIntent: ModelAndIntent,
64
91
  origin: string,
65
92
  base: string,
66
- getRandomValues: Context.Fn.FnOf<typeof GetRandomValues>,
67
- newNavigationState?: () => NavigationState
93
+ getRandomValues: Context.Tag.Service<typeof GetRandomValues>,
94
+ newNavigationState?: () => NavigationState,
68
95
  ) {
69
- const { beforeHandlers, canGoBack, canGoForward, commit, formDataHandlers, handlers, state } = modelAndIntent
70
- const entries = RefSubject.map(state, (s) => s.entries)
71
- const currentEntry = RefSubject.map(state, (s) => s.entries[s.index])
72
- const transition = RefSubject.map(state, (s) => s.transition)
73
-
74
- const runBeforeHandlers = (event: BeforeNavigationEvent) =>
75
- Effect.gen(function*() {
96
+ const { beforeHandlers, commit, handlers, state } = modelAndIntent
97
+ const canGoBack = LazyRef.map(state, (s) => s.index > 0)
98
+ const canGoForward = LazyRef.map(state, (s) => s.index < s.entries.length - 1)
99
+ const entries = LazyRef.map(state, (s) => s.entries)
100
+ const currentEntry = LazyRef.map(state, (s) => s.entries[s.index])
101
+ const transition = LazyRef.map(state, (s) => s.transition)
102
+
103
+ const runBeforeHandlers = (event: TransitionEvent) =>
104
+ Effect.gen(function* () {
76
105
  const handlers = yield* beforeHandlers
77
106
  const matches: Array<Effect.Effect<unknown, RedirectError | CancelNavigation>> = []
78
107
 
@@ -80,7 +109,7 @@ export function setupFromModelAndIntent(
80
109
  const exit = yield* handler(event).pipe(Effect.provide(ctx), Effect.either)
81
110
  if (Either.isRight(exit)) {
82
111
  const match = exit.right
83
- if (Option.isSome(match)) {
112
+ if (match !== undefined && Option.isSome(match)) {
84
113
  matches.push(Effect.provide(match.value, ctx))
85
114
  }
86
115
  } else {
@@ -101,13 +130,13 @@ export function setupFromModelAndIntent(
101
130
  })
102
131
 
103
132
  const runHandlers = (event: NavigationEvent) =>
104
- Effect.gen(function*() {
133
+ Effect.gen(function* () {
105
134
  const eventHandlers = yield* handlers
106
135
  const matches: Array<Effect.Effect<unknown>> = []
107
136
 
108
137
  for (const [handler, ctx] of eventHandlers) {
109
138
  const match = yield* Effect.provide(handler(event), ctx)
110
- if (Option.isSome(match)) {
139
+ if (match !== undefined && Option.isSome(match)) {
111
140
  matches.push(Effect.provide(match.value, ctx))
112
141
  }
113
142
  }
@@ -117,60 +146,18 @@ export function setupFromModelAndIntent(
117
146
  }
118
147
  })
119
148
 
120
- const runFormDataHandlers = (
121
- event: FormDataEvent
122
- ): Effect.Effect<
123
- Either.Either<Option.Option<HttpClient.response.ClientResponse>, RedirectError | CancelNavigation>,
124
- NavigationError | HttpClient.error.HttpClientError,
125
- Scope.Scope | HttpClient.client.Client.Default
126
- > =>
127
- Effect.gen(function*() {
128
- const handlers = yield* formDataHandlers
129
- const matches: Array<
130
- Effect.Effect<Option.Option<HttpClient.response.ClientResponse>, RedirectError | CancelNavigation>
131
- > = []
132
-
133
- for (const [handler, ctx] of handlers) {
134
- const exit = yield* handler(event).pipe(Effect.provide(ctx), Effect.either)
135
- if (Either.isRight(exit)) {
136
- const match = exit.right
137
- if (Option.isSome(match)) {
138
- matches.push(Effect.provide(match.value, ctx))
139
- }
140
- } else {
141
- return Either.left(exit.left)
142
- }
143
- }
144
-
145
- if (matches.length > 0) {
146
- for (const match of matches) {
147
- const exit = yield* Effect.either(match)
148
- if (Either.isLeft(exit)) {
149
- return Either.left(exit.left)
150
- } else if (Option.isSome(exit.right)) {
151
- return Either.right(exit.right)
152
- }
153
- }
154
- } else {
155
- // Only if there are 0 matches, we'll make a request to the server ourselves
156
- const response = yield* makeFormDataRequest(event, Option.getOrElse(event.action, () => event.from.url.href))
157
-
158
- return Either.right(Option.some(response))
159
- }
160
-
161
- return Either.right(Option.none())
162
- })
163
-
164
149
  const runNavigationEvent = (
165
- beforeEvent: BeforeNavigationEvent,
150
+ beforeEvent: TransitionEvent,
166
151
  get: Effect.Effect<NavigationState>,
167
152
  set: (a: NavigationState) => Effect.Effect<NavigationState>,
168
153
  depth: number,
169
- skipCommit: boolean = false
154
+ skipCommit = false,
170
155
  ): Effect.Effect<Destination, NavigationError> =>
171
- Effect.gen(function*() {
172
- let current = yield* get
173
- current = yield* set({ ...current, transition: Option.some(beforeEvent) })
156
+ Effect.gen(function* () {
157
+ const current = yield* set({
158
+ ...(yield* get),
159
+ transition: Option.some(beforeEvent),
160
+ })
174
161
 
175
162
  if (!skipCommit) {
176
163
  const beforeError = yield* runBeforeHandlers(beforeEvent)
@@ -180,13 +167,15 @@ export function setupFromModelAndIntent(
180
167
  }
181
168
  }
182
169
 
183
- const to = isDestination(beforeEvent.to) ? beforeEvent.to : yield* upgradeProposedDestination(beforeEvent.to)
170
+ const to = isDestination(beforeEvent.to)
171
+ ? beforeEvent.to
172
+ : yield* upgradeProposedDestination(beforeEvent.to)
184
173
 
185
174
  if (!skipCommit) {
186
175
  yield* commit(to, beforeEvent)
187
176
  }
188
177
 
189
- if (newNavigationState) {
178
+ if (newNavigationState !== undefined) {
190
179
  const { entries, index } = yield* set(newNavigationState())
191
180
 
192
181
  return entries[index]
@@ -194,86 +183,98 @@ export function setupFromModelAndIntent(
194
183
  const event: NavigationEvent = {
195
184
  type: beforeEvent.type,
196
185
  info: beforeEvent.info,
197
- destination: to
186
+ destination: to,
198
187
  }
199
188
 
200
- if (beforeEvent.type === "push") {
189
+ if (beforeEvent.type === 'push') {
201
190
  const index = current.index + 1
202
191
  const entries = current.entries.slice(0, index).concat([to])
203
192
 
204
193
  yield* set({ entries, index, transition: Option.none() })
205
- } else if (beforeEvent.type === "replace") {
194
+ } else if (beforeEvent.type === 'replace') {
206
195
  const index = current.index
207
196
  const before = current.entries.slice(0, index)
208
197
  const after = current.entries.slice(index + 1)
209
198
  const entries = [...before, to, ...after]
210
199
 
211
200
  yield* set({ entries, index, transition: Option.none() })
212
- } else if (beforeEvent.type === "reload") {
201
+ } else if (beforeEvent.type === 'reload') {
213
202
  yield* set({ ...current, transition: Option.none() })
214
203
  } else {
215
204
  const { delta } = beforeEvent
216
205
  const nextIndex = current.index + delta
217
206
 
218
- yield* set({ ...current, index: nextIndex, transition: Option.none() })
207
+ yield* set({
208
+ ...current,
209
+ index: nextIndex,
210
+ transition: Option.none(),
211
+ })
219
212
  }
220
213
 
221
214
  yield* runHandlers(event)
222
215
  }
223
216
 
224
217
  return to
225
- }).pipe(GetRandomValues.provide(getRandomValues))
218
+ }).pipe(Effect.provideService(GetRandomValues, getRandomValues))
226
219
 
227
220
  const handleError = (
228
221
  error: RedirectError | CancelNavigation,
229
222
  get: Effect.Effect<NavigationState>,
230
223
  set: (a: NavigationState) => Effect.Effect<NavigationState>,
231
- depth: number
224
+ depth: number,
232
225
  ): Effect.Effect<Destination, NavigationError> =>
233
- Effect.gen(function*() {
226
+ Effect.gen(function* () {
234
227
  if (depth >= 25) {
235
- return yield* Effect.dieMessage(`Redirect loop detected.`)
228
+ return yield* Effect.dieMessage('Redirect loop detected.')
236
229
  }
237
230
 
238
231
  const { entries, index } = yield* get
239
232
  const from = entries[index]
240
233
 
241
- if (error._tag === "CancelNavigation") {
234
+ if (error._tag === 'CancelNavigation') {
242
235
  yield* set({ entries, index, transition: Option.none() })
243
236
 
244
237
  return from
245
238
  } else {
246
- const event = yield* makeRedirectEvent(origin, error, from)
239
+ const event = yield* makeRedirectEvent(origin, error, from, base)
247
240
 
248
241
  return yield* runNavigationEvent(event, get, set, depth + 1)
249
242
  }
250
- }).pipe(GetRandomValues.provide(getRandomValues))
243
+ }).pipe(Effect.provideService(GetRandomValues, getRandomValues))
251
244
 
252
- const navigate = (pathOrUrl: string | URL, options?: NavigateOptions, skipCommit: boolean = false) =>
245
+ const navigate = (pathOrUrl: string | URL, options?: NavigateOptions, skipCommit = false) =>
253
246
  state.runUpdates(({ get, set }) =>
254
- Effect.gen(function*() {
247
+ Effect.gen(function* () {
255
248
  const state = yield* get
256
249
  const from = state.entries[state.index]
257
- const history = options?.history ?? "auto"
258
- const to = yield* makeOrUpdateDestination(state, getUrl(origin, pathOrUrl), options?.state, origin).pipe(
259
- GetRandomValues.provide(getRandomValues)
260
- )
261
- const type = history === "auto" ? from.key === to.key ? "replace" : "push" : history
262
- const event: BeforeNavigationEvent = {
250
+ const history = options?.history ?? 'auto'
251
+ const to = yield* makeOrUpdateDestination(
252
+ state,
253
+ getUrl(origin, pathOrUrl, base),
254
+ options?.state,
255
+ origin,
256
+ ).pipe(Effect.provideService(GetRandomValues, getRandomValues))
257
+
258
+ const type = history === 'auto' ? (from.key === to.key ? 'replace' : 'push') : history
259
+ const event: TransitionEvent = {
263
260
  type,
264
261
  from,
265
262
  to,
266
- delta: type === "replace" ? 0 : 1,
267
- info: options?.info
263
+ delta: type === 'replace' ? 0 : 1,
264
+ info: options?.info,
268
265
  }
269
266
 
270
267
  return yield* runNavigationEvent(event, get, set, 0, skipCommit)
271
- })
268
+ }),
272
269
  )
273
270
 
274
- const traverseTo = (key: Destination["key"], options?: { readonly info?: unknown }, skipCommit: boolean = false) =>
271
+ const traverseTo = (
272
+ key: Destination['key'],
273
+ options?: { readonly info?: unknown },
274
+ skipCommit = false,
275
+ ) =>
275
276
  state.runUpdates(({ get, set }) =>
276
- Effect.gen(function*() {
277
+ Effect.gen(function* () {
277
278
  const state = yield* get
278
279
  const { entries, index } = state
279
280
  const from = entries[index]
@@ -281,23 +282,23 @@ export function setupFromModelAndIntent(
281
282
 
282
283
  if (nextIndex === -1) return from
283
284
 
284
- const id = yield* makeUuid.pipe(GetRandomValues.provide(getRandomValues))
285
+ const id = yield* makeUuid4.pipe(Effect.provideService(GetRandomValues, getRandomValues))
285
286
  const to = { ...entries[nextIndex], id }
286
287
  const delta = nextIndex - index
287
- const event: BeforeNavigationEvent = {
288
- type: "traverse",
288
+ const event: TransitionEvent = {
289
+ type: 'traverse',
289
290
  from,
290
291
  to,
291
292
  delta,
292
- info: options?.info
293
+ info: options?.info,
293
294
  }
294
295
 
295
296
  return yield* runNavigationEvent(event, get, set, 0, skipCommit)
296
- })
297
+ }),
297
298
  )
298
299
 
299
- const back = (options?: { readonly info?: unknown }, skipCommit: boolean = false) =>
300
- Effect.gen(function*() {
300
+ const back = (options?: { readonly info?: unknown }, skipCommit = false) =>
301
+ Effect.gen(function* () {
301
302
  const { entries, index } = yield* state
302
303
  if (index === 0) return entries[index]
303
304
  const { key } = entries[index - 1]
@@ -305,8 +306,8 @@ export function setupFromModelAndIntent(
305
306
  return yield* traverseTo(key, options, skipCommit)
306
307
  })
307
308
 
308
- const forward = (options?: { readonly info?: unknown }, skipCommit: boolean = false) =>
309
- Effect.gen(function*() {
309
+ const forward = (options?: { readonly info?: unknown }, skipCommit = false) =>
310
+ Effect.gen(function* () {
310
311
  const { entries, index } = yield* state
311
312
  if (index === entries.length - 1) return entries[index]
312
313
  const { key } = entries[index + 1]
@@ -314,142 +315,95 @@ export function setupFromModelAndIntent(
314
315
  return yield* traverseTo(key, options, skipCommit)
315
316
  })
316
317
 
317
- const reload = (options?: { readonly info?: unknown }, skipCommit: boolean = false) =>
318
+ const reload = (options?: { readonly info?: unknown }, skipCommit = false) =>
318
319
  state.runUpdates(({ get, set }) =>
319
- Effect.gen(function*() {
320
+ Effect.gen(function* () {
320
321
  const { entries, index } = yield* state
321
322
  const current = entries[index]
322
323
 
323
- const event: BeforeNavigationEvent = {
324
- type: "reload",
324
+ const event: TransitionEvent = {
325
+ type: 'reload',
325
326
  from: current,
326
327
  to: current,
327
328
  delta: 0,
328
- info: options?.info
329
+ info: options?.info,
329
330
  }
330
331
 
331
332
  return yield* runNavigationEvent(event, get, set, 0, skipCommit)
332
- })
333
+ }),
333
334
  )
334
335
 
335
336
  const beforeNavigation = <R = never, R2 = never>(
336
- handler: BeforeNavigationHandler<R, R2>
337
+ handler: BeforeNavigationHandler<R, R2>,
337
338
  ): Effect.Effect<void, never, R | R2 | Scope.Scope> =>
338
339
  Effect.contextWithEffect((ctx) => {
339
340
  const entry = [handler, ctx] as const
340
-
341
- return Effect.zipRight(
342
- RefSubject.update(beforeHandlers, (handlers) => new Set([...handlers, entry])),
343
- Effect.addFinalizer(() =>
344
- RefSubject.update(beforeHandlers, (handlers) => {
345
- const updated = new Set(handlers)
346
- updated.delete(entry)
347
- return updated
348
- })
349
- )
341
+ const acquire = LazyRef.update(beforeHandlers, (handlers) => new Set([...handlers, entry]))
342
+ const release = Effect.ignoreLogged(
343
+ LazyRef.update(beforeHandlers, (handlers) => {
344
+ const updated = new Set(handlers)
345
+ updated.delete(entry)
346
+ return updated
347
+ }),
350
348
  )
349
+
350
+ return Effect.acquireRelease(acquire, () => release)
351
351
  })
352
352
 
353
353
  const onNavigation = <R = never, R2 = never>(
354
- handler: NavigationHandler<R, R2>
354
+ handler: NavigationHandler<R, R2>,
355
355
  ): Effect.Effect<void, never, R | R2 | Scope.Scope> =>
356
356
  Effect.contextWithEffect((ctx) => {
357
357
  const entry = [handler, ctx] as const
358
-
359
- return Effect.zipRight(
360
- RefSubject.update(handlers, (handlers) => new Set([...handlers, entry])),
361
- Effect.addFinalizer(() =>
362
- RefSubject.update(handlers, (handlers) => {
363
- const updated = new Set(handlers)
364
- updated.delete(entry)
365
- return updated
366
- })
367
- )
358
+ const acquire = LazyRef.update(handlers, (handlers) => new Set([...handlers, entry]))
359
+ const release = Effect.ignoreLogged(
360
+ LazyRef.update(handlers, (handlers) => {
361
+ const updated = new Set(handlers)
362
+ updated.delete(entry)
363
+ return updated
364
+ }),
368
365
  )
366
+
367
+ return Effect.acquireRelease(acquire, () => release)
369
368
  })
370
369
 
371
370
  const updateCurrentEntry = (options: { readonly state: unknown }) =>
372
371
  state.runUpdates(({ get, set }) =>
373
- Effect.gen(function*() {
372
+ Effect.gen(function* () {
374
373
  const { entries, index } = yield* get
375
374
  const current = entries[index]
376
- const event: BeforeNavigationEvent = {
377
- type: "replace",
375
+ const event: TransitionEvent = {
376
+ type: 'replace',
378
377
  from: current,
379
378
  to: { ...current, state: options.state },
380
379
  delta: 0,
381
- info: null
380
+ info: null,
382
381
  }
383
382
 
384
383
  return yield* runNavigationEvent(event, get, set, 0)
385
- })
384
+ }),
386
385
  )
387
386
 
388
- const submit = (
389
- data: FormData,
390
- input?: Omit<FormInputFrom, "data">
391
- ): Effect.Effect<
392
- Option.Option<HttpClient.response.ClientResponse>,
393
- NavigationError | HttpClient.error.HttpClientError,
394
- Scope.Scope | HttpClient.client.Client.Default
395
- > =>
396
- state.runUpdates(({ get, set }) =>
397
- Effect.gen(function*() {
398
- const { entries, index } = yield* get
399
- const from = entries[index]
400
- const event: FormDataEvent = {
401
- from,
402
- data,
403
- name: Option.fromNullable(input?.name),
404
- action: Option.fromNullable(input?.action),
405
- method: Option.fromNullable(input?.method),
406
- encoding: Option.fromNullable(input?.encoding)
407
- }
408
-
409
- const either = yield* runFormDataHandlers(event)
410
-
411
- if (Either.isLeft(either)) {
412
- yield* handleError(either.left, get, set, 0)
413
- return Option.none<HttpClient.response.ClientResponse>()
414
- } else {
415
- if (Option.isNone(either.right)) {
416
- return either.right
417
- }
418
-
419
- const response = either.right.value
420
-
421
- // If it is a redirect
422
- if (REDIRECT_STATUS_CODES.has(response.status)) {
423
- const location = HttpClient.headers.get(response.headers, "location")
424
-
425
- // And we have a location header
426
- if (Option.isSome(location)) {
427
- // Then we navigate to that location
428
- yield* navigate(location.value, { history: "replace" })
429
- }
430
- }
387
+ const submit = (form: FormSubmit) =>
388
+ Effect.gen(function* () {
389
+ const current = yield* currentEntry
390
+ // Utilize the action URL if provided, otherwise use the current URL
391
+ const url = form.action ? getUrl(origin, form.action, base) : current.url
392
+ // Submit the HTTP request
393
+ const response = yield* HttpClient[form.method](url, form).pipe(
394
+ Effect.mapErrorCause((cause) => Cause.fail(new FormSubmitError({ cause }))),
395
+ )
431
396
 
432
- return Option.some(response)
397
+ // If the response is a redirect, navigate to the new URL
398
+ if (REDIRECT_STATUS_CODES.has(response.status)) {
399
+ // Get the location header
400
+ const location = Headers.get(response.headers, 'location')
401
+ if (Option.isSome(location)) {
402
+ return [yield* navigate(getUrl(origin, location.value, base), form), response] as const
433
403
  }
434
- })
435
- )
436
-
437
- const onFormData = <R = never, R2 = never>(
438
- handler: FormDataHandler<R, R2>
439
- ): Effect.Effect<void, never, R | R2 | Scope.Scope> =>
440
- Effect.contextWithEffect((ctx) => {
441
- const entry = [handler, ctx] as const
404
+ }
442
405
 
443
- return Effect.zipRight(
444
- RefSubject.update(formDataHandlers, (handlers) => new Set([...handlers, entry])),
445
- Effect.addFinalizer(() =>
446
- RefSubject.update(formDataHandlers, (handlers) => {
447
- const updated = new Set(handlers)
448
- updated.delete(entry)
449
- return updated
450
- })
451
- )
452
- )
406
+ return [current, response] as const
453
407
  })
454
408
 
455
409
  const navigation = {
@@ -469,26 +423,26 @@ export function setupFromModelAndIntent(
469
423
  traverseTo,
470
424
  updateCurrentEntry,
471
425
  submit,
472
- onFormData
473
426
  } satisfies Navigation
474
427
 
475
428
  return navigation
476
429
  }
477
430
 
478
- export function makeRedirectEvent(
431
+ function makeRedirectEvent(
479
432
  origin: string,
480
433
  redirect: RedirectError,
481
- from: Destination
434
+ from: Destination,
435
+ base: string,
482
436
  ) {
483
- return Effect.gen(function*() {
484
- const url = getUrl(origin, redirect.path)
437
+ return Effect.gen(function* () {
438
+ const url = getUrl(origin, redirect.path, base)
485
439
  const to = yield* makeDestination(url, redirect.options?.state, origin)
486
- const event: BeforeNavigationEvent = {
487
- type: "replace",
440
+ const event: TransitionEvent = {
441
+ type: 'replace',
488
442
  from,
489
443
  to,
490
444
  delta: 0,
491
- info: redirect.options?.info
445
+ info: redirect.options?.info,
492
446
  }
493
447
 
494
448
  return event
@@ -499,20 +453,20 @@ export function makeOrUpdateDestination(
499
453
  navigationState: NavigationState,
500
454
  url: URL,
501
455
  state: unknown,
502
- origin: string
456
+ origin: string,
503
457
  ) {
504
- return Effect.gen(function*() {
458
+ return Effect.gen(function* () {
505
459
  const current = navigationState.entries[navigationState.index]
506
- const isSameOriginAndPath = url.origin === current.url.origin && url.pathname === current.url.pathname
507
-
460
+ const isSameOriginAndPath =
461
+ url.origin === current.url.origin && url.pathname === current.url.pathname
508
462
  if (isSameOriginAndPath) {
509
- const id = yield* makeUuid
463
+ const id = yield* makeUuid4
510
464
  const destination: Destination = {
511
465
  id,
512
466
  key: current.key,
513
467
  url,
514
468
  state: getOriginalState(state),
515
- sameDocument: url.origin === origin
469
+ sameDocument: url.origin === origin,
516
470
  }
517
471
 
518
472
  return destination
@@ -523,28 +477,28 @@ export function makeOrUpdateDestination(
523
477
  }
524
478
 
525
479
  export function makeDestination(url: URL, state: unknown, origin: string) {
526
- return Effect.gen(function*() {
480
+ return Effect.gen(function* () {
527
481
  if (isPatchedState(state)) {
528
482
  const destination: Destination = {
529
- id: state.id,
530
- key: state.key,
483
+ id: state.__typed__navigation__id__,
484
+ key: state.__typed__navigation__key__,
531
485
  url,
532
- state: state.originalHistoryState,
533
- sameDocument: url.origin === origin
486
+ state: state.__typed__navigation__state__,
487
+ sameDocument: url.origin === origin,
534
488
  }
535
489
 
536
490
  return destination
537
491
  }
538
492
 
539
- const id = yield* makeUuid
540
- const key = yield* makeUuid
493
+ const id = yield* makeUuid4
494
+ const key = yield* makeUuid4
541
495
 
542
496
  const destination: Destination = {
543
497
  id,
544
498
  key,
545
499
  url,
546
500
  state,
547
- sameDocument: url.origin === origin
501
+ sameDocument: url.origin === origin,
548
502
  }
549
503
 
550
504
  return destination
@@ -552,16 +506,16 @@ export function makeDestination(url: URL, state: unknown, origin: string) {
552
506
  }
553
507
 
554
508
  export function upgradeProposedDestination(proposed: ProposedDestination) {
555
- return Effect.gen(function*() {
556
- const id = yield* makeUuid
557
- const key = yield* makeUuid
509
+ return Effect.gen(function* () {
510
+ const id = yield* makeUuid4
511
+ const key = yield* makeUuid4
558
512
 
559
513
  const destination: Destination = {
560
514
  id,
561
515
  key,
562
516
  url: proposed.url,
563
517
  state: proposed.state,
564
- sameDocument: proposed.sameDocument
518
+ sameDocument: proposed.sameDocument,
565
519
  }
566
520
 
567
521
  return destination
@@ -569,79 +523,58 @@ export function upgradeProposedDestination(proposed: ProposedDestination) {
569
523
  }
570
524
 
571
525
  export type PatchedState = {
572
- readonly id: Uuid
573
- readonly key: Uuid
574
- readonly originalHistoryState: unknown
526
+ readonly __typed__navigation__id__: Uuid4
527
+ readonly __typed__navigation__key__: Uuid4
528
+ readonly __typed__navigation__state__: unknown
575
529
  }
576
530
 
577
531
  export function isPatchedState(state: unknown): state is PatchedState {
578
- if (state === null || !(typeof state === "object") || Array.isArray(state)) return false
579
- if ("id" in state && "key" in state && "originalHistoryState" in state) return true
532
+ if (state === null || !(typeof state === 'object') || Array.isArray(state)) {
533
+ return false
534
+ }
535
+ if ('__typed__navigation__id__' in state && '__typed__navigation__key__' in state) {
536
+ return true
537
+ }
580
538
  return false
581
539
  }
582
540
 
583
541
  export function getOriginalState(state: unknown) {
584
- if (isPatchedState(state)) return state.originalHistoryState
542
+ if (isPatchedState(state)) return state.__typed__navigation__state__
585
543
  return state
586
544
  }
587
545
 
588
546
  export function getOriginFromUrl(url: string | URL) {
589
547
  try {
590
- if (typeof url === "string") {
548
+ if (typeof url === 'string') {
591
549
  return new URL(url).origin
592
550
  } else {
593
551
  return url.origin
594
552
  }
595
553
  } catch {
596
- return "http://localhost"
554
+ return 'http://localhost'
597
555
  }
598
556
  }
599
557
 
600
558
  export function isDestination(proposed: ProposedDestination): proposed is Destination {
601
- return "id" in proposed && "key" in proposed
559
+ return 'id' in proposed && 'key' in proposed
602
560
  }
603
561
 
604
562
  const strictEqual = <A>(a: A, b: A) => a === b
605
563
 
606
- export function makeHandlersState() {
607
- return Effect.gen(function*() {
608
- const beforeHandlers = yield* RefSubject.fromEffect(
609
- Effect.sync(() => new Set<readonly [BeforeNavigationHandler<any, any>, Context.Context<any>]>()),
610
- { eq: strictEqual }
611
- )
612
- const handlers = yield* RefSubject.fromEffect(
613
- Effect.sync(() => new Set<readonly [NavigationHandler<any, any>, Context.Context<any>]>()),
614
- { eq: strictEqual }
615
- )
616
- const formDataHandlers = yield* RefSubject.fromEffect(
617
- Effect.sync(() => new Set<readonly [FormDataHandler<any, any>, Context.Context<any>]>()),
618
- { eq: strictEqual }
619
- )
620
-
621
- return {
622
- beforeHandlers,
623
- handlers,
624
- formDataHandlers
625
- }
626
- })
627
- }
628
-
629
- function makeFormDataRequest(event: FormDataEvent, url: string) {
630
- const headers = new Headers()
564
+ export const makeHandlersState = Effect.gen(function* () {
565
+ const beforeHandlers = yield* LazyRef.fromEffect(
566
+ Effect.sync(
567
+ () => new Set<readonly [BeforeNavigationHandler<any, any>, Context.Context<any>]>(),
568
+ ),
569
+ { eq: strictEqual },
570
+ )
571
+ const handlers = yield* LazyRef.fromEffect(
572
+ Effect.sync(() => new Set<readonly [NavigationHandler<any, any>, Context.Context<any>]>()),
573
+ { eq: strictEqual },
574
+ )
631
575
 
632
- if (Option.isSome(event.encoding)) {
633
- headers.set("Content-Type", event.encoding.value)
576
+ return {
577
+ beforeHandlers,
578
+ handlers,
634
579
  }
635
- const method = Option.getOrElse(event.method, () => "POST")
636
-
637
- return Effect.flatMap(
638
- HttpClient.client.Client,
639
- (client) =>
640
- client(
641
- HttpClient.request.make(method as "POST")(url, {
642
- headers: HttpClient.headers.fromInput(headers),
643
- body: HttpClient.body.formData(event.data)
644
- })
645
- )
646
- )
647
- }
580
+ })