@typed/navigation 0.1.0

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 (141) hide show
  1. package/dist/DOM.d.ts +12 -0
  2. package/dist/DOM.d.ts.map +1 -0
  3. package/dist/DOM.js +89 -0
  4. package/dist/DOM.js.map +1 -0
  5. package/dist/Memory.d.ts +10 -0
  6. package/dist/Memory.d.ts.map +1 -0
  7. package/dist/Memory.js +55 -0
  8. package/dist/Memory.js.map +1 -0
  9. package/dist/Navigation.d.ts +117 -0
  10. package/dist/Navigation.d.ts.map +1 -0
  11. package/dist/Navigation.js +36 -0
  12. package/dist/Navigation.js.map +1 -0
  13. package/dist/_makeServerWindow.d.ts +16 -0
  14. package/dist/_makeServerWindow.d.ts.map +1 -0
  15. package/dist/_makeServerWindow.js +9 -0
  16. package/dist/_makeServerWindow.js.map +1 -0
  17. package/dist/cjs/DOM.d.ts +12 -0
  18. package/dist/cjs/DOM.d.ts.map +1 -0
  19. package/dist/cjs/DOM.js +118 -0
  20. package/dist/cjs/DOM.js.map +1 -0
  21. package/dist/cjs/Memory.d.ts +10 -0
  22. package/dist/cjs/Memory.d.ts.map +1 -0
  23. package/dist/cjs/Memory.js +82 -0
  24. package/dist/cjs/Memory.js.map +1 -0
  25. package/dist/cjs/Navigation.d.ts +117 -0
  26. package/dist/cjs/Navigation.d.ts.map +1 -0
  27. package/dist/cjs/Navigation.js +69 -0
  28. package/dist/cjs/Navigation.js.map +1 -0
  29. package/dist/cjs/_makeServerWindow.d.ts +16 -0
  30. package/dist/cjs/_makeServerWindow.d.ts.map +1 -0
  31. package/dist/cjs/_makeServerWindow.js +36 -0
  32. package/dist/cjs/_makeServerWindow.js.map +1 -0
  33. package/dist/cjs/constant.d.ts +2 -0
  34. package/dist/cjs/constant.d.ts.map +1 -0
  35. package/dist/cjs/constant.js +30 -0
  36. package/dist/cjs/constant.js.map +1 -0
  37. package/dist/cjs/dom-intent.d.ts +29 -0
  38. package/dist/cjs/dom-intent.d.ts.map +1 -0
  39. package/dist/cjs/dom-intent.js +173 -0
  40. package/dist/cjs/dom-intent.js.map +1 -0
  41. package/dist/cjs/history.d.ts +31 -0
  42. package/dist/cjs/history.d.ts.map +1 -0
  43. package/dist/cjs/history.js +115 -0
  44. package/dist/cjs/history.js.map +1 -0
  45. package/dist/cjs/index.d.ts +4 -0
  46. package/dist/cjs/index.d.ts.map +1 -0
  47. package/dist/cjs/index.js +20 -0
  48. package/dist/cjs/index.js.map +1 -0
  49. package/dist/cjs/json.d.ts +13 -0
  50. package/dist/cjs/json.d.ts.map +1 -0
  51. package/dist/cjs/json.js +24 -0
  52. package/dist/cjs/json.js.map +1 -0
  53. package/dist/cjs/memory-intent.d.ts +28 -0
  54. package/dist/cjs/memory-intent.d.ts.map +1 -0
  55. package/dist/cjs/memory-intent.js +149 -0
  56. package/dist/cjs/memory-intent.js.map +1 -0
  57. package/dist/cjs/model.d.ts +22 -0
  58. package/dist/cjs/model.d.ts.map +1 -0
  59. package/dist/cjs/model.js +48 -0
  60. package/dist/cjs/model.js.map +1 -0
  61. package/dist/cjs/shared-intent.d.ts +15 -0
  62. package/dist/cjs/shared-intent.d.ts.map +1 -0
  63. package/dist/cjs/shared-intent.js +82 -0
  64. package/dist/cjs/shared-intent.js.map +1 -0
  65. package/dist/cjs/storage.d.ts +19 -0
  66. package/dist/cjs/storage.d.ts.map +1 -0
  67. package/dist/cjs/storage.js +101 -0
  68. package/dist/cjs/storage.js.map +1 -0
  69. package/dist/cjs/util.d.ts +5 -0
  70. package/dist/cjs/util.d.ts.map +1 -0
  71. package/dist/cjs/util.js +39 -0
  72. package/dist/cjs/util.js.map +1 -0
  73. package/dist/constant.d.ts +2 -0
  74. package/dist/constant.d.ts.map +1 -0
  75. package/dist/constant.js +4 -0
  76. package/dist/constant.js.map +1 -0
  77. package/dist/dom-intent.d.ts +29 -0
  78. package/dist/dom-intent.d.ts.map +1 -0
  79. package/dist/dom-intent.js +141 -0
  80. package/dist/dom-intent.js.map +1 -0
  81. package/dist/history.d.ts +31 -0
  82. package/dist/history.d.ts.map +1 -0
  83. package/dist/history.js +88 -0
  84. package/dist/history.js.map +1 -0
  85. package/dist/index.d.ts +4 -0
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +4 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/intent.d.ts +31 -0
  90. package/dist/intent.d.ts.map +1 -0
  91. package/dist/intent.js +157 -0
  92. package/dist/intent.js.map +1 -0
  93. package/dist/json.d.ts +13 -0
  94. package/dist/json.d.ts.map +1 -0
  95. package/dist/json.js +17 -0
  96. package/dist/json.js.map +1 -0
  97. package/dist/memory-intent.d.ts +28 -0
  98. package/dist/memory-intent.d.ts.map +1 -0
  99. package/dist/memory-intent.js +117 -0
  100. package/dist/memory-intent.js.map +1 -0
  101. package/dist/model.d.ts +22 -0
  102. package/dist/model.d.ts.map +1 -0
  103. package/dist/model.js +21 -0
  104. package/dist/model.js.map +1 -0
  105. package/dist/shared-intent.d.ts +15 -0
  106. package/dist/shared-intent.d.ts.map +1 -0
  107. package/dist/shared-intent.js +51 -0
  108. package/dist/shared-intent.js.map +1 -0
  109. package/dist/storage.d.ts +19 -0
  110. package/dist/storage.d.ts.map +1 -0
  111. package/dist/storage.js +73 -0
  112. package/dist/storage.js.map +1 -0
  113. package/dist/tsconfig.cjs.build.tsbuildinfo +1 -0
  114. package/dist/util.d.ts +5 -0
  115. package/dist/util.d.ts.map +1 -0
  116. package/dist/util.js +12 -0
  117. package/dist/util.js.map +1 -0
  118. package/eslintrc.json +3 -0
  119. package/package.json +38 -0
  120. package/project.json +43 -0
  121. package/src/DOM.test.ts +704 -0
  122. package/src/DOM.ts +165 -0
  123. package/src/Memory.test.ts +464 -0
  124. package/src/Memory.ts +102 -0
  125. package/src/Navigation.ts +192 -0
  126. package/src/_makeServerWindow.ts +28 -0
  127. package/src/constant.ts +5 -0
  128. package/src/dom-intent.ts +276 -0
  129. package/src/history.ts +141 -0
  130. package/src/index.ts +3 -0
  131. package/src/json.ts +31 -0
  132. package/src/memory-intent.ts +221 -0
  133. package/src/model.ts +54 -0
  134. package/src/shared-intent.ts +120 -0
  135. package/src/storage.ts +101 -0
  136. package/src/util.ts +20 -0
  137. package/tsconfig.build.json +4 -0
  138. package/tsconfig.build.tsbuildinfo +1 -0
  139. package/tsconfig.cjs.build.json +22 -0
  140. package/tsconfig.json +27 -0
  141. package/vite.config.js +3 -0
