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