@typed/navigation 0.5.3 → 0.6.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 (186) 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/constant.d.ts +0 -2
  85. package/dist/cjs/constant.d.ts.map +0 -1
  86. package/dist/cjs/constant.js +0 -30
  87. package/dist/cjs/constant.js.map +0 -1
  88. package/dist/cjs/dom-intent.d.ts +0 -28
  89. package/dist/cjs/dom-intent.d.ts.map +0 -1
  90. package/dist/cjs/dom-intent.js +0 -172
  91. package/dist/cjs/dom-intent.js.map +0 -1
  92. package/dist/cjs/history.d.ts +0 -31
  93. package/dist/cjs/history.d.ts.map +0 -1
  94. package/dist/cjs/history.js +0 -131
  95. package/dist/cjs/history.js.map +0 -1
  96. package/dist/cjs/index.d.ts +0 -4
  97. package/dist/cjs/index.d.ts.map +0 -1
  98. package/dist/cjs/json.d.ts +0 -13
  99. package/dist/cjs/json.d.ts.map +0 -1
  100. package/dist/cjs/json.js +0 -24
  101. package/dist/cjs/json.js.map +0 -1
  102. package/dist/cjs/memory-intent.d.ts +0 -27
  103. package/dist/cjs/memory-intent.d.ts.map +0 -1
  104. package/dist/cjs/memory-intent.js +0 -156
  105. package/dist/cjs/memory-intent.js.map +0 -1
  106. package/dist/cjs/model.d.ts +0 -22
  107. package/dist/cjs/model.d.ts.map +0 -1
  108. package/dist/cjs/model.js +0 -48
  109. package/dist/cjs/model.js.map +0 -1
  110. package/dist/cjs/shared-intent.d.ts +0 -14
  111. package/dist/cjs/shared-intent.d.ts.map +0 -1
  112. package/dist/cjs/shared-intent.js +0 -82
  113. package/dist/cjs/shared-intent.js.map +0 -1
  114. package/dist/cjs/storage.d.ts +0 -19
  115. package/dist/cjs/storage.d.ts.map +0 -1
  116. package/dist/cjs/storage.js +0 -101
  117. package/dist/cjs/storage.js.map +0 -1
  118. package/dist/cjs/util.d.ts +0 -5
  119. package/dist/cjs/util.d.ts.map +0 -1
  120. package/dist/cjs/util.js +0 -39
  121. package/dist/cjs/util.js.map +0 -1
  122. package/dist/constant.d.ts +0 -2
  123. package/dist/constant.d.ts.map +0 -1
  124. package/dist/constant.js +0 -4
  125. package/dist/constant.js.map +0 -1
  126. package/dist/dom-intent.d.ts +0 -28
  127. package/dist/dom-intent.d.ts.map +0 -1
  128. package/dist/dom-intent.js +0 -140
  129. package/dist/dom-intent.js.map +0 -1
  130. package/dist/history.d.ts +0 -31
  131. package/dist/history.d.ts.map +0 -1
  132. package/dist/history.js +0 -104
  133. package/dist/history.js.map +0 -1
  134. package/dist/index.d.ts +0 -4
  135. package/dist/index.d.ts.map +0 -1
  136. package/dist/index.js +0 -4
  137. package/dist/index.js.map +0 -1
  138. package/dist/intent.d.ts +0 -31
  139. package/dist/intent.d.ts.map +0 -1
  140. package/dist/intent.js +0 -157
  141. package/dist/intent.js.map +0 -1
  142. package/dist/json.d.ts +0 -13
  143. package/dist/json.d.ts.map +0 -1
  144. package/dist/json.js +0 -17
  145. package/dist/json.js.map +0 -1
  146. package/dist/memory-intent.d.ts +0 -27
  147. package/dist/memory-intent.d.ts.map +0 -1
  148. package/dist/memory-intent.js +0 -124
  149. package/dist/memory-intent.js.map +0 -1
  150. package/dist/model.d.ts +0 -22
  151. package/dist/model.d.ts.map +0 -1
  152. package/dist/model.js +0 -21
  153. package/dist/model.js.map +0 -1
  154. package/dist/shared-intent.d.ts +0 -14
  155. package/dist/shared-intent.d.ts.map +0 -1
  156. package/dist/shared-intent.js +0 -51
  157. package/dist/shared-intent.js.map +0 -1
  158. package/dist/storage.d.ts +0 -19
  159. package/dist/storage.d.ts.map +0 -1
  160. package/dist/storage.js +0 -73
  161. package/dist/storage.js.map +0 -1
  162. package/dist/tsconfig.cjs.build.tsbuildinfo +0 -1
  163. package/dist/util.d.ts +0 -5
  164. package/dist/util.d.ts.map +0 -1
  165. package/dist/util.js +0 -12
  166. package/dist/util.js.map +0 -1
  167. package/eslintrc.json +0 -3
  168. package/project.json +0 -46
  169. package/src/DOM.test.ts +0 -699
  170. package/src/DOM.ts +0 -163
  171. package/src/Memory.test.ts +0 -464
  172. package/src/Memory.ts +0 -102
  173. package/src/_makeServerWindow.ts +0 -28
  174. package/src/dom-intent.ts +0 -268
  175. package/src/history.ts +0 -165
  176. package/src/json.ts +0 -31
  177. package/src/memory-intent.ts +0 -224
  178. package/src/model.ts +0 -54
  179. package/src/shared-intent.ts +0 -117
  180. package/src/storage.ts +0 -101
  181. package/src/util.ts +0 -20
  182. package/tsconfig.build.json +0 -4
  183. package/tsconfig.build.tsbuildinfo +0 -1
  184. package/tsconfig.cjs.build.json +0 -22
  185. package/tsconfig.json +0 -27
  186. package/vite.config.mjs +0 -3
