tab-bridge 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +263 -10
- package/dist/{chunk-42VOZR6E.js → chunk-4JDWAUYM.js} +216 -93
- package/dist/{chunk-BQCNBNBT.cjs → chunk-TGEXRVAL.cjs} +219 -92
- package/dist/index.cjs +41 -25
- package/dist/index.d.cts +112 -5
- package/dist/index.d.ts +112 -5
- package/dist/index.js +2 -2
- package/dist/instance-hvEUHx6i.d.cts +194 -0
- package/dist/instance-hvEUHx6i.d.ts +194 -0
- package/dist/options-DmHyGTL0.d.cts +93 -0
- package/dist/options-iN7Rnvwj.d.ts +93 -0
- package/dist/react/index.cjs +82 -9
- package/dist/react/index.d.cts +48 -2
- package/dist/react/index.d.ts +48 -2
- package/dist/react/index.js +79 -9
- package/dist/zustand/index.cjs +80 -0
- package/dist/zustand/index.d.cts +87 -0
- package/dist/zustand/index.d.ts +87 -0
- package/dist/zustand/index.js +78 -0
- package/package.json +22 -6
- package/dist/types-BtK4ixKz.d.cts +0 -306
- package/dist/types-BtK4ixKz.d.ts +0 -306
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// src/types.ts
|
|
3
|
+
// src/types/messages.ts
|
|
4
4
|
var PROTOCOL_VERSION = 1;
|
|
5
5
|
|
|
6
6
|
// src/utils/id.ts
|
|
@@ -15,18 +15,56 @@ function generateTabId() {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// src/utils/
|
|
19
|
-
var
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
// src/utils/errors.ts
|
|
19
|
+
var ErrorCode = {
|
|
20
|
+
CHANNEL_CLOSED: "CHANNEL_CLOSED",
|
|
21
|
+
CHANNEL_SEND_FAILED: "CHANNEL_SEND_FAILED",
|
|
22
|
+
RPC_TIMEOUT: "RPC_TIMEOUT",
|
|
23
|
+
RPC_NO_HANDLER: "RPC_NO_HANDLER",
|
|
24
|
+
RPC_NO_LEADER: "RPC_NO_LEADER",
|
|
25
|
+
RPC_HANDLER_ERROR: "RPC_HANDLER_ERROR",
|
|
26
|
+
RPC_DESTROYED: "RPC_DESTROYED",
|
|
27
|
+
STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED",
|
|
28
|
+
MIDDLEWARE_REJECTED: "MIDDLEWARE_REJECTED",
|
|
29
|
+
ALREADY_DESTROYED: "ALREADY_DESTROYED"
|
|
30
|
+
};
|
|
31
|
+
var TabSyncError = class _TabSyncError extends Error {
|
|
32
|
+
constructor(message, code, cause) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.name = "TabSyncError";
|
|
36
|
+
this.cause = cause;
|
|
26
37
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
static timeout(method, ms) {
|
|
39
|
+
return new _TabSyncError(
|
|
40
|
+
`RPC "${method}" timed out after ${ms}ms`,
|
|
41
|
+
ErrorCode.RPC_TIMEOUT
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
static noLeader() {
|
|
45
|
+
return new _TabSyncError("No leader available", ErrorCode.RPC_NO_LEADER);
|
|
46
|
+
}
|
|
47
|
+
static noHandler(method) {
|
|
48
|
+
return new _TabSyncError(
|
|
49
|
+
`No handler registered for "${method}"`,
|
|
50
|
+
ErrorCode.RPC_NO_HANDLER
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
static destroyed() {
|
|
54
|
+
return new _TabSyncError("Instance has been destroyed", ErrorCode.ALREADY_DESTROYED);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/utils/logger.ts
|
|
59
|
+
function createLogger(enabled, tabId) {
|
|
60
|
+
if (!enabled) return { log: (() => {
|
|
61
|
+
}) };
|
|
62
|
+
const prefix = `%c[tab-sync:${tabId.slice(0, 8)}]`;
|
|
63
|
+
const style = "color:#818cf8;font-weight:600";
|
|
64
|
+
return {
|
|
65
|
+
log: (label, ...args) => console.log(prefix, style, label, ...args)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
30
68
|
|
|
31
69
|
// src/channels/broadcast.ts
|
|
32
70
|
var BroadcastChannelTransport = class {
|
|
@@ -52,21 +90,36 @@ var BroadcastChannelTransport = class {
|
|
|
52
90
|
}
|
|
53
91
|
};
|
|
54
92
|
|
|
93
|
+
// src/utils/env.ts
|
|
94
|
+
var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
95
|
+
var hasDocument = typeof document !== "undefined";
|
|
96
|
+
var hasLocalStorage = (() => {
|
|
97
|
+
try {
|
|
98
|
+
return typeof localStorage !== "undefined" && localStorage !== null;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
var hasBroadcastChannel = typeof BroadcastChannel !== "undefined";
|
|
104
|
+
var hasCrypto = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function";
|
|
105
|
+
|
|
55
106
|
// src/channels/storage.ts
|
|
56
107
|
var KEY_PREFIX = "__tab_sync__";
|
|
57
108
|
var StorageChannel = class {
|
|
58
|
-
constructor(channelName) {
|
|
109
|
+
constructor(channelName, onError) {
|
|
59
110
|
this.listeners = /* @__PURE__ */ new Set();
|
|
60
111
|
this.closed = false;
|
|
61
112
|
this.seq = 0;
|
|
62
113
|
this.key = `${KEY_PREFIX}${channelName}`;
|
|
114
|
+
this.onError = onError;
|
|
63
115
|
}
|
|
64
116
|
postMessage(message) {
|
|
65
117
|
if (this.closed || !hasLocalStorage) return;
|
|
66
118
|
try {
|
|
67
119
|
const wrapped = JSON.stringify({ m: message, s: this.seq++ });
|
|
68
120
|
localStorage.setItem(this.key, wrapped);
|
|
69
|
-
} catch {
|
|
121
|
+
} catch (e) {
|
|
122
|
+
this.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
70
123
|
}
|
|
71
124
|
}
|
|
72
125
|
onMessage(callback) {
|
|
@@ -106,9 +159,9 @@ var StorageChannel = class {
|
|
|
106
159
|
};
|
|
107
160
|
|
|
108
161
|
// src/channels/channel.ts
|
|
109
|
-
function createChannel(channelName, transport) {
|
|
162
|
+
function createChannel(channelName, transport, onError) {
|
|
110
163
|
if (transport === "local-storage") {
|
|
111
|
-
return new StorageChannel(channelName);
|
|
164
|
+
return new StorageChannel(channelName, onError);
|
|
112
165
|
}
|
|
113
166
|
if (transport === "broadcast-channel") {
|
|
114
167
|
return new BroadcastChannelTransport(channelName);
|
|
@@ -116,7 +169,7 @@ function createChannel(channelName, transport) {
|
|
|
116
169
|
if (typeof BroadcastChannel !== "undefined") {
|
|
117
170
|
return new BroadcastChannelTransport(channelName);
|
|
118
171
|
}
|
|
119
|
-
return new StorageChannel(channelName);
|
|
172
|
+
return new StorageChannel(channelName, onError);
|
|
120
173
|
}
|
|
121
174
|
|
|
122
175
|
// src/utils/timestamp.ts
|
|
@@ -358,7 +411,21 @@ var StateManager = class {
|
|
|
358
411
|
for (const [key, remote] of Object.entries(state)) {
|
|
359
412
|
const local = this.state.get(key);
|
|
360
413
|
if (!local || remote.timestamp > local.timestamp) {
|
|
361
|
-
|
|
414
|
+
let finalValue = remote.value;
|
|
415
|
+
if (this.mergeFn && local) {
|
|
416
|
+
finalValue = this.mergeFn(local.value, remote.value, key);
|
|
417
|
+
}
|
|
418
|
+
if (this.interceptRemote) {
|
|
419
|
+
const result = this.interceptRemote(
|
|
420
|
+
key,
|
|
421
|
+
finalValue,
|
|
422
|
+
local?.value,
|
|
423
|
+
meta
|
|
424
|
+
);
|
|
425
|
+
if (result === false) continue;
|
|
426
|
+
if (result && "value" in result) finalValue = result.value;
|
|
427
|
+
}
|
|
428
|
+
this.state.set(key, { value: finalValue, timestamp: remote.timestamp });
|
|
362
429
|
changedKeys.push(key);
|
|
363
430
|
}
|
|
364
431
|
}
|
|
@@ -374,15 +441,15 @@ var StateManager = class {
|
|
|
374
441
|
}
|
|
375
442
|
notifyKey(key, value, meta) {
|
|
376
443
|
const listeners = this.keyListeners.get(key);
|
|
377
|
-
if (!listeners) return;
|
|
378
|
-
for (const cb of listeners) {
|
|
444
|
+
if (!listeners || listeners.size === 0) return;
|
|
445
|
+
for (const cb of [...listeners]) {
|
|
379
446
|
cb(value, meta);
|
|
380
447
|
}
|
|
381
448
|
}
|
|
382
449
|
notifyChange(changedKeys, meta) {
|
|
383
450
|
if (this.changeListeners.size === 0) return;
|
|
384
451
|
const snapshot = this.getAll();
|
|
385
|
-
for (const cb of this.changeListeners) {
|
|
452
|
+
for (const cb of [...this.changeListeners]) {
|
|
386
453
|
cb(snapshot, changedKeys, meta);
|
|
387
454
|
}
|
|
388
455
|
}
|
|
@@ -589,6 +656,8 @@ var LeaderElection = class {
|
|
|
589
656
|
this.leaderWatchTimer = null;
|
|
590
657
|
this.lastLeaderHeartbeat = 0;
|
|
591
658
|
this.electing = false;
|
|
659
|
+
this.generation = 0;
|
|
660
|
+
this.currentClaimId = null;
|
|
592
661
|
this.leaderCallbacks = /* @__PURE__ */ new Set();
|
|
593
662
|
this.leaderCleanups = /* @__PURE__ */ new Map();
|
|
594
663
|
this.send = options.send;
|
|
@@ -633,7 +702,7 @@ var LeaderElection = class {
|
|
|
633
702
|
this.handleClaim(message.payload, message.senderId);
|
|
634
703
|
break;
|
|
635
704
|
case "LEADER_ACK":
|
|
636
|
-
this.handleAck(message.senderId);
|
|
705
|
+
this.handleAck(message.payload, message.senderId);
|
|
637
706
|
break;
|
|
638
707
|
case "LEADER_HEARTBEAT":
|
|
639
708
|
this.handleHeartbeat(message.senderId);
|
|
@@ -663,11 +732,17 @@ var LeaderElection = class {
|
|
|
663
732
|
startElection() {
|
|
664
733
|
if (this.electing) return;
|
|
665
734
|
this.electing = true;
|
|
735
|
+
this.generation++;
|
|
736
|
+
this.currentClaimId = generateTabId();
|
|
666
737
|
this.send({
|
|
667
738
|
type: "LEADER_CLAIM",
|
|
668
739
|
senderId: this.tabId,
|
|
669
740
|
timestamp: monotonic(),
|
|
670
|
-
payload: {
|
|
741
|
+
payload: {
|
|
742
|
+
createdAt: this.tabCreatedAt,
|
|
743
|
+
claimId: this.currentClaimId,
|
|
744
|
+
generation: this.generation
|
|
745
|
+
}
|
|
671
746
|
});
|
|
672
747
|
this.electionTimer = setTimeout(() => {
|
|
673
748
|
this.electionTimer = null;
|
|
@@ -683,9 +758,11 @@ var LeaderElection = class {
|
|
|
683
758
|
this.electing = false;
|
|
684
759
|
}
|
|
685
760
|
}
|
|
686
|
-
handleAck(senderId) {
|
|
761
|
+
handleAck(payload, senderId) {
|
|
762
|
+
if (payload.generation < this.generation) return;
|
|
687
763
|
this.clearElectionTimer();
|
|
688
764
|
this.electing = false;
|
|
765
|
+
this.generation = Math.max(this.generation, payload.generation);
|
|
689
766
|
this.setLeader(senderId);
|
|
690
767
|
}
|
|
691
768
|
handleHeartbeat(senderId) {
|
|
@@ -707,7 +784,10 @@ var LeaderElection = class {
|
|
|
707
784
|
type: "LEADER_ACK",
|
|
708
785
|
senderId: this.tabId,
|
|
709
786
|
timestamp: monotonic(),
|
|
710
|
-
payload:
|
|
787
|
+
payload: {
|
|
788
|
+
claimId: this.currentClaimId,
|
|
789
|
+
generation: this.generation
|
|
790
|
+
}
|
|
711
791
|
});
|
|
712
792
|
this.startHeartbeat();
|
|
713
793
|
}
|
|
@@ -717,7 +797,7 @@ var LeaderElection = class {
|
|
|
717
797
|
this.lastLeaderHeartbeat = Date.now();
|
|
718
798
|
if (this.isLeader() && !wasLeader) {
|
|
719
799
|
this.startHeartbeat();
|
|
720
|
-
for (const cb of this.leaderCallbacks) {
|
|
800
|
+
for (const cb of [...this.leaderCallbacks]) {
|
|
721
801
|
const cleanup = cb();
|
|
722
802
|
if (typeof cleanup === "function") {
|
|
723
803
|
this.leaderCleanups.set(cb, cleanup);
|
|
@@ -788,46 +868,6 @@ var LeaderElection = class {
|
|
|
788
868
|
}
|
|
789
869
|
};
|
|
790
870
|
|
|
791
|
-
// src/utils/errors.ts
|
|
792
|
-
var ErrorCode = {
|
|
793
|
-
CHANNEL_CLOSED: "CHANNEL_CLOSED",
|
|
794
|
-
CHANNEL_SEND_FAILED: "CHANNEL_SEND_FAILED",
|
|
795
|
-
RPC_TIMEOUT: "RPC_TIMEOUT",
|
|
796
|
-
RPC_NO_HANDLER: "RPC_NO_HANDLER",
|
|
797
|
-
RPC_NO_LEADER: "RPC_NO_LEADER",
|
|
798
|
-
RPC_HANDLER_ERROR: "RPC_HANDLER_ERROR",
|
|
799
|
-
RPC_DESTROYED: "RPC_DESTROYED",
|
|
800
|
-
STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED",
|
|
801
|
-
MIDDLEWARE_REJECTED: "MIDDLEWARE_REJECTED",
|
|
802
|
-
ALREADY_DESTROYED: "ALREADY_DESTROYED"
|
|
803
|
-
};
|
|
804
|
-
var TabSyncError = class _TabSyncError extends Error {
|
|
805
|
-
constructor(message, code, cause) {
|
|
806
|
-
super(message);
|
|
807
|
-
this.code = code;
|
|
808
|
-
this.name = "TabSyncError";
|
|
809
|
-
this.cause = cause;
|
|
810
|
-
}
|
|
811
|
-
static timeout(method, ms) {
|
|
812
|
-
return new _TabSyncError(
|
|
813
|
-
`RPC "${method}" timed out after ${ms}ms`,
|
|
814
|
-
ErrorCode.RPC_TIMEOUT
|
|
815
|
-
);
|
|
816
|
-
}
|
|
817
|
-
static noLeader() {
|
|
818
|
-
return new _TabSyncError("No leader available", ErrorCode.RPC_NO_LEADER);
|
|
819
|
-
}
|
|
820
|
-
static noHandler(method) {
|
|
821
|
-
return new _TabSyncError(
|
|
822
|
-
`No handler registered for "${method}"`,
|
|
823
|
-
ErrorCode.RPC_NO_HANDLER
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
static destroyed() {
|
|
827
|
-
return new _TabSyncError("Instance has been destroyed", ErrorCode.ALREADY_DESTROYED);
|
|
828
|
-
}
|
|
829
|
-
};
|
|
830
|
-
|
|
831
871
|
// src/core/rpc.ts
|
|
832
872
|
var DEFAULT_TIMEOUT = 5e3;
|
|
833
873
|
var RPCHandler = class {
|
|
@@ -837,6 +877,7 @@ var RPCHandler = class {
|
|
|
837
877
|
this.send = options.send;
|
|
838
878
|
this.tabId = options.tabId;
|
|
839
879
|
this.resolveLeaderId = options.resolveLeaderId ?? (() => null);
|
|
880
|
+
this.resolveTabIds = options.resolveTabIds ?? (() => []);
|
|
840
881
|
this.onError = options.onError ?? (() => {
|
|
841
882
|
});
|
|
842
883
|
}
|
|
@@ -865,6 +906,15 @@ var RPCHandler = class {
|
|
|
865
906
|
});
|
|
866
907
|
});
|
|
867
908
|
}
|
|
909
|
+
callAll(method, args, timeout = DEFAULT_TIMEOUT) {
|
|
910
|
+
const tabIds = this.resolveTabIds().filter((id) => id !== this.tabId);
|
|
911
|
+
if (tabIds.length === 0) return Promise.resolve([]);
|
|
912
|
+
return Promise.all(
|
|
913
|
+
tabIds.map(
|
|
914
|
+
(targetId) => this.call(targetId, method, args, timeout).then((result) => ({ tabId: targetId, result })).catch((err) => ({ tabId: targetId, error: err.message }))
|
|
915
|
+
)
|
|
916
|
+
);
|
|
917
|
+
}
|
|
868
918
|
handle(method, handler) {
|
|
869
919
|
this.handlers.set(
|
|
870
920
|
method,
|
|
@@ -933,13 +983,32 @@ var RPCHandler = class {
|
|
|
933
983
|
}
|
|
934
984
|
}
|
|
935
985
|
sendResponse(targetId, callId, result, error) {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
986
|
+
try {
|
|
987
|
+
this.send({
|
|
988
|
+
type: "RPC_RESPONSE",
|
|
989
|
+
senderId: this.tabId,
|
|
990
|
+
targetId,
|
|
991
|
+
timestamp: monotonic(),
|
|
992
|
+
payload: { callId, result, error }
|
|
993
|
+
});
|
|
994
|
+
} catch (e) {
|
|
995
|
+
const serErr = new TabSyncError(
|
|
996
|
+
`Failed to serialize RPC response for "${callId}": ${e instanceof Error ? e.message : String(e)}`,
|
|
997
|
+
ErrorCode.CHANNEL_SEND_FAILED,
|
|
998
|
+
e
|
|
999
|
+
);
|
|
1000
|
+
this.onError(serErr);
|
|
1001
|
+
try {
|
|
1002
|
+
this.send({
|
|
1003
|
+
type: "RPC_RESPONSE",
|
|
1004
|
+
senderId: this.tabId,
|
|
1005
|
+
targetId,
|
|
1006
|
+
timestamp: monotonic(),
|
|
1007
|
+
payload: { callId, result: void 0, error: serErr.message }
|
|
1008
|
+
});
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
943
1012
|
}
|
|
944
1013
|
};
|
|
945
1014
|
|
|
@@ -970,7 +1039,8 @@ function destroyMiddleware(middlewares) {
|
|
|
970
1039
|
}
|
|
971
1040
|
}
|
|
972
1041
|
|
|
973
|
-
// src/core/
|
|
1042
|
+
// src/core/persist.ts
|
|
1043
|
+
var DEFAULT_KEY = "tab-sync:state";
|
|
974
1044
|
function resolvePersistOptions(opt) {
|
|
975
1045
|
if (!opt) return null;
|
|
976
1046
|
if (opt === true) return {};
|
|
@@ -979,12 +1049,23 @@ function resolvePersistOptions(opt) {
|
|
|
979
1049
|
function loadPersistedState(opts) {
|
|
980
1050
|
const storage = opts.storage ?? (hasLocalStorage ? localStorage : null);
|
|
981
1051
|
if (!storage) return {};
|
|
982
|
-
const key = opts.key ??
|
|
1052
|
+
const key = opts.key ?? DEFAULT_KEY;
|
|
1053
|
+
const versionKey = `${key}:version`;
|
|
983
1054
|
const deserialize = opts.deserialize ?? JSON.parse;
|
|
984
1055
|
try {
|
|
985
1056
|
const raw = storage.getItem(key);
|
|
986
1057
|
if (!raw) return {};
|
|
987
|
-
|
|
1058
|
+
let parsed = deserialize(raw);
|
|
1059
|
+
if (opts.version !== void 0 && opts.migrate) {
|
|
1060
|
+
const rawVersion = storage.getItem(versionKey);
|
|
1061
|
+
const oldVersion = rawVersion ? Number(rawVersion) : 0;
|
|
1062
|
+
if (oldVersion !== opts.version) {
|
|
1063
|
+
parsed = opts.migrate(parsed, oldVersion);
|
|
1064
|
+
const serialize = opts.serialize ?? JSON.stringify;
|
|
1065
|
+
storage.setItem(key, serialize(parsed));
|
|
1066
|
+
storage.setItem(versionKey, String(opts.version));
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
988
1069
|
return filterPersistKeys(parsed, opts);
|
|
989
1070
|
} catch {
|
|
990
1071
|
return {};
|
|
@@ -1008,9 +1089,10 @@ function createPersistSaver(opts, onError) {
|
|
|
1008
1089
|
}, flush() {
|
|
1009
1090
|
}, destroy() {
|
|
1010
1091
|
} };
|
|
1011
|
-
const key = opts.key ??
|
|
1092
|
+
const key = opts.key ?? DEFAULT_KEY;
|
|
1012
1093
|
const serialize = opts.serialize ?? JSON.stringify;
|
|
1013
1094
|
const debounce = opts.debounce ?? 100;
|
|
1095
|
+
const versionKey = `${key}:version`;
|
|
1014
1096
|
let timer = null;
|
|
1015
1097
|
let latestState = null;
|
|
1016
1098
|
function doSave() {
|
|
@@ -1018,6 +1100,9 @@ function createPersistSaver(opts, onError) {
|
|
|
1018
1100
|
try {
|
|
1019
1101
|
const filtered = filterPersistKeys({ ...latestState }, opts);
|
|
1020
1102
|
storage.setItem(key, serialize(filtered));
|
|
1103
|
+
if (opts.version !== void 0) {
|
|
1104
|
+
storage.setItem(versionKey, String(opts.version));
|
|
1105
|
+
}
|
|
1021
1106
|
} catch (e) {
|
|
1022
1107
|
onError(e instanceof Error ? e : new Error(String(e)));
|
|
1023
1108
|
}
|
|
@@ -1049,15 +1134,8 @@ function createPersistSaver(opts, onError) {
|
|
|
1049
1134
|
}
|
|
1050
1135
|
};
|
|
1051
1136
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
}) };
|
|
1055
|
-
const prefix = `%c[tab-sync:${tabId.slice(0, 8)}]`;
|
|
1056
|
-
const style = "color:#818cf8;font-weight:600";
|
|
1057
|
-
return {
|
|
1058
|
-
log: (label, ...args) => console.log(prefix, style, label, ...args)
|
|
1059
|
-
};
|
|
1060
|
-
}
|
|
1137
|
+
|
|
1138
|
+
// src/core/tab-sync.ts
|
|
1061
1139
|
function createTabSync(options) {
|
|
1062
1140
|
const opts = options ?? {};
|
|
1063
1141
|
const tabId = generateTabId();
|
|
@@ -1084,7 +1162,7 @@ function createTabSync(options) {
|
|
|
1084
1162
|
initialState = { ...initialState, ...restored };
|
|
1085
1163
|
}
|
|
1086
1164
|
}
|
|
1087
|
-
const channel = createChannel(channelName, opts.transport);
|
|
1165
|
+
const channel = createChannel(channelName, opts.transport, onError);
|
|
1088
1166
|
const { log } = createLogger(debug, tabId);
|
|
1089
1167
|
const send = (message) => {
|
|
1090
1168
|
log("\u2192", message.type, message.payload);
|
|
@@ -1121,6 +1199,7 @@ function createTabSync(options) {
|
|
|
1121
1199
|
send,
|
|
1122
1200
|
tabId,
|
|
1123
1201
|
resolveLeaderId: () => election?.getLeaderId() ?? null,
|
|
1202
|
+
resolveTabIds: () => registry.getTabs().map((t) => t.id),
|
|
1124
1203
|
onError
|
|
1125
1204
|
});
|
|
1126
1205
|
const unsubChannel = channel.onMessage((message) => {
|
|
@@ -1161,7 +1240,11 @@ function createTabSync(options) {
|
|
|
1161
1240
|
election?.start();
|
|
1162
1241
|
let ready = true;
|
|
1163
1242
|
let destroyed = false;
|
|
1243
|
+
function assertAlive() {
|
|
1244
|
+
if (destroyed) throw TabSyncError.destroyed();
|
|
1245
|
+
}
|
|
1164
1246
|
function middlewareSet(key, value) {
|
|
1247
|
+
assertAlive();
|
|
1165
1248
|
if (middlewares.length === 0) {
|
|
1166
1249
|
stateManager.set(key, value);
|
|
1167
1250
|
if (persister) persister.save(stateManager.getAll());
|
|
@@ -1183,6 +1266,7 @@ function createTabSync(options) {
|
|
|
1183
1266
|
if (persister) persister.save(stateManager.getAll());
|
|
1184
1267
|
}
|
|
1185
1268
|
function middlewarePatch(partial) {
|
|
1269
|
+
assertAlive();
|
|
1186
1270
|
if (middlewares.length === 0) {
|
|
1187
1271
|
stateManager.patch(partial);
|
|
1188
1272
|
if (persister) persister.save(stateManager.getAll());
|
|
@@ -1218,6 +1302,13 @@ function createTabSync(options) {
|
|
|
1218
1302
|
getAll: () => stateManager.getAll(),
|
|
1219
1303
|
set: middlewareSet,
|
|
1220
1304
|
patch: middlewarePatch,
|
|
1305
|
+
transaction: (fn) => {
|
|
1306
|
+
assertAlive();
|
|
1307
|
+
const current = stateManager.getAll();
|
|
1308
|
+
const result = fn(current);
|
|
1309
|
+
if (result === null) return;
|
|
1310
|
+
middlewarePatch(result);
|
|
1311
|
+
},
|
|
1221
1312
|
// Subscriptions
|
|
1222
1313
|
on: (key, callback) => stateManager.on(key, callback),
|
|
1223
1314
|
once: (key, callback) => {
|
|
@@ -1228,15 +1319,30 @@ function createTabSync(options) {
|
|
|
1228
1319
|
return unsub;
|
|
1229
1320
|
},
|
|
1230
1321
|
onChange: (callback) => stateManager.onChange(callback),
|
|
1231
|
-
select: (selector, callback,
|
|
1322
|
+
select: (selector, callback, options2) => {
|
|
1323
|
+
const isEqual = options2?.isEqual ?? Object.is;
|
|
1324
|
+
const debounceMs = options2?.debounce;
|
|
1232
1325
|
let prev = selector(stateManager.getAll());
|
|
1233
|
-
|
|
1326
|
+
let debounceTimer = null;
|
|
1327
|
+
const unsub = stateManager.onChange((state, _keys, meta) => {
|
|
1234
1328
|
const next = selector(state);
|
|
1235
1329
|
if (!isEqual(prev, next)) {
|
|
1236
1330
|
prev = next;
|
|
1237
|
-
|
|
1331
|
+
if (debounceMs !== void 0 && debounceMs > 0) {
|
|
1332
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1333
|
+
debounceTimer = setTimeout(() => {
|
|
1334
|
+
debounceTimer = null;
|
|
1335
|
+
callback(prev, meta);
|
|
1336
|
+
}, debounceMs);
|
|
1337
|
+
} else {
|
|
1338
|
+
callback(next, meta);
|
|
1339
|
+
}
|
|
1238
1340
|
}
|
|
1239
1341
|
});
|
|
1342
|
+
return () => {
|
|
1343
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1344
|
+
unsub();
|
|
1345
|
+
};
|
|
1240
1346
|
},
|
|
1241
1347
|
// Leader
|
|
1242
1348
|
isLeader: () => election?.isLeader() ?? true,
|
|
@@ -1255,6 +1361,13 @@ function createTabSync(options) {
|
|
|
1255
1361
|
return registry.getTab(leaderId) ?? null;
|
|
1256
1362
|
},
|
|
1257
1363
|
waitForLeader: () => {
|
|
1364
|
+
if (!leaderEnabled) {
|
|
1365
|
+
const selfInfo = registry.getTab(tabId);
|
|
1366
|
+
if (selfInfo) return Promise.resolve(selfInfo);
|
|
1367
|
+
return Promise.reject(
|
|
1368
|
+
new TabSyncError("Leader election is disabled", ErrorCode.RPC_NO_LEADER)
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1258
1371
|
const leader = instance.getLeader();
|
|
1259
1372
|
if (leader) return Promise.resolve(leader);
|
|
1260
1373
|
return new Promise((resolve) => {
|
|
@@ -1282,8 +1395,18 @@ function createTabSync(options) {
|
|
|
1282
1395
|
getTabCount: () => registry.getTabCount(),
|
|
1283
1396
|
onTabChange: (callback) => registry.onTabChange(callback),
|
|
1284
1397
|
// RPC
|
|
1285
|
-
call: ((target, method, args, timeout) =>
|
|
1286
|
-
|
|
1398
|
+
call: ((target, method, args, timeout) => {
|
|
1399
|
+
assertAlive();
|
|
1400
|
+
return rpc.call(target, method, args, timeout);
|
|
1401
|
+
}),
|
|
1402
|
+
handle: ((method, handler) => {
|
|
1403
|
+
assertAlive();
|
|
1404
|
+
return rpc.handle(method, handler);
|
|
1405
|
+
}),
|
|
1406
|
+
callAll: ((method, args, timeout) => {
|
|
1407
|
+
assertAlive();
|
|
1408
|
+
return rpc.callAll(method, args, timeout);
|
|
1409
|
+
}),
|
|
1287
1410
|
// Lifecycle
|
|
1288
1411
|
destroy: () => {
|
|
1289
1412
|
if (destroyed) return;
|
|
@@ -1319,6 +1442,8 @@ exports.TabRegistry = TabRegistry;
|
|
|
1319
1442
|
exports.TabSyncError = TabSyncError;
|
|
1320
1443
|
exports.createBatcher = createBatcher;
|
|
1321
1444
|
exports.createChannel = createChannel;
|
|
1445
|
+
exports.createLogger = createLogger;
|
|
1446
|
+
exports.createPersistSaver = createPersistSaver;
|
|
1322
1447
|
exports.createTabSync = createTabSync;
|
|
1323
1448
|
exports.destroyMiddleware = destroyMiddleware;
|
|
1324
1449
|
exports.generateTabId = generateTabId;
|
|
@@ -1327,6 +1452,8 @@ exports.hasCrypto = hasCrypto;
|
|
|
1327
1452
|
exports.hasDocument = hasDocument;
|
|
1328
1453
|
exports.hasLocalStorage = hasLocalStorage;
|
|
1329
1454
|
exports.isBrowser = isBrowser;
|
|
1455
|
+
exports.loadPersistedState = loadPersistedState;
|
|
1330
1456
|
exports.monotonic = monotonic;
|
|
1331
1457
|
exports.notifyMiddleware = notifyMiddleware;
|
|
1458
|
+
exports.resolvePersistOptions = resolvePersistOptions;
|
|
1332
1459
|
exports.runMiddleware = runMiddleware;
|