package/src/Memory.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { pipe } from '@effect/data/Function'
2
+ import * as Option from '@effect/data/Option'
3
+ import * as Cause from '@effect/io/Cause'
4
+ import * as Effect from '@effect/io/Effect'
5
+ import * as Layer from '@effect/io/Layer'
6
+
7
+ import type { DomNavigationOptions } from './DOM.js'
8
+ import {
9
+ Destination,
10
+ Navigation,
11
+ NavigationError,
12
+ NavigationEvent,
13
+ NavigationType,
14
+ } from './Navigation.js'
15
+ import { makeIntent } from './memory-intent.js'
16
+ import { Model, makeModel } from './model.js'
17
+ import { createKey } from './util.js'
18
+
19
+ export interface MemoryNavigationOptions extends DomNavigationOptions {
20
+ readonly initialUrl: URL
21
+ readonly initialState?: unknown
22
+ readonly base?: string
23
+ }
24
+
25
+ export function memory(options: MemoryNavigationOptions): Layer.Layer<never, never, Navigation> {
26
+ return Navigation.layerScoped(
27
+ Effect.gen(function* ($) {
28
+ const initial: Destination = {
29
+ key: options.initialKey ?? (yield* $(createKey)),
30
+ url: options.initialUrl,
31
+ state: options.initialState,
32
+ }
33
+ const initialEvent: NavigationEvent = {
34
+ destination: initial,
35
+ hashChange: false,
36
+ navigationType: NavigationType.Push,
37
+ }
38
+
39
+ const model: Model = yield* $(makeModel([initialEvent], 0))
40
+ const intent = makeIntent(model, options)
41
+
42
+ // Used to ensure ordering of navigation events
43
+ const lock = Effect.unsafeMakeSemaphore(1).withPermits(1)
44
+
45
+ const handleNavigationError =
46
+ (depth: number) =>
47
+ (
48
+ error: NavigationError | Cause.NoSuchElementException,
49
+ ): Effect.Effect<never, never, Destination> =>
50
+ Effect.gen(function* ($) {
51
+ if (depth >= 50) {
52
+ throw new Error(
53
+ 'Too many redirects. You may have an infinite loop of onNavigation handlers that are redirecting.',
54
+ )
55
+ }
56
+
57
+ switch (error._tag) {
58
+ case 'NoSuchElementException':
59
+ case 'CancelNavigation':
60
+ return yield* $(model.currentEntry.get)
61
+ case 'RedirectNavigation':
62
+ return yield* $(
63
+ Effect.catchAll(
64
+ intent.navigate(error.url, error),
65
+ handleNavigationError(depth + 1),
66
+ ),
67
+ )
68
+ }
69
+ })
70
+
71
+ const catchNavigationError = <R, A>(
72
+ effect: Effect.Effect<R, NavigationError | Cause.NoSuchElementException, A>,
73
+ ) => Effect.catchAll(effect, handleNavigationError(0))
74
+
75
+ // Construct our service
76
+ const navigation: Navigation = {
77
+ back: lock(catchNavigationError(intent.back)),
78
+ base: '/',
79
+ canGoBack: model.canGoBack,
80
+ canGoForward: model.canGoForward,
81
+ currentEntry: model.currentEntry,
82
+ entries: model.entries,
83
+ forward: lock(catchNavigationError(intent.forward)),
84
+ goTo: (n) =>
85
+ pipe(
86
+ n,
87
+ intent.goTo,
88
+ Effect.catchAll((a) => pipe(a, handleNavigationError(0), Effect.map(Option.some))),
89
+ lock,
90
+ ),
91
+ navigate: (url, options) => pipe( intent.navigate(url, options), catchNavigationError, lock),
92
+ onNavigation: (handler, options) =>
93
+ pipe(intent.onNavigation(handler, options), catchNavigationError, Effect.asUnit),
94
+ onNavigationEnd: (handler, options) =>
95
+ Effect.asUnit(intent.onNavigationEnd(handler, options)),
96
+ reload: lock(catchNavigationError(intent.reload)),
97
+ }
98
+
99
+ return navigation
100
+ }),
101
+ )
102
+ }
@@ -0,0 +1,192 @@
1
+ import * as Brand from '@effect/data/Brand'
2
+ import { Option } from '@effect/data/Option'
3
+ import * as Cause from '@effect/io/Cause'
4
+ import * as Effect from '@effect/io/Effect'
5
+ import * as Scope from '@effect/io/Scope'
6
+ import * as Context from '@typed/context'
7
+ import * as Fx from '@typed/fx'
8
+
9
+ export interface Navigation {
10
+ /**
11
+ * Base path for all navigation entries.
12
+ */
13
+ readonly base: string
14
+
15
+ /**
16
+ * The list of navigation entries that are currently kept in-memory and
17
+ * saved within Local/Session Storage.
18
+ */
19
+ readonly entries: Fx.Computed<never, never, readonly Destination[]>
20
+
21
+ /**
22
+ * The currently focused navigation entry.
23
+ */
24
+ readonly currentEntry: Fx.Computed<never, never, Destination>
25
+
26
+ /**
27
+ * Navigate to a new URL. NavigateOptions can be used to control how the
28
+ * navigation is handled via history.pushState or history.replaceState,
29
+ * set/update the state of the navigation entry, or provide a key to use
30
+ * for the navigation entry.
31
+ */
32
+ readonly navigate: (
33
+ url: string,
34
+ options?: NavigateOptions,
35
+ ) => Effect.Effect<never, never, Destination>
36
+
37
+ /**
38
+ * Subscribe to navigation events. Any handler can cancel the or redirect
39
+ * the navigation by failing with a CancelNavigation or RedirectNavigation
40
+ * error.
41
+ */
42
+ readonly onNavigation: <R>(
43
+ handler: (event: NavigationEvent) => Effect.Effect<R, NavigationError, void>,
44
+ options?: OnNavigationOptions,
45
+ ) => Effect.Effect<R | Scope.Scope, never, void>
46
+
47
+ /**
48
+ * Subscribe to navigation events after they have been commited.
49
+ */
50
+ readonly onNavigationEnd: <R>(
51
+ handler: (event: NavigationEvent) => Effect.Effect<R, never, void>,
52
+ options?: OnNavigationOptions,
53
+ ) => Effect.Effect<R | Scope.Scope, never, void>
54
+
55
+ /**
56
+ * Returns true if there is a previous navigation entry to navigate to.
57
+ */
58
+ readonly canGoBack: Fx.Computed<never, never, boolean>
59
+
60
+ /**
61
+ * Navigate to the previous navigation entry. If you're on the first entry
62
+ * then this will do nothing.
63
+ */
64
+ readonly back: Effect.Effect<never, never, Destination>
65
+
66
+ /**
67
+ * Returns true if there is a next navigation entry to navigate to after you have gone back.
68
+ */
69
+ readonly canGoForward: Fx.Computed<never, never, boolean>
70
+
71
+ /**
72
+ * Navigate to the next navigation entry. If you're on the last entry then
73
+ * this will do nothing.
74
+ */
75
+ readonly forward: Effect.Effect<never, never, Destination>
76
+
77
+ /**
78
+ * Reload the current navigation entry.
79
+ */
80
+ readonly reload: Effect.Effect<never, never, Destination>
81
+
82
+ /**
83
+ * Navigate to a specific navigation entry by key. If the key does not
84
+ * exist then this will do nothing visible to the user and return Option.none().
85
+ */
86
+ readonly goTo: (key: DestinationKey) => Effect.Effect<never, never, Option<Destination>>
87
+ }
88
+
89
+ export const Navigation = Context.Tag<Navigation>('Navigation')
90
+
91
+ export const navigate = (url: string, options?: NavigateOptions) =>
92
+ Navigation.withEffect((n) => n.navigate(url, options))
93
+
94
+ export const onNavigation = <R>(
95
+ handler: (event: NavigationEvent) => Effect.Effect<R, never, void>,
96
+ ) => Navigation.withEffect((n) => n.onNavigation(handler))
97
+
98
+ export const canGoBack: Effect.Effect<Navigation, Cause.NoSuchElementException, boolean> &
99
+ Fx.Fx<Navigation, never, boolean> = Object.assign(
100
+ Navigation.withEffect((n) => n.canGoBack),
101
+ Navigation.withFx((n) => n.canGoBack),
102
+ )
103
+
104
+ export const back = Navigation.withEffect((n) => n.back)
105
+
106
+ export const canGoForward: Effect.Effect<Navigation, Cause.NoSuchElementException, boolean> &
107
+ Fx.Fx<Navigation, never, boolean> = Object.assign(
108
+ Navigation.withEffect((n) => n.canGoForward),
109
+ Navigation.withFx((n) => n.canGoForward),
110
+ )
111
+
112
+ export const forward = Navigation.withEffect((n) => n.forward)
113
+
114
+ export const reload = Navigation.withEffect((n) => n.reload)
115
+
116
+ export interface NavigateOptions {
117
+ // State to save to history
118
+ readonly state?: unknown
119
+ // History type
120
+ readonly history?: 'push' | 'replace'
121
+ // Key to use for Navigation entry
122
+ readonly key?: string
123
+ }
124
+
125
+ export interface NavigationEvent {
126
+ readonly destination: Destination
127
+ readonly hashChange: boolean
128
+ readonly navigationType: NavigationType
129
+ }
130
+
131
+ export function NavigationEvent(
132
+ destination: Destination,
133
+ hashChange: boolean,
134
+ navigationType: NavigationType,
135
+ ): NavigationEvent {
136
+ return { destination, hashChange, navigationType }
137
+ }
138
+
139
+ export interface Destination {
140
+ readonly key: DestinationKey
141
+ readonly url: URL
142
+ readonly state: unknown
143
+ }
144
+
145
+ export type DestinationKey = string & Brand.Brand<'DestinationKey'>
146
+ export const DestinationKey = Brand.nominal<DestinationKey>()
147
+
148
+ export function Destination(key: DestinationKey, url: URL, state?: unknown): Destination {
149
+ return { key, url, state }
150
+ }
151
+
152
+ export enum NavigationType {
153
+ Push = 'push',
154
+ Reload = 'reload',
155
+ Replace = 'replace',
156
+ Back = 'back',
157
+ Forward = 'forward',
158
+ }
159
+
160
+ export type NavigationError = CancelNavigation | RedirectNavigation
161
+
162
+ export interface CancelNavigation {
163
+ readonly _tag: 'CancelNavigation'
164
+ }
165
+
166
+ export const cancelNavigation = Effect.fail<CancelNavigation>({ _tag: 'CancelNavigation' })
167
+
168
+ export function isCancelNavigation(error: NavigationError): error is CancelNavigation {
169
+ return error._tag === 'CancelNavigation'
170
+ }
171
+
172
+ export interface RedirectNavigation extends NavigateOptions {
173
+ readonly _tag: 'RedirectNavigation'
174
+ readonly url: string
175
+ }
176
+
177
+ export const redirect = (
178
+ url: string,
179
+ options: NavigateOptions = {},
180
+ ): Effect.Effect<never, RedirectNavigation, never> =>
181
+ Effect.fail({ _tag: 'RedirectNavigation', url, ...options })
182
+
183
+ export function isRedirectNavigation(error: NavigationError): error is RedirectNavigation {
184
+ return error._tag === 'RedirectNavigation'
185
+ }
186
+
187
+ export interface OnNavigationOptions {
188
+ readonly passive?: boolean
189
+ }
190
+
191
+ export const getCurrentUrl: Effect.Effect<Navigation, Cause.NoSuchElementException, URL> =
192
+ Navigation.withEffect((n) => n.currentEntry.map((d) => d.url))
@@ -0,0 +1,28 @@
1
+ import type { Window, GlobalThis } from '@typed/dom'
2
+ import * as happyDom from 'happy-dom'
3
+
4
+ export interface ServerWindowOptions {
5
+ readonly url: string
6
+
7
+ readonly innerWidth?: number
8
+ readonly innerHeight?: number
9
+ readonly settings?: {
10
+ readonly disableJavaScriptEvaluation: boolean
11
+ readonly disableJavaScriptFileLoading: boolean
12
+ readonly disableCSSFileLoading: boolean
13
+ readonly enableFileSystemHttpRequests: boolean
14
+ }
15
+ }
16
+
17
+ export function makeServerWindow(
18
+ options?: ServerWindowOptions,
19
+ ): Window & GlobalThis & Pick<InstanceType<typeof happyDom.Window>, 'happyDOM'> {
20
+ const win: Window & GlobalThis & Pick<InstanceType<typeof happyDom.Window>, 'happyDOM'> =
21
+ new happyDom.Window({
22
+ ...options,
23
+ }) as any
24
+
25
+ return win
26
+ }
27
+
28
+ export const html5Doctype = '<!DOCTYPE html>'
@@ -0,0 +1,5 @@
1
+ import * as Global from '@effect/data/Global'
2
+
3
+ const id = '@typed/navigation/ServiceId'
4
+
5
+ export const ServiceId: any = Global.globalValue(id, () => Symbol.for(id))
@@ -0,0 +1,276 @@
1
+ import { Option } from '@effect/data/Option'
2
+ import * as Cause from '@effect/io/Cause'
3
+ import * as Effect from '@effect/io/Effect'
4
+ import { History, Location } from '@typed/dom'
5
+
6
+ import type { DomNavigationOptions } from './DOM.js'
7
+ import {
8
+ Destination,
9
+ NavigateOptions,
10
+ NavigationError,
11
+ NavigationEvent,
12
+ NavigationType,
13
+ } from './Navigation.js'
14
+ import { ServiceId } from './constant.js'
15
+ import { encodeEvent } from './json.js'
16
+ import { Model } from './model.js'
17
+ import {
18
+ Notify,
19
+ NotifyEnd,
20
+ Save,
21
+ makeGoTo,
22
+ makeNotify,
23
+ makeNotifyEnd,
24
+ makeOnNavigation,
25
+ makeOnNavigationEnd,
26
+ } from './shared-intent.js'
27
+ import { saveToStorage } from './storage.js'
28
+ import { createKey, getUrl } from './util.js'
29
+
30
+ // Roughly the number of History entries in a browser anyways
31
+ const DEFAULT_MAX_ENTRIES = 50
32
+
33
+ export type DomIntent = {
34
+ readonly back: (skipHistory: boolean) => ReturnType<ReturnType<typeof makeGo>>
35
+
36
+ readonly forward: (skipHistory: boolean) => ReturnType<ReturnType<typeof makeGo>>
37
+
38
+ readonly push: ReturnType<typeof makePush>
39
+
40
+ readonly replace: ReturnType<typeof makeReplace>
41
+
42
+ readonly navigate: (
43
+ url: string,
44
+ options?: NavigateOptions,
45
+ ) => ReturnType<ReturnType<typeof makePush | typeof makeReplace>>
46
+
47
+ readonly notify: Notify
48
+
49
+ readonly go: ReturnType<typeof makeGo>
50
+
51
+ readonly goTo: (
52
+ key: string,
53
+ ) => Effect.Effect<
54
+ Storage | History,
55
+ Cause.NoSuchElementException | NavigationError,
56
+ Option<Destination>
57
+ >
58
+
59
+ readonly reload: ReturnType<typeof makeReload>
60
+
61
+ readonly onNavigation: ReturnType<typeof makeOnNavigation>
62
+
63
+ readonly onNavigationEnd: ReturnType<typeof makeOnNavigationEnd>
64
+ }
65
+
66
+ export const makeIntent = (
67
+ model: Model,
68
+ base: string,
69
+ options: DomNavigationOptions,
70
+ ): DomIntent => {
71
+ const maxEntries = Math.abs(options.maxEntries ?? DEFAULT_MAX_ENTRIES)
72
+ const notify = makeNotify(model)
73
+ const notifyEnd = makeNotifyEnd(model)
74
+ const save = makeSave(model)
75
+ const go = makeGo(model, notify, notifyEnd, save)
76
+ const replace = makeReplace(model, notify, notifyEnd, save, base)
77
+ const push = makePush(model, notify, notifyEnd, save, base, maxEntries)
78
+
79
+ return {
80
+ back: (skipHistory: boolean) => go(-1, skipHistory),
81
+ forward: (skipHistory: boolean) => go(1, skipHistory),
82
+ push,
83
+ replace,
84
+ navigate: (url: string, options: NavigateOptions = {}) =>
85
+ options.history === 'replace' ? replace(url, options) : push(url, options),
86
+ notify,
87
+ go: go,
88
+ goTo: makeGoTo(model, go),
89
+ reload: makeReload(model, notify, save),
90
+ onNavigation: makeOnNavigation(model),
91
+ onNavigationEnd: makeOnNavigationEnd(model),
92
+ } as const
93
+ }
94
+
95
+ export type Intent = ReturnType<typeof makeIntent>
96
+
97
+ export const makeSave =
98
+ (model: Model) =>
99
+ (event: NavigationEvent): Effect.Effect<Storage, Cause.NoSuchElementException, void> =>
100
+ Effect.gen(function* ($) {
101
+ const events = yield* $(model.events)
102
+ const index = yield* $(model.index)
103
+
104
+ // Save to storage
105
+ yield* $(saveToStorage(events, index))
106
+
107
+ // Update current entry
108
+ yield* $(model.currentEntry.set(event.destination))
109
+
110
+ // Update canGoBack
111
+ yield* $(model.canGoBack.set(index > 0))
112
+
113
+ // Update canGoForward
114
+ yield* $(model.canGoForward.set(index < events.length - 1))
115
+ })
116
+
117
+ export const makeReload = (model: Model, notify: Notify, save: Save<Storage>) =>
118
+ Effect.gen(function* ($) {
119
+ const i = yield* $(model.index.get)
120
+ const e = yield* $(model.events)
121
+ const event = e[i]
122
+ const reloadEvent = { ...event, navigationType: NavigationType.Reload }
123
+
124
+ yield* $(notify(reloadEvent))
125
+ yield* $(save(reloadEvent))
126
+
127
+ const location = yield* $(Location)
128
+
129
+ location.reload()
130
+
131
+ return event.destination
132
+ })
133
+
134
+ export const makeReplace =
135
+ (model: Model, notify: Notify, notifyEnd: NotifyEnd, save: Save<Storage>, base: string) =>
136
+ (url: string, options: NavigateOptions = {}, skipHistory = false) =>
137
+ Effect.gen(function* ($) {
138
+ const location = yield* $(Location)
139
+ const entry = yield* $(model.currentEntry.get)
140
+ const destination: Destination = {
141
+ key: entry.key,
142
+ url: getUrl(url, base, location.origin),
143
+ state: options.state,
144
+ }
145
+ const event: NavigationEvent = {
146
+ destination,
147
+ hashChange: entry.url.hash !== destination.url.hash,
148
+ navigationType: NavigationType.Replace,
149
+ }
150
+
151
+ yield* $(notify(event))
152
+
153
+ if (!skipHistory) {
154
+ const history = yield* $(History)
155
+
156
+ history.replaceState.call(
157
+ ServiceId,
158
+ { state: options.state, event: encodeEvent(event) },
159
+ '',
160
+ url,
161
+ )
162
+ }
163
+
164
+ const currentIndex = yield* $(model.index)
165
+
166
+ yield* $(
167
+ model.events.update((entries) => {
168
+ const updated = entries.slice(0)
169
+ updated[currentIndex] = event
170
+ return updated
171
+ }),
172
+ )
173
+
174
+ yield* $(save(event))
175
+ yield* $(notifyEnd(event))
176
+
177
+ return destination
178
+ })
179
+
180
+ export const makePush =
181
+ (
182
+ model: Model,
183
+ notify: Notify,
184
+ notifyEnd: NotifyEnd,
185
+ save: Save<Storage>,
186
+ base: string,
187
+ maxEntries: number,
188
+ ) =>
189
+ (url: string, options: NavigateOptions = {}, skipHistory = false) =>
190
+ Effect.gen(function* ($) {
191
+ const location = yield* $(Location)
192
+ const entry = yield* $(model.currentEntry.get)
193
+ const destination: Destination = {
194
+ key: yield* $(createKey),
195
+ url: getUrl(url, base, location.origin),
196
+ state: options.state,
197
+ }
198
+ const event: NavigationEvent = {
199
+ destination,
200
+ hashChange: entry.url.hash !== destination.url.hash,
201
+ navigationType: NavigationType.Push,
202
+ }
203
+
204
+ // Notify event handlers
205
+ yield* $(notify(event))
206
+
207
+ if (!skipHistory) {
208
+ const history = yield* $(History)
209
+
210
+ history.pushState.call(
211
+ ServiceId,
212
+ { state: options.state, event: encodeEvent(event) },
213
+ '',
214
+ url,
215
+ )
216
+ }
217
+
218
+ const currentIndex = yield* $(model.index)
219
+
220
+ // Remove all entries after the current index
221
+ // and add the new destination to the end
222
+ yield* $(
223
+ model.events.update((entries) => {
224
+ const updated = entries.slice(0, currentIndex + 1)
225
+ updated.push(event)
226
+ return updated.slice(-maxEntries)
227
+ }),
228
+ )
229
+
230
+ // Update the index to the new destination
231
+ yield* $(model.index.update((i) => i + 1))
232
+
233
+ yield* $(save(event))
234
+ yield* $(notifyEnd(event))
235
+
236
+ return destination
237
+ })
238
+
239
+ export const makeGo =
240
+ (model: Model, notify: Notify, notifyEnd: NotifyEnd, save: Save<Storage>) =>
241
+ (delta: number, skipHistory = false) =>
242
+ Effect.gen(function* ($) {
243
+ const currentEntries = yield* $(model.events)
244
+ const totalEntries = currentEntries.length
245
+ const currentIndex = yield* $(model.index)
246
+
247
+ // Nothing to do here
248
+ if (delta === 0) return currentEntries[currentIndex].destination
249
+
250
+ const nextIndex =
251
+ delta > 0
252
+ ? Math.min(currentIndex + delta, totalEntries - 1)
253
+ : Math.max(currentIndex + delta, 0)
254
+ const nextEntry = currentEntries[nextIndex]
255
+
256
+ yield* $(
257
+ notify({
258
+ ...nextEntry,
259
+ navigationType: nextIndex > currentIndex ? NavigationType.Forward : NavigationType.Back,
260
+ }),
261
+ )
262
+
263
+ if (!skipHistory) {
264
+ const history = yield* $(History)
265
+
266
+ history.go.call(ServiceId, delta)
267
+ }
268
+
269
+ yield* $(model.index.set(nextIndex))
270
+
271
+ yield* $(save(nextEntry))
272
+
273
+ yield* $(notifyEnd(nextEntry))
274
+
275
+ return nextEntry.destination
276
+ })