@typed/navigation 0.5.4 → 0.6.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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/cjs/Blocking.js +53 -0
  4. package/dist/cjs/Blocking.js.map +1 -0
  5. package/dist/cjs/Layer.js +27 -0
  6. package/dist/cjs/Layer.js.map +1 -0
  7. package/dist/cjs/Navigation.js +184 -62
  8. package/dist/cjs/Navigation.js.map +1 -1
  9. package/dist/cjs/index.js +36 -17
  10. package/dist/cjs/index.js.map +1 -1
  11. package/dist/cjs/internal/fromWindow.js +376 -0
  12. package/dist/cjs/internal/fromWindow.js.map +1 -0
  13. package/dist/cjs/internal/memory.js +67 -0
  14. package/dist/cjs/internal/memory.js.map +1 -0
  15. package/dist/cjs/internal/shared.js +403 -0
  16. package/dist/cjs/internal/shared.js.map +1 -0
  17. package/dist/dts/Blocking.d.ts +33 -0
  18. package/dist/dts/Blocking.d.ts.map +1 -0
  19. package/dist/dts/Layer.d.ts +44 -0
  20. package/dist/dts/Layer.d.ts.map +1 -0
  21. package/dist/dts/Navigation.d.ts +377 -0
  22. package/dist/dts/Navigation.d.ts.map +1 -0
  23. package/dist/dts/index.d.ts +17 -0
  24. package/dist/dts/index.d.ts.map +1 -0
  25. package/dist/dts/internal/fromWindow.d.ts +12 -0
  26. package/dist/dts/internal/fromWindow.d.ts.map +1 -0
  27. package/dist/dts/internal/memory.d.ts +6 -0
  28. package/dist/dts/internal/memory.d.ts.map +1 -0
  29. package/dist/dts/internal/shared.d.ts +139 -0
  30. package/dist/dts/internal/shared.d.ts.map +1 -0
  31. package/dist/esm/Blocking.js +39 -0
  32. package/dist/esm/Blocking.js.map +1 -0
  33. package/dist/esm/Layer.js +18 -0
  34. package/dist/esm/Layer.js.map +1 -0
  35. package/dist/esm/Navigation.js +166 -0
  36. package/dist/esm/Navigation.js.map +1 -0
  37. package/dist/esm/index.js +17 -0
  38. package/dist/esm/index.js.map +1 -0
  39. package/dist/esm/internal/fromWindow.js +273 -0
  40. package/dist/esm/internal/fromWindow.js.map +1 -0
  41. package/dist/esm/internal/memory.js +54 -0
  42. package/dist/esm/internal/memory.js.map +1 -0
  43. package/dist/esm/internal/shared.js +336 -0
  44. package/dist/esm/internal/shared.js.map +1 -0
  45. package/dist/esm/package.json +4 -0
  46. package/package.json +52 -32
  47. package/src/Blocking.ts +102 -0
  48. package/src/Layer.ts +53 -0
  49. package/src/Navigation.ts +342 -159
  50. package/src/index.ts +17 -3
  51. package/src/internal/fromWindow.ts +421 -0
  52. package/src/internal/memory.ts +79 -0
  53. package/src/internal/shared.ts +514 -0
  54. package/dist/DOM.d.ts +0 -12
  55. package/dist/DOM.d.ts.map +0 -1
  56. package/dist/DOM.js +0 -87
  57. package/dist/DOM.js.map +0 -1
  58. package/dist/Memory.d.ts +0 -10
  59. package/dist/Memory.d.ts.map +0 -1
  60. package/dist/Memory.js +0 -55
  61. package/dist/Memory.js.map +0 -1
  62. package/dist/Navigation.d.ts +0 -116
  63. package/dist/Navigation.d.ts.map +0 -1
  64. package/dist/Navigation.js +0 -36
  65. package/dist/Navigation.js.map +0 -1
  66. package/dist/_makeServerWindow.d.ts +0 -16
  67. package/dist/_makeServerWindow.d.ts.map +0 -1
  68. package/dist/_makeServerWindow.js +0 -9
  69. package/dist/_makeServerWindow.js.map +0 -1
  70. package/dist/cjs/DOM.d.ts +0 -12
  71. package/dist/cjs/DOM.d.ts.map +0 -1
  72. package/dist/cjs/DOM.js +0 -116
  73. package/dist/cjs/DOM.js.map +0 -1
  74. package/dist/cjs/Memory.d.ts +0 -10
  75. package/dist/cjs/Memory.d.ts.map +0 -1
  76. package/dist/cjs/Memory.js +0 -82
  77. package/dist/cjs/Memory.js.map +0 -1
  78. package/dist/cjs/Navigation.d.ts +0 -116
  79. package/dist/cjs/Navigation.d.ts.map +0 -1
  80. package/dist/cjs/_makeServerWindow.d.ts +0 -16
  81. package/dist/cjs/_makeServerWindow.d.ts.map +0 -1
  82. package/dist/cjs/_makeServerWindow.js +0 -36
  83. package/dist/cjs/_makeServerWindow.js.map +0 -1
  84. package/dist/cjs/dom-intent.d.ts +0 -28
  85. package/dist/cjs/dom-intent.d.ts.map +0 -1
  86. package/dist/cjs/dom-intent.js +0 -172
  87. package/dist/cjs/dom-intent.js.map +0 -1
  88. package/dist/cjs/history.d.ts +0 -31
  89. package/dist/cjs/history.d.ts.map +0 -1
  90. package/dist/cjs/history.js +0 -131
  91. package/dist/cjs/history.js.map +0 -1
  92. package/dist/cjs/index.d.ts +0 -4
  93. package/dist/cjs/index.d.ts.map +0 -1
  94. package/dist/cjs/json.d.ts +0 -13
  95. package/dist/cjs/json.d.ts.map +0 -1
  96. package/dist/cjs/json.js +0 -24
  97. package/dist/cjs/json.js.map +0 -1
  98. package/dist/cjs/memory-intent.d.ts +0 -27
  99. package/dist/cjs/memory-intent.d.ts.map +0 -1
  100. package/dist/cjs/memory-intent.js +0 -156
  101. package/dist/cjs/memory-intent.js.map +0 -1
  102. package/dist/cjs/model.d.ts +0 -22
  103. package/dist/cjs/model.d.ts.map +0 -1
  104. package/dist/cjs/model.js +0 -48
  105. package/dist/cjs/model.js.map +0 -1
  106. package/dist/cjs/shared-intent.d.ts +0 -14
  107. package/dist/cjs/shared-intent.d.ts.map +0 -1
  108. package/dist/cjs/shared-intent.js +0 -82
  109. package/dist/cjs/shared-intent.js.map +0 -1
  110. package/dist/cjs/storage.d.ts +0 -19
  111. package/dist/cjs/storage.d.ts.map +0 -1
  112. package/dist/cjs/storage.js +0 -101
  113. package/dist/cjs/storage.js.map +0 -1
  114. package/dist/cjs/util.d.ts +0 -5
  115. package/dist/cjs/util.d.ts.map +0 -1
  116. package/dist/cjs/util.js +0 -39
  117. package/dist/cjs/util.js.map +0 -1
  118. package/dist/dom-intent.d.ts +0 -28
  119. package/dist/dom-intent.d.ts.map +0 -1
  120. package/dist/dom-intent.js +0 -140
  121. package/dist/dom-intent.js.map +0 -1
  122. package/dist/history.d.ts +0 -31
  123. package/dist/history.d.ts.map +0 -1
  124. package/dist/history.js +0 -104
  125. package/dist/history.js.map +0 -1
  126. package/dist/index.d.ts +0 -4
  127. package/dist/index.d.ts.map +0 -1
  128. package/dist/index.js +0 -4
  129. package/dist/index.js.map +0 -1
  130. package/dist/json.d.ts +0 -13
  131. package/dist/json.d.ts.map +0 -1
  132. package/dist/json.js +0 -17
  133. package/dist/json.js.map +0 -1
  134. package/dist/memory-intent.d.ts +0 -27
  135. package/dist/memory-intent.d.ts.map +0 -1
  136. package/dist/memory-intent.js +0 -124
  137. package/dist/memory-intent.js.map +0 -1
  138. package/dist/model.d.ts +0 -22
  139. package/dist/model.d.ts.map +0 -1
  140. package/dist/model.js +0 -21
  141. package/dist/model.js.map +0 -1
  142. package/dist/shared-intent.d.ts +0 -14
  143. package/dist/shared-intent.d.ts.map +0 -1
  144. package/dist/shared-intent.js +0 -51
  145. package/dist/shared-intent.js.map +0 -1
  146. package/dist/storage.d.ts +0 -19
  147. package/dist/storage.d.ts.map +0 -1
  148. package/dist/storage.js +0 -73
  149. package/dist/storage.js.map +0 -1
  150. package/dist/tsconfig.cjs.build.tsbuildinfo +0 -1
  151. package/dist/util.d.ts +0 -5
  152. package/dist/util.d.ts.map +0 -1
  153. package/dist/util.js +0 -12
  154. package/dist/util.js.map +0 -1
  155. package/eslintrc.json +0 -3
  156. package/project.json +0 -46
  157. package/src/DOM.test.ts +0 -699
  158. package/src/DOM.ts +0 -163
  159. package/src/Memory.test.ts +0 -464
  160. package/src/Memory.ts +0 -102
  161. package/src/_makeServerWindow.ts +0 -28
  162. package/src/dom-intent.ts +0 -268
  163. package/src/history.ts +0 -165
  164. package/src/json.ts +0 -31
  165. package/src/memory-intent.ts +0 -224
  166. package/src/model.ts +0 -54
  167. package/src/shared-intent.ts +0 -117
  168. package/src/storage.ts +0 -101
  169. package/src/util.ts +0 -20
  170. package/tsconfig.build.json +0 -4
  171. package/tsconfig.build.tsbuildinfo +0 -1
  172. package/tsconfig.cjs.build.json +0 -22
  173. package/tsconfig.json +0 -27
  174. package/vite.config.mjs +0 -3
