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