space-router 1.1.0 → 1.2.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.
package/dist/history.d.ts CHANGED
@@ -5,8 +5,13 @@ export interface History {
5
5
  push(url: string): void;
6
6
  replace(url: string): void;
7
7
  }
8
+ export interface ScheduleInfo {
9
+ traversal: boolean;
10
+ }
11
+ export type Schedule = (fire: () => void, info: ScheduleInfo) => void;
8
12
  export interface CreateHistoryOptions {
9
13
  mode?: Mode;
10
14
  sync?: boolean;
15
+ schedule?: Schedule;
11
16
  }
12
17
  export declare function createHistory(options?: CreateHistoryOptions): History;
package/dist/history.js CHANGED
@@ -1,8 +1,9 @@
1
1
  export function createHistory(options = {}) {
2
- const sync = !!options.sync;
2
+ const schedule = options.schedule || (options.sync ? (fire) => fire() : (fire) => queueMicrotask(fire));
3
3
  let mode = options.mode || 'history';
4
4
  let listener = null;
5
- let pending = false;
5
+ let seq = 0;
6
+ let selfNavs = 0;
6
7
  const memory = [];
7
8
  if (typeof window === 'undefined') {
8
9
  mode = 'memory';
@@ -11,16 +12,24 @@ export function createHistory(options = {}) {
11
12
  if (listener)
12
13
  listener(getUrl());
13
14
  }
14
- function schedule() {
15
- if (sync)
16
- return emit();
17
- if (pending)
18
- return;
19
- pending = true;
20
- queueMicrotask(() => {
21
- pending = false;
22
- emit();
23
- });
15
+ // each scheduled fire captures its seq: superseded fires no-op, and the
16
+ // surviving one reads the url fresh in emit() — so racing schedules of any
17
+ // mix (e.g. a deferred traversal emit overtaken by a push's microtask emit)
18
+ // coalesce into a single emit of the final url
19
+ function scheduleEmit(traversal) {
20
+ const s = ++seq;
21
+ schedule(() => {
22
+ if (s === seq)
23
+ emit();
24
+ }, { traversal });
25
+ }
26
+ function onTraversal() {
27
+ // a hashchange caused by our own push/replace is a navigation,
28
+ // not a back/forward traversal
29
+ const traversal = selfNavs === 0;
30
+ if (selfNavs > 0)
31
+ selfNavs--;
32
+ scheduleEmit(traversal);
24
33
  }
25
34
  function listen(onChange) {
26
35
  if (listener)
@@ -28,8 +37,8 @@ export function createHistory(options = {}) {
28
37
  listener = onChange;
29
38
  let off;
30
39
  if (mode !== 'memory') {
31
- off = on(window, mode === 'history' ? 'popstate' : 'hashchange', schedule);
32
- schedule();
40
+ off = on(window, mode === 'history' ? 'popstate' : 'hashchange', onTraversal);
41
+ scheduleEmit(false);
33
42
  }
34
43
  return () => {
35
44
  listener = null;
@@ -41,16 +50,18 @@ export function createHistory(options = {}) {
41
50
  url = url.replace(/^\/?#?\/?/, '/').replace(/\/$/, '') || '/';
42
51
  if (mode === 'history') {
43
52
  history[replace ? 'replaceState' : 'pushState']({}, '', url);
44
- schedule();
53
+ scheduleEmit(false);
45
54
  }
46
55
  else if (mode === 'hash') {
47
56
  // hashchange only fires when the URL actually changes; if it doesn't,
48
57
  // we schedule the emit manually so navigation stays consistent with
49
58
  // history mode (where pushState is silent and we always schedule).
50
59
  const same = url === getUrl();
60
+ if (!same)
61
+ selfNavs++;
51
62
  location[replace ? 'replace' : 'assign']('#' + url);
52
63
  if (same)
53
- schedule();
64
+ scheduleEmit(false);
54
65
  }
55
66
  else if (mode === 'memory') {
56
67
  if (replace && memory.length) {
@@ -59,7 +70,7 @@ export function createHistory(options = {}) {
59
70
  else {
60
71
  memory.push(url);
61
72
  }
62
- schedule();
73
+ scheduleEmit(false);
63
74
  }
64
75
  }
65
76
  function getUrl() {
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@ export { qs } from './qs.ts';
2
2
  export { createHistory } from './history.ts';
3
3
  export { createMatcher, createRouter, merge } from './router.ts';
4
4
  export type { Qs } from './qs.ts';
5
- export type { Mode, History, CreateHistoryOptions } from './history.ts';
5
+ export type { Mode, History, CreateHistoryOptions, Schedule, ScheduleInfo } from './history.ts';
6
6
  export type { MatcherOptions, RouteData, RouterOptions, Route, NavigateTarget, To, Redirect, RouteDefinition, Matcher, Router, } from './router.ts';
package/dist/router.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { type MatchedRoute } from './match.ts';
2
- import { type Mode } from './history.ts';
2
+ import { type Mode, type Schedule } from './history.ts';
3
3
  import { type Qs } from './qs.ts';
4
4
  export interface RouterOptions {
5
5
  mode?: Mode;
6
6
  qs?: Qs;
7
7
  sync?: boolean;
8
+ schedule?: Schedule;
8
9
  }
9
10
  export interface MatcherOptions {
10
11
  qs?: Qs;
package/dist/router.js CHANGED
@@ -6,8 +6,7 @@ const MAX_REDIRECTS = 10;
6
6
  export function createRouter(options = {}) {
7
7
  const mode = options.mode || 'history';
8
8
  const qs = options.qs || defaultQs;
9
- const sync = options.sync || false;
10
- const history = createHistory({ mode, sync });
9
+ const history = createHistory({ mode, sync: options.sync, schedule: options.schedule });
11
10
  let matcher = createMatcher([], { qs });
12
11
  const router = {
13
12
  listen(routeMap, cb) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "space-router",
3
3
  "description": "All the routing essentials.",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
package/src/history.ts CHANGED
@@ -7,16 +7,24 @@ export interface History {
7
7
  replace(url: string): void
8
8
  }
9
9
 
10
+ export interface ScheduleInfo {
11
+ traversal: boolean
12
+ }
13
+
14
+ export type Schedule = (fire: () => void, info: ScheduleInfo) => void
15
+
10
16
  export interface CreateHistoryOptions {
11
17
  mode?: Mode
12
18
  sync?: boolean
19
+ schedule?: Schedule
13
20
  }
14
21
 
15
22
  export function createHistory(options: CreateHistoryOptions = {}): History {
16
- const sync = !!options.sync
23
+ const schedule: Schedule = options.schedule || (options.sync ? (fire) => fire() : (fire) => queueMicrotask(fire))
17
24
  let mode: Mode = options.mode || 'history'
18
25
  let listener: ((url: string) => void) | null = null
19
- let pending = false
26
+ let seq = 0
27
+ let selfNavs = 0
20
28
 
21
29
  const memory: string[] = []
22
30
 
@@ -28,14 +36,26 @@ export function createHistory(options: CreateHistoryOptions = {}): History {
28
36
  if (listener) listener(getUrl())
29
37
  }
30
38
 
31
- function schedule() {
32
- if (sync) return emit()
33
- if (pending) return
34
- pending = true
35
- queueMicrotask(() => {
36
- pending = false
37
- emit()
38
- })
39
+ // each scheduled fire captures its seq: superseded fires no-op, and the
40
+ // surviving one reads the url fresh in emit() — so racing schedules of any
41
+ // mix (e.g. a deferred traversal emit overtaken by a push's microtask emit)
42
+ // coalesce into a single emit of the final url
43
+ function scheduleEmit(traversal: boolean) {
44
+ const s = ++seq
45
+ schedule(
46
+ () => {
47
+ if (s === seq) emit()
48
+ },
49
+ { traversal },
50
+ )
51
+ }
52
+
53
+ function onTraversal() {
54
+ // a hashchange caused by our own push/replace is a navigation,
55
+ // not a back/forward traversal
56
+ const traversal = selfNavs === 0
57
+ if (selfNavs > 0) selfNavs--
58
+ scheduleEmit(traversal)
39
59
  }
40
60
 
41
61
  function listen(onChange: (url: string) => void) {
@@ -43,8 +63,8 @@ export function createHistory(options: CreateHistoryOptions = {}): History {
43
63
  listener = onChange
44
64
  let off: (() => void) | undefined
45
65
  if (mode !== 'memory') {
46
- off = on(window, mode === 'history' ? 'popstate' : 'hashchange', schedule)
47
- schedule()
66
+ off = on(window, mode === 'history' ? 'popstate' : 'hashchange', onTraversal)
67
+ scheduleEmit(false)
48
68
  }
49
69
  return () => {
50
70
  listener = null
@@ -56,21 +76,22 @@ export function createHistory(options: CreateHistoryOptions = {}): History {
56
76
  url = url.replace(/^\/?#?\/?/, '/').replace(/\/$/, '') || '/'
57
77
  if (mode === 'history') {
58
78
  history[replace ? 'replaceState' : 'pushState']({}, '', url)
59
- schedule()
79
+ scheduleEmit(false)
60
80
  } else if (mode === 'hash') {
61
81
  // hashchange only fires when the URL actually changes; if it doesn't,
62
82
  // we schedule the emit manually so navigation stays consistent with
63
83
  // history mode (where pushState is silent and we always schedule).
64
84
  const same = url === getUrl()
85
+ if (!same) selfNavs++
65
86
  location[replace ? 'replace' : 'assign']('#' + url)
66
- if (same) schedule()
87
+ if (same) scheduleEmit(false)
67
88
  } else if (mode === 'memory') {
68
89
  if (replace && memory.length) {
69
90
  memory[memory.length - 1] = url
70
91
  } else {
71
92
  memory.push(url)
72
93
  }
73
- schedule()
94
+ scheduleEmit(false)
74
95
  }
75
96
  }
76
97
 
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ export { createHistory } from './history.ts'
3
3
  export { createMatcher, createRouter, merge } from './router.ts'
4
4
 
5
5
  export type { Qs } from './qs.ts'
6
- export type { Mode, History, CreateHistoryOptions } from './history.ts'
6
+ export type { Mode, History, CreateHistoryOptions, Schedule, ScheduleInfo } from './history.ts'
7
7
  export type {
8
8
  MatcherOptions,
9
9
  RouteData,
package/src/router.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { matchOne, type MatchedRoute } from './match.ts'
2
- import { createHistory, type Mode } from './history.ts'
2
+ import { createHistory, type Mode, type Schedule } from './history.ts'
3
3
  import { qs as defaultQs, type Qs } from './qs.ts'
4
4
 
5
5
  export interface RouterOptions {
6
6
  mode?: Mode
7
7
  qs?: Qs
8
8
  sync?: boolean
9
+ schedule?: Schedule
9
10
  }
10
11
 
11
12
  export interface MatcherOptions {
@@ -64,8 +65,7 @@ const MAX_REDIRECTS = 10
64
65
  export function createRouter<Data = Record<string, unknown>>(options: RouterOptions = {}): Router<Data> {
65
66
  const mode = options.mode || 'history'
66
67
  const qs = options.qs || defaultQs
67
- const sync = options.sync || false
68
- const history = createHistory({ mode, sync })
68
+ const history = createHistory({ mode, sync: options.sync, schedule: options.schedule })
69
69
 
70
70
  let matcher = createMatcher<Data>([], { qs })
71
71