@typed/navigation 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Navigation.js +72 -2
- package/dist/cjs/Navigation.js.map +1 -1
- package/dist/cjs/internal/fromWindow.js +4 -0
- package/dist/cjs/internal/fromWindow.js.map +1 -1
- package/dist/cjs/internal/memory.js +2 -0
- package/dist/cjs/internal/memory.js.map +1 -1
- package/dist/cjs/internal/shared.js +108 -5
- package/dist/cjs/internal/shared.js.map +1 -1
- package/dist/dts/Navigation.d.ts +128 -2
- package/dist/dts/Navigation.d.ts.map +1 -1
- package/dist/dts/internal/shared.d.ts +6 -1
- package/dist/dts/internal/shared.d.ts.map +1 -1
- package/dist/esm/Navigation.js +57 -1
- package/dist/esm/Navigation.js.map +1 -1
- package/dist/esm/internal/fromWindow.js +4 -2
- package/dist/esm/internal/fromWindow.js.map +1 -1
- package/dist/esm/internal/memory.js +2 -1
- package/dist/esm/internal/memory.js.map +1 -1
- package/dist/esm/internal/shared.js +98 -6
- package/dist/esm/internal/shared.js.map +1 -1
- package/package.json +8 -7
- package/src/Navigation.ts +148 -4
- package/src/internal/fromWindow.ts +5 -3
- package/src/internal/memory.ts +2 -1
- package/src/internal/shared.ts +167 -8
package/src/internal/shared.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as HttpClient from "@effect/platform/HttpClient"
|
|
2
|
+
|
|
1
3
|
import { Schema } from "@effect/schema"
|
|
2
4
|
import type * as Context from "@typed/context"
|
|
3
5
|
import * as RefSubject from "@typed/fx/RefSubject"
|
|
@@ -12,6 +14,9 @@ import type {
|
|
|
12
14
|
BeforeNavigationEvent,
|
|
13
15
|
BeforeNavigationHandler,
|
|
14
16
|
CancelNavigation,
|
|
17
|
+
FormDataEvent,
|
|
18
|
+
FormDataHandler,
|
|
19
|
+
FormInputFrom,
|
|
15
20
|
NavigateOptions,
|
|
16
21
|
Navigation,
|
|
17
22
|
NavigationError,
|
|
@@ -60,9 +65,18 @@ export type ModelAndIntent = {
|
|
|
60
65
|
never,
|
|
61
66
|
Set<readonly [NavigationHandler<any, any>, Context.Context<any>]>
|
|
62
67
|
>
|
|
68
|
+
|
|
69
|
+
readonly formDataHandlers: RefSubject.RefSubject<
|
|
70
|
+
never,
|
|
71
|
+
never,
|
|
72
|
+
Set<readonly [FormDataHandler<any, any>, Context.Context<any>]>
|
|
73
|
+
>
|
|
74
|
+
|
|
63
75
|
readonly commit: Commit
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308])
|
|
79
|
+
|
|
66
80
|
export function setupFromModelAndIntent(
|
|
67
81
|
modelAndIntent: ModelAndIntent,
|
|
68
82
|
origin: string,
|
|
@@ -70,8 +84,7 @@ export function setupFromModelAndIntent(
|
|
|
70
84
|
getRandomValues: Context.Fn.FnOf<typeof GetRandomValues>,
|
|
71
85
|
newNavigationState?: () => NavigationState
|
|
72
86
|
) {
|
|
73
|
-
const { beforeHandlers, canGoBack, canGoForward, commit, handlers, state } = modelAndIntent
|
|
74
|
-
|
|
87
|
+
const { beforeHandlers, canGoBack, canGoForward, commit, formDataHandlers, handlers, state } = modelAndIntent
|
|
75
88
|
const entries = RefSubject.map(state, (s) => s.entries)
|
|
76
89
|
const currentEntry = RefSubject.map(state, (s) => s.entries[s.index])
|
|
77
90
|
const transition = RefSubject.map(state, (s) => s.transition)
|
|
@@ -122,6 +135,52 @@ export function setupFromModelAndIntent(
|
|
|
122
135
|
}
|
|
123
136
|
})
|
|
124
137
|
|
|
138
|
+
const runFormDataHandlers = (
|
|
139
|
+
event: FormDataEvent
|
|
140
|
+
): Effect.Effect<
|
|
141
|
+
HttpClient.client.Client.Default,
|
|
142
|
+
NavigationError | HttpClient.error.HttpClientError,
|
|
143
|
+
Either.Either<RedirectError | CancelNavigation, Option.Option<HttpClient.response.ClientResponse>>
|
|
144
|
+
> =>
|
|
145
|
+
Effect.gen(function*(_) {
|
|
146
|
+
const handlers = yield* _(formDataHandlers)
|
|
147
|
+
const matches: Array<
|
|
148
|
+
Effect.Effect<never, RedirectError | CancelNavigation, Option.Option<HttpClient.response.ClientResponse>>
|
|
149
|
+
> = []
|
|
150
|
+
|
|
151
|
+
for (const [handler, ctx] of handlers) {
|
|
152
|
+
const exit = yield* _(handler(event), Effect.provide(ctx), Effect.either)
|
|
153
|
+
if (Either.isRight(exit)) {
|
|
154
|
+
const match = exit.right
|
|
155
|
+
if (Option.isSome(match)) {
|
|
156
|
+
matches.push(Effect.provide(match.value, ctx))
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
return Either.left(exit.left)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (matches.length > 0) {
|
|
164
|
+
for (const match of matches) {
|
|
165
|
+
const exit = yield* _(match, Effect.either)
|
|
166
|
+
if (Either.isLeft(exit)) {
|
|
167
|
+
return Either.left(exit.left)
|
|
168
|
+
} else if (Option.isSome(exit.right)) {
|
|
169
|
+
return Either.right(exit.right)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Only if there are 0 matches, we'll make a request to the server ourselves
|
|
174
|
+
const response = yield* _(
|
|
175
|
+
makeFormDataRequest(event, Option.getOrElse(event.action, () => event.from.url.href))
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return Either.right(Option.some(response))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return Either.right(Option.none())
|
|
182
|
+
})
|
|
183
|
+
|
|
125
184
|
const runNavigationEvent = (
|
|
126
185
|
beforeEvent: BeforeNavigationEvent,
|
|
127
186
|
get: Effect.Effect<never, never, NavigationState>,
|
|
@@ -190,7 +249,7 @@ export function setupFromModelAndIntent(
|
|
|
190
249
|
get: Effect.Effect<never, never, NavigationState>,
|
|
191
250
|
set: (a: NavigationState) => Effect.Effect<never, never, NavigationState>,
|
|
192
251
|
depth: number
|
|
193
|
-
) =>
|
|
252
|
+
): Effect.Effect<never, NavigationError, Destination> =>
|
|
194
253
|
Effect.gen(function*(_) {
|
|
195
254
|
if (depth >= 25) {
|
|
196
255
|
return yield* _(Effect.dieMessage(`Redirect loop detected.`))
|
|
@@ -208,7 +267,7 @@ export function setupFromModelAndIntent(
|
|
|
208
267
|
|
|
209
268
|
return yield* _(runNavigationEvent(event, get, set, depth + 1))
|
|
210
269
|
}
|
|
211
|
-
})
|
|
270
|
+
}).pipe(GetRandomValues.provide(getRandomValues))
|
|
212
271
|
|
|
213
272
|
const navigate = (pathOrUrl: string | URL, options?: NavigateOptions, skipCommit: boolean = false) =>
|
|
214
273
|
state.runUpdates(({ get, set }) =>
|
|
@@ -347,6 +406,73 @@ export function setupFromModelAndIntent(
|
|
|
347
406
|
})
|
|
348
407
|
)
|
|
349
408
|
|
|
409
|
+
const submit = (
|
|
410
|
+
data: FormData,
|
|
411
|
+
input?: Omit<FormInputFrom, "data">
|
|
412
|
+
): Effect.Effect<
|
|
413
|
+
HttpClient.client.Client.Default,
|
|
414
|
+
NavigationError | HttpClient.error.HttpClientError,
|
|
415
|
+
Option.Option<HttpClient.response.ClientResponse>
|
|
416
|
+
> =>
|
|
417
|
+
state.runUpdates(({ get, set }) =>
|
|
418
|
+
Effect.gen(function*(_) {
|
|
419
|
+
const { entries, index } = yield* _(get)
|
|
420
|
+
const from = entries[index]
|
|
421
|
+
const event: FormDataEvent = {
|
|
422
|
+
from,
|
|
423
|
+
data,
|
|
424
|
+
name: Option.fromNullable(input?.name),
|
|
425
|
+
action: Option.fromNullable(input?.action),
|
|
426
|
+
method: Option.fromNullable(input?.method),
|
|
427
|
+
encoding: Option.fromNullable(input?.encoding)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const either = yield* _(runFormDataHandlers(event))
|
|
431
|
+
|
|
432
|
+
if (Either.isLeft(either)) {
|
|
433
|
+
yield* _(handleError(either.left, get, set, 0))
|
|
434
|
+
return Option.none<HttpClient.response.ClientResponse>()
|
|
435
|
+
} else {
|
|
436
|
+
if (Option.isNone(either.right)) {
|
|
437
|
+
return either.right
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const response = either.right.value
|
|
441
|
+
|
|
442
|
+
// If it is a redirect
|
|
443
|
+
if (REDIRECT_STATUS_CODES.has(response.status)) {
|
|
444
|
+
const location = HttpClient.headers.get(response.headers, "location")
|
|
445
|
+
|
|
446
|
+
// And we have a location header
|
|
447
|
+
if (Option.isSome(location)) {
|
|
448
|
+
// Then we navigate to that location
|
|
449
|
+
yield* _(navigate(location.value, { history: "replace" }))
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return Option.some(response)
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
const onFormData = <R = never, R2 = never>(
|
|
459
|
+
handler: FormDataHandler<R, R2>
|
|
460
|
+
): Effect.Effect<R | R2 | Scope.Scope, never, void> =>
|
|
461
|
+
Effect.contextWithEffect((ctx) => {
|
|
462
|
+
const entry = [handler, ctx] as const
|
|
463
|
+
|
|
464
|
+
return Effect.zipRight(
|
|
465
|
+
RefSubject.update(formDataHandlers, (handlers) => new Set([...handlers, entry])),
|
|
466
|
+
Effect.addFinalizer(() =>
|
|
467
|
+
RefSubject.update(formDataHandlers, (handlers) => {
|
|
468
|
+
const updated = new Set(handlers)
|
|
469
|
+
updated.delete(entry)
|
|
470
|
+
return updated
|
|
471
|
+
})
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
})
|
|
475
|
+
|
|
350
476
|
const navigation = {
|
|
351
477
|
back,
|
|
352
478
|
base,
|
|
@@ -362,7 +488,9 @@ export function setupFromModelAndIntent(
|
|
|
362
488
|
reload,
|
|
363
489
|
transition,
|
|
364
490
|
traverseTo,
|
|
365
|
-
updateCurrentEntry
|
|
491
|
+
updateCurrentEntry,
|
|
492
|
+
submit,
|
|
493
|
+
onFormData
|
|
366
494
|
} satisfies Navigation
|
|
367
495
|
|
|
368
496
|
return navigation
|
|
@@ -494,22 +622,53 @@ export function isDestination(proposed: ProposedDestination): proposed is Destin
|
|
|
494
622
|
return "id" in proposed && "key" in proposed
|
|
495
623
|
}
|
|
496
624
|
|
|
625
|
+
const strictEqual = <A>(a: A, b: A) => a === b
|
|
626
|
+
|
|
497
627
|
export function makeHandlersState() {
|
|
498
628
|
return Effect.gen(function*(_) {
|
|
499
629
|
const beforeHandlers = yield* _(
|
|
500
630
|
RefSubject.fromEffect(
|
|
501
|
-
Effect.sync(() => new Set<readonly [BeforeNavigationHandler<any, any>, Context.Context<any>]>())
|
|
631
|
+
Effect.sync(() => new Set<readonly [BeforeNavigationHandler<any, any>, Context.Context<any>]>()),
|
|
632
|
+
{ eq: strictEqual }
|
|
502
633
|
)
|
|
503
634
|
)
|
|
504
635
|
const handlers = yield* _(
|
|
505
636
|
RefSubject.fromEffect(
|
|
506
|
-
Effect.sync(() => new Set<readonly [NavigationHandler<any, any>, Context.Context<any>]>())
|
|
637
|
+
Effect.sync(() => new Set<readonly [NavigationHandler<any, any>, Context.Context<any>]>()),
|
|
638
|
+
{ eq: strictEqual }
|
|
639
|
+
)
|
|
640
|
+
)
|
|
641
|
+
const formDataHandlers = yield* _(
|
|
642
|
+
RefSubject.fromEffect(
|
|
643
|
+
Effect.sync(() => new Set<readonly [FormDataHandler<any, any>, Context.Context<any>]>()),
|
|
644
|
+
{ eq: strictEqual }
|
|
507
645
|
)
|
|
508
646
|
)
|
|
509
647
|
|
|
510
648
|
return {
|
|
511
649
|
beforeHandlers,
|
|
512
|
-
handlers
|
|
650
|
+
handlers,
|
|
651
|
+
formDataHandlers
|
|
513
652
|
}
|
|
514
653
|
})
|
|
515
654
|
}
|
|
655
|
+
|
|
656
|
+
function makeFormDataRequest(event: FormDataEvent, url: string) {
|
|
657
|
+
const headers = new Headers()
|
|
658
|
+
|
|
659
|
+
if (Option.isSome(event.encoding)) {
|
|
660
|
+
headers.set("Content-Type", event.encoding.value)
|
|
661
|
+
}
|
|
662
|
+
const method = Option.getOrElse(event.method, () => "POST")
|
|
663
|
+
|
|
664
|
+
return Effect.flatMap(
|
|
665
|
+
HttpClient.client.Client,
|
|
666
|
+
(client) =>
|
|
667
|
+
client(
|
|
668
|
+
HttpClient.request.make(method as "POST")(url, {
|
|
669
|
+
headers: HttpClient.headers.fromInput(headers),
|
|
670
|
+
body: HttpClient.body.formData(event.data)
|
|
671
|
+
})
|
|
672
|
+
)
|
|
673
|
+
)
|
|
674
|
+
}
|