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