@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.
- package/.nvmrc +1 -0
- package/biome.json +39 -0
- package/dist/Blocking.d.ts +23 -0
- package/dist/Blocking.js +41 -0
- package/dist/Blocking.js.map +1 -0
- package/dist/Destination.d.ts +11 -0
- package/dist/Destination.js +10 -0
- package/dist/Destination.js.map +1 -0
- package/dist/Error.d.ts +33 -0
- package/dist/Error.js +22 -0
- package/dist/Error.js.map +1 -0
- package/dist/Event.d.ts +45 -0
- package/dist/Event.js +17 -0
- package/dist/Event.js.map +1 -0
- package/dist/Forms.d.ts +79 -0
- package/dist/Forms.js +111 -0
- package/dist/Forms.js.map +1 -0
- package/dist/Handler.d.ts +6 -0
- package/dist/Handler.js +2 -0
- package/dist/Handler.js.map +1 -0
- package/dist/{dts/Layer.d.ts → Layer.d.ts} +11 -8
- package/dist/{esm/Layer.js → Layer.js} +2 -2
- package/dist/Layer.js.map +1 -0
- package/dist/NavigateOptions.d.ts +7 -0
- package/dist/NavigateOptions.js +7 -0
- package/dist/NavigateOptions.js.map +1 -0
- package/dist/Navigation.d.ts +79 -0
- package/dist/Navigation.js +49 -0
- package/dist/Navigation.js.map +1 -0
- package/dist/NavigationType.d.ts +3 -0
- package/dist/NavigationType.js +3 -0
- package/dist/NavigationType.js.map +1 -0
- package/dist/ProposedDestination.d.ts +13 -0
- package/dist/ProposedDestination.js +4 -0
- package/dist/ProposedDestination.js.map +1 -0
- package/dist/Url.d.ts +13 -0
- package/dist/Url.js +72 -0
- package/dist/Url.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/fromWindow.d.ts +4 -0
- package/dist/internal/fromWindow.js +358 -0
- package/dist/internal/fromWindow.js.map +1 -0
- package/dist/internal/memory.d.ts +6 -0
- package/dist/internal/memory.js +59 -0
- package/dist/internal/memory.js.map +1 -0
- package/dist/internal/shared.d.ts +109 -0
- package/dist/{esm/internal → internal}/shared.js +134 -165
- package/dist/internal/shared.js.map +1 -0
- package/package.json +35 -52
- package/readme.md +243 -0
- package/src/Blocking.ts +65 -65
- package/src/Destination.ts +14 -0
- package/src/Error.ts +28 -0
- package/src/Event.ts +26 -0
- package/src/Forms.ts +216 -0
- package/src/Handler.ts +16 -0
- package/src/Layer.ts +20 -9
- package/src/NavigateOptions.ts +9 -0
- package/src/Navigation.test.ts +697 -0
- package/src/Navigation.ts +133 -468
- package/src/NavigationType.ts +5 -0
- package/src/ProposedDestination.ts +8 -0
- package/src/Url.ts +106 -0
- package/src/index.ts +12 -17
- package/src/internal/fromWindow.ts +250 -180
- package/src/internal/memory.ts +62 -49
- package/src/internal/shared.ts +238 -305
- package/tsconfig.json +30 -0
- package/Blocking/package.json +0 -6
- package/LICENSE +0 -21
- package/Layer/package.json +0 -6
- package/Navigation/package.json +0 -6
- package/README.md +0 -5
- package/dist/cjs/Blocking.js +0 -58
- package/dist/cjs/Blocking.js.map +0 -1
- package/dist/cjs/Layer.js +0 -27
- package/dist/cjs/Layer.js.map +0 -1
- package/dist/cjs/Navigation.js +0 -275
- package/dist/cjs/Navigation.js.map +0 -1
- package/dist/cjs/index.js +0 -39
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/internal/fromWindow.js +0 -421
- package/dist/cjs/internal/fromWindow.js.map +0 -1
- package/dist/cjs/internal/memory.js +0 -72
- package/dist/cjs/internal/memory.js.map +0 -1
- package/dist/cjs/internal/shared.js +0 -522
- package/dist/cjs/internal/shared.js.map +0 -1
- package/dist/dts/Blocking.d.ts +0 -34
- package/dist/dts/Blocking.d.ts.map +0 -1
- package/dist/dts/Layer.d.ts.map +0 -1
- package/dist/dts/Navigation.d.ts +0 -462
- package/dist/dts/Navigation.d.ts.map +0 -1
- package/dist/dts/index.d.ts +0 -17
- package/dist/dts/index.d.ts.map +0 -1
- package/dist/dts/internal/fromWindow.d.ts +0 -12
- package/dist/dts/internal/fromWindow.d.ts.map +0 -1
- package/dist/dts/internal/memory.d.ts +0 -6
- package/dist/dts/internal/memory.d.ts.map +0 -1
- package/dist/dts/internal/shared.d.ts +0 -114
- package/dist/dts/internal/shared.d.ts.map +0 -1
- package/dist/esm/Blocking.js +0 -46
- package/dist/esm/Blocking.js.map +0 -1
- package/dist/esm/Layer.js.map +0 -1
- package/dist/esm/Navigation.js +0 -237
- package/dist/esm/Navigation.js.map +0 -1
- package/dist/esm/index.js +0 -17
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/internal/fromWindow.js +0 -310
- package/dist/esm/internal/fromWindow.js.map +0 -1
- package/dist/esm/internal/memory.js +0 -56
- package/dist/esm/internal/memory.js.map +0 -1
- package/dist/esm/internal/shared.js.map +0 -1
- package/dist/esm/package.json +0 -4
package/src/internal/shared.ts
CHANGED
|
@@ -1,58 +1,85 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import type
|
|
7
|
-
import
|
|
8
|
-
import * as
|
|
9
|
-
import * as
|
|
10
|
-
import * as
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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<
|
|
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(
|
|
35
|
+
transition: Schema.OptionFromNullishOr(TransitionEvent, null),
|
|
40
36
|
})
|
|
41
37
|
|
|
42
|
-
export
|
|
43
|
-
|
|
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:
|
|
48
|
-
|
|
49
|
-
readonly
|
|
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
|
|
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.
|
|
67
|
-
newNavigationState?: () => NavigationState
|
|
93
|
+
getRandomValues: Context.Tag.Service<typeof GetRandomValues>,
|
|
94
|
+
newNavigationState?: () => NavigationState,
|
|
68
95
|
) {
|
|
69
|
-
const { beforeHandlers,
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
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:
|
|
150
|
+
beforeEvent: TransitionEvent,
|
|
166
151
|
get: Effect.Effect<NavigationState>,
|
|
167
152
|
set: (a: NavigationState) => Effect.Effect<NavigationState>,
|
|
168
153
|
depth: number,
|
|
169
|
-
skipCommit
|
|
154
|
+
skipCommit = false,
|
|
170
155
|
): Effect.Effect<Destination, NavigationError> =>
|
|
171
|
-
Effect.gen(function*() {
|
|
172
|
-
|
|
173
|
-
|
|
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)
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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({
|
|
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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
243
|
+
}).pipe(Effect.provideService(GetRandomValues, getRandomValues))
|
|
251
244
|
|
|
252
|
-
const navigate = (pathOrUrl: string | URL, options?: NavigateOptions, skipCommit
|
|
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 ??
|
|
258
|
-
const to = yield* makeOrUpdateDestination(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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 ===
|
|
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 = (
|
|
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*
|
|
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:
|
|
288
|
-
type:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
324
|
-
type:
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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:
|
|
377
|
-
type:
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
487
|
-
type:
|
|
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 =
|
|
507
|
-
|
|
460
|
+
const isSameOriginAndPath =
|
|
461
|
+
url.origin === current.url.origin && url.pathname === current.url.pathname
|
|
508
462
|
if (isSameOriginAndPath) {
|
|
509
|
-
const id = yield*
|
|
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.
|
|
530
|
-
key: state.
|
|
483
|
+
id: state.__typed__navigation__id__,
|
|
484
|
+
key: state.__typed__navigation__key__,
|
|
531
485
|
url,
|
|
532
|
-
state: state.
|
|
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*
|
|
540
|
-
const key = yield*
|
|
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*
|
|
557
|
-
const key = yield*
|
|
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
|
|
573
|
-
readonly
|
|
574
|
-
readonly
|
|
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 ===
|
|
579
|
-
|
|
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.
|
|
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 ===
|
|
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
|
|
554
|
+
return 'http://localhost'
|
|
597
555
|
}
|
|
598
556
|
}
|
|
599
557
|
|
|
600
558
|
export function isDestination(proposed: ProposedDestination): proposed is Destination {
|
|
601
|
-
return
|
|
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
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
633
|
-
|
|
576
|
+
return {
|
|
577
|
+
beforeHandlers,
|
|
578
|
+
handlers,
|
|
634
579
|
}
|
|
635
|
-
|
|
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
|
+
})
|