package/src/Memory.ts DELETED
@@ -1,102 +0,0 @@
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
- }
@@ -1,28 +0,0 @@
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>'
package/src/dom-intent.ts DELETED
@@ -1,268 +0,0 @@
1
- import { Option } from '@effect/data/Option'
2
- import * as Effect from '@effect/io/Effect'
3
- import { Location } from '@typed/dom'
4
-
5
- import type { DomNavigationOptions } from './DOM.js'
6
- import {
7
- Destination,
8
- NavigateOptions,
9
- NavigationError,
10
- NavigationEvent,
11
- NavigationType,
12
- } from './Navigation.js'
13
- import { Model } from './model.js'
14
- import {
15
- Notify,
16
- NotifyEnd,
17
- Save,
18
- makeGoTo,
19
- makeNotify,
20
- makeNotifyEnd,
21
- makeOnNavigation,
22
- makeOnNavigationEnd,
23
- } from './shared-intent.js'
24
- import { saveToStorage } from './storage.js'
25
- import { createKey, getUrl } from './util.js'
26
-
27
- // Roughly the number of History entries in a browser anyways
28
- const DEFAULT_MAX_ENTRIES = 50
29
-
30
- export type DomIntent = {
31
- readonly back: (skipHistory: boolean) => ReturnType<ReturnType<typeof makeGo>>
32
-
33
- readonly forward: (skipHistory: boolean) => ReturnType<ReturnType<typeof makeGo>>
34
-
35
- readonly push: ReturnType<typeof makePush>
36
-
37
- readonly replace: ReturnType<typeof makeReplace>
38
-
39
- readonly navigate: (
40
- url: string,
41
- options?: NavigateOptions,
42
- ) => ReturnType<ReturnType<typeof makePush | typeof makeReplace>>
43
-
44
- readonly notify: Notify
45
-
46
- readonly go: ReturnType<typeof makeGo>
47
-
48
- readonly goTo: (
49
- key: string,
50
- ) => Effect.Effect<Storage | History, NavigationError, Option<Destination>>
51
-
52
- readonly reload: ReturnType<typeof makeReload>
53
-
54
- readonly onNavigation: ReturnType<typeof makeOnNavigation>
55
-
56
- readonly onNavigationEnd: ReturnType<typeof makeOnNavigationEnd>
57
- }
58
-
59
- export const makeIntent = (
60
- model: Model,
61
- base: string,
62
- history: History,
63
- options: DomNavigationOptions,
64
- ): DomIntent => {
65
- const maxEntries = Math.abs(options.maxEntries ?? DEFAULT_MAX_ENTRIES)
66
- const notify = makeNotify(model)
67
- const notifyEnd = makeNotifyEnd(model)
68
- const save = makeSave(model)
69
- const go = makeGo(model, notify, notifyEnd, save, history)
70
- const replace = makeReplace(model, notify, notifyEnd, save, history, base)
71
- const push = makePush(model, notify, notifyEnd, save, base, history, maxEntries)
72
-
73
- return {
74
- back: (skipHistory: boolean) => go(-1, skipHistory),
75
- forward: (skipHistory: boolean) => go(1, skipHistory),
76
- push,
77
- replace,
78
- navigate: (url: string, options: NavigateOptions = {}) =>
79
- options.history === 'replace' ? replace(url, options) : push(url, options),
80
- notify,
81
- go: go,
82
- goTo: makeGoTo(model, go),
83
- reload: makeReload(model, notify, save),
84
- onNavigation: makeOnNavigation(model),
85
- onNavigationEnd: makeOnNavigationEnd(model),
86
- } as const
87
- }
88
-
89
- export type Intent = ReturnType<typeof makeIntent>
90
-
91
- export const makeSave =
92
- (model: Model) =>
93
- (event: NavigationEvent): Effect.Effect<Storage, never, void> =>
94
- Effect.gen(function* ($) {
95
- const events = yield* $(model.events)
96
- const index = yield* $(model.index)
97
-
98
- // Save to storage
99
- yield* $(saveToStorage(events, index))
100
-
101
- // Update current entry
102
- yield* $(model.currentEntry.set(event.destination))
103
-
104
- // Update canGoBack
105
- yield* $(model.canGoBack.set(index > 0))
106
-
107
- // Update canGoForward
108
- yield* $(model.canGoForward.set(index < events.length - 1))
109
- })
110
-
111
- export const makeReload = (model: Model, notify: Notify, save: Save<Storage>) =>
112
- Effect.gen(function* ($) {
113
- const i = yield* $(model.index.get)
114
- const e = yield* $(model.events)
115
- const event = e[i]
116
- const reloadEvent = { ...event, navigationType: NavigationType.Reload }
117
-
118
- yield* $(notify(reloadEvent))
119
- yield* $(save(reloadEvent))
120
-
121
- const location = yield* $(Location)
122
-
123
- location.reload()
124
-
125
- return event.destination
126
- })
127
-
128
- export const makeReplace =
129
- (
130
- model: Model,
131
- notify: Notify,
132
- notifyEnd: NotifyEnd,
133
- save: Save<Storage>,
134
- history: History,
135
- base: string,
136
- ) =>
137
- (url: string, options: NavigateOptions = {}, skipHistory = false) =>
138
- Effect.gen(function* ($) {
139
- const location = yield* $(Location)
140
- const entry = yield* $(model.currentEntry.get)
141
- const destination: Destination = {
142
- key: entry.key,
143
- url: getUrl(url, base, location.origin),
144
- state: options.state,
145
- }
146
- const event: NavigationEvent = {
147
- destination,
148
- hashChange: entry.url.hash !== destination.url.hash,
149
- navigationType: NavigationType.Replace,
150
- }
151
-
152
- yield* $(notify(event))
153
-
154
- if (!skipHistory) {
155
- history.replaceState({ key: destination.key, state: options.state }, '', url)
156
- }
157
-
158
- const currentIndex = yield* $(model.index)
159
-
160
- yield* $(
161
- model.events.update((entries) => {
162
- const updated = entries.slice(0)
163
- updated[currentIndex] = event
164
- return updated
165
- }),
166
- )
167
-
168
- yield* $(save(event))
169
- yield* $(notifyEnd(event))
170
-
171
- return destination
172
- })
173
-
174
- export const makePush =
175
- (
176
- model: Model,
177
- notify: Notify,
178
- notifyEnd: NotifyEnd,
179
- save: Save<Storage>,
180
- base: string,
181
- history: History,
182
- maxEntries: number,
183
- ) =>
184
- (url: string, options: NavigateOptions = {}, skipHistory = false) =>
185
- Effect.gen(function* ($) {
186
- const location = yield* $(Location)
187
- const entry = yield* $(model.currentEntry.get)
188
- const destinationUrl = getUrl(url, base, location.origin)
189
-
190
- if (entry.url.href === destinationUrl.href) {
191
- return entry
192
- }
193
-
194
- const destination: Destination = {
195
- key: yield* $(createKey),
196
- url: destinationUrl,
197
- state: options.state,
198
- }
199
- const event: NavigationEvent = {
200
- destination,
201
- hashChange: entry.url.hash !== destination.url.hash,
202
- navigationType: NavigationType.Push,
203
- }
204
-
205
- // Notify event handlers
206
- yield* $(notify(event))
207
-
208
- if (!skipHistory) {
209
- history.pushState({ key: destination.key, state: options.state }, '', url)
210
- }
211
-
212
- const currentIndex = yield* $(model.index)
213
-
214
- // Remove all entries after the current index
215
- // and add the new destination to the end
216
- yield* $(
217
- model.events.update((entries) => {
218
- const updated = entries.slice(0, currentIndex + 1)
219
- updated.push(event)
220
- return updated.slice(-maxEntries)
221
- }),
222
- )
223
-
224
- // Update the index to the new destination
225
- yield* $(model.index.update((i) => Math.min(i + 1, maxEntries)))
226
-
227
- yield* $(save(event))
228
- yield* $(notifyEnd(event))
229
-
230
- return destination
231
- })
232
-
233
- export const makeGo =
234
- (model: Model, notify: Notify, notifyEnd: NotifyEnd, save: Save<Storage>, history: History) =>
235
- (delta: number, skipHistory = false) =>
236
- Effect.gen(function* ($) {
237
- const currentEntries = yield* $(model.events)
238
- const totalEntries = currentEntries.length
239
- const currentIndex = yield* $(model.index)
240
-
241
- // Nothing to do here
242
- if (delta === 0) return currentEntries[currentIndex].destination
243
-
244
- const nextIndex =
245
- delta > 0
246
- ? Math.min(currentIndex + delta, totalEntries - 1)
247
- : Math.min(Math.max(currentIndex + delta, 0))
248
- const nextEntry = currentEntries[nextIndex]
249
-
250
- yield* $(
251
- notify({
252
- ...nextEntry,
253
- navigationType: nextIndex > currentIndex ? NavigationType.Forward : NavigationType.Back,
254
- }),
255
- )
256
-
257
- if (!skipHistory) {
258
- history.go(delta)
259
- }
260
-
261
- yield* $(model.index.set(nextIndex))
262
-
263
- yield* $(save(nextEntry))
264
-
265
- yield* $(notifyEnd(nextEntry))
266
-
267
- return nextEntry.destination
268
- })
package/src/history.ts DELETED
@@ -1,165 +0,0 @@
1
- import * as Cause from '@effect/io/Cause'
2
- import * as Effect from '@effect/io/Effect'
3
- import * as Fiber from '@effect/io/Fiber'
4
- import * as Runtime from '@effect/io/Runtime'
5
- import * as Scope from '@effect/io/Scope'
6
- import { History } from '@typed/dom'
7
- import * as Fx from '@typed/fx'
8
-
9
- import { Destination, NavigationError } from './Navigation.js'
10
- import { DomIntent } from './dom-intent.js'
11
-
12
- export type HistoryEvent = PushStateEvent | ReplaceStateEvent | GoEvent | BackEvent | ForwardEvent
13
-
14
- export interface PushStateEvent {
15
- readonly _tag: 'PushState'
16
- readonly state: unknown
17
- readonly url: string
18
- }
19
-
20
- export interface ReplaceStateEvent {
21
- readonly _tag: 'ReplaceState'
22
- readonly state: unknown
23
- readonly url: string
24
- }
25
-
26
- export interface GoEvent {
27
- readonly _tag: 'Go'
28
- readonly delta: number
29
- }
30
-
31
- export interface BackEvent {
32
- readonly _tag: 'Back'
33
- }
34
-
35
- export interface ForwardEvent {
36
- readonly _tag: 'Forward'
37
- }
38
-
39
- export const patchHistory: Effect.Effect<
40
- History | Scope.Scope,
41
- never,
42
- readonly [History, Fx.Subject<never, HistoryEvent>]
43
- > = Effect.gen(function* ($) {
44
- const history = yield* $(History)
45
-
46
- const stateDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(history), 'state')
47
-
48
- // Create a clone that always operates on the original history
49
- const clone: History = {
50
- get length() {
51
- return history.length
52
- },
53
- get scrollRestoration() {
54
- return history.scrollRestoration
55
- },
56
- set scrollRestoration(value) {
57
- history.scrollRestoration = value
58
- },
59
- get state() {
60
- const state = stateDescriptor?.get?.call(history)
61
-
62
- if (state && state.key && state.state) return state.state
63
-
64
- return state
65
- },
66
- back: history.back.bind(history),
67
- forward: history.forward.bind(history),
68
- go: history.go.bind(history),
69
- pushState: history.pushState.bind(history),
70
- replaceState: history.replaceState.bind(history),
71
- }
72
-
73
- const scope = yield* $(Effect.scope)
74
- const historyEvents = Fx.makeSubject<never, HistoryEvent>()
75
- const runtime = yield* $(Effect.runtime<never>())
76
- const runFork = Runtime.runFork(runtime)
77
- const cleanup = patchHistory_(history, (event: HistoryEvent) =>
78
- runFork(Effect.flatMap(Effect.forkIn(historyEvents.event(event), scope), Fiber.join)),
79
- )
80
-
81
- // unpatch history upon finalization
82
- yield* $(Effect.addFinalizer(() => Effect.sync(cleanup)))
83
-
84
- return [clone, historyEvents]
85
- })
86
-
87
- function patchHistory_(history: History, sendEvent: (event: HistoryEvent) => void) {
88
- const pushState = history.pushState
89
- const replaceState = history.replaceState
90
- const go = history.go
91
- const back = history.back
92
- const forward = history.forward
93
-
94
- history.pushState = function (state, _, url) {
95
- if (url) sendEvent({ _tag: 'PushState', state, url: url.toString() })
96
- }
97
-
98
- history.replaceState = function (state, _, url) {
99
- if (url) sendEvent({ _tag: 'ReplaceState', state, url: url.toString() })
100
- }
101
-
102
- history.go = function (delta) {
103
- if (!delta) return
104
-
105
- sendEvent({ _tag: 'Go', delta })
106
- }
107
-
108
- history.back = function () {
109
- sendEvent({ _tag: 'Back' })
110
- }
111
-
112
- history.forward = function () {
113
- sendEvent({ _tag: 'Forward' })
114
- }
115
-
116
- const stateDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(history), 'state')
117
-
118
- // Override state to always return the original state
119
- Object.defineProperty(history, 'state', {
120
- get() {
121
- const state = stateDescriptor?.get?.call(history)
122
-
123
- if (state && state.key && state.state) return state.state
124
-
125
- return state
126
- },
127
- })
128
-
129
- // Reset history to original state
130
- return () => {
131
- history.pushState = pushState
132
- history.replaceState = replaceState
133
- history.go = go
134
- history.back = back
135
- history.forward = forward
136
-
137
- if (stateDescriptor) {
138
- Object.defineProperty(history, 'state', stateDescriptor)
139
- }
140
- }
141
- }
142
-
143
- export function onHistoryEvent(
144
- event: HistoryEvent,
145
- intent: DomIntent,
146
- ): Effect.Effect<
147
- History | Location | Storage,
148
- Cause.NoSuchElementException | NavigationError,
149
- Destination
150
- > {
151
- return Effect.gen(function* ($) {
152
- switch (event._tag) {
153
- case 'PushState':
154
- return yield* $(intent.push(event.url, { state: event.state }, true))
155
- case 'ReplaceState':
156
- return yield* $(intent.replace(event.url, { state: event.state }, true))
157
- case 'Back':
158
- return yield* $(intent.back(true))
159
- case 'Forward':
160
- return yield* $(intent.forward(true))
161
- case 'Go':
162
- return yield* $(intent.go(event.delta, true))
163
- }
164
- })
165
- }
package/src/json.ts DELETED
@@ -1,31 +0,0 @@
1
- import { Destination, NavigationEvent } from './Navigation.js'
2
-
3
- export type NavigationEventJson = {
4
- readonly [K in keyof NavigationEvent]: NavigationEvent[K] extends Destination
5
- ? DestinationJson
6
- : NavigationEvent[K]
7
- }
8
-
9
- type DestinationJson = {
10
- readonly [K in keyof Destination]: Destination[K] extends URL ? string : Destination[K]
11
- }
12
-
13
- export const decodeDestination = (d: DestinationJson): Destination => ({
14
- ...d,
15
- url: new URL(d.url),
16
- })
17
-
18
- export const decodeNavigationEvent = (event: NavigationEventJson): NavigationEvent => ({
19
- ...event,
20
- destination: decodeDestination(event.destination),
21
- })
22
-
23
- export const encodeEvent = (event: NavigationEvent): NavigationEventJson => ({
24
- ...event,
25
- destination: encodeDestination(event.destination),
26
- })
27
-
28
- export const encodeDestination = (destination: Destination): DestinationJson => ({
29
- ...destination,
30
- url: destination.url.href,
31
- })