@@ -0,0 +1,421 @@
1
+ import { Window } from "@typed/dom/Window"
2
+ import type { Computed } from "@typed/fx/Computed"
3
+ import { scopedRuntime } from "@typed/fx/internal/helpers"
4
+ import * as RefSubject from "@typed/fx/RefSubject"
5
+ import { GetRandomValues, Uuid } from "@typed/id"
6
+ import { Effect, Option } from "effect"
7
+ import type { Context, Layer, Scope } from "effect"
8
+ import type { Commit } from "../Layer"
9
+ import type {
10
+ BeforeNavigationEvent,
11
+ BeforeNavigationHandler,
12
+ Destination,
13
+ NavigationEvent,
14
+ NavigationHandler,
15
+ Transition
16
+ } from "../Navigation"
17
+ import { Navigation, NavigationError } from "../Navigation"
18
+ import type { NavigationState } from "./shared"
19
+ import {
20
+ getOriginalState,
21
+ getUrl,
22
+ isPatchedState,
23
+ makeDestination,
24
+ makeHandlersState,
25
+ setupFromModelAndIntent
26
+ } from "./shared"
27
+
28
+ /* eslint-disable @typescript-eslint/consistent-type-imports */
29
+ type NativeNavigation = import("@virtualstate/navigation").Navigation
30
+ type NativeEntry = import("@virtualstate/navigation").NavigationHistoryEntry
31
+ type NativeEvent = import("@virtualstate/navigation").NavigationEventMap["navigate"]
32
+ /* eslint-enable @typescript-eslint/consistent-type-imports */
33
+
34
+ declare global {
35
+ export interface Window {
36
+ navigation?: NativeNavigation
37
+ }
38
+ }
39
+
40
+ export const fromWindow: Layer.Layer<Window, never, Navigation> = Navigation.scoped(
41
+ Window.withEffect((window) => {
42
+ const getRandomValues = (length: number) => Effect.sync(() => window.crypto.getRandomValues(new Uint8Array(length)))
43
+ return Effect.gen(function*(_) {
44
+ const { run, runPromise } = yield* _(scopedRuntime<never>())
45
+ const hasNativeNavigation = !!window.navigation
46
+ const modelAndIntent = yield* _(
47
+ hasNativeNavigation
48
+ ? setupWithNavigation(window.navigation!, runPromise)
49
+ : setupWithHistory(window, (event) => run(handleHistoryEvent(event)))
50
+ )
51
+
52
+ const navigation = setupFromModelAndIntent(
53
+ modelAndIntent,
54
+ window.location.origin,
55
+ getBaseHref(window),
56
+ getRandomValues,
57
+ hasNativeNavigation ? () => getNavigationState(window.navigation!) : undefined
58
+ )
59
+
60
+ return navigation
61
+
62
+ function handleHistoryEvent(event: HistoryEvent) {
63
+ return Effect.gen(function*(_) {
64
+ if (event._tag === "PushState") {
65
+ return yield* _(navigation.navigate(event.url, {}, event.skipCommit))
66
+ } else if (event._tag === "ReplaceState") {
67
+ if (Option.isSome(event.url)) {
68
+ return yield* _(
69
+ navigation.navigate(event.url.value, { history: "replace", state: event.state }, event.skipCommit)
70
+ )
71
+ } else {
72
+ return yield* _(navigation.updateCurrentEntry(event))
73
+ }
74
+ } else if (event._tag === "Traverse") {
75
+ const { entries, index } = yield* _(modelAndIntent.state)
76
+ const toIndex = Math.min(Math.max(0, index + event.delta), entries.length - 1)
77
+ const to = entries[toIndex]
78
+
79
+ return yield* _(navigation.traverseTo(to.key, {}, event.skipCommit))
80
+ } else {
81
+ yield* _(navigation.traverseTo(event.key, {}, event.skipCommit))
82
+ return yield* _(navigation.updateCurrentEntry({ state: event.state }))
83
+ }
84
+ })
85
+ }
86
+ }).pipe(
87
+ GetRandomValues.provide(getRandomValues)
88
+ )
89
+ })
90
+ )
91
+
92
+ function getBaseHref(window: Window) {
93
+ const base = window.document.querySelector("base")
94
+ return base ? base.href : "/"
95
+ }
96
+
97
+ type ModelAndIntent = {
98
+ readonly state: RefSubject.RefSubject<never, never, NavigationState>
99
+ readonly canGoBack: Computed<
100
+ never,
101
+ never,
102
+ boolean
103
+ >
104
+ readonly canGoForward: Computed<
105
+ never,
106
+ never,
107
+ boolean
108
+ >
109
+ readonly beforeHandlers: RefSubject.RefSubject<
110
+ never,
111
+ never,
112
+ Set<readonly [BeforeNavigationHandler<any, any>, Context.Context<any>]>
113
+ >
114
+ readonly handlers: RefSubject.RefSubject<
115
+ never,
116
+ never,
117
+ Set<readonly [NavigationHandler<any, any>, Context.Context<any>]>
118
+ >
119
+ readonly commit: Commit
120
+ }
121
+
122
+ const getNavigationState = (navigation: NativeNavigation): NavigationState => {
123
+ const entries = navigation.entries().map(nativeEntryToDestination)
124
+ const { index } = navigation.currentEntry
125
+
126
+ return {
127
+ entries,
128
+ index,
129
+ transition: Option.none<Transition>()
130
+ }
131
+ }
132
+
133
+ function setupWithNavigation(
134
+ navigation: NativeNavigation,
135
+ runPromise: <E, A>(effect: Effect.Effect<Scope.Scope, E, A>) => Promise<A>
136
+ ): Effect.Effect<
137
+ Scope.Scope | GetRandomValues,
138
+ never,
139
+ ModelAndIntent
140
+ > {
141
+ return Effect.gen(function*(_) {
142
+ const state = yield* _(
143
+ RefSubject.fromEffect(
144
+ Effect.sync((): NavigationState => getNavigationState(navigation))
145
+ )
146
+ )
147
+ const canGoBack = state.map((s) => s.index > 0)
148
+ const canGoForward = state.map((s) => s.index < s.entries.length - 1)
149
+ const { beforeHandlers, handlers } = yield* _(makeHandlersState())
150
+ const commit: Commit = (to: Destination, event: BeforeNavigationEvent) =>
151
+ Effect.gen(function*(_) {
152
+ const { key, state, url } = to
153
+ const { info, type } = event
154
+
155
+ if (type === "push" || type === "replace") {
156
+ yield* _(
157
+ Effect.promise(() => navigation.navigate(url.toString(), { history: type, state, info }).committed),
158
+ Effect.catchAllDefect((error) => Effect.fail(new NavigationError({ error })))
159
+ )
160
+ } else if (event.type === "reload") {
161
+ yield* _(
162
+ Effect.promise(() => navigation.reload({ state, info }).committed),
163
+ Effect.catchAllDefect((error) => Effect.fail(new NavigationError({ error })))
164
+ )
165
+ } else {
166
+ yield* _(
167
+ Effect.promise(() => navigation.traverseTo(key, { info }).committed),
168
+ Effect.catchAllDefect((error) => Effect.fail(new NavigationError({ error })))
169
+ )
170
+ }
171
+ })
172
+
173
+ const runHandlers = (native: NativeEvent) =>
174
+ Effect.gen(function*(_) {
175
+ const eventHandlers = yield* _(handlers)
176
+ const matches: Array<Effect.Effect<never, never, unknown>> = []
177
+
178
+ const event: NavigationEvent = {
179
+ type: native.navigationType,
180
+ destination: nativeEntryToDestination(navigation.currentEntry),
181
+ info: native.info
182
+ }
183
+
184
+ for (const [handler, ctx] of eventHandlers) {
185
+ const match = yield* _(handler(event), Effect.provide(ctx))
186
+ if (Option.isSome(match)) {
187
+ matches.push(Effect.provide(match.value, ctx))
188
+ }
189
+ }
190
+
191
+ if (matches.length > 0) {
192
+ yield* _(Effect.all(matches))
193
+ }
194
+ })
195
+
196
+ navigation.addEventListener("navigate", (ev) => {
197
+ if (shouldNotIntercept(ev)) return
198
+
199
+ ev.intercept({
200
+ handler: () => runPromise(runHandlers(ev))
201
+ })
202
+ })
203
+
204
+ return {
205
+ state,
206
+ canGoBack,
207
+ canGoForward,
208
+ beforeHandlers,
209
+ handlers,
210
+ commit
211
+ } as const
212
+ })
213
+ }
214
+
215
+ function nativeEntryToDestination(
216
+ entry: Pick<NativeEntry, "id" | "key" | "url" | "getState" | "sameDocument">
217
+ ): Destination {
218
+ return {
219
+ id: Uuid(entry.id),
220
+ key: Uuid(entry.key),
221
+ url: new URL(entry.url!),
222
+ state: entry.getState(),
223
+ sameDocument: entry.sameDocument
224
+ }
225
+ }
226
+
227
+ function shouldNotIntercept(navigationEvent: NativeEvent): boolean {
228
+ return (
229
+ !navigationEvent.canIntercept ||
230
+ // If this is just a hashChange,
231
+ // just let the browser handle scrolling to the content.
232
+ navigationEvent.hashChange ||
233
+ // If this is a download,
234
+ // let the browser perform the download.
235
+ !!navigationEvent.downloadRequest ||
236
+ // If this is a form submission,
237
+ // let that go to the server.
238
+ !!navigationEvent.formData
239
+ )
240
+ }
241
+
242
+ function setupWithHistory(
243
+ window: Window,
244
+ onEvent: (event: HistoryEvent) => void
245
+ ): Effect.Effect<
246
+ GetRandomValues | Scope.Scope,
247
+ never,
248
+ ModelAndIntent
249
+ > {
250
+ return Effect.gen(function*(_) {
251
+ const { location } = window
252
+ const { original: history, unpatch } = patchHistory(window, onEvent)
253
+
254
+ yield* _(Effect.addFinalizer(() => unpatch))
255
+
256
+ const state = yield* _(
257
+ RefSubject.fromEffect(
258
+ Effect.suspend(() =>
259
+ Effect.map(
260
+ makeDestination(
261
+ new URL(location.href),
262
+ history.state,
263
+ location.origin
264
+ ),
265
+ (destination): NavigationState => ({ entries: [destination], index: 0, transition: Option.none() })
266
+ )
267
+ )
268
+ )
269
+ )
270
+ const canGoBack = state.map((s) => s.index > 0)
271
+ const canGoForward = state.map((s) => s.index < s.entries.length - 1)
272
+ const { beforeHandlers, handlers } = yield* _(makeHandlersState())
273
+ const commit: Commit = ({ id, key, state, url }: Destination, event: BeforeNavigationEvent) =>
274
+ Effect.sync(() => {
275
+ const { type } = event
276
+
277
+ if (type === "push") {
278
+ history.pushState({ id, key, originalHistoryState: state }, "", url)
279
+ } else if (type === "replace") {
280
+ history.replaceState({ id, key, originalHistoryState: state }, "", url)
281
+ } else if (event.type === "reload") {
282
+ location.reload()
283
+ } else {
284
+ history.go(event.delta)
285
+ }
286
+ })
287
+
288
+ return {
289
+ state,
290
+ canGoBack,
291
+ canGoForward,
292
+ beforeHandlers,
293
+ handlers,
294
+ commit
295
+ } as const
296
+ })
297
+ }
298
+
299
+ type HistoryEvent = PushStateEvent | ReplaceStateEvent | TraverseEvent | TraverseToEvent
300
+
301
+ type PushStateEvent = { _tag: "PushState"; state: unknown; url: URL; skipCommit: boolean }
302
+ type ReplaceStateEvent = { _tag: "ReplaceState"; state: unknown; url: Option.Option<URL>; skipCommit: boolean }
303
+ type TraverseEvent = { _tag: "Traverse"; delta: number; skipCommit: boolean }
304
+ type TraverseToEvent = { _tag: "TraverseTo"; key: Uuid; state: unknown; skipCommit: boolean }
305
+
306
+ function patchHistory(window: Window, onEvent: (event: HistoryEvent) => void) {
307
+ const { history, location } = window
308
+ const stateDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(history), "state")
309
+
310
+ const methods = {
311
+ pushState: history.pushState.bind(history),
312
+ replaceState: history.replaceState.bind(history),
313
+ go: history.go.bind(history),
314
+ back: history.back.bind(history),
315
+ forward: history.forward.bind(history)
316
+ }
317
+ const getState = stateDescriptor?.get?.bind(history)
318
+
319
+ const original: History = {
320
+ get length() {
321
+ return history.length
322
+ },
323
+ get scrollRestoration() {
324
+ return history.scrollRestoration
325
+ },
326
+ set scrollRestoration(mode) {
327
+ history.scrollRestoration = mode
328
+ },
329
+ get state() {
330
+ return getState?.() ?? history.state
331
+ },
332
+ ...methods,
333
+ pushState(data, _, url) {
334
+ if (!stateDescriptor) {
335
+ ;(history as any).state = data
336
+ }
337
+
338
+ return methods.pushState(data, _, url)
339
+ },
340
+ replaceState(data, _, url) {
341
+ if (!stateDescriptor) {
342
+ ;(history as any).state = data
343
+ }
344
+
345
+ return methods.replaceState(data, _, url)
346
+ }
347
+ }
348
+
349
+ history.pushState = (state, _, url) => {
350
+ if (url) {
351
+ onEvent({ _tag: "PushState", state, url: getUrl(location.origin, url), skipCommit: false })
352
+ } else {
353
+ onEvent({ _tag: "ReplaceState", state, url: Option.none(), skipCommit: false })
354
+ }
355
+ }
356
+ history.replaceState = (state, _, url) => {
357
+ onEvent({
358
+ _tag: "ReplaceState",
359
+ state,
360
+ url: url ? Option.some(getUrl(location.origin, url)) : Option.none(),
361
+ skipCommit: false
362
+ })
363
+ }
364
+ history.go = (delta) => {
365
+ if (delta && delta !== 0) {
366
+ onEvent({ _tag: "Traverse", delta, skipCommit: false })
367
+ }
368
+ }
369
+ history.back = () => {
370
+ onEvent({ _tag: "Traverse", delta: -1, skipCommit: false })
371
+ }
372
+ history.forward = () => {
373
+ onEvent({ _tag: "Traverse", delta: 1, skipCommit: false })
374
+ }
375
+
376
+ // In a proper browser this will allow patching to hide the id/key's associated with the state
377
+ if (stateDescriptor) {
378
+ Object.defineProperty(history, "state", {
379
+ get() {
380
+ return getOriginalState(stateDescriptor.get!.call(history))
381
+ }
382
+ })
383
+ }
384
+
385
+ const onHashChange = () => {
386
+ onEvent({ _tag: "ReplaceState", state: history.state, url: Option.some(new URL(location.href)), skipCommit: false })
387
+ }
388
+
389
+ window.addEventListener("hashchange", onHashChange, { capture: true })
390
+
391
+ const onPopState = (ev: PopStateEvent) => {
392
+ if (isPatchedState(ev.state)) {
393
+ onEvent({ _tag: "TraverseTo", key: ev.state.key, state: ev.state.originalHistoryState, skipCommit: true })
394
+ } else {
395
+ onEvent({ _tag: "ReplaceState", state: ev.state, url: Option.some(new URL(location.href)), skipCommit: true })
396
+ }
397
+ }
398
+
399
+ window.addEventListener("popstate", onPopState, { capture: true })
400
+
401
+ const unpatch = Effect.sync(() => {
402
+ history.pushState = original.pushState
403
+ history.replaceState = original.replaceState
404
+ history.go = original.go
405
+ history.back = original.back
406
+ history.forward = original.forward
407
+
408
+ if (stateDescriptor) {
409
+ Object.defineProperty(history, "state", stateDescriptor)
410
+ }
411
+
412
+ window.removeEventListener("hashchange", onHashChange)
413
+ window.removeEventListener("popstate", onPopState)
414
+ })
415
+
416
+ return {
417
+ original,
418
+ patched: history,
419
+ unpatch
420
+ } as const
421
+ }
@@ -0,0 +1,79 @@
1
+ import * as RefSubject from "@typed/fx/RefSubject"
2
+ import { GetRandomValues, getRandomValues } from "@typed/id"
3
+ import { Effect, Option } from "effect"
4
+ import type { Layer, Scope } from "effect"
5
+ import type { Commit, InitialMemoryOptions, MemoryOptions } from "../Layer"
6
+ import { Navigation } from "../Navigation"
7
+ import type { ModelAndIntent, NavigationState } from "./shared"
8
+ import { getOriginFromUrl, getUrl, makeDestination, makeHandlersState, setupFromModelAndIntent } from "./shared"
9
+
10
+ export const memory = (options: MemoryOptions): Layer.Layer<never, never, Navigation> =>
11
+ Navigation.scoped(
12
+ Effect.gen(function*(_) {
13
+ const getRandomValues = yield* _(GetRandomValues)
14
+ const modelAndIntent = yield* _(setupMemory(options))
15
+ const current = options.entries[options.currentIndex ?? 0]
16
+ const origin = options.origin ?? getOriginFromUrl(current.url)
17
+ const base = options.base ?? "/"
18
+
19
+ return setupFromModelAndIntent(modelAndIntent, origin, base, getRandomValues)
20
+ }).pipe(Effect.provide(getRandomValues))
21
+ )
22
+
23
+ export function initialMemory(
24
+ options: InitialMemoryOptions
25
+ ): Layer.Layer<never, never, Navigation> {
26
+ return Navigation.scoped(
27
+ Effect.gen(function*(_) {
28
+ const getRandomValues = yield* _(GetRandomValues)
29
+ const origin = options.origin ?? getOriginFromUrl(options.url)
30
+ const base = options.base ?? "/"
31
+ const destination = yield* _(makeDestination(getUrl(origin, options.url), options.state, origin))
32
+ const memoryOptions: MemoryOptions = {
33
+ entries: [destination],
34
+ origin,
35
+ base,
36
+ currentIndex: 0,
37
+ maxEntries: options.maxEntries
38
+ }
39
+ const modelAndIntent = yield* _(setupMemory(memoryOptions))
40
+
41
+ return setupFromModelAndIntent(modelAndIntent, origin, base, getRandomValues)
42
+ }).pipe(Effect.provide(getRandomValues))
43
+ )
44
+ }
45
+
46
+ function setupMemory(
47
+ options: MemoryOptions
48
+ ): Effect.Effect<
49
+ GetRandomValues | Scope.Scope,
50
+ never,
51
+ ModelAndIntent
52
+ > {
53
+ return Effect.gen(function*(_) {
54
+ const state = yield* _(
55
+ RefSubject.fromEffect(
56
+ Effect.sync((): NavigationState => {
57
+ return {
58
+ entries: options.entries,
59
+ index: options.currentIndex ?? options.entries.length - 1,
60
+ transition: Option.none()
61
+ }
62
+ })
63
+ )
64
+ )
65
+ const canGoBack = state.map((s) => s.index > 0)
66
+ const canGoForward = state.map((s) => s.index < s.entries.length - 1)
67
+ const { beforeHandlers, handlers } = yield* _(makeHandlersState())
68
+ const commit: Commit = options.commit ?? (() => Effect.unit)
69
+
70
+ return {
71
+ state,
72
+ canGoBack,
73
+ canGoForward,
74
+ beforeHandlers,
75
+ handlers,
76
+ commit
77
+ } as const
78
+ })
79
+ }