chayns-api 3.1.0-beta.0 → 3.1.0-beta.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.
@@ -8,10 +8,11 @@ const BLOCK_TIMEOUT_MS = 30000;
8
8
  let _nextId = 1;
9
9
  class BlockRegistry {
10
10
  layerBlocks = new Map();
11
+ changeListeners = new Set();
11
12
  beforeUnloadCount = 0;
12
13
  beforeUnloadHandler = e => {
13
14
  e.preventDefault();
14
- e.returnValue = '';
15
+ Reflect.set(e, 'returnValue', '');
15
16
  };
16
17
  add(layer, callback, opts = {}) {
17
18
  var _opts$scope, _opts$isBeforeUnload;
@@ -32,16 +33,19 @@ class BlockRegistry {
32
33
  if (entry.opts.isBeforeUnload) {
33
34
  this.incrementBeforeUnload();
34
35
  }
36
+ this.notifyChange();
35
37
  return () => this.remove(layer.id, entry);
36
38
  }
37
39
  remove(layerId, entry) {
38
40
  const set = this.layerBlocks.get(layerId);
39
41
  if (!set) return;
40
- set.delete(entry);
42
+ const didDelete = set.delete(entry);
43
+ if (!didDelete) return;
41
44
  if (set.size === 0) this.layerBlocks.delete(layerId);
42
45
  if (entry.opts.isBeforeUnload) {
43
46
  this.decrementBeforeUnload();
44
47
  }
48
+ this.notifyChange();
45
49
  }
46
50
  removeAllForLayer(layerId) {
47
51
  const set = this.layerBlocks.get(layerId);
@@ -50,6 +54,13 @@ class BlockRegistry {
50
54
  if (entry.opts.isBeforeUnload) this.decrementBeforeUnload();
51
55
  }
52
56
  this.layerBlocks.delete(layerId);
57
+ this.notifyChange();
58
+ }
59
+ subscribeToChanges(listener) {
60
+ this.changeListeners.add(listener);
61
+ return () => {
62
+ this.changeListeners.delete(listener);
63
+ };
53
64
  }
54
65
  collectApplicableBlocks(targetLayer) {
55
66
  const result = [];
@@ -62,6 +73,20 @@ class BlockRegistry {
62
73
  this.collectGlobalFromActiveDescendants(targetLayer, result);
63
74
  return result;
64
75
  }
76
+ hasActiveBlocks(rootLayer) {
77
+ return this.collectActiveChainBlocks(rootLayer).length > 0;
78
+ }
79
+ async checkActiveBlocks(rootLayer) {
80
+ const blocks = this.collectActiveChainBlocks(rootLayer);
81
+ if (blocks.length === 0) return true;
82
+ const results = await Promise.all(blocks.map(b => this.runBlock(b)));
83
+ return results.every(Boolean);
84
+ }
85
+ collectActiveChainBlocks(rootLayer) {
86
+ const result = [];
87
+ this.collectFromActiveChain(rootLayer, result);
88
+ return result;
89
+ }
65
90
  collectGlobalFromActiveDescendants(layer, out) {
66
91
  const activeChildId = layer.getActiveChildId();
67
92
  if (!activeChildId) return;
@@ -77,6 +102,19 @@ class BlockRegistry {
77
102
  }
78
103
  this.collectGlobalFromActiveDescendants(child, out);
79
104
  }
105
+ collectFromActiveChain(layer, out) {
106
+ const set = this.layerBlocks.get(layer.id);
107
+ if (set) {
108
+ for (const entry of set) {
109
+ out.push(entry);
110
+ }
111
+ }
112
+ const activeChildId = layer.getActiveChildId();
113
+ if (!activeChildId) return;
114
+ const child = layer.getChildLayer(activeChildId);
115
+ if (!child) return;
116
+ this.collectFromActiveChain(child, out);
117
+ }
80
118
  async checkBlocks(targetLayer) {
81
119
  const blocks = this.collectApplicableBlocks(targetLayer);
82
120
  if (blocks.length === 0) return true;
@@ -89,7 +127,7 @@ class BlockRegistry {
89
127
  resolve(false);
90
128
  }, BLOCK_TIMEOUT_MS))]);
91
129
  return result;
92
- } catch (err) {
130
+ } catch {
93
131
  return false;
94
132
  }
95
133
  }
@@ -106,5 +144,10 @@ class BlockRegistry {
106
144
  window.removeEventListener('beforeunload', this.beforeUnloadHandler);
107
145
  }
108
146
  }
147
+ notifyChange() {
148
+ for (const listener of this.changeListeners) {
149
+ listener();
150
+ }
151
+ }
109
152
  }
110
153
  exports.BlockRegistry = BlockRegistry;
