@flrande/bak-extension 0.6.0 → 0.6.2
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/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +692 -266
- package/dist/content.global.js +83 -22
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/public/manifest.json +1 -1
- package/src/background.ts +728 -202
- package/src/content.ts +93 -22
- package/src/network-debugger.ts +1 -1
- package/src/session-binding-storage.ts +50 -68
- package/src/{workspace.ts → session-binding.ts} +228 -213
|
@@ -389,7 +389,7 @@
|
|
|
389
389
|
version: "1.2",
|
|
390
390
|
creator: {
|
|
391
391
|
name: "bak",
|
|
392
|
-
version: "0.6.
|
|
392
|
+
version: "0.6.1"
|
|
393
393
|
},
|
|
394
394
|
entries: entries.map((entry) => ({
|
|
395
395
|
startedDateTime: new Date(entry.startedAt ?? entry.ts).toISOString(),
|
|
@@ -465,22 +465,20 @@
|
|
|
465
465
|
|
|
466
466
|
// src/session-binding-storage.ts
|
|
467
467
|
var STORAGE_KEY_SESSION_BINDINGS = "sessionBindings";
|
|
468
|
-
|
|
469
|
-
var LEGACY_STORAGE_KEY_WORKSPACE = "agentWorkspace";
|
|
470
|
-
function isWorkspaceRecord(value) {
|
|
468
|
+
function isSessionBindingRecord(value) {
|
|
471
469
|
if (typeof value !== "object" || value === null) {
|
|
472
470
|
return false;
|
|
473
471
|
}
|
|
474
472
|
const candidate = value;
|
|
475
473
|
return typeof candidate.id === "string" && Array.isArray(candidate.tabIds) && (typeof candidate.windowId === "number" || candidate.windowId === null) && (typeof candidate.groupId === "number" || candidate.groupId === null) && (typeof candidate.activeTabId === "number" || candidate.activeTabId === null) && (typeof candidate.primaryTabId === "number" || candidate.primaryTabId === null);
|
|
476
474
|
}
|
|
477
|
-
function
|
|
475
|
+
function cloneSessionBindingRecord(state) {
|
|
478
476
|
return {
|
|
479
477
|
...state,
|
|
480
478
|
tabIds: [...state.tabIds]
|
|
481
479
|
};
|
|
482
480
|
}
|
|
483
|
-
function
|
|
481
|
+
function normalizeSessionBindingRecordMap(value) {
|
|
484
482
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
485
483
|
return {
|
|
486
484
|
found: false,
|
|
@@ -488,11 +486,11 @@
|
|
|
488
486
|
};
|
|
489
487
|
}
|
|
490
488
|
const normalizedEntries = [];
|
|
491
|
-
for (const [
|
|
492
|
-
if (!
|
|
489
|
+
for (const [bindingId, entry] of Object.entries(value)) {
|
|
490
|
+
if (!isSessionBindingRecord(entry)) {
|
|
493
491
|
continue;
|
|
494
492
|
}
|
|
495
|
-
normalizedEntries.push([
|
|
493
|
+
normalizedEntries.push([bindingId, cloneSessionBindingRecord(entry)]);
|
|
496
494
|
}
|
|
497
495
|
return {
|
|
498
496
|
found: true,
|
|
@@ -500,27 +498,17 @@
|
|
|
500
498
|
};
|
|
501
499
|
}
|
|
502
500
|
function resolveSessionBindingStateMap(stored) {
|
|
503
|
-
const current =
|
|
504
|
-
|
|
505
|
-
return current.map;
|
|
506
|
-
}
|
|
507
|
-
const legacyMap = normalizeWorkspaceRecordMap(stored[LEGACY_STORAGE_KEY_WORKSPACES]);
|
|
508
|
-
if (legacyMap.found) {
|
|
509
|
-
return legacyMap.map;
|
|
510
|
-
}
|
|
511
|
-
const legacySingle = stored[LEGACY_STORAGE_KEY_WORKSPACE];
|
|
512
|
-
if (isWorkspaceRecord(legacySingle)) {
|
|
513
|
-
return {
|
|
514
|
-
[legacySingle.id]: cloneWorkspaceRecord(legacySingle)
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
return {};
|
|
501
|
+
const current = normalizeSessionBindingRecordMap(stored[STORAGE_KEY_SESSION_BINDINGS]);
|
|
502
|
+
return current.found ? current.map : {};
|
|
518
503
|
}
|
|
519
504
|
|
|
520
|
-
// src/
|
|
521
|
-
var
|
|
522
|
-
var
|
|
523
|
-
var
|
|
505
|
+
// src/session-binding.ts
|
|
506
|
+
var DEFAULT_SESSION_BINDING_LABEL = "bak agent";
|
|
507
|
+
var DEFAULT_SESSION_BINDING_COLOR = "blue";
|
|
508
|
+
var DEFAULT_SESSION_BINDING_URL = "about:blank";
|
|
509
|
+
var WINDOW_LOOKUP_TIMEOUT_MS = 1500;
|
|
510
|
+
var GROUP_LOOKUP_TIMEOUT_MS = 1e3;
|
|
511
|
+
var WINDOW_TABS_LOOKUP_TIMEOUT_MS = 1500;
|
|
524
512
|
var SessionBindingManager = class {
|
|
525
513
|
storage;
|
|
526
514
|
browser;
|
|
@@ -528,21 +516,21 @@
|
|
|
528
516
|
this.storage = storage;
|
|
529
517
|
this.browser = browser;
|
|
530
518
|
}
|
|
531
|
-
async
|
|
532
|
-
return this.
|
|
519
|
+
async getBindingInfo(bindingId) {
|
|
520
|
+
return this.inspectBinding(bindingId);
|
|
533
521
|
}
|
|
534
|
-
async
|
|
535
|
-
const
|
|
522
|
+
async ensureBinding(options = {}) {
|
|
523
|
+
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
536
524
|
const repairActions = [];
|
|
537
|
-
const initialUrl = options.initialUrl ??
|
|
538
|
-
const persisted = await this.storage.load(
|
|
525
|
+
const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
|
|
526
|
+
const persisted = await this.storage.load(bindingId);
|
|
539
527
|
const created = !persisted;
|
|
540
|
-
let state = this.normalizeState(persisted,
|
|
528
|
+
let state = this.normalizeState(persisted, bindingId);
|
|
541
529
|
const originalWindowId = state.windowId;
|
|
542
530
|
let window2 = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
543
531
|
let tabs = [];
|
|
544
532
|
if (!window2) {
|
|
545
|
-
const rebound = await this.
|
|
533
|
+
const rebound = await this.rebindBindingWindow(state);
|
|
546
534
|
if (rebound) {
|
|
547
535
|
window2 = rebound.window;
|
|
548
536
|
tabs = rebound.tabs;
|
|
@@ -573,7 +561,7 @@
|
|
|
573
561
|
repairActions.push(created ? "created-window" : "recreated-window");
|
|
574
562
|
}
|
|
575
563
|
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
576
|
-
const recoveredTabs = await this.
|
|
564
|
+
const recoveredTabs = await this.recoverBindingTabs(state, tabs);
|
|
577
565
|
if (recoveredTabs.length > tabs.length) {
|
|
578
566
|
tabs = recoveredTabs;
|
|
579
567
|
repairActions.push("recovered-tracked-tabs");
|
|
@@ -583,9 +571,9 @@
|
|
|
583
571
|
}
|
|
584
572
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
585
573
|
if (state.windowId !== null) {
|
|
586
|
-
const ownership = await this.
|
|
574
|
+
const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
|
|
587
575
|
if (ownership.foreignTabs.length > 0) {
|
|
588
|
-
const migrated = await this.
|
|
576
|
+
const migrated = await this.moveBindingIntoDedicatedWindow(state, ownership, initialUrl);
|
|
589
577
|
window2 = migrated.window;
|
|
590
578
|
tabs = migrated.tabs;
|
|
591
579
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
@@ -593,7 +581,7 @@
|
|
|
593
581
|
}
|
|
594
582
|
}
|
|
595
583
|
if (tabs.length === 0) {
|
|
596
|
-
const primary = await this.
|
|
584
|
+
const primary = await this.createBindingTab({
|
|
597
585
|
windowId: state.windowId,
|
|
598
586
|
url: initialUrl,
|
|
599
587
|
active: true
|
|
@@ -612,7 +600,7 @@
|
|
|
612
600
|
state.activeTabId = state.primaryTabId ?? tabs[0]?.id ?? null;
|
|
613
601
|
repairActions.push("reassigned-active-tab");
|
|
614
602
|
}
|
|
615
|
-
let group = state.groupId !== null ? await this.
|
|
603
|
+
let group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
|
|
616
604
|
if (!group || group.windowId !== state.windowId) {
|
|
617
605
|
const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
|
|
618
606
|
group = await this.browser.updateGroup(groupId, {
|
|
@@ -635,7 +623,7 @@
|
|
|
635
623
|
repairActions.push("regrouped-tabs");
|
|
636
624
|
}
|
|
637
625
|
tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
638
|
-
tabs = await this.
|
|
626
|
+
tabs = await this.recoverBindingTabs(state, tabs);
|
|
639
627
|
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId) : null;
|
|
640
628
|
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
641
629
|
tabs = [...tabs, activeTab];
|
|
@@ -655,7 +643,7 @@
|
|
|
655
643
|
}
|
|
656
644
|
await this.storage.save(state);
|
|
657
645
|
return {
|
|
658
|
-
|
|
646
|
+
binding: {
|
|
659
647
|
...state,
|
|
660
648
|
tabs
|
|
661
649
|
},
|
|
@@ -665,16 +653,16 @@
|
|
|
665
653
|
};
|
|
666
654
|
}
|
|
667
655
|
async openTab(options = {}) {
|
|
668
|
-
const
|
|
669
|
-
const
|
|
670
|
-
const ensured = await this.
|
|
671
|
-
|
|
656
|
+
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
657
|
+
const hadBinding = await this.loadBindingRecord(bindingId) !== null;
|
|
658
|
+
const ensured = await this.ensureBinding({
|
|
659
|
+
bindingId,
|
|
672
660
|
focus: false,
|
|
673
|
-
initialUrl:
|
|
661
|
+
initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL
|
|
674
662
|
});
|
|
675
|
-
let state = { ...ensured.
|
|
663
|
+
let state = { ...ensured.binding, tabIds: [...ensured.binding.tabIds], tabs: [...ensured.binding.tabs] };
|
|
676
664
|
if (state.windowId !== null && state.tabs.length === 0) {
|
|
677
|
-
const rebound = await this.
|
|
665
|
+
const rebound = await this.rebindBindingWindow(state);
|
|
678
666
|
if (rebound) {
|
|
679
667
|
state.windowId = rebound.window.id;
|
|
680
668
|
state.tabs = rebound.tabs;
|
|
@@ -682,7 +670,7 @@
|
|
|
682
670
|
}
|
|
683
671
|
}
|
|
684
672
|
const active = options.active === true;
|
|
685
|
-
const desiredUrl = options.url ??
|
|
673
|
+
const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
|
|
686
674
|
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
687
675
|
state,
|
|
688
676
|
ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab") || ensured.repairActions.includes("migrated-dirty-window")
|
|
@@ -692,7 +680,7 @@
|
|
|
692
680
|
createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
|
|
693
681
|
url: desiredUrl,
|
|
694
682
|
active
|
|
695
|
-
}) : await this.
|
|
683
|
+
}) : await this.createBindingTab({
|
|
696
684
|
windowId: state.windowId,
|
|
697
685
|
url: desiredUrl,
|
|
698
686
|
active
|
|
@@ -701,17 +689,17 @@
|
|
|
701
689
|
if (!this.isMissingWindowError(error)) {
|
|
702
690
|
throw error;
|
|
703
691
|
}
|
|
704
|
-
const repaired = await this.
|
|
705
|
-
|
|
692
|
+
const repaired = await this.ensureBinding({
|
|
693
|
+
bindingId,
|
|
706
694
|
focus: false,
|
|
707
695
|
initialUrl: desiredUrl
|
|
708
696
|
});
|
|
709
|
-
state = { ...repaired.
|
|
697
|
+
state = { ...repaired.binding };
|
|
710
698
|
reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
|
|
711
699
|
createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
|
|
712
700
|
url: desiredUrl,
|
|
713
701
|
active
|
|
714
|
-
}) : await this.
|
|
702
|
+
}) : await this.createBindingTab({
|
|
715
703
|
windowId: state.windowId,
|
|
716
704
|
url: desiredUrl,
|
|
717
705
|
active
|
|
@@ -731,7 +719,7 @@
|
|
|
731
719
|
windowId: state.windowId,
|
|
732
720
|
groupId,
|
|
733
721
|
tabIds: nextTabIds,
|
|
734
|
-
activeTabId: createdTab.id,
|
|
722
|
+
activeTabId: active || options.focus === true ? createdTab.id : state.activeTabId ?? state.primaryTabId ?? createdTab.id,
|
|
735
723
|
primaryTabId: state.primaryTabId ?? createdTab.id
|
|
736
724
|
};
|
|
737
725
|
if (options.focus === true) {
|
|
@@ -742,95 +730,95 @@
|
|
|
742
730
|
const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
|
|
743
731
|
const tab = tabs.find((item) => item.id === createdTab.id) ?? createdTab;
|
|
744
732
|
return {
|
|
745
|
-
|
|
733
|
+
binding: {
|
|
746
734
|
...nextState,
|
|
747
735
|
tabs
|
|
748
736
|
},
|
|
749
737
|
tab
|
|
750
738
|
};
|
|
751
739
|
}
|
|
752
|
-
async listTabs(
|
|
753
|
-
const ensured = await this.
|
|
740
|
+
async listTabs(bindingId) {
|
|
741
|
+
const ensured = await this.inspectBinding(bindingId);
|
|
754
742
|
if (!ensured) {
|
|
755
|
-
throw new Error(`
|
|
743
|
+
throw new Error(`Binding ${bindingId} does not exist`);
|
|
756
744
|
}
|
|
757
745
|
return {
|
|
758
|
-
|
|
746
|
+
binding: ensured,
|
|
759
747
|
tabs: ensured.tabs
|
|
760
748
|
};
|
|
761
749
|
}
|
|
762
|
-
async getActiveTab(
|
|
763
|
-
const ensured = await this.
|
|
750
|
+
async getActiveTab(bindingId) {
|
|
751
|
+
const ensured = await this.inspectBinding(bindingId);
|
|
764
752
|
if (!ensured) {
|
|
765
|
-
const
|
|
753
|
+
const normalizedBindingId = this.normalizeBindingId(bindingId);
|
|
766
754
|
return {
|
|
767
|
-
|
|
768
|
-
...this.normalizeState(null,
|
|
755
|
+
binding: {
|
|
756
|
+
...this.normalizeState(null, normalizedBindingId),
|
|
769
757
|
tabs: []
|
|
770
758
|
},
|
|
771
759
|
tab: null
|
|
772
760
|
};
|
|
773
761
|
}
|
|
774
762
|
return {
|
|
775
|
-
|
|
763
|
+
binding: ensured,
|
|
776
764
|
tab: ensured.tabs.find((tab) => tab.id === ensured.activeTabId) ?? null
|
|
777
765
|
};
|
|
778
766
|
}
|
|
779
|
-
async setActiveTab(tabId,
|
|
780
|
-
const ensured = await this.
|
|
781
|
-
if (!ensured.
|
|
782
|
-
throw new Error(`Tab ${tabId} does not belong to
|
|
767
|
+
async setActiveTab(tabId, bindingId) {
|
|
768
|
+
const ensured = await this.ensureBinding({ bindingId });
|
|
769
|
+
if (!ensured.binding.tabIds.includes(tabId)) {
|
|
770
|
+
throw new Error(`Tab ${tabId} does not belong to binding ${bindingId}`);
|
|
783
771
|
}
|
|
784
772
|
const nextState = {
|
|
785
|
-
id: ensured.
|
|
786
|
-
label: ensured.
|
|
787
|
-
color: ensured.
|
|
788
|
-
windowId: ensured.
|
|
789
|
-
groupId: ensured.
|
|
790
|
-
tabIds: [...ensured.
|
|
773
|
+
id: ensured.binding.id,
|
|
774
|
+
label: ensured.binding.label,
|
|
775
|
+
color: ensured.binding.color,
|
|
776
|
+
windowId: ensured.binding.windowId,
|
|
777
|
+
groupId: ensured.binding.groupId,
|
|
778
|
+
tabIds: [...ensured.binding.tabIds],
|
|
791
779
|
activeTabId: tabId,
|
|
792
|
-
primaryTabId: ensured.
|
|
780
|
+
primaryTabId: ensured.binding.primaryTabId ?? tabId
|
|
793
781
|
};
|
|
794
782
|
await this.storage.save(nextState);
|
|
795
783
|
const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
|
|
796
784
|
const tab = tabs.find((item) => item.id === tabId);
|
|
797
785
|
if (!tab) {
|
|
798
|
-
throw new Error(`Tab ${tabId} is missing from
|
|
786
|
+
throw new Error(`Tab ${tabId} is missing from binding ${bindingId}`);
|
|
799
787
|
}
|
|
800
788
|
return {
|
|
801
|
-
|
|
789
|
+
binding: {
|
|
802
790
|
...nextState,
|
|
803
791
|
tabs
|
|
804
792
|
},
|
|
805
793
|
tab
|
|
806
794
|
};
|
|
807
795
|
}
|
|
808
|
-
async focus(
|
|
809
|
-
const ensured = await this.
|
|
810
|
-
if (ensured.
|
|
811
|
-
await this.browser.updateTab(ensured.
|
|
796
|
+
async focus(bindingId) {
|
|
797
|
+
const ensured = await this.ensureBinding({ bindingId, focus: false });
|
|
798
|
+
if (ensured.binding.activeTabId !== null) {
|
|
799
|
+
await this.browser.updateTab(ensured.binding.activeTabId, { active: true });
|
|
812
800
|
}
|
|
813
|
-
if (ensured.
|
|
814
|
-
await this.browser.updateWindow(ensured.
|
|
801
|
+
if (ensured.binding.windowId !== null) {
|
|
802
|
+
await this.browser.updateWindow(ensured.binding.windowId, { focused: true });
|
|
815
803
|
}
|
|
816
|
-
const refreshed = await this.
|
|
817
|
-
return { ok: true,
|
|
804
|
+
const refreshed = await this.ensureBinding({ bindingId, focus: false });
|
|
805
|
+
return { ok: true, binding: refreshed.binding };
|
|
818
806
|
}
|
|
819
807
|
async reset(options = {}) {
|
|
820
|
-
const
|
|
821
|
-
await this.close(
|
|
822
|
-
return this.
|
|
808
|
+
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
809
|
+
await this.close(bindingId);
|
|
810
|
+
return this.ensureBinding({
|
|
823
811
|
...options,
|
|
824
|
-
|
|
812
|
+
bindingId
|
|
825
813
|
});
|
|
826
814
|
}
|
|
827
|
-
async close(
|
|
828
|
-
const state = await this.
|
|
815
|
+
async close(bindingId) {
|
|
816
|
+
const state = await this.loadBindingRecord(bindingId);
|
|
829
817
|
if (!state) {
|
|
830
|
-
await this.storage.delete(
|
|
818
|
+
await this.storage.delete(bindingId);
|
|
831
819
|
return { ok: true };
|
|
832
820
|
}
|
|
833
|
-
await this.storage.delete(
|
|
821
|
+
await this.storage.delete(bindingId);
|
|
834
822
|
if (state.windowId !== null) {
|
|
835
823
|
const existingWindow = await this.browser.getWindow(state.windowId);
|
|
836
824
|
if (existingWindow) {
|
|
@@ -847,20 +835,20 @@
|
|
|
847
835
|
}
|
|
848
836
|
return {
|
|
849
837
|
tab: explicitTab,
|
|
850
|
-
|
|
838
|
+
binding: null,
|
|
851
839
|
resolution: "explicit-tab",
|
|
852
|
-
|
|
840
|
+
createdBinding: false,
|
|
853
841
|
repaired: false,
|
|
854
842
|
repairActions: []
|
|
855
843
|
};
|
|
856
844
|
}
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
const ensured = await this.
|
|
860
|
-
|
|
845
|
+
const explicitBindingId = typeof options.bindingId === "string" ? this.normalizeBindingId(options.bindingId) : void 0;
|
|
846
|
+
if (explicitBindingId) {
|
|
847
|
+
const ensured = await this.ensureBinding({
|
|
848
|
+
bindingId: explicitBindingId,
|
|
861
849
|
focus: false
|
|
862
850
|
});
|
|
863
|
-
return this.
|
|
851
|
+
return this.buildBindingResolution(ensured, "explicit-binding");
|
|
864
852
|
}
|
|
865
853
|
if (options.createIfMissing !== true) {
|
|
866
854
|
const activeTab = await this.browser.getActiveTab();
|
|
@@ -869,27 +857,27 @@
|
|
|
869
857
|
}
|
|
870
858
|
return {
|
|
871
859
|
tab: activeTab,
|
|
872
|
-
|
|
860
|
+
binding: null,
|
|
873
861
|
resolution: "browser-active",
|
|
874
|
-
|
|
862
|
+
createdBinding: false,
|
|
875
863
|
repaired: false,
|
|
876
864
|
repairActions: []
|
|
877
865
|
};
|
|
878
866
|
}
|
|
879
|
-
throw new Error("
|
|
867
|
+
throw new Error("bindingId is required when createIfMissing is true");
|
|
880
868
|
}
|
|
881
|
-
|
|
882
|
-
const candidate =
|
|
869
|
+
normalizeBindingId(bindingId) {
|
|
870
|
+
const candidate = bindingId?.trim();
|
|
883
871
|
if (!candidate) {
|
|
884
|
-
throw new Error("
|
|
872
|
+
throw new Error("bindingId is required");
|
|
885
873
|
}
|
|
886
874
|
return candidate;
|
|
887
875
|
}
|
|
888
|
-
normalizeState(state,
|
|
876
|
+
normalizeState(state, bindingId) {
|
|
889
877
|
return {
|
|
890
|
-
id:
|
|
891
|
-
label: state?.label ??
|
|
892
|
-
color: state?.color ??
|
|
878
|
+
id: bindingId,
|
|
879
|
+
label: state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
|
|
880
|
+
color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
|
|
893
881
|
windowId: state?.windowId ?? null,
|
|
894
882
|
groupId: state?.groupId ?? null,
|
|
895
883
|
tabIds: state?.tabIds ?? [],
|
|
@@ -897,37 +885,37 @@
|
|
|
897
885
|
primaryTabId: state?.primaryTabId ?? null
|
|
898
886
|
};
|
|
899
887
|
}
|
|
900
|
-
async
|
|
888
|
+
async listBindingRecords() {
|
|
901
889
|
return await this.storage.list();
|
|
902
890
|
}
|
|
903
|
-
async
|
|
904
|
-
const
|
|
905
|
-
const state = await this.storage.load(
|
|
906
|
-
if (!state || state.id !==
|
|
891
|
+
async loadBindingRecord(bindingId) {
|
|
892
|
+
const normalizedBindingId = this.normalizeBindingId(bindingId);
|
|
893
|
+
const state = await this.storage.load(normalizedBindingId);
|
|
894
|
+
if (!state || state.id !== normalizedBindingId) {
|
|
907
895
|
return null;
|
|
908
896
|
}
|
|
909
|
-
return this.normalizeState(state,
|
|
897
|
+
return this.normalizeState(state, normalizedBindingId);
|
|
910
898
|
}
|
|
911
|
-
async
|
|
912
|
-
const tab = ensured.
|
|
899
|
+
async buildBindingResolution(ensured, resolution) {
|
|
900
|
+
const tab = ensured.binding.tabs.find((item) => item.id === ensured.binding.activeTabId) ?? ensured.binding.tabs[0] ?? null;
|
|
913
901
|
if (tab) {
|
|
914
902
|
return {
|
|
915
903
|
tab,
|
|
916
|
-
|
|
904
|
+
binding: ensured.binding,
|
|
917
905
|
resolution,
|
|
918
|
-
|
|
906
|
+
createdBinding: ensured.created,
|
|
919
907
|
repaired: ensured.repaired,
|
|
920
908
|
repairActions: ensured.repairActions
|
|
921
909
|
};
|
|
922
910
|
}
|
|
923
|
-
if (ensured.
|
|
924
|
-
const
|
|
925
|
-
if (
|
|
911
|
+
if (ensured.binding.activeTabId !== null) {
|
|
912
|
+
const activeBindingTab = await this.waitForTrackedTab(ensured.binding.activeTabId, ensured.binding.windowId);
|
|
913
|
+
if (activeBindingTab) {
|
|
926
914
|
return {
|
|
927
|
-
tab:
|
|
928
|
-
|
|
915
|
+
tab: activeBindingTab,
|
|
916
|
+
binding: ensured.binding,
|
|
929
917
|
resolution,
|
|
930
|
-
|
|
918
|
+
createdBinding: ensured.created,
|
|
931
919
|
repaired: ensured.repaired,
|
|
932
920
|
repairActions: ensured.repairActions
|
|
933
921
|
};
|
|
@@ -939,9 +927,9 @@
|
|
|
939
927
|
}
|
|
940
928
|
return {
|
|
941
929
|
tab: activeTab,
|
|
942
|
-
|
|
930
|
+
binding: null,
|
|
943
931
|
resolution: "browser-active",
|
|
944
|
-
|
|
932
|
+
createdBinding: ensured.created,
|
|
945
933
|
repaired: ensured.repaired,
|
|
946
934
|
repairActions: ensured.repairActions
|
|
947
935
|
};
|
|
@@ -972,7 +960,7 @@
|
|
|
972
960
|
collectCandidateTabIds(state) {
|
|
973
961
|
return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number")))];
|
|
974
962
|
}
|
|
975
|
-
async
|
|
963
|
+
async rebindBindingWindow(state) {
|
|
976
964
|
const candidateWindowIds = [];
|
|
977
965
|
const pushWindowId = (windowId) => {
|
|
978
966
|
if (typeof windowId !== "number") {
|
|
@@ -982,7 +970,7 @@
|
|
|
982
970
|
candidateWindowIds.push(windowId);
|
|
983
971
|
}
|
|
984
972
|
};
|
|
985
|
-
const group = state.groupId !== null ? await this.
|
|
973
|
+
const group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
|
|
986
974
|
pushWindowId(group?.windowId);
|
|
987
975
|
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
988
976
|
for (const tab of trackedTabs) {
|
|
@@ -995,7 +983,7 @@
|
|
|
995
983
|
}
|
|
996
984
|
let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
|
|
997
985
|
if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
|
|
998
|
-
const windowTabs = await this.waitForWindowTabs(candidateWindowId,
|
|
986
|
+
const windowTabs = await this.waitForWindowTabs(candidateWindowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
|
|
999
987
|
tabs = windowTabs.filter((tab) => tab.groupId === group.id);
|
|
1000
988
|
}
|
|
1001
989
|
if (tabs.length === 0) {
|
|
@@ -1015,19 +1003,19 @@
|
|
|
1015
1003
|
}
|
|
1016
1004
|
return null;
|
|
1017
1005
|
}
|
|
1018
|
-
async
|
|
1006
|
+
async inspectBindingWindowOwnership(state, windowId) {
|
|
1019
1007
|
const windowTabs = await this.waitForWindowTabs(windowId, 500);
|
|
1020
1008
|
const trackedIds = new Set(this.collectCandidateTabIds(state));
|
|
1021
1009
|
return {
|
|
1022
|
-
|
|
1010
|
+
bindingTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || state.groupId !== null && tab.groupId === state.groupId),
|
|
1023
1011
|
foreignTabs: windowTabs.filter((tab) => !trackedIds.has(tab.id) && (state.groupId === null || tab.groupId !== state.groupId))
|
|
1024
1012
|
};
|
|
1025
1013
|
}
|
|
1026
|
-
async
|
|
1027
|
-
const sourceTabs = this.
|
|
1014
|
+
async moveBindingIntoDedicatedWindow(state, ownership, initialUrl) {
|
|
1015
|
+
const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
|
|
1028
1016
|
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
1029
1017
|
const window2 = await this.browser.createWindow({
|
|
1030
|
-
url: seedUrl ||
|
|
1018
|
+
url: seedUrl || DEFAULT_SESSION_BINDING_URL,
|
|
1031
1019
|
focused: false
|
|
1032
1020
|
});
|
|
1033
1021
|
const recreatedTabs = await this.waitForWindowTabs(window2.id);
|
|
@@ -1037,7 +1025,7 @@
|
|
|
1037
1025
|
tabIdMap.set(sourceTabs[0].id, firstTab.id);
|
|
1038
1026
|
}
|
|
1039
1027
|
for (const sourceTab of sourceTabs.slice(1)) {
|
|
1040
|
-
const recreated = await this.
|
|
1028
|
+
const recreated = await this.createBindingTab({
|
|
1041
1029
|
windowId: window2.id,
|
|
1042
1030
|
url: sourceTab.url,
|
|
1043
1031
|
active: false
|
|
@@ -1055,15 +1043,15 @@
|
|
|
1055
1043
|
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
1056
1044
|
state.primaryTabId = nextPrimaryTabId;
|
|
1057
1045
|
state.activeTabId = nextActiveTabId;
|
|
1058
|
-
for (const
|
|
1059
|
-
await this.browser.closeTab(
|
|
1046
|
+
for (const bindingTab of ownership.bindingTabs) {
|
|
1047
|
+
await this.browser.closeTab(bindingTab.id);
|
|
1060
1048
|
}
|
|
1061
1049
|
return {
|
|
1062
1050
|
window: window2,
|
|
1063
1051
|
tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
|
|
1064
1052
|
};
|
|
1065
1053
|
}
|
|
1066
|
-
|
|
1054
|
+
orderSessionBindingTabsForMigration(state, tabs) {
|
|
1067
1055
|
const ordered = [];
|
|
1068
1056
|
const seen = /* @__PURE__ */ new Set();
|
|
1069
1057
|
const pushById = (tabId) => {
|
|
@@ -1088,7 +1076,7 @@
|
|
|
1088
1076
|
}
|
|
1089
1077
|
return ordered;
|
|
1090
1078
|
}
|
|
1091
|
-
async
|
|
1079
|
+
async recoverBindingTabs(state, existingTabs) {
|
|
1092
1080
|
if (state.windowId === null) {
|
|
1093
1081
|
return existingTabs;
|
|
1094
1082
|
}
|
|
@@ -1114,9 +1102,9 @@
|
|
|
1114
1102
|
}
|
|
1115
1103
|
return existingTabs;
|
|
1116
1104
|
}
|
|
1117
|
-
async
|
|
1105
|
+
async createBindingTab(options) {
|
|
1118
1106
|
if (options.windowId === null) {
|
|
1119
|
-
throw new Error("
|
|
1107
|
+
throw new Error("Binding window is unavailable");
|
|
1120
1108
|
}
|
|
1121
1109
|
const deadline = Date.now() + 1500;
|
|
1122
1110
|
let lastError2 = null;
|
|
@@ -1137,8 +1125,8 @@
|
|
|
1137
1125
|
}
|
|
1138
1126
|
throw lastError2 ?? new Error(`No window with id: ${options.windowId}.`);
|
|
1139
1127
|
}
|
|
1140
|
-
async
|
|
1141
|
-
const state = await this.
|
|
1128
|
+
async inspectBinding(bindingId) {
|
|
1129
|
+
const state = await this.loadBindingRecord(bindingId);
|
|
1142
1130
|
if (!state) {
|
|
1143
1131
|
return null;
|
|
1144
1132
|
}
|
|
@@ -1159,34 +1147,34 @@
|
|
|
1159
1147
|
tabs
|
|
1160
1148
|
};
|
|
1161
1149
|
}
|
|
1162
|
-
async resolveReusablePrimaryTab(
|
|
1163
|
-
if (
|
|
1150
|
+
async resolveReusablePrimaryTab(binding, allowReuse) {
|
|
1151
|
+
if (binding.windowId === null) {
|
|
1164
1152
|
return null;
|
|
1165
1153
|
}
|
|
1166
|
-
if (
|
|
1167
|
-
const trackedPrimary =
|
|
1168
|
-
if (trackedPrimary && (allowReuse || this.
|
|
1154
|
+
if (binding.primaryTabId !== null) {
|
|
1155
|
+
const trackedPrimary = binding.tabs.find((tab) => tab.id === binding.primaryTabId) ?? await this.waitForTrackedTab(binding.primaryTabId, binding.windowId);
|
|
1156
|
+
if (trackedPrimary && (allowReuse || this.isReusableBlankSessionBindingTab(trackedPrimary, binding))) {
|
|
1169
1157
|
return trackedPrimary;
|
|
1170
1158
|
}
|
|
1171
1159
|
}
|
|
1172
|
-
const windowTabs = await this.waitForWindowTabs(
|
|
1160
|
+
const windowTabs = await this.waitForWindowTabs(binding.windowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
|
|
1173
1161
|
if (windowTabs.length !== 1) {
|
|
1174
1162
|
return null;
|
|
1175
1163
|
}
|
|
1176
1164
|
const candidate = windowTabs[0];
|
|
1177
|
-
if (allowReuse || this.
|
|
1165
|
+
if (allowReuse || this.isReusableBlankSessionBindingTab(candidate, binding)) {
|
|
1178
1166
|
return candidate;
|
|
1179
1167
|
}
|
|
1180
1168
|
return null;
|
|
1181
1169
|
}
|
|
1182
|
-
|
|
1183
|
-
if (
|
|
1170
|
+
isReusableBlankSessionBindingTab(tab, binding) {
|
|
1171
|
+
if (binding.tabIds.length > 1) {
|
|
1184
1172
|
return false;
|
|
1185
1173
|
}
|
|
1186
1174
|
const normalizedUrl = tab.url.trim().toLowerCase();
|
|
1187
|
-
return normalizedUrl === "" || normalizedUrl ===
|
|
1175
|
+
return normalizedUrl === "" || normalizedUrl === DEFAULT_SESSION_BINDING_URL;
|
|
1188
1176
|
}
|
|
1189
|
-
async waitForWindow(windowId, timeoutMs =
|
|
1177
|
+
async waitForWindow(windowId, timeoutMs = WINDOW_LOOKUP_TIMEOUT_MS) {
|
|
1190
1178
|
const deadline = Date.now() + timeoutMs;
|
|
1191
1179
|
while (Date.now() < deadline) {
|
|
1192
1180
|
const window2 = await this.browser.getWindow(windowId);
|
|
@@ -1197,6 +1185,17 @@
|
|
|
1197
1185
|
}
|
|
1198
1186
|
return null;
|
|
1199
1187
|
}
|
|
1188
|
+
async waitForGroup(groupId, timeoutMs = GROUP_LOOKUP_TIMEOUT_MS) {
|
|
1189
|
+
const deadline = Date.now() + timeoutMs;
|
|
1190
|
+
while (Date.now() < deadline) {
|
|
1191
|
+
const group = await this.browser.getGroup(groupId);
|
|
1192
|
+
if (group) {
|
|
1193
|
+
return group;
|
|
1194
|
+
}
|
|
1195
|
+
await this.delay(50);
|
|
1196
|
+
}
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1200
1199
|
async waitForTrackedTab(tabId, windowId, timeoutMs = 1e3) {
|
|
1201
1200
|
const deadline = Date.now() + timeoutMs;
|
|
1202
1201
|
while (Date.now() < deadline) {
|
|
@@ -1208,7 +1207,7 @@
|
|
|
1208
1207
|
}
|
|
1209
1208
|
return null;
|
|
1210
1209
|
}
|
|
1211
|
-
async waitForWindowTabs(windowId, timeoutMs =
|
|
1210
|
+
async waitForWindowTabs(windowId, timeoutMs = WINDOW_TABS_LOOKUP_TIMEOUT_MS) {
|
|
1212
1211
|
const deadline = Date.now() + timeoutMs;
|
|
1213
1212
|
while (Date.now() < deadline) {
|
|
1214
1213
|
const tabs = await this.browser.listTabs({ windowId });
|
|
@@ -1236,6 +1235,9 @@
|
|
|
1236
1235
|
var DEFAULT_TAB_LOAD_TIMEOUT_MS = 4e4;
|
|
1237
1236
|
var textEncoder2 = new TextEncoder();
|
|
1238
1237
|
var textDecoder2 = new TextDecoder();
|
|
1238
|
+
var DATA_TIMESTAMP_CONTEXT_PATTERN = /\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|freshness|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\b/i;
|
|
1239
|
+
var CONTRACT_TIMESTAMP_CONTEXT_PATTERN = /\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\b/i;
|
|
1240
|
+
var EVENT_TIMESTAMP_CONTEXT_PATTERN = /\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\b/i;
|
|
1239
1241
|
var REPLAY_FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
1240
1242
|
"accept-encoding",
|
|
1241
1243
|
"authorization",
|
|
@@ -1254,6 +1256,7 @@
|
|
|
1254
1256
|
var reconnectAttempt = 0;
|
|
1255
1257
|
var lastError = null;
|
|
1256
1258
|
var manualDisconnect = false;
|
|
1259
|
+
var sessionBindingStateMutationQueue = Promise.resolve();
|
|
1257
1260
|
async function getConfig() {
|
|
1258
1261
|
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
1259
1262
|
return {
|
|
@@ -1308,10 +1311,10 @@
|
|
|
1308
1311
|
if (lower.includes("no tab with id") || lower.includes("no window with id")) {
|
|
1309
1312
|
return toError("E_NOT_FOUND", message);
|
|
1310
1313
|
}
|
|
1311
|
-
if (lower.includes("
|
|
1314
|
+
if (lower.includes("binding") && lower.includes("does not exist")) {
|
|
1312
1315
|
return toError("E_NOT_FOUND", message);
|
|
1313
1316
|
}
|
|
1314
|
-
if (lower.includes("does not belong to
|
|
1317
|
+
if (lower.includes("does not belong to binding") || lower.includes("is missing from binding")) {
|
|
1315
1318
|
return toError("E_NOT_FOUND", message);
|
|
1316
1319
|
}
|
|
1317
1320
|
if (lower.includes("invalid url") || lower.includes("url is invalid")) {
|
|
@@ -1335,38 +1338,55 @@
|
|
|
1335
1338
|
groupId: typeof tab.groupId === "number" && tab.groupId >= 0 ? tab.groupId : null
|
|
1336
1339
|
};
|
|
1337
1340
|
}
|
|
1338
|
-
async function
|
|
1339
|
-
const stored = await chrome.storage.local.get([
|
|
1340
|
-
STORAGE_KEY_SESSION_BINDINGS,
|
|
1341
|
-
LEGACY_STORAGE_KEY_WORKSPACES,
|
|
1342
|
-
LEGACY_STORAGE_KEY_WORKSPACE
|
|
1343
|
-
]);
|
|
1341
|
+
async function readSessionBindingStateMap() {
|
|
1342
|
+
const stored = await chrome.storage.local.get([STORAGE_KEY_SESSION_BINDINGS]);
|
|
1344
1343
|
return resolveSessionBindingStateMap(stored);
|
|
1345
1344
|
}
|
|
1346
|
-
async function
|
|
1347
|
-
const stateMap = await loadWorkspaceStateMap();
|
|
1348
|
-
return stateMap[workspaceId] ?? null;
|
|
1349
|
-
}
|
|
1350
|
-
async function listWorkspaceStates() {
|
|
1351
|
-
return Object.values(await loadWorkspaceStateMap());
|
|
1352
|
-
}
|
|
1353
|
-
async function saveWorkspaceState(state) {
|
|
1354
|
-
const stateMap = await loadWorkspaceStateMap();
|
|
1355
|
-
stateMap[state.id] = state;
|
|
1356
|
-
await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
|
|
1357
|
-
await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
1358
|
-
}
|
|
1359
|
-
async function deleteWorkspaceState(workspaceId) {
|
|
1360
|
-
const stateMap = await loadWorkspaceStateMap();
|
|
1361
|
-
delete stateMap[workspaceId];
|
|
1345
|
+
async function flushSessionBindingStateMap(stateMap) {
|
|
1362
1346
|
if (Object.keys(stateMap).length === 0) {
|
|
1363
|
-
await chrome.storage.local.remove([STORAGE_KEY_SESSION_BINDINGS
|
|
1347
|
+
await chrome.storage.local.remove([STORAGE_KEY_SESSION_BINDINGS]);
|
|
1364
1348
|
return;
|
|
1365
1349
|
}
|
|
1366
1350
|
await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
|
|
1367
|
-
await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
1368
1351
|
}
|
|
1369
|
-
|
|
1352
|
+
async function runSessionBindingStateMutation(operation) {
|
|
1353
|
+
const run = sessionBindingStateMutationQueue.then(operation, operation);
|
|
1354
|
+
sessionBindingStateMutationQueue = run.then(
|
|
1355
|
+
() => void 0,
|
|
1356
|
+
() => void 0
|
|
1357
|
+
);
|
|
1358
|
+
return run;
|
|
1359
|
+
}
|
|
1360
|
+
async function mutateSessionBindingStateMap(mutator) {
|
|
1361
|
+
return await runSessionBindingStateMutation(async () => {
|
|
1362
|
+
const stateMap = await readSessionBindingStateMap();
|
|
1363
|
+
const result = await mutator(stateMap);
|
|
1364
|
+
await flushSessionBindingStateMap(stateMap);
|
|
1365
|
+
return result;
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
async function loadSessionBindingStateMap() {
|
|
1369
|
+
await sessionBindingStateMutationQueue;
|
|
1370
|
+
return await readSessionBindingStateMap();
|
|
1371
|
+
}
|
|
1372
|
+
async function loadSessionBindingState(bindingId) {
|
|
1373
|
+
const stateMap = await loadSessionBindingStateMap();
|
|
1374
|
+
return stateMap[bindingId] ?? null;
|
|
1375
|
+
}
|
|
1376
|
+
async function listSessionBindingStates() {
|
|
1377
|
+
return Object.values(await loadSessionBindingStateMap());
|
|
1378
|
+
}
|
|
1379
|
+
async function saveSessionBindingState(state) {
|
|
1380
|
+
await mutateSessionBindingStateMap((stateMap) => {
|
|
1381
|
+
stateMap[state.id] = state;
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
async function deleteSessionBindingState(bindingId) {
|
|
1385
|
+
await mutateSessionBindingStateMap((stateMap) => {
|
|
1386
|
+
delete stateMap[bindingId];
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
var sessionBindingBrowser = {
|
|
1370
1390
|
async getTab(tabId) {
|
|
1371
1391
|
try {
|
|
1372
1392
|
return toTabInfo(await chrome.tabs.get(tabId));
|
|
@@ -1498,12 +1518,12 @@
|
|
|
1498
1518
|
};
|
|
1499
1519
|
var bindingManager = new SessionBindingManager(
|
|
1500
1520
|
{
|
|
1501
|
-
load:
|
|
1502
|
-
save:
|
|
1503
|
-
delete:
|
|
1504
|
-
list:
|
|
1521
|
+
load: loadSessionBindingState,
|
|
1522
|
+
save: saveSessionBindingState,
|
|
1523
|
+
delete: deleteSessionBindingState,
|
|
1524
|
+
list: listSessionBindingStates
|
|
1505
1525
|
},
|
|
1506
|
-
|
|
1526
|
+
sessionBindingBrowser
|
|
1507
1527
|
);
|
|
1508
1528
|
async function waitForTabComplete(tabId, timeoutMs = DEFAULT_TAB_LOAD_TIMEOUT_MS) {
|
|
1509
1529
|
try {
|
|
@@ -1592,7 +1612,7 @@
|
|
|
1592
1612
|
return raw;
|
|
1593
1613
|
}
|
|
1594
1614
|
}
|
|
1595
|
-
async function
|
|
1615
|
+
async function finalizeOpenedSessionBindingTab(opened, expectedUrl) {
|
|
1596
1616
|
if (expectedUrl && expectedUrl !== "about:blank") {
|
|
1597
1617
|
await waitForTabUrl(opened.tab.id, expectedUrl).catch(() => void 0);
|
|
1598
1618
|
}
|
|
@@ -1607,14 +1627,14 @@
|
|
|
1607
1627
|
url: effectiveUrl
|
|
1608
1628
|
};
|
|
1609
1629
|
} catch {
|
|
1610
|
-
refreshedTab = await
|
|
1630
|
+
refreshedTab = await sessionBindingBrowser.getTab(opened.tab.id) ?? opened.tab;
|
|
1611
1631
|
}
|
|
1612
|
-
const
|
|
1613
|
-
...opened.
|
|
1614
|
-
tabs: opened.
|
|
1632
|
+
const refreshedBinding = await bindingManager.getBindingInfo(opened.binding.id) ?? {
|
|
1633
|
+
...opened.binding,
|
|
1634
|
+
tabs: opened.binding.tabs.map((tab) => tab.id === refreshedTab.id ? refreshedTab : tab)
|
|
1615
1635
|
};
|
|
1616
1636
|
return {
|
|
1617
|
-
|
|
1637
|
+
binding: refreshedBinding,
|
|
1618
1638
|
tab: refreshedTab
|
|
1619
1639
|
};
|
|
1620
1640
|
}
|
|
@@ -1638,7 +1658,7 @@
|
|
|
1638
1658
|
}
|
|
1639
1659
|
const resolved = await bindingManager.resolveTarget({
|
|
1640
1660
|
tabId: target.tabId,
|
|
1641
|
-
|
|
1661
|
+
bindingId: typeof target.bindingId === "string" ? target.bindingId : void 0,
|
|
1642
1662
|
createIfMissing: false
|
|
1643
1663
|
});
|
|
1644
1664
|
const tab = await chrome.tabs.get(resolved.tab.id);
|
|
@@ -1774,6 +1794,7 @@
|
|
|
1774
1794
|
framePath,
|
|
1775
1795
|
expr: typeof params.expr === "string" ? params.expr : "",
|
|
1776
1796
|
path: typeof params.path === "string" ? params.path : "",
|
|
1797
|
+
resolver: typeof params.resolver === "string" ? params.resolver : void 0,
|
|
1777
1798
|
url: typeof params.url === "string" ? params.url : "",
|
|
1778
1799
|
method: typeof params.method === "string" ? params.method : "GET",
|
|
1779
1800
|
headers: typeof params.headers === "object" && params.headers !== null ? params.headers : void 0,
|
|
@@ -1856,6 +1877,54 @@
|
|
|
1856
1877
|
}
|
|
1857
1878
|
return currentWindow;
|
|
1858
1879
|
};
|
|
1880
|
+
const buildPathExpression = (path) => parsePath(path).map((segment, index) => {
|
|
1881
|
+
if (typeof segment === "number") {
|
|
1882
|
+
return `[${segment}]`;
|
|
1883
|
+
}
|
|
1884
|
+
if (index === 0) {
|
|
1885
|
+
return segment;
|
|
1886
|
+
}
|
|
1887
|
+
return `.${segment}`;
|
|
1888
|
+
}).join("");
|
|
1889
|
+
const readPath = (targetWindow, path) => {
|
|
1890
|
+
const segments = parsePath(path);
|
|
1891
|
+
let current = targetWindow;
|
|
1892
|
+
for (const segment of segments) {
|
|
1893
|
+
if (current === null || current === void 0 || !(segment in current)) {
|
|
1894
|
+
throw { code: "E_NOT_FOUND", message: `path not found: ${path}` };
|
|
1895
|
+
}
|
|
1896
|
+
current = current[segment];
|
|
1897
|
+
}
|
|
1898
|
+
return current;
|
|
1899
|
+
};
|
|
1900
|
+
const resolveExtractValue = (targetWindow, path, resolver) => {
|
|
1901
|
+
const strategy = resolver === "globalThis" || resolver === "lexical" ? resolver : "auto";
|
|
1902
|
+
const lexicalExpression = buildPathExpression(path);
|
|
1903
|
+
const readLexical = () => {
|
|
1904
|
+
try {
|
|
1905
|
+
return targetWindow.eval(lexicalExpression);
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
if (error instanceof ReferenceError) {
|
|
1908
|
+
throw { code: "E_NOT_FOUND", message: `path not found: ${path}` };
|
|
1909
|
+
}
|
|
1910
|
+
throw error;
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
if (strategy === "globalThis") {
|
|
1914
|
+
return { resolver: "globalThis", value: readPath(targetWindow, path) };
|
|
1915
|
+
}
|
|
1916
|
+
if (strategy === "lexical") {
|
|
1917
|
+
return { resolver: "lexical", value: readLexical() };
|
|
1918
|
+
}
|
|
1919
|
+
try {
|
|
1920
|
+
return { resolver: "globalThis", value: readPath(targetWindow, path) };
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
if (typeof error !== "object" || error === null || error.code !== "E_NOT_FOUND") {
|
|
1923
|
+
throw error;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return { resolver: "lexical", value: readLexical() };
|
|
1927
|
+
};
|
|
1859
1928
|
try {
|
|
1860
1929
|
const targetWindow = payload.scope === "main" ? window : payload.scope === "current" ? resolveFrameWindow(payload.framePath ?? []) : window;
|
|
1861
1930
|
if (payload.action === "eval") {
|
|
@@ -1864,16 +1933,15 @@
|
|
|
1864
1933
|
return { url: targetWindow.location.href, framePath: payload.scope === "current" ? payload.framePath ?? [] : [], value: serialized.value, bytes: serialized.bytes };
|
|
1865
1934
|
}
|
|
1866
1935
|
if (payload.action === "extract") {
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
return { url: targetWindow.location.href, framePath: payload.scope === "current" ? payload.framePath ?? [] : [], value: serialized.value, bytes: serialized.bytes };
|
|
1936
|
+
const extracted = resolveExtractValue(targetWindow, payload.path, payload.resolver);
|
|
1937
|
+
const serialized = serializeValue(extracted.value, payload.maxBytes);
|
|
1938
|
+
return {
|
|
1939
|
+
url: targetWindow.location.href,
|
|
1940
|
+
framePath: payload.scope === "current" ? payload.framePath ?? [] : [],
|
|
1941
|
+
value: serialized.value,
|
|
1942
|
+
bytes: serialized.bytes,
|
|
1943
|
+
resolver: extracted.resolver
|
|
1944
|
+
};
|
|
1877
1945
|
}
|
|
1878
1946
|
if (payload.action === "fetch") {
|
|
1879
1947
|
const headers = { ...payload.headers ?? {} };
|
|
@@ -2021,6 +2089,31 @@
|
|
|
2021
2089
|
}
|
|
2022
2090
|
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
2023
2091
|
}
|
|
2092
|
+
function collectTimestampMatchesFromText(text, source, patterns) {
|
|
2093
|
+
const regexes = (patterns ?? [
|
|
2094
|
+
String.raw`\b20\d{2}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b`,
|
|
2095
|
+
String.raw`\b20\d{2}-\d{2}-\d{2}\b`,
|
|
2096
|
+
String.raw`\b20\d{2}\/\d{2}\/\d{2}\b`
|
|
2097
|
+
]).map((pattern) => new RegExp(pattern, "gi"));
|
|
2098
|
+
const collected = /* @__PURE__ */ new Map();
|
|
2099
|
+
for (const regex of regexes) {
|
|
2100
|
+
for (const match of text.matchAll(regex)) {
|
|
2101
|
+
const value = match[0];
|
|
2102
|
+
if (!value) {
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
const index = match.index ?? text.indexOf(value);
|
|
2106
|
+
const start = Math.max(0, index - 28);
|
|
2107
|
+
const end = Math.min(text.length, index + value.length + 28);
|
|
2108
|
+
const context = text.slice(start, end).replace(/\s+/g, " ").trim();
|
|
2109
|
+
const key = `${value}::${context}`;
|
|
2110
|
+
if (!collected.has(key)) {
|
|
2111
|
+
collected.set(key, { value, source, context });
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return [...collected.values()];
|
|
2116
|
+
}
|
|
2024
2117
|
function parseTimestampCandidate(value, now = Date.now()) {
|
|
2025
2118
|
const normalized = value.trim().toLowerCase();
|
|
2026
2119
|
if (!normalized) {
|
|
@@ -2035,13 +2128,61 @@
|
|
|
2035
2128
|
const parsed = Date.parse(value);
|
|
2036
2129
|
return Number.isNaN(parsed) ? null : parsed;
|
|
2037
2130
|
}
|
|
2038
|
-
function
|
|
2039
|
-
|
|
2131
|
+
function nearestPatternDistance(text, anchor, pattern) {
|
|
2132
|
+
const normalizedText = text.toLowerCase();
|
|
2133
|
+
const normalizedAnchor = anchor.toLowerCase();
|
|
2134
|
+
const anchorIndex = normalizedText.indexOf(normalizedAnchor);
|
|
2135
|
+
if (anchorIndex < 0) {
|
|
2040
2136
|
return null;
|
|
2041
2137
|
}
|
|
2138
|
+
const regex = new RegExp(pattern.source, "gi");
|
|
2139
|
+
let match;
|
|
2140
|
+
let best = null;
|
|
2141
|
+
while ((match = regex.exec(normalizedText)) !== null) {
|
|
2142
|
+
best = best === null ? Math.abs(anchorIndex - match.index) : Math.min(best, Math.abs(anchorIndex - match.index));
|
|
2143
|
+
}
|
|
2144
|
+
return best;
|
|
2145
|
+
}
|
|
2146
|
+
function classifyTimestampCandidate(candidate, now = Date.now()) {
|
|
2147
|
+
const normalizedPath = (candidate.path ?? "").toLowerCase();
|
|
2148
|
+
if (DATA_TIMESTAMP_CONTEXT_PATTERN.test(normalizedPath)) {
|
|
2149
|
+
return "data";
|
|
2150
|
+
}
|
|
2151
|
+
if (CONTRACT_TIMESTAMP_CONTEXT_PATTERN.test(normalizedPath)) {
|
|
2152
|
+
return "contract";
|
|
2153
|
+
}
|
|
2154
|
+
if (EVENT_TIMESTAMP_CONTEXT_PATTERN.test(normalizedPath)) {
|
|
2155
|
+
return "event";
|
|
2156
|
+
}
|
|
2157
|
+
const context = candidate.context ?? "";
|
|
2158
|
+
const distances = [
|
|
2159
|
+
{ category: "data", distance: nearestPatternDistance(context, candidate.value, DATA_TIMESTAMP_CONTEXT_PATTERN) },
|
|
2160
|
+
{ category: "contract", distance: nearestPatternDistance(context, candidate.value, CONTRACT_TIMESTAMP_CONTEXT_PATTERN) },
|
|
2161
|
+
{ category: "event", distance: nearestPatternDistance(context, candidate.value, EVENT_TIMESTAMP_CONTEXT_PATTERN) }
|
|
2162
|
+
].filter((entry) => typeof entry.distance === "number");
|
|
2163
|
+
if (distances.length > 0) {
|
|
2164
|
+
distances.sort((left, right) => left.distance - right.distance);
|
|
2165
|
+
return distances[0].category;
|
|
2166
|
+
}
|
|
2167
|
+
const parsed = parseTimestampCandidate(candidate.value, now);
|
|
2168
|
+
return typeof parsed === "number" && parsed > now + 36 * 60 * 60 * 1e3 ? "contract" : "unknown";
|
|
2169
|
+
}
|
|
2170
|
+
function normalizeTimestampCandidates(candidates, now = Date.now()) {
|
|
2171
|
+
return candidates.map((candidate) => ({
|
|
2172
|
+
value: candidate.value,
|
|
2173
|
+
source: candidate.source,
|
|
2174
|
+
category: candidate.category ?? classifyTimestampCandidate(candidate, now),
|
|
2175
|
+
context: candidate.context,
|
|
2176
|
+
path: candidate.path
|
|
2177
|
+
}));
|
|
2178
|
+
}
|
|
2179
|
+
function latestTimestampFromCandidates(candidates, now = Date.now()) {
|
|
2042
2180
|
let latest = null;
|
|
2043
|
-
for (const
|
|
2044
|
-
|
|
2181
|
+
for (const candidate of candidates) {
|
|
2182
|
+
if (candidate.category === "contract" || candidate.category === "event") {
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
const parsed = parseTimestampCandidate(candidate.value, now);
|
|
2045
2186
|
if (parsed === null) {
|
|
2046
2187
|
continue;
|
|
2047
2188
|
}
|
|
@@ -2051,15 +2192,24 @@
|
|
|
2051
2192
|
}
|
|
2052
2193
|
function computeFreshnessAssessment(input) {
|
|
2053
2194
|
const now = Date.now();
|
|
2054
|
-
const
|
|
2055
|
-
if (
|
|
2195
|
+
const latestPageVisibleTimestamp = [input.latestPageDataTimestamp, input.latestInlineDataTimestamp, input.domVisibleTimestamp].filter((value) => typeof value === "number").sort((left, right) => right - left)[0] ?? null;
|
|
2196
|
+
if (latestPageVisibleTimestamp !== null && now - latestPageVisibleTimestamp <= input.freshWindowMs) {
|
|
2056
2197
|
return "fresh";
|
|
2057
2198
|
}
|
|
2199
|
+
const networkHasFreshData = typeof input.latestNetworkDataTimestamp === "number" && now - input.latestNetworkDataTimestamp <= input.freshWindowMs;
|
|
2200
|
+
if (networkHasFreshData) {
|
|
2201
|
+
return "lagged";
|
|
2202
|
+
}
|
|
2058
2203
|
const recentSignals = [input.latestNetworkTimestamp, input.lastMutationAt].filter((value) => typeof value === "number").some((value) => now - value <= input.freshWindowMs);
|
|
2059
|
-
if (recentSignals &&
|
|
2204
|
+
if (recentSignals && latestPageVisibleTimestamp !== null && now - latestPageVisibleTimestamp > input.freshWindowMs) {
|
|
2060
2205
|
return "lagged";
|
|
2061
2206
|
}
|
|
2062
|
-
const staleSignals = [
|
|
2207
|
+
const staleSignals = [
|
|
2208
|
+
input.latestNetworkTimestamp,
|
|
2209
|
+
input.lastMutationAt,
|
|
2210
|
+
latestPageVisibleTimestamp,
|
|
2211
|
+
input.latestNetworkDataTimestamp
|
|
2212
|
+
].filter((value) => typeof value === "number");
|
|
2063
2213
|
if (staleSignals.length > 0 && staleSignals.every((value) => now - value > input.staleWindowMs)) {
|
|
2064
2214
|
return "stale";
|
|
2065
2215
|
}
|
|
@@ -2068,25 +2218,167 @@
|
|
|
2068
2218
|
async function collectPageInspection(tabId, params = {}) {
|
|
2069
2219
|
return await forwardContentRpc(tabId, "bak.internal.inspectState", params);
|
|
2070
2220
|
}
|
|
2221
|
+
async function probePageDataCandidatesForTab(tabId, inspection) {
|
|
2222
|
+
const candidateNames = [
|
|
2223
|
+
...Array.isArray(inspection.suspiciousGlobals) ? inspection.suspiciousGlobals.map(String) : [],
|
|
2224
|
+
...Array.isArray(inspection.globalsPreview) ? inspection.globalsPreview.map(String) : []
|
|
2225
|
+
].filter((name, index, array) => /^[A-Za-z_$][\w$]*$/.test(name) && array.indexOf(name) === index).slice(0, 16);
|
|
2226
|
+
if (candidateNames.length === 0) {
|
|
2227
|
+
return [];
|
|
2228
|
+
}
|
|
2229
|
+
const expr = `(() => {
|
|
2230
|
+
const candidates = ${JSON.stringify(candidateNames)};
|
|
2231
|
+
const dataPattern = /\\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\\b/i;
|
|
2232
|
+
const contractPattern = /\\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\\b/i;
|
|
2233
|
+
const eventPattern = /\\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\\b/i;
|
|
2234
|
+
const isTimestampString = (value) => typeof value === 'string' && value.trim().length > 0 && !Number.isNaN(Date.parse(value.trim()));
|
|
2235
|
+
const classify = (path, value) => {
|
|
2236
|
+
const normalized = String(path || '').toLowerCase();
|
|
2237
|
+
if (dataPattern.test(normalized)) return 'data';
|
|
2238
|
+
if (contractPattern.test(normalized)) return 'contract';
|
|
2239
|
+
if (eventPattern.test(normalized)) return 'event';
|
|
2240
|
+
const parsed = Date.parse(String(value || '').trim());
|
|
2241
|
+
return Number.isFinite(parsed) && parsed > Date.now() + 36 * 60 * 60 * 1000 ? 'contract' : 'unknown';
|
|
2242
|
+
};
|
|
2243
|
+
const sampleValue = (value, depth = 0) => {
|
|
2244
|
+
if (depth >= 2 || value == null || typeof value !== 'object') {
|
|
2245
|
+
if (typeof value === 'string') {
|
|
2246
|
+
return value.length > 160 ? value.slice(0, 160) : value;
|
|
2247
|
+
}
|
|
2248
|
+
if (typeof value === 'function') {
|
|
2249
|
+
return '[Function]';
|
|
2250
|
+
}
|
|
2251
|
+
return value;
|
|
2252
|
+
}
|
|
2253
|
+
if (Array.isArray(value)) {
|
|
2254
|
+
return value.slice(0, 3).map((item) => sampleValue(item, depth + 1));
|
|
2255
|
+
}
|
|
2256
|
+
const sampled = {};
|
|
2257
|
+
for (const key of Object.keys(value).slice(0, 8)) {
|
|
2258
|
+
try {
|
|
2259
|
+
sampled[key] = sampleValue(value[key], depth + 1);
|
|
2260
|
+
} catch {
|
|
2261
|
+
sampled[key] = '[Unreadable]';
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return sampled;
|
|
2265
|
+
};
|
|
2266
|
+
const collectTimestamps = (value, path, depth, collected) => {
|
|
2267
|
+
if (collected.length >= 16) return;
|
|
2268
|
+
if (isTimestampString(value)) {
|
|
2269
|
+
collected.push({ path, value: String(value), category: classify(path, value) });
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
if (depth >= 3) return;
|
|
2273
|
+
if (Array.isArray(value)) {
|
|
2274
|
+
value.slice(0, 3).forEach((item, index) => collectTimestamps(item, path + '[' + index + ']', depth + 1, collected));
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
if (value && typeof value === 'object') {
|
|
2278
|
+
Object.keys(value)
|
|
2279
|
+
.slice(0, 8)
|
|
2280
|
+
.forEach((key) => {
|
|
2281
|
+
try {
|
|
2282
|
+
collectTimestamps(value[key], path ? path + '.' + key : key, depth + 1, collected);
|
|
2283
|
+
} catch {
|
|
2284
|
+
// Ignore unreadable nested properties.
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
const readCandidate = (name) => {
|
|
2290
|
+
if (name in globalThis) {
|
|
2291
|
+
return { resolver: 'globalThis', value: globalThis[name] };
|
|
2292
|
+
}
|
|
2293
|
+
return { resolver: 'lexical', value: globalThis.eval(name) };
|
|
2294
|
+
};
|
|
2295
|
+
const results = [];
|
|
2296
|
+
for (const name of candidates) {
|
|
2297
|
+
try {
|
|
2298
|
+
const resolved = readCandidate(name);
|
|
2299
|
+
const timestamps = [];
|
|
2300
|
+
collectTimestamps(resolved.value, name, 0, timestamps);
|
|
2301
|
+
results.push({
|
|
2302
|
+
name,
|
|
2303
|
+
resolver: resolved.resolver,
|
|
2304
|
+
sample: sampleValue(resolved.value),
|
|
2305
|
+
timestamps
|
|
2306
|
+
});
|
|
2307
|
+
} catch {
|
|
2308
|
+
// Ignore inaccessible candidates.
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return results;
|
|
2312
|
+
})()`;
|
|
2313
|
+
try {
|
|
2314
|
+
const evaluated = await executePageWorld(tabId, "eval", {
|
|
2315
|
+
expr,
|
|
2316
|
+
scope: "current",
|
|
2317
|
+
maxBytes: 64 * 1024
|
|
2318
|
+
});
|
|
2319
|
+
const frameResult = evaluated.result ?? evaluated.results?.find((candidate) => candidate.value || candidate.error);
|
|
2320
|
+
return Array.isArray(frameResult?.value) ? frameResult.value : [];
|
|
2321
|
+
} catch {
|
|
2322
|
+
return [];
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2071
2325
|
async function buildFreshnessForTab(tabId, params = {}) {
|
|
2072
2326
|
const inspection = await collectPageInspection(tabId, params);
|
|
2073
|
-
const
|
|
2074
|
-
const inlineTimestamps = Array.isArray(inspection.inlineTimestamps) ? inspection.inlineTimestamps.map(String) : [];
|
|
2327
|
+
const probedPageDataCandidates = await probePageDataCandidatesForTab(tabId, inspection);
|
|
2075
2328
|
const now = Date.now();
|
|
2076
2329
|
const freshWindowMs = typeof params.freshWindowMs === "number" ? Math.max(1, Math.floor(params.freshWindowMs)) : 15 * 60 * 1e3;
|
|
2077
2330
|
const staleWindowMs = typeof params.staleWindowMs === "number" ? Math.max(freshWindowMs, Math.floor(params.staleWindowMs)) : 24 * 60 * 60 * 1e3;
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2331
|
+
const visibleCandidates = normalizeTimestampCandidates(
|
|
2332
|
+
Array.isArray(inspection.visibleTimestampCandidates) ? inspection.visibleTimestampCandidates.filter((candidate) => typeof candidate === "object" && candidate !== null).map((candidate) => ({
|
|
2333
|
+
value: String(candidate.value ?? ""),
|
|
2334
|
+
context: typeof candidate.context === "string" ? candidate.context : void 0,
|
|
2335
|
+
source: "visible"
|
|
2336
|
+
})) : Array.isArray(inspection.visibleTimestamps) ? inspection.visibleTimestamps.map((value) => ({ value: String(value), source: "visible" })) : [],
|
|
2337
|
+
now
|
|
2338
|
+
);
|
|
2339
|
+
const inlineCandidates = normalizeTimestampCandidates(
|
|
2340
|
+
Array.isArray(inspection.inlineTimestampCandidates) ? inspection.inlineTimestampCandidates.filter((candidate) => typeof candidate === "object" && candidate !== null).map((candidate) => ({
|
|
2341
|
+
value: String(candidate.value ?? ""),
|
|
2342
|
+
context: typeof candidate.context === "string" ? candidate.context : void 0,
|
|
2343
|
+
source: "inline"
|
|
2344
|
+
})) : Array.isArray(inspection.inlineTimestamps) ? inspection.inlineTimestamps.map((value) => ({ value: String(value), source: "inline" })) : [],
|
|
2345
|
+
now
|
|
2346
|
+
);
|
|
2347
|
+
const pageDataCandidates = probedPageDataCandidates.flatMap(
|
|
2348
|
+
(candidate) => Array.isArray(candidate.timestamps) ? candidate.timestamps.map((timestamp) => ({
|
|
2349
|
+
value: String(timestamp.value ?? ""),
|
|
2350
|
+
source: "page-data",
|
|
2351
|
+
path: typeof timestamp.path === "string" ? timestamp.path : candidate.name,
|
|
2352
|
+
category: timestamp.category === "data" || timestamp.category === "contract" || timestamp.category === "event" || timestamp.category === "unknown" ? timestamp.category : "unknown"
|
|
2353
|
+
})) : []
|
|
2354
|
+
);
|
|
2355
|
+
const networkEntries = listNetworkEntries(tabId, { limit: 25 });
|
|
2356
|
+
const networkCandidates = normalizeTimestampCandidates(
|
|
2357
|
+
networkEntries.flatMap((entry) => {
|
|
2358
|
+
const previews = [entry.responseBodyPreview, entry.requestBodyPreview].filter((value) => typeof value === "string");
|
|
2359
|
+
return previews.flatMap((preview) => collectTimestampMatchesFromText(preview, "network", Array.isArray(params.patterns) ? params.patterns.map(String) : void 0));
|
|
2360
|
+
}),
|
|
2361
|
+
now
|
|
2362
|
+
);
|
|
2363
|
+
const latestInlineDataTimestamp = latestTimestampFromCandidates(inlineCandidates, now);
|
|
2364
|
+
const latestPageDataTimestamp = latestTimestampFromCandidates(pageDataCandidates, now);
|
|
2365
|
+
const latestNetworkDataTimestamp = latestTimestampFromCandidates(networkCandidates, now);
|
|
2366
|
+
const domVisibleTimestamp = latestTimestampFromCandidates(visibleCandidates, now);
|
|
2080
2367
|
const latestNetworkTs = latestNetworkTimestamp(tabId);
|
|
2081
2368
|
const lastMutationAt = typeof inspection.lastMutationAt === "number" ? inspection.lastMutationAt : null;
|
|
2369
|
+
const allCandidates = [...visibleCandidates, ...inlineCandidates, ...pageDataCandidates, ...networkCandidates];
|
|
2082
2370
|
return {
|
|
2083
2371
|
pageLoadedAt: typeof inspection.pageLoadedAt === "number" ? inspection.pageLoadedAt : null,
|
|
2084
2372
|
lastMutationAt,
|
|
2085
2373
|
latestNetworkTimestamp: latestNetworkTs,
|
|
2086
2374
|
latestInlineDataTimestamp,
|
|
2375
|
+
latestPageDataTimestamp,
|
|
2376
|
+
latestNetworkDataTimestamp,
|
|
2087
2377
|
domVisibleTimestamp,
|
|
2088
2378
|
assessment: computeFreshnessAssessment({
|
|
2089
2379
|
latestInlineDataTimestamp,
|
|
2380
|
+
latestPageDataTimestamp,
|
|
2381
|
+
latestNetworkDataTimestamp,
|
|
2090
2382
|
latestNetworkTimestamp: latestNetworkTs,
|
|
2091
2383
|
domVisibleTimestamp,
|
|
2092
2384
|
lastMutationAt,
|
|
@@ -2094,17 +2386,116 @@
|
|
|
2094
2386
|
staleWindowMs
|
|
2095
2387
|
}),
|
|
2096
2388
|
evidence: {
|
|
2097
|
-
visibleTimestamps,
|
|
2098
|
-
inlineTimestamps,
|
|
2389
|
+
visibleTimestamps: visibleCandidates.map((candidate) => candidate.value),
|
|
2390
|
+
inlineTimestamps: inlineCandidates.map((candidate) => candidate.value),
|
|
2391
|
+
pageDataTimestamps: pageDataCandidates.map((candidate) => candidate.value),
|
|
2392
|
+
networkDataTimestamps: networkCandidates.map((candidate) => candidate.value),
|
|
2393
|
+
classifiedTimestamps: allCandidates,
|
|
2099
2394
|
networkSampleIds: recentNetworkSampleIds(tabId)
|
|
2100
2395
|
}
|
|
2101
2396
|
};
|
|
2102
2397
|
}
|
|
2398
|
+
function summarizeNetworkCadence(entries) {
|
|
2399
|
+
const relevant = entries.filter((entry) => entry.kind === "fetch" || entry.kind === "xhr").slice().sort((left, right) => left.ts - right.ts);
|
|
2400
|
+
if (relevant.length === 0) {
|
|
2401
|
+
return {
|
|
2402
|
+
sampleCount: 0,
|
|
2403
|
+
classification: "none",
|
|
2404
|
+
averageIntervalMs: null,
|
|
2405
|
+
medianIntervalMs: null,
|
|
2406
|
+
latestGapMs: null,
|
|
2407
|
+
endpoints: []
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
const intervals = [];
|
|
2411
|
+
for (let index = 1; index < relevant.length; index += 1) {
|
|
2412
|
+
intervals.push(Math.max(0, relevant[index].ts - relevant[index - 1].ts));
|
|
2413
|
+
}
|
|
2414
|
+
const sortedIntervals = intervals.slice().sort((left, right) => left - right);
|
|
2415
|
+
const averageIntervalMs = intervals.length > 0 ? Math.round(intervals.reduce((sum, value) => sum + value, 0) / intervals.length) : null;
|
|
2416
|
+
const medianIntervalMs = sortedIntervals.length > 0 ? sortedIntervals[Math.floor(sortedIntervals.length / 2)] ?? null : null;
|
|
2417
|
+
const latestGapMs = Math.max(0, Date.now() - relevant[relevant.length - 1].ts);
|
|
2418
|
+
const classification = relevant.length >= 3 && medianIntervalMs !== null && medianIntervalMs <= 3e4 ? "polling" : relevant.length >= 2 ? "bursty" : "single-request";
|
|
2419
|
+
return {
|
|
2420
|
+
sampleCount: relevant.length,
|
|
2421
|
+
classification,
|
|
2422
|
+
averageIntervalMs,
|
|
2423
|
+
medianIntervalMs,
|
|
2424
|
+
latestGapMs,
|
|
2425
|
+
endpoints: [...new Set(relevant.slice(-5).map((entry) => entry.url))].slice(0, 5)
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
function extractReplayRowsCandidate(json) {
|
|
2429
|
+
if (Array.isArray(json)) {
|
|
2430
|
+
return { rows: json, source: "$" };
|
|
2431
|
+
}
|
|
2432
|
+
if (typeof json !== "object" || json === null) {
|
|
2433
|
+
return null;
|
|
2434
|
+
}
|
|
2435
|
+
const record = json;
|
|
2436
|
+
const preferredKeys = ["data", "rows", "results", "items"];
|
|
2437
|
+
for (const key of preferredKeys) {
|
|
2438
|
+
if (Array.isArray(record[key])) {
|
|
2439
|
+
return { rows: record[key], source: `$.${key}` };
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
return null;
|
|
2443
|
+
}
|
|
2444
|
+
async function enrichReplayWithSchema(tabId, response) {
|
|
2445
|
+
const candidate = extractReplayRowsCandidate(response.json);
|
|
2446
|
+
if (!candidate || candidate.rows.length === 0) {
|
|
2447
|
+
return response;
|
|
2448
|
+
}
|
|
2449
|
+
const firstRow = candidate.rows[0];
|
|
2450
|
+
const tablesResult = await forwardContentRpc(tabId, "table.list", {});
|
|
2451
|
+
const tables = Array.isArray(tablesResult.tables) ? tablesResult.tables : [];
|
|
2452
|
+
if (tables.length === 0) {
|
|
2453
|
+
return response;
|
|
2454
|
+
}
|
|
2455
|
+
const schemas = [];
|
|
2456
|
+
for (const table of tables) {
|
|
2457
|
+
const schemaResult = await forwardContentRpc(tabId, "table.schema", { table: table.id });
|
|
2458
|
+
if (schemaResult.schema && Array.isArray(schemaResult.schema.columns)) {
|
|
2459
|
+
schemas.push({ table: schemaResult.table ?? table, schema: schemaResult.schema });
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
if (schemas.length === 0) {
|
|
2463
|
+
return response;
|
|
2464
|
+
}
|
|
2465
|
+
if (Array.isArray(firstRow)) {
|
|
2466
|
+
const matchingSchema = schemas.find(({ schema }) => schema.columns.length === firstRow.length) ?? schemas[0];
|
|
2467
|
+
if (!matchingSchema) {
|
|
2468
|
+
return response;
|
|
2469
|
+
}
|
|
2470
|
+
const mappedRows = candidate.rows.filter((row) => Array.isArray(row)).map((row) => {
|
|
2471
|
+
const mapped = {};
|
|
2472
|
+
matchingSchema.schema.columns.forEach((column, index) => {
|
|
2473
|
+
mapped[column.label] = row[index];
|
|
2474
|
+
});
|
|
2475
|
+
return mapped;
|
|
2476
|
+
});
|
|
2477
|
+
return {
|
|
2478
|
+
...response,
|
|
2479
|
+
table: matchingSchema.table,
|
|
2480
|
+
schema: matchingSchema.schema,
|
|
2481
|
+
mappedRows,
|
|
2482
|
+
mappingSource: candidate.source
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
if (typeof firstRow === "object" && firstRow !== null) {
|
|
2486
|
+
return {
|
|
2487
|
+
...response,
|
|
2488
|
+
mappedRows: candidate.rows.filter((row) => typeof row === "object" && row !== null),
|
|
2489
|
+
mappingSource: candidate.source
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
return response;
|
|
2493
|
+
}
|
|
2103
2494
|
async function handleRequest(request) {
|
|
2104
2495
|
const params = request.params ?? {};
|
|
2105
2496
|
const target = {
|
|
2106
2497
|
tabId: typeof params.tabId === "number" ? params.tabId : void 0,
|
|
2107
|
-
|
|
2498
|
+
bindingId: typeof params.bindingId === "string" ? params.bindingId : void 0
|
|
2108
2499
|
};
|
|
2109
2500
|
const rpcForwardMethods = /* @__PURE__ */ new Set([
|
|
2110
2501
|
"page.title",
|
|
@@ -2203,63 +2594,92 @@
|
|
|
2203
2594
|
await chrome.tabs.remove(tabId);
|
|
2204
2595
|
return { ok: true };
|
|
2205
2596
|
}
|
|
2206
|
-
case "
|
|
2597
|
+
case "sessionBinding.ensure": {
|
|
2207
2598
|
return preserveHumanFocus(params.focus !== true, async () => {
|
|
2208
|
-
const result = await bindingManager.
|
|
2209
|
-
|
|
2599
|
+
const result = await bindingManager.ensureBinding({
|
|
2600
|
+
bindingId: String(params.bindingId ?? ""),
|
|
2210
2601
|
focus: params.focus === true,
|
|
2211
2602
|
initialUrl: typeof params.url === "string" ? params.url : void 0
|
|
2212
2603
|
});
|
|
2213
|
-
for (const tab of result.
|
|
2604
|
+
for (const tab of result.binding.tabs) {
|
|
2214
2605
|
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2215
2606
|
}
|
|
2216
|
-
return
|
|
2607
|
+
return {
|
|
2608
|
+
browser: result.binding,
|
|
2609
|
+
created: result.created,
|
|
2610
|
+
repaired: result.repaired,
|
|
2611
|
+
repairActions: result.repairActions
|
|
2612
|
+
};
|
|
2217
2613
|
});
|
|
2218
2614
|
}
|
|
2219
|
-
case "
|
|
2615
|
+
case "sessionBinding.info": {
|
|
2220
2616
|
return {
|
|
2221
|
-
|
|
2617
|
+
browser: await bindingManager.getBindingInfo(String(params.bindingId ?? ""))
|
|
2222
2618
|
};
|
|
2223
2619
|
}
|
|
2224
|
-
case "
|
|
2620
|
+
case "sessionBinding.openTab": {
|
|
2225
2621
|
const expectedUrl = typeof params.url === "string" ? params.url : void 0;
|
|
2226
2622
|
const opened = await preserveHumanFocus(params.focus !== true, async () => {
|
|
2227
2623
|
return await bindingManager.openTab({
|
|
2228
|
-
|
|
2624
|
+
bindingId: String(params.bindingId ?? ""),
|
|
2229
2625
|
url: expectedUrl,
|
|
2230
2626
|
active: params.active === true,
|
|
2231
2627
|
focus: params.focus === true
|
|
2232
2628
|
});
|
|
2233
2629
|
});
|
|
2234
|
-
const finalized = await
|
|
2630
|
+
const finalized = await finalizeOpenedSessionBindingTab(opened, expectedUrl);
|
|
2235
2631
|
void ensureNetworkDebugger(finalized.tab.id).catch(() => void 0);
|
|
2236
|
-
return
|
|
2632
|
+
return {
|
|
2633
|
+
browser: finalized.binding,
|
|
2634
|
+
tab: finalized.tab
|
|
2635
|
+
};
|
|
2237
2636
|
}
|
|
2238
|
-
case "
|
|
2239
|
-
|
|
2637
|
+
case "sessionBinding.listTabs": {
|
|
2638
|
+
const listed = await bindingManager.listTabs(String(params.bindingId ?? ""));
|
|
2639
|
+
return {
|
|
2640
|
+
browser: listed.binding,
|
|
2641
|
+
tabs: listed.tabs
|
|
2642
|
+
};
|
|
2240
2643
|
}
|
|
2241
|
-
case "
|
|
2242
|
-
|
|
2644
|
+
case "sessionBinding.getActiveTab": {
|
|
2645
|
+
const active = await bindingManager.getActiveTab(String(params.bindingId ?? ""));
|
|
2646
|
+
return {
|
|
2647
|
+
browser: active.binding,
|
|
2648
|
+
tab: active.tab
|
|
2649
|
+
};
|
|
2243
2650
|
}
|
|
2244
|
-
case "
|
|
2245
|
-
const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.
|
|
2651
|
+
case "sessionBinding.setActiveTab": {
|
|
2652
|
+
const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.bindingId ?? ""));
|
|
2246
2653
|
void ensureNetworkDebugger(result.tab.id).catch(() => void 0);
|
|
2247
|
-
return
|
|
2654
|
+
return {
|
|
2655
|
+
browser: result.binding,
|
|
2656
|
+
tab: result.tab
|
|
2657
|
+
};
|
|
2248
2658
|
}
|
|
2249
|
-
case "
|
|
2250
|
-
|
|
2659
|
+
case "sessionBinding.focus": {
|
|
2660
|
+
const result = await bindingManager.focus(String(params.bindingId ?? ""));
|
|
2661
|
+
return {
|
|
2662
|
+
ok: true,
|
|
2663
|
+
browser: result.binding
|
|
2664
|
+
};
|
|
2251
2665
|
}
|
|
2252
|
-
case "
|
|
2666
|
+
case "sessionBinding.reset": {
|
|
2253
2667
|
return await preserveHumanFocus(params.focus !== true, async () => {
|
|
2254
|
-
|
|
2255
|
-
|
|
2668
|
+
const result = await bindingManager.reset({
|
|
2669
|
+
bindingId: String(params.bindingId ?? ""),
|
|
2256
2670
|
focus: params.focus === true,
|
|
2257
2671
|
initialUrl: typeof params.url === "string" ? params.url : void 0
|
|
2258
2672
|
});
|
|
2673
|
+
return {
|
|
2674
|
+
browser: result.binding,
|
|
2675
|
+
created: result.created,
|
|
2676
|
+
repaired: result.repaired,
|
|
2677
|
+
repairActions: result.repairActions
|
|
2678
|
+
};
|
|
2259
2679
|
});
|
|
2260
2680
|
}
|
|
2261
|
-
case "
|
|
2262
|
-
return await bindingManager.close(String(params.
|
|
2681
|
+
case "sessionBinding.close": {
|
|
2682
|
+
return await bindingManager.close(String(params.bindingId ?? ""));
|
|
2263
2683
|
}
|
|
2264
2684
|
case "page.goto": {
|
|
2265
2685
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
@@ -2562,7 +2982,7 @@
|
|
|
2562
2982
|
if (!first) {
|
|
2563
2983
|
throw toError("E_EXECUTION", "network replay returned no response payload");
|
|
2564
2984
|
}
|
|
2565
|
-
return first;
|
|
2985
|
+
return params.withSchema === "auto" && params.mode === "json" ? await enrichReplayWithSchema(tab.id, first) : first;
|
|
2566
2986
|
});
|
|
2567
2987
|
}
|
|
2568
2988
|
case "page.freshness": {
|
|
@@ -2638,15 +3058,17 @@
|
|
|
2638
3058
|
const tab = await withTab(target);
|
|
2639
3059
|
await ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2640
3060
|
const inspection = await collectPageInspection(tab.id, params);
|
|
3061
|
+
const pageDataCandidates = await probePageDataCandidatesForTab(tab.id, inspection);
|
|
2641
3062
|
const network = listNetworkEntries(tab.id, { limit: 10 });
|
|
2642
3063
|
return {
|
|
2643
3064
|
suspiciousGlobals: inspection.suspiciousGlobals ?? [],
|
|
2644
3065
|
tables: inspection.tables ?? [],
|
|
2645
3066
|
visibleTimestamps: inspection.visibleTimestamps ?? [],
|
|
2646
3067
|
inlineTimestamps: inspection.inlineTimestamps ?? [],
|
|
3068
|
+
pageDataCandidates,
|
|
2647
3069
|
recentNetwork: network,
|
|
2648
3070
|
recommendedNextSteps: [
|
|
2649
|
-
"bak page extract --path table_data",
|
|
3071
|
+
"bak page extract --path table_data --resolver auto",
|
|
2650
3072
|
"bak network search --pattern table_data",
|
|
2651
3073
|
"bak page freshness"
|
|
2652
3074
|
]
|
|
@@ -2663,6 +3085,7 @@
|
|
|
2663
3085
|
lastMutationAt: inspection.lastMutationAt ?? null,
|
|
2664
3086
|
timers: inspection.timers ?? { timeouts: 0, intervals: 0 },
|
|
2665
3087
|
networkCount: network.length,
|
|
3088
|
+
networkCadence: summarizeNetworkCadence(network),
|
|
2666
3089
|
recentNetwork: network.slice(0, 10)
|
|
2667
3090
|
};
|
|
2668
3091
|
});
|
|
@@ -2673,7 +3096,10 @@
|
|
|
2673
3096
|
const freshness = await buildFreshnessForTab(tab.id, params);
|
|
2674
3097
|
return {
|
|
2675
3098
|
...freshness,
|
|
2676
|
-
lagMs: typeof freshness.latestNetworkTimestamp === "number" && typeof freshness.latestInlineDataTimestamp === "number" ? Math.max(
|
|
3099
|
+
lagMs: typeof freshness.latestNetworkTimestamp === "number" && typeof (freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp) === "number" ? Math.max(
|
|
3100
|
+
0,
|
|
3101
|
+
freshness.latestNetworkTimestamp - (freshness.latestPageDataTimestamp ?? freshness.latestInlineDataTimestamp ?? freshness.latestNetworkTimestamp)
|
|
3102
|
+
) : null
|
|
2677
3103
|
};
|
|
2678
3104
|
});
|
|
2679
3105
|
}
|
|
@@ -2777,7 +3203,7 @@
|
|
|
2777
3203
|
ws?.send(JSON.stringify({
|
|
2778
3204
|
type: "hello",
|
|
2779
3205
|
role: "extension",
|
|
2780
|
-
version: "0.6.
|
|
3206
|
+
version: "0.6.1",
|
|
2781
3207
|
ts: Date.now()
|
|
2782
3208
|
}));
|
|
2783
3209
|
});
|
|
@@ -2813,48 +3239,48 @@
|
|
|
2813
3239
|
}
|
|
2814
3240
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
2815
3241
|
dropNetworkCapture(tabId);
|
|
2816
|
-
void
|
|
2817
|
-
for (const state of
|
|
3242
|
+
void mutateSessionBindingStateMap((stateMap) => {
|
|
3243
|
+
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2818
3244
|
if (!state.tabIds.includes(tabId)) {
|
|
2819
3245
|
continue;
|
|
2820
3246
|
}
|
|
2821
3247
|
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
2822
|
-
|
|
3248
|
+
stateMap[bindingId] = {
|
|
2823
3249
|
...state,
|
|
2824
3250
|
tabIds: nextTabIds,
|
|
2825
3251
|
activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
|
|
2826
3252
|
primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
|
|
2827
|
-
}
|
|
3253
|
+
};
|
|
2828
3254
|
}
|
|
2829
3255
|
});
|
|
2830
3256
|
});
|
|
2831
3257
|
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
2832
|
-
void
|
|
2833
|
-
for (const state of
|
|
3258
|
+
void mutateSessionBindingStateMap((stateMap) => {
|
|
3259
|
+
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2834
3260
|
if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
|
|
2835
3261
|
continue;
|
|
2836
3262
|
}
|
|
2837
|
-
|
|
3263
|
+
stateMap[bindingId] = {
|
|
2838
3264
|
...state,
|
|
2839
3265
|
activeTabId: activeInfo.tabId
|
|
2840
|
-
}
|
|
3266
|
+
};
|
|
2841
3267
|
}
|
|
2842
3268
|
});
|
|
2843
3269
|
});
|
|
2844
3270
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
2845
|
-
void
|
|
2846
|
-
for (const state of
|
|
3271
|
+
void mutateSessionBindingStateMap((stateMap) => {
|
|
3272
|
+
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2847
3273
|
if (state.windowId !== windowId) {
|
|
2848
3274
|
continue;
|
|
2849
3275
|
}
|
|
2850
|
-
|
|
3276
|
+
stateMap[bindingId] = {
|
|
2851
3277
|
...state,
|
|
2852
3278
|
windowId: null,
|
|
2853
3279
|
groupId: null,
|
|
2854
3280
|
tabIds: [],
|
|
2855
3281
|
activeTabId: null,
|
|
2856
3282
|
primaryTabId: null
|
|
2857
|
-
}
|
|
3283
|
+
};
|
|
2858
3284
|
}
|
|
2859
3285
|
});
|
|
2860
3286
|
});
|