@@ -60,7 +60,6 @@ class NavigationQueue {
60
60
  return this.processPopstate(op);
61
61
  default:
62
62
  {
63
- const _exhaustive = op;
64
63
  return {
65
64
  isOk: false,
66
65
  reason: 'error',
@@ -296,7 +295,7 @@ class NavigationQueue {
296
295
  changedLayerIds.add(this.deps.getRoot().id);
297
296
  }
298
297
  const target = this.resolveLowestCommonLayer(changedLayerIds);
299
- if (target) {
298
+ if (target && op.skipBlockCheck !== true) {
300
299
  const allowed = await this.deps.checkBlocks(target);
301
300
  if (!allowed) {
302
301
  await this.deps.silentGo(+1);
@@ -367,6 +366,7 @@ class NavigationQueue {
367
366
  return lca;
368
367
  }
369
368
  commit(isReplace) {
369
+ var _this$deps$onCommit, _this$deps;
370
370
  if (!(0, _window.hasWindowHistory)()) return;
371
371
  const url = this.deps.projectUrl();
372
372
  const state = this.deps.projectState();
@@ -383,6 +383,7 @@ class NavigationQueue {
383
383
  } else {
384
384
  window.history.pushState(stateWithMeta, '', url);
385
385
  }
386
+ (_this$deps$onCommit = (_this$deps = this.deps).onCommit) === null || _this$deps$onCommit === void 0 || _this$deps$onCommit.call(_this$deps);
386
387
  }
387
388
  }
388
389
  exports.NavigationQueue = NavigationQueue;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NativeBackHandler = void 0;
7
+ var _calls = require("../../calls");
8
+ var _IChaynsReact = require("../../types/IChaynsReact");
9
+ var _navigationIndex = require("./navigationIndex");
10
+ var _window = require("./window");
11
+ const DISABLE_SWIPE_BACK_GESTURE_ACTION = 249;
12
+ class NativeBackHandler {
13
+ bypassNextPopstateBlockCheck = false;
14
+ constructor(opts) {
15
+ this.opts = opts;
16
+ }
17
+ sync = () => {
18
+ if (!(0, _window.hasWindowHistory)() || !NativeBackHandler.isSupported()) {
19
+ return;
20
+ }
21
+ const next = this.shouldEnableInterception();
22
+ if (this.isInterceptionEnabled === next) {
23
+ return;
24
+ }
25
+ void (0, _calls.invokeCall)({
26
+ action: DISABLE_SWIPE_BACK_GESTURE_ACTION,
27
+ value: {
28
+ enabled: next
29
+ }
30
+ }, next ? this.handleNativeBack : undefined);
31
+ this.isInterceptionEnabled = next;
32
+ };
33
+ consumeBypassFlag() {
34
+ if (!this.bypassNextPopstateBlockCheck) return false;
35
+ this.bypassNextPopstateBlockCheck = false;
36
+ return true;
37
+ }
38
+ static isSupported() {
39
+ try {
40
+ var _getDevice$app;
41
+ return ((_getDevice$app = (0, _calls.getDevice)().app) === null || _getDevice$app === void 0 ? void 0 : _getDevice$app.flavor) === _IChaynsReact.AppFlavor.Chayns;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+ shouldEnableInterception() {
47
+ return this.opts.blockRegistry.hasActiveBlocks(this.opts.rootLayer) || (0, _navigationIndex.getCurrentIdx)() > 0;
48
+ }
49
+ handleNativeBack = () => {
50
+ void this.runNativeBack();
51
+ };
52
+ async runNativeBack() {
53
+ const isAllowed = await this.opts.blockRegistry.checkActiveBlocks(this.opts.rootLayer);
54
+ if (!isAllowed || !(0, _window.hasWindowHistory)()) {
55
+ return;
56
+ }
57
+ this.bypassNextPopstateBlockCheck = true;
58
+ window.history.back();
59
+ }
60
+ }
61
+ exports.NativeBackHandler = NativeBackHandler;
@@ -4,10 +4,14 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.consumeSilent = consumeSilent;
7
+ exports.extractHistoryIndex = extractHistoryIndex;
7
8
  exports.getCurrentIdx = getCurrentIdx;
8
9
  exports.incrementIdx = incrementIdx;
10
+ exports.setCurrentIdx = setCurrentIdx;
9
11
  exports.silentGo = silentGo;
12
+ exports.syncCurrentIdxFromState = syncCurrentIdxFromState;
10
13
  var _window = require("./window");
14
+ const CHAYNS_HISTORY_STATE_KEY = '__chaynsHistory';
11
15
  let currentIdx = 0;
12
16
  let pendingSilentCount = 0;
13
17
  let silentResolve = null;
@@ -17,6 +21,34 @@ function incrementIdx() {
17
21
  function getCurrentIdx() {
18
22
  return currentIdx;
19
23
  }
24
+ function setCurrentIdx(idx) {
25
+ if (!Number.isInteger(idx) || idx < 0) {
26
+ return;
27
+ }
28
+ currentIdx = idx;
29
+ }
30
+ function extractHistoryIndex(raw) {
31
+ if (!raw || typeof raw !== 'object') {
32
+ return null;
33
+ }
34
+ const chaynsHistory = raw[CHAYNS_HISTORY_STATE_KEY];
35
+ if (!chaynsHistory || typeof chaynsHistory !== 'object') {
36
+ return null;
37
+ }
38
+ const idx = chaynsHistory.__idx;
39
+ if (typeof idx !== 'number' || !Number.isInteger(idx) || idx < 0) {
40
+ return null;
41
+ }
42
+ return idx;
43
+ }
44
+ function syncCurrentIdxFromState(raw) {
45
+ const idx = extractHistoryIndex(raw);
46
+ if (idx === null) {
47
+ return null;
48
+ }
49
+ currentIdx = idx;
50
+ return idx;
51
+ }
20
52
  function silentGo(delta) {
21
53
  if (!(0, _window.hasWindowHistory)()) return Promise.resolve();
22
54
  return new Promise(resolve => {
@@ -17,6 +17,7 @@ var _window = require("./window");
17
17
  var _equality = require("../equality");
18
18
  var _calls = require("../../calls");
19
19
  var _segments = require("./segments");
20
+ var _nativeBackHandling = require("./nativeBackHandling");
20
21
  function getInitialPathname(overrideUrl) {
21
22
  if (overrideUrl) {
22
23
  try {
@@ -48,21 +49,30 @@ function resolveSegmentsFrom(overrideUrl, startIndex) {
48
49
  function initRootChaynsHistoryLayer(opts = {}) {
49
50
  var _opts$segmentCount;
50
51
  const blockRegistry = new _BlockRegistry.BlockRegistry();
51
- let rootLayer;
52
- let queue;
52
+ let queueRef = null;
53
53
  const deps = {
54
54
  getRoot: () => rootLayer,
55
- getQueue: () => queue,
55
+ getQueue: () => {
56
+ if (!queueRef) {
57
+ throw new Error('[chaynsHistory] NavigationQueue not initialized yet.');
58
+ }
59
+ return queueRef;
60
+ },
56
61
  getBlockRegistry: () => blockRegistry
57
62
  };
58
- rootLayer = new _HistoryLayer.ChaynsHistoryLayer({
63
+ const rootLayer = new _HistoryLayer.ChaynsHistoryLayer({
59
64
  id: 'root',
60
65
  parent: null,
61
66
  deps,
62
67
  segmentCount: (_opts$segmentCount = opts.segmentCount) !== null && _opts$segmentCount !== void 0 ? _opts$segmentCount : 0,
63
68
  segments: opts.segmentCount ? resolveInitialSegments(opts.url, opts.segmentCount) : []
64
69
  });
65
- queue = new _NavigationQueue.NavigationQueue({
70
+ const nativeBackHandler = new _nativeBackHandling.NativeBackHandler({
71
+ rootLayer,
72
+ blockRegistry
73
+ });
74
+ const syncNativeHandling = nativeBackHandler.sync;
75
+ const queue = new _NavigationQueue.NavigationQueue({
66
76
  getRoot: () => rootLayer,
67
77
  findLayer: id => (0, _layerTree.findChaynsHistoryLayerById)(rootLayer, id),
68
78
  checkBlocks: target => blockRegistry.checkBlocks(target),
@@ -80,6 +90,7 @@ function initRootChaynsHistoryLayer(opts = {}) {
80
90
  silentGo: delta => (0, _navigationIndex.silentGo)(delta),
81
91
  getCurrentIdx: () => (0, _navigationIndex.getCurrentIdx)(),
82
92
  incrementIdx: () => (0, _navigationIndex.incrementIdx)(),
93
+ onCommit: syncNativeHandling,
83
94
  applyUrlSegments: () => {
84
95
  if (!(0, _window.hasWindowHistory)()) return {
85
96
  changedLayerIds: new Set()
@@ -103,7 +114,9 @@ function initRootChaynsHistoryLayer(opts = {}) {
103
114
  };
104
115
  }
105
116
  });
117
+ queueRef = queue;
106
118
  const existingState = (0, _window.hasWindowHistory)() ? window.history.state : null;
119
+ (0, _navigationIndex.syncCurrentIdxFromState)(existingState);
107
120
  if (!(0, _stateProjector.hasChaynsHistoryState)(existingState)) {
108
121
  var _opts$segmentCount2, _opts$url;
109
122
  const segmentCount = (_opts$segmentCount2 = opts.segmentCount) !== null && _opts$segmentCount2 !== void 0 ? _opts$segmentCount2 : 0;
@@ -137,7 +150,7 @@ function initRootChaynsHistoryLayer(opts = {}) {
137
150
  };
138
151
  delete foreign.__chaynsHistory;
139
152
  const initialState = (0, _stateProjector.projectToState)(rootLayer, foreign);
140
- const idx = (0, _navigationIndex.incrementIdx)();
153
+ const idx = (0, _navigationIndex.getCurrentIdx)();
141
154
  window.history.replaceState({
142
155
  ...initialState,
143
156
  __chaynsHistory: {
@@ -147,17 +160,42 @@ function initRootChaynsHistoryLayer(opts = {}) {
147
160
  }, '', window.location.href);
148
161
  } else {
149
162
  (0, _stateProjector.applyStateToTree)(rootLayer, existing);
163
+ if ((0, _navigationIndex.syncCurrentIdxFromState)(existing) === null) {
164
+ const foreign = {
165
+ ...(existing !== null && existing !== void 0 ? existing : {})
166
+ };
167
+ delete foreign.__chaynsHistory;
168
+ const currentState = (0, _stateProjector.projectToState)(rootLayer, foreign);
169
+ const idx = (0, _navigationIndex.getCurrentIdx)();
170
+ window.history.replaceState({
171
+ ...currentState,
172
+ __chaynsHistory: {
173
+ ...currentState.__chaynsHistory,
174
+ __idx: idx
175
+ }
176
+ }, '', window.location.href);
177
+ }
150
178
  }
179
+ blockRegistry.subscribeToChanges(syncNativeHandling);
151
180
  window.addEventListener('popstate', event => {
152
- if ((0, _navigationIndex.consumeSilent)()) return;
181
+ (0, _navigationIndex.syncCurrentIdxFromState)(event.state);
182
+ if ((0, _navigationIndex.consumeSilent)()) {
183
+ syncNativeHandling();
184
+ return;
185
+ }
153
186
  const raw = event.state;
154
- if (!(0, _stateProjector.hasChaynsHistoryState)(raw)) {} else {
187
+ if (!(0, _stateProjector.hasChaynsHistoryState)(raw)) {
188
+ syncNativeHandling();
189
+ } else {
190
+ const skipBlockCheck = nativeBackHandler.consumeBypassFlag();
155
191
  void queue.enqueue({
156
192
  kind: 'popstate',
157
- rawState: raw
158
- });
193
+ rawState: raw,
194
+ skipBlockCheck
195
+ }).finally(syncNativeHandling);
159
196
  }
160
197
  });
198
+ syncNativeHandling();
161
199
  }
162
200
  return {
163
201
  rootLayer
@@ -6,10 +6,11 @@ let _nextId = 1;
6
6
  export class BlockRegistry {
7
7
  constructor() {
8
8
  _defineProperty(this, "layerBlocks", new Map());
9
+ _defineProperty(this, "changeListeners", new Set());
9
10
  _defineProperty(this, "beforeUnloadCount", 0);
10
11
  _defineProperty(this, "beforeUnloadHandler", e => {
11
12
  e.preventDefault();
12
- e.returnValue = '';
13
+ Reflect.set(e, 'returnValue', '');
13
14
  });
14
15
  }
15
16
  add(layer, callback, opts = {}) {
@@ -31,16 +32,19 @@ export class BlockRegistry {
31
32
  if (entry.opts.isBeforeUnload) {
32
33
  this.incrementBeforeUnload();
33
34
  }
35
+ this.notifyChange();
34
36
  return () => this.remove(layer.id, entry);
35
37
  }
36
38
  remove(layerId, entry) {
37
39
  const set = this.layerBlocks.get(layerId);
38
40
  if (!set) return;
39
- set.delete(entry);
41
+ const didDelete = set.delete(entry);
42
+ if (!didDelete) return;
40
43
  if (set.size === 0) this.layerBlocks.delete(layerId);
41
44
  if (entry.opts.isBeforeUnload) {
42
45
  this.decrementBeforeUnload();
43
46
  }
47
+ this.notifyChange();
44
48
  }
45
49
  removeAllForLayer(layerId) {
46
50
  const set = this.layerBlocks.get(layerId);
@@ -49,6 +53,13 @@ export class BlockRegistry {
49
53
  if (entry.opts.isBeforeUnload) this.decrementBeforeUnload();
50
54
  }
51
55
  this.layerBlocks.delete(layerId);
56
+ this.notifyChange();
57
+ }
58
+ subscribeToChanges(listener) {
59
+ this.changeListeners.add(listener);
60
+ return () => {
61
+ this.changeListeners.delete(listener);
62
+ };
52
63
  }
53
64
  collectApplicableBlocks(targetLayer) {
54
65
  const result = [];
@@ -61,6 +72,20 @@ export class BlockRegistry {
61
72
  this.collectGlobalFromActiveDescendants(targetLayer, result);
62
73
  return result;
63
74
  }
75
+ hasActiveBlocks(rootLayer) {
76
+ return this.collectActiveChainBlocks(rootLayer).length > 0;
77
+ }
78
+ async checkActiveBlocks(rootLayer) {
79
+ const blocks = this.collectActiveChainBlocks(rootLayer);
80
+ if (blocks.length === 0) return true;
81
+ const results = await Promise.all(blocks.map(b => this.runBlock(b)));
82
+ return results.every(Boolean);
83
+ }
84
+ collectActiveChainBlocks(rootLayer) {
85
+ const result = [];
86
+ this.collectFromActiveChain(rootLayer, result);
87
+ return result;
88
+ }
64
89
  collectGlobalFromActiveDescendants(layer, out) {
65
90
  const activeChildId = layer.getActiveChildId();
66
91
  if (!activeChildId) return;
@@ -76,6 +101,19 @@ export class BlockRegistry {
76
101
  }
77
102
  this.collectGlobalFromActiveDescendants(child, out);
78
103
  }
104
+ collectFromActiveChain(layer, out) {
105
+ const set = this.layerBlocks.get(layer.id);
106
+ if (set) {
107
+ for (const entry of set) {
108
+ out.push(entry);
109
+ }
110
+ }
111
+ const activeChildId = layer.getActiveChildId();
112
+ if (!activeChildId) return;
113
+ const child = layer.getChildLayer(activeChildId);
114
+ if (!child) return;
115
+ this.collectFromActiveChain(child, out);
116
+ }
79
117
  async checkBlocks(targetLayer) {
80
118
  const blocks = this.collectApplicableBlocks(targetLayer);
81
119
  if (blocks.length === 0) return true;
@@ -88,7 +126,7 @@ export class BlockRegistry {
88
126
  resolve(false);
89
127
  }, BLOCK_TIMEOUT_MS))]);
90
128
  return result;
91
- } catch (err) {
129
+ } catch {
92
130
  return false;
93
131
  }
94
132
  }
@@ -105,4 +143,9 @@ export class BlockRegistry {
105
143
  window.removeEventListener('beforeunload', this.beforeUnloadHandler);
106
144
  }
107
145
  }
146
+ notifyChange() {
147
+ for (const listener of this.changeListeners) {
148
+ listener();
149
+ }
150
+ }
108
151
  }
@@ -58,7 +58,6 @@ export class NavigationQueue {
58
58
  return this.processPopstate(op);
59
59
  default:
60
60
  {
61
- const _exhaustive = op;
62
61
  return {
63
62
  isOk: false,
64
63
  reason: 'error',
@@ -294,7 +293,7 @@ export class NavigationQueue {
294
293
  changedLayerIds.add(this.deps.getRoot().id);
295
294
  }
296
295
  const target = this.resolveLowestCommonLayer(changedLayerIds);
297
- if (target) {
296
+ if (target && op.skipBlockCheck !== true) {
298
297
  const allowed = await this.deps.checkBlocks(target);
299
298
  if (!allowed) {
300
299
  await this.deps.silentGo(+1);
@@ -365,6 +364,7 @@ export class NavigationQueue {
365
364
  return lca;
366
365
  }
367
366
  commit(isReplace) {
367
+ var _this$deps$onCommit, _this$deps;
368
368
  if (!hasWindowHistory()) return;
369
369
  const url = this.deps.projectUrl();
370
370
  const state = this.deps.projectState();
@@ -381,5 +381,6 @@ export class NavigationQueue {
381
381
  } else {
382
382
  window.history.pushState(stateWithMeta, '', url);
383
383
  }
384
+ (_this$deps$onCommit = (_this$deps = this.deps).onCommit) === null || _this$deps$onCommit === void 0 || _this$deps$onCommit.call(_this$deps);
384
385
  }
385
386
  }
@@ -0,0 +1,59 @@
1
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
2
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
3
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
4
+ import { getDevice, invokeCall } from '../../calls';
5
+ import { AppFlavor } from '../../types/IChaynsReact';
6
+ import { getCurrentIdx } from './navigationIndex';
7
+ import { hasWindowHistory } from './window';
8
+ const DISABLE_SWIPE_BACK_GESTURE_ACTION = 249;
9
+ export class NativeBackHandler {
10
+ constructor(opts) {
11
+ _defineProperty(this, "opts", void 0);
12
+ _defineProperty(this, "isInterceptionEnabled", void 0);
13
+ _defineProperty(this, "bypassNextPopstateBlockCheck", false);
14
+ _defineProperty(this, "sync", () => {
15
+ if (!hasWindowHistory() || !NativeBackHandler.isSupported()) {
16
+ return;
17
+ }
18
+ const next = this.shouldEnableInterception();
19
+ if (this.isInterceptionEnabled === next) {
20
+ return;
21
+ }
22
+ void invokeCall({
23
+ action: DISABLE_SWIPE_BACK_GESTURE_ACTION,
24
+ value: {
25
+ enabled: next
26
+ }
27
+ }, next ? this.handleNativeBack : undefined);
28
+ this.isInterceptionEnabled = next;
29
+ });
30
+ _defineProperty(this, "handleNativeBack", () => {
31
+ void this.runNativeBack();
32
+ });
33
+ this.opts = opts;
34
+ }
35
+ consumeBypassFlag() {
36
+ if (!this.bypassNextPopstateBlockCheck) return false;
37
+ this.bypassNextPopstateBlockCheck = false;
38
+ return true;
39
+ }
40
+ static isSupported() {
41
+ try {
42
+ var _getDevice$app;
43
+ return ((_getDevice$app = getDevice().app) === null || _getDevice$app === void 0 ? void 0 : _getDevice$app.flavor) === AppFlavor.Chayns;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ shouldEnableInterception() {
49
+ return this.opts.blockRegistry.hasActiveBlocks(this.opts.rootLayer) || getCurrentIdx() > 0;
50
+ }
51
+ async runNativeBack() {
52
+ const isAllowed = await this.opts.blockRegistry.checkActiveBlocks(this.opts.rootLayer);
53
+ if (!isAllowed || !hasWindowHistory()) {
54
+ return;
55
+ }
56
+ this.bypassNextPopstateBlockCheck = true;
57
+ window.history.back();
58
+ }
59
+ }
@@ -1,4 +1,5 @@
1
1
  import { hasWindowHistory } from './window';
2
+ const CHAYNS_HISTORY_STATE_KEY = '__chaynsHistory';
2
3
  let currentIdx = 0;
3
4
  let pendingSilentCount = 0;
4
5
  let silentResolve = null;
@@ -8,6 +9,34 @@ export function incrementIdx() {
8
9
  export function getCurrentIdx() {
9
10
  return currentIdx;
10
11
  }
12
+ export function setCurrentIdx(idx) {
13
+ if (!Number.isInteger(idx) || idx < 0) {
14
+ return;
15
+ }
16
+ currentIdx = idx;
17
+ }
18
+ export function extractHistoryIndex(raw) {
19
+ if (!raw || typeof raw !== 'object') {
20
+ return null;
21
+ }
22
+ const chaynsHistory = raw[CHAYNS_HISTORY_STATE_KEY];
23
+ if (!chaynsHistory || typeof chaynsHistory !== 'object') {
24
+ return null;
25
+ }
26
+ const idx = chaynsHistory.__idx;
27
+ if (typeof idx !== 'number' || !Number.isInteger(idx) || idx < 0) {
28
+ return null;
29
+ }
30
+ return idx;
31
+ }
32
+ export function syncCurrentIdxFromState(raw) {
33
+ const idx = extractHistoryIndex(raw);
34
+ if (idx === null) {
35
+ return null;
36
+ }
37
+ currentIdx = idx;
38
+ return idx;
39
+ }
11
40
  export function silentGo(delta) {
12
41
  if (!hasWindowHistory()) return Promise.resolve();
13
42
  return new Promise(resolve => {
@@ -4,11 +4,12 @@ import { BlockRegistry } from './BlockRegistry';
4
4
  import { findChaynsHistoryLayerById } from './layerTree';
5
5
  import { projectToUrl, parseFromUrl } from './url';
6
6
  import { projectToState, applyStateToTree, diffIncomingState, hasChaynsHistoryState } from './stateProjector';
7
- import { silentGo, consumeSilent, incrementIdx, getCurrentIdx } from './navigationIndex';
7
+ import { silentGo, consumeSilent, incrementIdx, getCurrentIdx, syncCurrentIdxFromState } from './navigationIndex';
8
8
  import { hasWindowHistory } from './window';
9
9
  import { shallowEqualArr } from '../equality';
10
- import { getSite } from "../../calls";
10
+ import { getSite } from '../../calls';
11
11
  import { normalizeHistorySegments } from './segments';
12
+ import { NativeBackHandler } from './nativeBackHandling';
12
13
  function getInitialPathname(overrideUrl) {
13
14
  if (overrideUrl) {
14
15
  try {
@@ -40,21 +41,30 @@ export function resolveSegmentsFrom(overrideUrl, startIndex) {
40
41
  export function initRootChaynsHistoryLayer(opts = {}) {
41
42
  var _opts$segmentCount;
42
43
  const blockRegistry = new BlockRegistry();
43
- let rootLayer;
44
- let queue;
44
+ let queueRef = null;
45
45
  const deps = {
46
46
  getRoot: () => rootLayer,
47
- getQueue: () => queue,
47
+ getQueue: () => {
48
+ if (!queueRef) {
49
+ throw new Error('[chaynsHistory] NavigationQueue not initialized yet.');
50
+ }
51
+ return queueRef;
52
+ },
48
53
  getBlockRegistry: () => blockRegistry
49
54
  };
50
- rootLayer = new ChaynsHistoryLayer({
55
+ const rootLayer = new ChaynsHistoryLayer({
51
56
  id: 'root',
52
57
  parent: null,
53
58
  deps,
54
59
  segmentCount: (_opts$segmentCount = opts.segmentCount) !== null && _opts$segmentCount !== void 0 ? _opts$segmentCount : 0,
55
60
  segments: opts.segmentCount ? resolveInitialSegments(opts.url, opts.segmentCount) : []
56
61
  });
57
- queue = new NavigationQueue({
62
+ const nativeBackHandler = new NativeBackHandler({
63
+ rootLayer,
64
+ blockRegistry
65
+ });
66
+ const syncNativeHandling = nativeBackHandler.sync;
67
+ const queue = new NavigationQueue({
58
68
  getRoot: () => rootLayer,
59
69
  findLayer: id => findChaynsHistoryLayerById(rootLayer, id),
60
70
  checkBlocks: target => blockRegistry.checkBlocks(target),
@@ -72,6 +82,7 @@ export function initRootChaynsHistoryLayer(opts = {}) {
72
82
  silentGo: delta => silentGo(delta),
73
83
  getCurrentIdx: () => getCurrentIdx(),
74
84
  incrementIdx: () => incrementIdx(),
85
+ onCommit: syncNativeHandling,
75
86
  applyUrlSegments: () => {
76
87
  if (!hasWindowHistory()) return {
77
88
  changedLayerIds: new Set()
@@ -95,7 +106,9 @@ export function initRootChaynsHistoryLayer(opts = {}) {
95
106
  };
96
107
  }
97
108
  });
109
+ queueRef = queue;
98
110
  const existingState = hasWindowHistory() ? window.history.state : null;
111
+ syncCurrentIdxFromState(existingState);
99
112
  if (!hasChaynsHistoryState(existingState)) {
100
113
  var _opts$segmentCount2, _opts$url;
101
114
  const segmentCount = (_opts$segmentCount2 = opts.segmentCount) !== null && _opts$segmentCount2 !== void 0 ? _opts$segmentCount2 : 0;
@@ -129,7 +142,7 @@ export function initRootChaynsHistoryLayer(opts = {}) {
129
142
  };
130
143
  delete foreign.__chaynsHistory;
131
144
  const initialState = projectToState(rootLayer, foreign);
132
- const idx = incrementIdx();
145
+ const idx = getCurrentIdx();
133
146
  window.history.replaceState({
134
147
  ...initialState,
135
148
  __chaynsHistory: {
@@ -139,17 +152,42 @@ export function initRootChaynsHistoryLayer(opts = {}) {
139
152
  }, '', window.location.href);
140
153
  } else {
141
154
  applyStateToTree(rootLayer, existing);
155
+ if (syncCurrentIdxFromState(existing) === null) {
156
+ const foreign = {
157
+ ...(existing !== null && existing !== void 0 ? existing : {})
158
+ };
159
+ delete foreign.__chaynsHistory;
160
+ const currentState = projectToState(rootLayer, foreign);
161
+ const idx = getCurrentIdx();
162
+ window.history.replaceState({
163
+ ...currentState,
164
+ __chaynsHistory: {
165
+ ...currentState.__chaynsHistory,
166
+ __idx: idx
167
+ }
168
+ }, '', window.location.href);
169
+ }
142
170
  }
171
+ blockRegistry.subscribeToChanges(syncNativeHandling);
143
172
  window.addEventListener('popstate', event => {
144
- if (consumeSilent()) return;
173
+ syncCurrentIdxFromState(event.state);
174
+ if (consumeSilent()) {
175
+ syncNativeHandling();
176
+ return;
177
+ }
145
178
  const raw = event.state;
146
- if (!hasChaynsHistoryState(raw)) {} else {
179
+ if (!hasChaynsHistoryState(raw)) {
180
+ syncNativeHandling();
181
+ } else {
182
+ const skipBlockCheck = nativeBackHandler.consumeBypassFlag();
147
183
  void queue.enqueue({
148
184
  kind: 'popstate',
149
- rawState: raw
150
- });
185
+ rawState: raw,
186
+ skipBlockCheck
187
+ }).finally(syncNativeHandling);
151
188
  }
152
189
  });
190
+ syncNativeHandling();
153
191
  }
154
192
  return {
155
193
  rootLayer
@@ -8,6 +8,7 @@ interface BlockEntry {
8
8
  export declare class BlockRegistry {
9
9
  /** Per-layer block list. */
10
10
  private readonly layerBlocks;
11
+ private readonly changeListeners;
11
12
  /** Number of blocks with `isBeforeUnload: true`. When > 0, listener is attached. */
12
13
  private beforeUnloadCount;
13
14
  private readonly beforeUnloadHandler;
@@ -15,6 +16,7 @@ export declare class BlockRegistry {
15
16
  remove(layerId: string, entry: BlockEntry): void;
16
17
  /** Remove all blocks registered for a layer (called on destroy). */
17
18
  removeAllForLayer(layerId: string): void;
19
+ subscribeToChanges(listener: () => void): () => void;
18
20
  /**
19
21
  * Collects all blocks applicable to a navigation targeting `targetLayer`.
20
22
  *
@@ -24,7 +26,11 @@ export declare class BlockRegistry {
24
26
  * **active-chain descendants** (not inactive subtrees).
25
27
  */
26
28
  collectApplicableBlocks(targetLayer: ChaynsHistoryLayer): BlockEntry[];
29
+ hasActiveBlocks(rootLayer: ChaynsHistoryLayer): boolean;
30
+ checkActiveBlocks(rootLayer: ChaynsHistoryLayer): Promise<boolean>;
31
+ private collectActiveChainBlocks;
27
32
  private collectGlobalFromActiveDescendants;
33
+ private collectFromActiveChain;
28
34
  /**
29
35
  * Runs all applicable block callbacks in parallel.
30
36
  * Returns `true` if navigation is allowed (no block), `false` otherwise.
@@ -34,5 +40,6 @@ export declare class BlockRegistry {
34
40
  private runBlock;
35
41
  private incrementBeforeUnload;
36
42
  private decrementBeforeUnload;
43
+ private notifyChange;
37
44
  }
38
45
  export {};
@@ -48,6 +48,13 @@ export type NavOp = {
48
48
  } | {
49
49
  kind: 'popstate';
50
50
  rawState: unknown;
51
+ /**
52
+ * @internal Skips the block check pipeline for this popstate. Used when
53
+ * the popstate was triggered by us right after a native-back callback
54
+ * that already ran `checkActiveBlocks` — re-running it would prompt the
55
+ * user twice.
56
+ */
57
+ skipBlockCheck?: boolean;
51
58
  };
52
59
  export type NavResult = ChaynsHistoryActionResult;
53
60
  export interface NavigationQueueDeps {
@@ -78,6 +85,8 @@ export interface NavigationQueueDeps {
78
85
  getCurrentIdx: () => number;
79
86
  /** Increments the navigation index and returns the new value. */
80
87
  incrementIdx: () => number;
88
+ /** Called after a browser history commit finished. */
89
+ onCommit?: () => void;
81
90
  /**
82
91
  * Re-parse segments from `window.location.pathname` and apply them to each
83
92
  * layer in the active chain based on its `segmentCount`. Called after
@@ -0,0 +1,47 @@
1
+ import type { ChaynsHistoryLayer } from '../../handler/history/HistoryLayer';
2
+ import type { BlockRegistry } from './BlockRegistry';
3
+ export interface NativeBackHandlerOptions {
4
+ rootLayer: ChaynsHistoryLayer;
5
+ blockRegistry: BlockRegistry;
6
+ }
7
+ /**
8
+ * Coordinates the native swipe-back gesture (Chayns app action 249) with the
9
+ * JS history queue.
10
+ *
11
+ * Why we disable the gesture as soon as the user has navigated inside the app
12
+ * (`getCurrentIdx() > 0`) and not only while a block is registered:
13
+ * The native swipe animation runs independently of our JS handling. Without
14
+ * intercepting it, a swipe-back would (a) play the native pop animation,
15
+ * (b) pop the browser entry, (c) fire popstate, and only THEN would the
16
+ * queue evaluate the block and silently push forward again — producing the
17
+ * "navigate back, then snap forward" jitter described in the bug report.
18
+ * By intercepting from the first own history entry on, every back must come
19
+ * through the registered callback where we can resolve blocks before
20
+ * mutating history.
21
+ *
22
+ * Instances are scoped to a single root layer; module-level state is avoided
23
+ * so re-inits (HMR, tests, multiple roots) cannot desynchronise.
24
+ */
25
+ export declare class NativeBackHandler {
26
+ private readonly opts;
27
+ private isInterceptionEnabled;
28
+ /**
29
+ * Set to `true` right before `history.back()` is triggered from
30
+ * {@link handleNativeBack}. Consumed by the popstate listener so the
31
+ * queue can skip the duplicate block check (we already ran it).
32
+ */
33
+ private bypassNextPopstateBlockCheck;
34
+ constructor(opts: NativeBackHandlerOptions);
35
+ /** Re-evaluates the desired native gesture state and pushes it to the app. */
36
+ sync: () => void;
37
+ /**
38
+ * Returns `true` exactly once after a {@link handleNativeBack}-initiated
39
+ * `history.back()`. The popstate listener uses this so the queue can skip
40
+ * the (already performed) block check.
41
+ */
42
+ consumeBypassFlag(): boolean;
43
+ private static isSupported;
44
+ private shouldEnableInterception;
45
+ private handleNativeBack;
46
+ private runNativeBack;
47
+ }
@@ -2,6 +2,9 @@
2
2
  export declare function incrementIdx(): number;
3
3
  /** Get current index for stamping real entries or direction comparison. */
4
4
  export declare function getCurrentIdx(): number;
5
+ export declare function setCurrentIdx(idx: number): void;
6
+ export declare function extractHistoryIndex(raw: unknown): number | null;
7
+ export declare function syncCurrentIdxFromState(raw: unknown): number | null;
5
8
  /**
6
9
  * Performs a `history.go(delta)` that is silently ignored by our popstate handler.
7
10
  * Returns a promise that resolves when the popstate for this go() fires.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chayns-api",
3
- "version": "3.1.0-beta.0",
3
+ "version": "3.1.0-beta.1",
4
4
  "description": "new chayns api",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",