chromium-tabs 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.
@@ -0,0 +1,1320 @@
1
+ import {
2
+ AddTabFlags,
3
+ NO_TAB,
4
+ TAB_GROUP_COLORS
5
+ } from "../chunk-NDM7JYY6.js";
6
+
7
+ // src/session/command-storage-manager.ts
8
+ var SAVE_DELAY_MS = 2500;
9
+ var CommandStorageManager = class {
10
+ backend_;
11
+ delegate_;
12
+ saveDelayMs_;
13
+ pendingCommands_ = [];
14
+ /** Starts true: the first save is always a complete rewrite (cc:117). */
15
+ pendingReset_ = true;
16
+ commandsSinceReset_ = 0;
17
+ saveTimer_ = null;
18
+ /** Sequenced "task runner" for backend writes. */
19
+ queue_ = Promise.resolve();
20
+ /** Backend operations started but not yet settled. */
21
+ inflight_ = 0;
22
+ constructor(options) {
23
+ this.backend_ = options.backend;
24
+ this.delegate_ = options.delegate ?? {};
25
+ this.saveDelayMs_ = options.saveDelayMs ?? SAVE_DELAY_MS;
26
+ if (options.ready) {
27
+ this.inflight_++;
28
+ this.queue_ = options.ready.then(
29
+ () => {
30
+ this.inflight_--;
31
+ },
32
+ () => {
33
+ this.inflight_--;
34
+ }
35
+ );
36
+ }
37
+ }
38
+ /**
39
+ * Runs a backend operation. When nothing is in flight it runs inline — a
40
+ * synchronous backend then completes before this returns, which is what
41
+ * makes a pagehide flush reliable (the web stand-in for Chrome's
42
+ * BLOCK_SHUTDOWN task traits). Otherwise it is sequenced behind the queue.
43
+ */
44
+ enqueue_(operation) {
45
+ const wasIdle = this.inflight_ === 0;
46
+ this.inflight_++;
47
+ if (wasIdle) {
48
+ let result;
49
+ try {
50
+ result = operation();
51
+ } catch {
52
+ this.inflight_--;
53
+ this.delegate_.onErrorWritingSessionCommands?.();
54
+ return this.queue_;
55
+ }
56
+ if (result && typeof result.then === "function") {
57
+ this.queue_ = result.then(
58
+ () => {
59
+ this.inflight_--;
60
+ },
61
+ () => {
62
+ this.inflight_--;
63
+ this.delegate_.onErrorWritingSessionCommands?.();
64
+ }
65
+ );
66
+ } else {
67
+ this.inflight_--;
68
+ }
69
+ return this.queue_;
70
+ }
71
+ this.queue_ = this.queue_.then(() => operation()).then(
72
+ () => {
73
+ this.inflight_--;
74
+ },
75
+ () => {
76
+ this.inflight_--;
77
+ this.delegate_.onErrorWritingSessionCommands?.();
78
+ }
79
+ );
80
+ return this.queue_;
81
+ }
82
+ get pendingReset() {
83
+ return this.pendingReset_;
84
+ }
85
+ /** Mirrors set_pending_reset (command_storage_manager.h:77). */
86
+ setPendingReset(value) {
87
+ this.pendingReset_ = value;
88
+ }
89
+ get commandsSinceReset() {
90
+ return this.commandsSinceReset_;
91
+ }
92
+ /** Buffers a command and starts the save timer. Mirrors cc:192. */
93
+ scheduleCommand(command) {
94
+ this.commandsSinceReset_++;
95
+ this.pendingCommands_.push(command);
96
+ this.startSaveTimer();
97
+ }
98
+ /** Buffers rebuild commands without starting the timer. Mirrors cc:207. */
99
+ appendRebuildCommands(commands) {
100
+ this.commandsSinceReset_ += commands.length;
101
+ this.pendingCommands_.push(...commands);
102
+ }
103
+ appendRebuildCommand(command) {
104
+ this.appendRebuildCommands([command]);
105
+ }
106
+ /** Removes a not-yet-saved command. Mirrors EraseCommand (cc:216). */
107
+ eraseCommand(command) {
108
+ const i = this.pendingCommands_.indexOf(command);
109
+ if (i === -1) throw new Error("eraseCommand: command is not pending");
110
+ this.pendingCommands_.splice(i, 1);
111
+ this.commandsSinceReset_--;
112
+ }
113
+ /** Replaces a not-yet-saved command in place. Mirrors SwapCommand (cc:225). */
114
+ swapCommand(oldCommand, newCommand) {
115
+ const i = this.pendingCommands_.indexOf(oldCommand);
116
+ if (i === -1) throw new Error("swapCommand: command is not pending");
117
+ this.pendingCommands_[i] = newCommand;
118
+ }
119
+ /** Mirrors ClearPendingCommands (cc:233) — note it does not zero the reset counter. */
120
+ clearPendingCommands() {
121
+ this.commandsSinceReset_ -= this.pendingCommands_.length;
122
+ this.pendingCommands_ = [];
123
+ }
124
+ /** Read-only view for ReplacePendingCommand-style optimizations. */
125
+ get pendingCommands() {
126
+ return this.pendingCommands_;
127
+ }
128
+ /** True between startSaveTimer() and the save it scheduled. Mirrors cc:316. */
129
+ get hasPendingSave() {
130
+ return this.saveTimer_ !== null;
131
+ }
132
+ /** Schedules a save in saveDelayMs unless one is already pending. Mirrors cc:239. */
133
+ startSaveTimer() {
134
+ if (this.saveTimer_ !== null) return;
135
+ this.saveTimer_ = setTimeout(() => {
136
+ this.saveTimer_ = null;
137
+ this.save();
138
+ }, this.saveDelayMs_);
139
+ }
140
+ /**
141
+ * Flushes pending commands to the backend. Mirrors Save (cc:251): the
142
+ * buffer and reset flag are snapshotted synchronously; the write itself is
143
+ * sequenced behind earlier writes. Returns once this write has settled.
144
+ */
145
+ save() {
146
+ if (this.saveTimer_ !== null) {
147
+ clearTimeout(this.saveTimer_);
148
+ this.saveTimer_ = null;
149
+ }
150
+ this.delegate_.onWillSaveCommands?.();
151
+ if (this.pendingCommands_.length === 0) return this.queue_;
152
+ const commands = this.pendingCommands_;
153
+ const truncate = this.pendingReset_;
154
+ this.pendingCommands_ = [];
155
+ if (this.pendingReset_) {
156
+ this.commandsSinceReset_ = 0;
157
+ this.pendingReset_ = false;
158
+ }
159
+ return this.enqueue_(() => this.backend_.appendCommands(commands, truncate));
160
+ }
161
+ /** Cancels any timer and flushes immediately. */
162
+ saveNow() {
163
+ return this.save();
164
+ }
165
+ /**
166
+ * Promotes the current session to "last". Pending commands are flushed
167
+ * first, mirroring MoveCurrentSessionToLastSession (cc:320). The caller is
168
+ * expected to follow up with setPendingReset(true) plus a full re-emit of
169
+ * live state, since the current session is now empty.
170
+ */
171
+ moveCurrentSessionToLastSession() {
172
+ this.save();
173
+ return this.enqueue_(() => this.backend_.moveCurrentSessionToLastSession());
174
+ }
175
+ /** Stops the save timer without flushing. */
176
+ dispose() {
177
+ if (this.saveTimer_ !== null) {
178
+ clearTimeout(this.saveTimer_);
179
+ this.saveTimer_ = null;
180
+ }
181
+ }
182
+ };
183
+
184
+ // src/session/backends/in-memory.ts
185
+ var InMemoryStorageBackend = class {
186
+ current_ = null;
187
+ last_ = null;
188
+ appendCommands(commands, truncate) {
189
+ if (truncate || this.current_ === null) this.current_ = [];
190
+ this.current_.push(...commands.map((c) => ({ ...c })));
191
+ }
192
+ readLastSessionCommands() {
193
+ return { commands: (this.last_ ?? []).map((c) => ({ ...c })), errorReading: false };
194
+ }
195
+ moveCurrentSessionToLastSession() {
196
+ if (this.current_ !== null) {
197
+ this.last_ = this.current_;
198
+ this.current_ = null;
199
+ }
200
+ }
201
+ /** Test hook: the commands persisted for the current session, if any. */
202
+ get currentSessionCommands() {
203
+ return this.current_;
204
+ }
205
+ /** Test hook: the commands persisted for the last session, if any. */
206
+ get lastSessionCommands() {
207
+ return this.last_;
208
+ }
209
+ };
210
+
211
+ // src/session/process-singleton.ts
212
+ var CLAIM_ATTEMPT_DELAYS_MS = [0, 0, 50];
213
+ var WebLocksProcessSingleton = class {
214
+ name_;
215
+ locks_;
216
+ acquirePromise_ = null;
217
+ releaseHold_ = null;
218
+ released_ = false;
219
+ constructor(options) {
220
+ this.name_ = options.name;
221
+ this.locks_ = options.locks ?? (typeof navigator !== "undefined" ? navigator.locks : void 0);
222
+ }
223
+ acquire() {
224
+ if (!this.acquirePromise_) this.acquirePromise_ = this.acquireWithRetries_();
225
+ return this.acquirePromise_;
226
+ }
227
+ /**
228
+ * Claim with bounded retries. Chrome's POSIX singleton retries its claim
229
+ * the same way — kRetryAttempts with timeout/retry_attempts sleeps
230
+ * (process_singleton_posix.cc:137-140, :794) — because a dying holder's
231
+ * lock disappears asynchronously. Web Lock releases also propagate
232
+ * asynchronously (the holder resolves its callback promise), so a realm
233
+ * booting right after another one released (HMR, dispose-then-reconstruct)
234
+ * needs a beat before the lock reads as free. A lock still held after the
235
+ * final attempt belongs to a live realm: stand down.
236
+ */
237
+ async acquireWithRetries_() {
238
+ const locks = this.locks_;
239
+ if (!locks) {
240
+ return this.released_ ? "secondary" : "owner";
241
+ }
242
+ for (const delayMs of CLAIM_ATTEMPT_DELAYS_MS) {
243
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
244
+ if (this.released_) return "secondary";
245
+ const result = await this.tryClaim_(locks);
246
+ if (result === "owner") return "owner";
247
+ }
248
+ return "secondary";
249
+ }
250
+ tryClaim_(locks) {
251
+ return new Promise((resolve) => {
252
+ locks.request(this.name_, { ifAvailable: true }, (lock) => {
253
+ if (lock === null) {
254
+ resolve("secondary");
255
+ return void 0;
256
+ }
257
+ if (this.released_) {
258
+ resolve("secondary");
259
+ return void 0;
260
+ }
261
+ return new Promise((releaseHold) => {
262
+ this.releaseHold_ = releaseHold;
263
+ resolve("owner");
264
+ });
265
+ }).catch(() => {
266
+ resolve("owner");
267
+ });
268
+ });
269
+ }
270
+ release() {
271
+ this.released_ = true;
272
+ if (this.releaseHold_) {
273
+ this.releaseHold_();
274
+ this.releaseHold_ = null;
275
+ }
276
+ }
277
+ };
278
+ function createDefaultProcessSingleton(profileLockName) {
279
+ if (!profileLockName) return null;
280
+ if (typeof navigator === "undefined" || !navigator.locks) return null;
281
+ return new WebLocksProcessSingleton({ name: profileLockName });
282
+ }
283
+
284
+ // src/session/backends/web-storage.ts
285
+ var ENVELOPE_VERSION = 1;
286
+ var WebStorageBackend = class {
287
+ /** Two realms over the same key contend for the same profile singleton. */
288
+ profileLockName;
289
+ storage_;
290
+ currentKey_;
291
+ lastKey_;
292
+ /** Parsed mirror of the current slot, so appends don't re-parse. */
293
+ currentCache_ = null;
294
+ constructor(options = {}) {
295
+ const key = options.key ?? "chromium-tabs/session";
296
+ const storage = options.storage ?? (typeof localStorage !== "undefined" ? localStorage : void 0);
297
+ if (!storage) {
298
+ throw new Error("WebStorageBackend: no Storage available; pass options.storage");
299
+ }
300
+ this.storage_ = storage;
301
+ this.profileLockName = `chromium-tabs-profile/${key}`;
302
+ this.currentKey_ = `${key}/current`;
303
+ this.lastKey_ = `${key}/last`;
304
+ }
305
+ appendCommands(commands, truncate) {
306
+ const existing = truncate ? [] : this.currentCache_ ?? this.readSlot_(this.currentKey_).commands;
307
+ existing.push(...commands);
308
+ this.currentCache_ = existing;
309
+ this.storage_.setItem(
310
+ this.currentKey_,
311
+ JSON.stringify({ version: ENVELOPE_VERSION, commands: existing })
312
+ );
313
+ }
314
+ readLastSessionCommands() {
315
+ return this.readSlot_(this.lastKey_);
316
+ }
317
+ moveCurrentSessionToLastSession() {
318
+ const current = this.storage_.getItem(this.currentKey_);
319
+ if (current !== null) {
320
+ this.storage_.setItem(this.lastKey_, current);
321
+ this.storage_.removeItem(this.currentKey_);
322
+ }
323
+ this.currentCache_ = null;
324
+ }
325
+ readSlot_(key) {
326
+ const raw = this.storage_.getItem(key);
327
+ if (raw === null) return { commands: [], errorReading: false };
328
+ try {
329
+ const parsed = JSON.parse(raw);
330
+ if (parsed === null || parsed.version !== ENVELOPE_VERSION || !Array.isArray(parsed.commands)) {
331
+ return { commands: [], errorReading: true };
332
+ }
333
+ return { commands: parsed.commands, errorReading: false };
334
+ } catch {
335
+ return { commands: [], errorReading: true };
336
+ }
337
+ }
338
+ };
339
+
340
+ // src/session/session-service-commands.ts
341
+ var SessionCommandId = {
342
+ SET_TAB_WINDOW: 0,
343
+ // cc:58
344
+ SET_TAB_INDEX_IN_WINDOW: 2,
345
+ // cc:61
346
+ UPDATE_TAB_NAVIGATION: 6,
347
+ // cc:68
348
+ SET_SELECTED_NAVIGATION_INDEX: 7,
349
+ // cc:69
350
+ SET_SELECTED_TAB_IN_INDEX: 8,
351
+ // cc:70
352
+ SET_PINNED_STATE: 12,
353
+ // cc:80
354
+ TAB_CLOSED: 16,
355
+ // cc:84
356
+ WINDOW_CLOSED: 17,
357
+ // cc:85
358
+ SET_ACTIVE_WINDOW: 20,
359
+ // cc:89
360
+ LAST_ACTIVE_TIME: 21,
361
+ // cc:90
362
+ TAB_NAVIGATION_PATH_PRUNED: 24,
363
+ // cc:94
364
+ SET_TAB_GROUP: 25,
365
+ // cc:95
366
+ SET_TAB_GROUP_METADATA2: 27,
367
+ // cc:98
368
+ SET_TAB_DATA: 30,
369
+ // cc:101
370
+ ADD_TAB_EXTRA_DATA: 33,
371
+ // cc:105
372
+ ADD_WINDOW_EXTRA_DATA: 34
373
+ // cc:106
374
+ };
375
+ function createSetTabWindowCommand(windowId, tabId) {
376
+ return { id: SessionCommandId.SET_TAB_WINDOW, windowId, tabId };
377
+ }
378
+ function createSetTabIndexInWindowCommand(tabId, index) {
379
+ return { id: SessionCommandId.SET_TAB_INDEX_IN_WINDOW, tabId, index };
380
+ }
381
+ function createUpdateTabNavigationCommand(tabId, navigation) {
382
+ return { id: SessionCommandId.UPDATE_TAB_NAVIGATION, tabId, navigation };
383
+ }
384
+ function createSetSelectedNavigationIndexCommand(tabId, index) {
385
+ return { id: SessionCommandId.SET_SELECTED_NAVIGATION_INDEX, tabId, index };
386
+ }
387
+ function createSetSelectedTabInWindowCommand(windowId, index) {
388
+ return { id: SessionCommandId.SET_SELECTED_TAB_IN_INDEX, windowId, index };
389
+ }
390
+ function createPinnedStateCommand(tabId, pinned) {
391
+ return { id: SessionCommandId.SET_PINNED_STATE, tabId, pinned };
392
+ }
393
+ function createTabClosedCommand(tabId, closeTime = Date.now()) {
394
+ return { id: SessionCommandId.TAB_CLOSED, tabId, closeTime };
395
+ }
396
+ function createWindowClosedCommand(windowId, closeTime = Date.now()) {
397
+ return { id: SessionCommandId.WINDOW_CLOSED, windowId, closeTime };
398
+ }
399
+ function createSetActiveWindowCommand(windowId) {
400
+ return { id: SessionCommandId.SET_ACTIVE_WINDOW, windowId };
401
+ }
402
+ function createLastActiveTimeCommand(tabId, lastActiveTime) {
403
+ return { id: SessionCommandId.LAST_ACTIVE_TIME, tabId, lastActiveTime };
404
+ }
405
+ function createTabNavigationPathPrunedCommand(tabId, index, count) {
406
+ return { id: SessionCommandId.TAB_NAVIGATION_PATH_PRUNED, tabId, index, count };
407
+ }
408
+ function createTabGroupCommand(tabId, groupId) {
409
+ return { id: SessionCommandId.SET_TAB_GROUP, tabId, groupId };
410
+ }
411
+ function createTabGroupMetadataUpdateCommand(groupId, visualData) {
412
+ return { id: SessionCommandId.SET_TAB_GROUP_METADATA2, groupId, visualData: { ...visualData } };
413
+ }
414
+ function createSetTabDataCommand(tabId, data) {
415
+ return { id: SessionCommandId.SET_TAB_DATA, tabId, data };
416
+ }
417
+ function createAddTabExtraDataCommand(tabId, key, value) {
418
+ return { id: SessionCommandId.ADD_TAB_EXTRA_DATA, tabId, key, value };
419
+ }
420
+ function createAddWindowExtraDataCommand(windowId, key, value) {
421
+ return { id: SessionCommandId.ADD_WINDOW_EXTRA_DATA, windowId, key, value };
422
+ }
423
+ function isClosingCommand(command) {
424
+ return command.id === SessionCommandId.TAB_CLOSED || command.id === SessionCommandId.WINDOW_CLOSED;
425
+ }
426
+ function getTab(tabs, tabId) {
427
+ let tab = tabs.get(tabId);
428
+ if (!tab) {
429
+ tab = {
430
+ tabId,
431
+ windowId: "",
432
+ hasWindow: false,
433
+ visualIndex: -1,
434
+ pinned: false,
435
+ groupId: null,
436
+ data: void 0,
437
+ navigations: [],
438
+ currentNavigationIndex: -1,
439
+ extraData: {}
440
+ };
441
+ tabs.set(tabId, tab);
442
+ }
443
+ return tab;
444
+ }
445
+ function getWindow(windows, windowId) {
446
+ let window2 = windows.get(windowId);
447
+ if (!window2) {
448
+ window2 = { windowId, selectedTabIndex: -1, tabs: [], tabGroups: [], extraData: {} };
449
+ windows.set(windowId, window2);
450
+ }
451
+ return window2;
452
+ }
453
+ function findClosestNavigationWithIndex(navigations, index) {
454
+ for (let i = 0; i < navigations.length; i++) {
455
+ if (navigations[i].index >= index) return i;
456
+ }
457
+ return navigations.length;
458
+ }
459
+ function processTabNavigationPathPruned(tab, index, count) {
460
+ if (tab.currentNavigationIndex >= index && tab.currentNavigationIndex < index + count) {
461
+ tab.currentNavigationIndex = index - 1;
462
+ } else if (tab.currentNavigationIndex >= index + count) {
463
+ tab.currentNavigationIndex -= count;
464
+ }
465
+ const from = findClosestNavigationWithIndex(tab.navigations, index);
466
+ const to = findClosestNavigationWithIndex(tab.navigations, index + count);
467
+ tab.navigations.splice(from, to - from);
468
+ for (const navigation of tab.navigations) {
469
+ if (navigation.index >= index) navigation.index -= count;
470
+ }
471
+ }
472
+ var isStr = (v) => typeof v === "string";
473
+ var isNum = (v) => typeof v === "number" && Number.isFinite(v);
474
+ var isBool = (v) => typeof v === "boolean";
475
+ function isValidNavigation(nav) {
476
+ if (typeof nav !== "object" || nav === null) return false;
477
+ const n = nav;
478
+ return isNum(n.index) && isStr(n.url);
479
+ }
480
+ function isValidVisualData(v) {
481
+ if (typeof v !== "object" || v === null) return false;
482
+ const d = v;
483
+ return isStr(d.title) && TAB_GROUP_COLORS.includes(d.color) && isBool(d.isCollapsed);
484
+ }
485
+ function processCommand(state, command) {
486
+ const { tabs, tabGroups, windows } = state;
487
+ const c = command;
488
+ switch (c.id) {
489
+ case SessionCommandId.SET_TAB_WINDOW: {
490
+ if (!isStr(c.tabId) || !isStr(c.windowId)) return false;
491
+ const tab = getTab(tabs, c.tabId);
492
+ tab.windowId = c.windowId;
493
+ tab.hasWindow = true;
494
+ return true;
495
+ }
496
+ case SessionCommandId.SET_TAB_INDEX_IN_WINDOW: {
497
+ if (!isStr(c.tabId) || !isNum(c.index)) return false;
498
+ getTab(tabs, c.tabId).visualIndex = c.index;
499
+ return true;
500
+ }
501
+ case SessionCommandId.UPDATE_TAB_NAVIGATION: {
502
+ if (!isStr(c.tabId) || !isValidNavigation(c.navigation)) return false;
503
+ const tab = getTab(tabs, c.tabId);
504
+ const navigation = { ...c.navigation };
505
+ const i = findClosestNavigationWithIndex(tab.navigations, navigation.index);
506
+ if (i < tab.navigations.length && tab.navigations[i].index === navigation.index) {
507
+ tab.navigations[i] = navigation;
508
+ } else {
509
+ tab.navigations.splice(i, 0, navigation);
510
+ }
511
+ return true;
512
+ }
513
+ case SessionCommandId.SET_SELECTED_NAVIGATION_INDEX: {
514
+ if (!isStr(c.tabId) || !isNum(c.index)) return false;
515
+ getTab(tabs, c.tabId).currentNavigationIndex = c.index;
516
+ return true;
517
+ }
518
+ case SessionCommandId.SET_SELECTED_TAB_IN_INDEX: {
519
+ if (!isStr(c.windowId) || !isNum(c.index)) return false;
520
+ getWindow(windows, c.windowId).selectedTabIndex = c.index;
521
+ return true;
522
+ }
523
+ case SessionCommandId.SET_PINNED_STATE: {
524
+ if (!isStr(c.tabId) || !isBool(c.pinned)) return false;
525
+ getTab(tabs, c.tabId).pinned = c.pinned;
526
+ return true;
527
+ }
528
+ case SessionCommandId.TAB_CLOSED: {
529
+ if (!isStr(c.tabId)) return false;
530
+ tabs.delete(c.tabId);
531
+ return true;
532
+ }
533
+ case SessionCommandId.WINDOW_CLOSED: {
534
+ if (!isStr(c.windowId)) return false;
535
+ windows.delete(c.windowId);
536
+ return true;
537
+ }
538
+ case SessionCommandId.SET_ACTIVE_WINDOW: {
539
+ if (!isStr(c.windowId)) return false;
540
+ state.activeWindowId = c.windowId;
541
+ return true;
542
+ }
543
+ case SessionCommandId.LAST_ACTIVE_TIME: {
544
+ if (!isStr(c.tabId) || !isNum(c.lastActiveTime)) return false;
545
+ getTab(tabs, c.tabId).lastActiveTime = c.lastActiveTime;
546
+ return true;
547
+ }
548
+ case SessionCommandId.TAB_NAVIGATION_PATH_PRUNED: {
549
+ if (!isStr(c.tabId) || !isNum(c.index) || !isNum(c.count)) return false;
550
+ if (c.index < 0 || c.count < 1) return false;
551
+ processTabNavigationPathPruned(getTab(tabs, c.tabId), c.index, c.count);
552
+ return true;
553
+ }
554
+ case SessionCommandId.SET_TAB_GROUP: {
555
+ if (!isStr(c.tabId) || !(c.groupId === null || isStr(c.groupId))) return false;
556
+ getTab(tabs, c.tabId).groupId = c.groupId;
557
+ return true;
558
+ }
559
+ case SessionCommandId.SET_TAB_GROUP_METADATA2: {
560
+ if (!isStr(c.groupId) || !isValidVisualData(c.visualData)) return false;
561
+ tabGroups.set(c.groupId, { groupId: c.groupId, visualData: { ...c.visualData } });
562
+ return true;
563
+ }
564
+ case SessionCommandId.SET_TAB_DATA: {
565
+ if (!isStr(c.tabId)) return false;
566
+ getTab(tabs, c.tabId).data = c.data;
567
+ return true;
568
+ }
569
+ case SessionCommandId.ADD_TAB_EXTRA_DATA: {
570
+ if (!isStr(c.tabId) || !isStr(c.key) || !isStr(c.value)) return false;
571
+ getTab(tabs, c.tabId).extraData[c.key] = c.value;
572
+ return true;
573
+ }
574
+ case SessionCommandId.ADD_WINDOW_EXTRA_DATA: {
575
+ if (!isStr(c.windowId) || !isStr(c.key) || !isStr(c.value)) return false;
576
+ getWindow(windows, c.windowId).extraData[c.key] = c.value;
577
+ return true;
578
+ }
579
+ default:
580
+ return false;
581
+ }
582
+ }
583
+ function restoreSessionFromCommands(commands) {
584
+ const state = {
585
+ tabs: /* @__PURE__ */ new Map(),
586
+ tabGroups: /* @__PURE__ */ new Map(),
587
+ windows: /* @__PURE__ */ new Map(),
588
+ activeWindowId: null
589
+ };
590
+ let errorReading = false;
591
+ for (const command of commands) {
592
+ if (!processCommand(state, command)) {
593
+ errorReading = true;
594
+ break;
595
+ }
596
+ }
597
+ for (const tab of state.tabs.values()) {
598
+ if (!tab.hasWindow) continue;
599
+ if (tab.navigations.length === 0 && tab.data === void 0) continue;
600
+ const window2 = getWindow(state.windows, tab.windowId);
601
+ window2.tabs.push(tab);
602
+ if (tab.navigations.length > 0) {
603
+ const j = findClosestNavigationWithIndex(tab.navigations, tab.currentNavigationIndex);
604
+ tab.currentNavigationIndex = j === tab.navigations.length ? tab.navigations.length - 1 : j;
605
+ } else {
606
+ tab.currentNavigationIndex = -1;
607
+ }
608
+ }
609
+ for (const window2 of state.windows.values()) {
610
+ const seen = /* @__PURE__ */ new Set();
611
+ for (const tab of window2.tabs) {
612
+ if (tab.groupId === null || seen.has(tab.groupId)) continue;
613
+ seen.add(tab.groupId);
614
+ window2.tabGroups.push(
615
+ state.tabGroups.get(tab.groupId) ?? {
616
+ groupId: tab.groupId,
617
+ visualData: { title: "", color: "grey", isCollapsed: false }
618
+ }
619
+ );
620
+ }
621
+ }
622
+ const validWindows = [...state.windows.values()].filter((w) => w.tabs.length > 0);
623
+ for (const window2 of validWindows) {
624
+ window2.tabs.sort((a, b) => a.visualIndex - b.visualIndex);
625
+ }
626
+ validWindows.sort((a, b) => a.windowId < b.windowId ? -1 : a.windowId > b.windowId ? 1 : 0);
627
+ for (const window2 of validWindows) {
628
+ const i = window2.tabs.findIndex((t) => t.visualIndex === window2.selectedTabIndex);
629
+ window2.selectedTabIndex = i === -1 ? 0 : i;
630
+ }
631
+ for (const window2 of validWindows) {
632
+ for (const tab of window2.tabs) delete tab.hasWindow;
633
+ }
634
+ return {
635
+ windows: validWindows,
636
+ activeWindowId: state.activeWindowId,
637
+ errorReading
638
+ };
639
+ }
640
+
641
+ // src/session/session-restore.ts
642
+ function restoreSessionWindow(model, window2, options = {}) {
643
+ const createTabData = options.createTabData ?? ((tab) => tab.data);
644
+ const preserveIds = options.preserveTabIds !== false;
645
+ const tabIdMap = /* @__PURE__ */ new Map();
646
+ const groupIdMap = /* @__PURE__ */ new Map();
647
+ const liveTabs = [];
648
+ for (const sessionTab of window2.tabs) {
649
+ const id = preserveIds && model.getTabById(sessionTab.tabId) === null ? sessionTab.tabId : void 0;
650
+ const tab = model.insertTabAt(model.count, createTabData(sessionTab), {
651
+ ...id !== void 0 ? { id } : {},
652
+ flags: sessionTab.pinned ? AddTabFlags.PINNED : AddTabFlags.NONE
653
+ });
654
+ liveTabs.push(tab);
655
+ tabIdMap.set(sessionTab.tabId, tab.id);
656
+ }
657
+ if (model.supportsTabGroups) {
658
+ for (const sessionGroup of window2.tabGroups) {
659
+ const indices = [];
660
+ window2.tabs.forEach((sessionTab, i) => {
661
+ if (sessionTab.groupId === sessionGroup.groupId) indices.push(model.indexOfTab(liveTabs[i]));
662
+ });
663
+ if (indices.length === 0) continue;
664
+ const liveGroupId = model.addToNewGroup(indices, sessionGroup.visualData);
665
+ groupIdMap.set(sessionGroup.groupId, liveGroupId);
666
+ }
667
+ }
668
+ if (liveTabs.length > 0) {
669
+ const selected = Math.max(0, Math.min(window2.selectedTabIndex, liveTabs.length - 1));
670
+ model.activateTabAt(model.indexOfTab(liveTabs[selected]));
671
+ if (options.deferLoading) {
672
+ for (let i = 0; i < liveTabs.length; i++) {
673
+ if (i === selected) continue;
674
+ model.discardTabAt(model.indexOfTab(liveTabs[i]));
675
+ }
676
+ }
677
+ }
678
+ return { tabsRestored: liveTabs.length, tabIdMap, groupIdMap };
679
+ }
680
+
681
+ // src/session/session-types.ts
682
+ var DEFAULT_WINDOW_ID = "window-1";
683
+ function currentNavigationEntry(tab) {
684
+ if (tab.navigations.length === 0) return null;
685
+ const i = Math.max(0, Math.min(tab.currentNavigationIndex, tab.navigations.length - 1));
686
+ return tab.navigations[i];
687
+ }
688
+
689
+ // src/session/session-service.ts
690
+ var WRITES_PER_RESET = 250;
691
+ var MAX_PERSISTED_NAVIGATIONS = 6;
692
+ var SessionService = class {
693
+ storage_;
694
+ manager_;
695
+ serializeTabData_;
696
+ deserializeTabData_;
697
+ writesPerReset_;
698
+ maxPersistedNavigations_;
699
+ onError_;
700
+ /**
701
+ * Resolves once the profile claim is settled and, for the owner, the
702
+ * previous session has been rotated to the last slot.
703
+ */
704
+ ready_;
705
+ windows_ = /* @__PURE__ */ new Map();
706
+ navState_ = /* @__PURE__ */ new Map();
707
+ tabExtraData_ = /* @__PURE__ */ new Map();
708
+ windowExtraData_ = /* @__PURE__ */ new Map();
709
+ activeWindowId_ = null;
710
+ /** Mirrors rebuild_on_next_save_ (session_service_base.cc). */
711
+ rebuildOnNextSave_ = false;
712
+ pageHideListener_ = null;
713
+ disposed_ = false;
714
+ singleton_;
715
+ ownership_ = "pending";
716
+ /** Mirrors is_saving_enabled_ (session_service_base.h:337). */
717
+ savingEnabled_ = false;
718
+ constructor(options) {
719
+ this.storage_ = options.storage;
720
+ this.serializeTabData_ = options.serializeTabData ?? ((data) => data);
721
+ this.deserializeTabData_ = options.deserializeTabData ?? ((raw) => raw);
722
+ this.writesPerReset_ = options.writesPerReset ?? WRITES_PER_RESET;
723
+ this.maxPersistedNavigations_ = options.maxPersistedNavigations ?? MAX_PERSISTED_NAVIGATIONS;
724
+ this.onError_ = options.onError ?? ((error) => console.error("chromium-tabs session:", error));
725
+ this.singleton_ = options.processSingleton !== void 0 ? options.processSingleton : createDefaultProcessSingleton(this.storage_.profileLockName);
726
+ let gate = null;
727
+ if (this.singleton_ === null) {
728
+ this.ownership_ = "owner";
729
+ gate = this.rotateNow_();
730
+ this.ready_ = gate ?? Promise.resolve();
731
+ } else {
732
+ gate = this.singleton_.acquire().then(async (result) => {
733
+ this.ownership_ = result;
734
+ if (result === "owner" && !this.disposed_) {
735
+ const rotation = this.rotateNow_();
736
+ if (rotation) await rotation;
737
+ this.setSavingEnabled_(true);
738
+ }
739
+ });
740
+ this.ready_ = gate;
741
+ }
742
+ this.manager_ = new CommandStorageManager({
743
+ backend: this.storage_,
744
+ saveDelayMs: options.saveDelayMs,
745
+ ...gate ? { ready: gate } : {},
746
+ delegate: {
747
+ // RebuildCommandsIfRequired (session_service_base.cc:856).
748
+ onWillSaveCommands: () => {
749
+ if (this.rebuildOnNextSave_) this.scheduleResetCommands();
750
+ },
751
+ onErrorWritingSessionCommands: () => {
752
+ this.rebuildOnNextSave_ = true;
753
+ this.manager_.startSaveTimer();
754
+ }
755
+ }
756
+ });
757
+ if (this.singleton_ === null) this.setSavingEnabled_(true);
758
+ const flushOnPageHide = options.flushOnPageHide ?? true;
759
+ if (flushOnPageHide && typeof window !== "undefined" && typeof window.addEventListener === "function") {
760
+ this.pageHideListener_ = () => {
761
+ void this.saveNow();
762
+ };
763
+ window.addEventListener("pagehide", this.pageHideListener_);
764
+ }
765
+ }
766
+ /**
767
+ * Which side of the profile claim this service landed on: 'pending' until
768
+ * resolved, then 'owner' (this realm persists the session) or 'secondary'
769
+ * (another realm owns the storage area; this service records and restores
770
+ * nothing). Settled by the time getLastSession/restoreInto resolve.
771
+ */
772
+ get ownership() {
773
+ return this.ownership_;
774
+ }
775
+ /** Starts rotation; returns the pending promise when async, else null. */
776
+ rotateNow_() {
777
+ let rotation;
778
+ try {
779
+ rotation = this.storage_.moveCurrentSessionToLastSession();
780
+ } catch (error) {
781
+ this.onError_(error);
782
+ rotation = void 0;
783
+ }
784
+ if (rotation && typeof rotation.then === "function") {
785
+ return rotation.then(
786
+ () => void 0,
787
+ (error) => {
788
+ this.onError_(error);
789
+ }
790
+ );
791
+ }
792
+ return null;
793
+ }
794
+ /** Port of SetSavingEnabled (session_service_base.cc:877). */
795
+ setSavingEnabled_(enabled) {
796
+ if (this.savingEnabled_ === enabled) return;
797
+ this.savingEnabled_ = enabled;
798
+ if (!this.savingEnabled_) {
799
+ this.manager_.clearPendingCommands();
800
+ } else {
801
+ this.scheduleResetCommands();
802
+ }
803
+ }
804
+ // Attach / detach ////////////////////////////////////////////////////////
805
+ /**
806
+ * Starts recording a model under the given window id and schedules a full
807
+ * snapshot of its current state (the SetSavingEnabled(true) startup path,
808
+ * session_service_base.cc:412). Returns a detach function.
809
+ */
810
+ attach(model, options = {}) {
811
+ this.checkNotDisposed_();
812
+ const windowId = options.windowId ?? DEFAULT_WINDOW_ID;
813
+ if (this.windows_.has(windowId)) {
814
+ throw new Error(`session: window "${windowId}" is already attached`);
815
+ }
816
+ for (const entry2 of this.windows_.values()) {
817
+ if (entry2.model === model) throw new Error("session: model is already attached");
818
+ }
819
+ const entry = { model, unsubscribe: () => {
820
+ }, lastSelectedIndex: NO_TAB };
821
+ entry.unsubscribe = model.addObserver(this.createObserver_(windowId, entry));
822
+ this.windows_.set(windowId, entry);
823
+ if (this.activeWindowId_ === null) this.activeWindowId_ = windowId;
824
+ this.scheduleResetCommands();
825
+ return () => this.detach(windowId);
826
+ }
827
+ /**
828
+ * Stops recording a window. The window's commands stay in the log until
829
+ * the next full rewrite; use markWindowClosed first to drop it eagerly.
830
+ */
831
+ detach(windowId) {
832
+ const entry = this.windows_.get(windowId);
833
+ if (!entry) return;
834
+ entry.unsubscribe();
835
+ this.windows_.delete(windowId);
836
+ for (const tab of entry.model.getTabs()) {
837
+ this.navState_.delete(tab.id);
838
+ this.tabExtraData_.delete(tab.id);
839
+ }
840
+ this.windowExtraData_.delete(windowId);
841
+ if (this.activeWindowId_ === windowId) this.activeWindowId_ = null;
842
+ }
843
+ /**
844
+ * Records the window as closed and detaches it. Tab closes are committed
845
+ * alongside kCommandWindowClosed, mirroring CommitPendingCloses
846
+ * (session_service.cc) — otherwise the tabs' surviving commands would
847
+ * resurrect the window husk during rebuild.
848
+ */
849
+ markWindowClosed(windowId) {
850
+ const entry = this.windows_.get(windowId);
851
+ if (!entry) return;
852
+ const tabs = entry.model.getTabs();
853
+ this.detach(windowId);
854
+ for (const tab of tabs) this.scheduleCommand_(createTabClosedCommand(tab.id));
855
+ this.scheduleCommand_(createWindowClosedCommand(windowId));
856
+ }
857
+ /** Records which window is active (kCommandSetActiveWindow). */
858
+ setActiveWindow(windowId) {
859
+ if (!this.windows_.has(windowId)) return;
860
+ this.activeWindowId_ = windowId;
861
+ this.scheduleCommand_(createSetActiveWindowCommand(windowId));
862
+ }
863
+ // Navigation tracking (the SessionTabHelper surface) /////////////////////
864
+ /**
865
+ * Records that a tab committed a new navigation: forward history is pruned
866
+ * and the new entry becomes current — normal browser semantics. Untracked
867
+ * tab ids are ignored, like commands for untracked windows in Chrome.
868
+ */
869
+ navigateTab(tabId, entry) {
870
+ if (!this.findTab_(tabId)) return;
871
+ let nav = this.navState_.get(tabId);
872
+ if (!nav) {
873
+ nav = { navigations: [], currentNavigationIndex: -1 };
874
+ this.navState_.set(tabId, nav);
875
+ }
876
+ const newIndex = nav.currentNavigationIndex + 1;
877
+ const last = nav.navigations[nav.navigations.length - 1];
878
+ if (last && last.index >= newIndex) {
879
+ const count = last.index - newIndex + 1;
880
+ processTabNavigationPathPruned(nav, newIndex, count);
881
+ this.scheduleCommand_(createTabNavigationPathPrunedCommand(tabId, newIndex, count));
882
+ }
883
+ const navigation = {
884
+ index: newIndex,
885
+ url: entry.url,
886
+ ...entry.title !== void 0 ? { title: entry.title } : {},
887
+ ...entry.state !== void 0 ? { state: entry.state } : {},
888
+ timestamp: entry.timestamp ?? Date.now()
889
+ };
890
+ this.insertNavigation_(nav, navigation);
891
+ nav.currentNavigationIndex = newIndex;
892
+ this.scheduleCommand_(createUpdateTabNavigationCommand(tabId, navigation));
893
+ this.scheduleCommand_(createSetSelectedNavigationIndexCommand(tabId, newIndex));
894
+ }
895
+ /**
896
+ * Replaces-or-inserts one navigation entry by its index — e.g. the current
897
+ * page's title or scroll state changed in place. Mirrors
898
+ * UpdateTabNavigation (session_service.cc).
899
+ */
900
+ updateTabNavigation(tabId, navigation) {
901
+ if (!this.findTab_(tabId)) return;
902
+ let nav = this.navState_.get(tabId);
903
+ if (!nav) {
904
+ nav = { navigations: [], currentNavigationIndex: -1 };
905
+ this.navState_.set(tabId, nav);
906
+ }
907
+ this.insertNavigation_(nav, { ...navigation });
908
+ this.scheduleCommand_(createUpdateTabNavigationCommand(tabId, { ...navigation }));
909
+ }
910
+ /** Records back/forward movement. Mirrors SetSelectedNavigationIndex. */
911
+ setSelectedNavigationIndex(tabId, index) {
912
+ if (!this.findTab_(tabId)) return;
913
+ const nav = this.navState_.get(tabId);
914
+ if (!nav || !nav.navigations.some((n) => n.index === index)) return;
915
+ nav.currentNavigationIndex = index;
916
+ this.scheduleCommand_(createSetSelectedNavigationIndexCommand(tabId, index));
917
+ }
918
+ /** Removes `count` entries from index value `index`. Mirrors TabNavigationPathPruned. */
919
+ pruneTabNavigations(tabId, index, count) {
920
+ if (!this.findTab_(tabId)) return;
921
+ const nav = this.navState_.get(tabId);
922
+ if (!nav || index < 0 || count < 1) return;
923
+ processTabNavigationPathPruned(nav, index, count);
924
+ this.scheduleCommand_(createTabNavigationPathPrunedCommand(tabId, index, count));
925
+ }
926
+ // Extra data (kCommandAddTabExtraData / kCommandAddWindowExtraData) //////
927
+ setTabExtraData(tabId, key, value) {
928
+ if (!this.findTab_(tabId)) return;
929
+ let map = this.tabExtraData_.get(tabId);
930
+ if (!map) {
931
+ map = /* @__PURE__ */ new Map();
932
+ this.tabExtraData_.set(tabId, map);
933
+ }
934
+ map.set(key, value);
935
+ this.scheduleCommand_(createAddTabExtraDataCommand(tabId, key, value));
936
+ }
937
+ setWindowExtraData(windowId, key, value) {
938
+ if (!this.windows_.has(windowId)) return;
939
+ let map = this.windowExtraData_.get(windowId);
940
+ if (!map) {
941
+ map = /* @__PURE__ */ new Map();
942
+ this.windowExtraData_.set(windowId, map);
943
+ }
944
+ map.set(key, value);
945
+ this.scheduleCommand_(createAddWindowExtraDataCommand(windowId, key, value));
946
+ }
947
+ // Reading and restoring //////////////////////////////////////////////////
948
+ /** Replays the previous session's log. Mirrors GetLastSession. */
949
+ async getLastSession() {
950
+ this.checkNotDisposed_();
951
+ await this.ready_;
952
+ if (this.ownership_ === "secondary") {
953
+ return { windows: [], activeWindowId: null, errorReading: false };
954
+ }
955
+ let commands;
956
+ let errorReading;
957
+ try {
958
+ const result = await this.storage_.readLastSessionCommands();
959
+ commands = result.commands;
960
+ errorReading = result.errorReading;
961
+ } catch (error) {
962
+ this.onError_(error);
963
+ return { windows: [], activeWindowId: null, errorReading: true };
964
+ }
965
+ const snapshot = restoreSessionFromCommands(commands);
966
+ return { ...snapshot, errorReading: snapshot.errorReading || errorReading };
967
+ }
968
+ /**
969
+ * The one-call integration: restores the previous session's window into
970
+ * `model` (when there is one) and attaches the model so the new session is
971
+ * recorded. Restored navigation histories and extra data are re-adopted so
972
+ * they survive future log rewrites.
973
+ */
974
+ async restoreInto(model, options = {}) {
975
+ this.checkNotDisposed_();
976
+ const snapshot = await this.getLastSession();
977
+ const window2 = options.windowId ? snapshot.windows.find((w) => w.windowId === options.windowId) : snapshot.windows[0];
978
+ const windowId = options.windowId ?? window2?.windowId ?? DEFAULT_WINDOW_ID;
979
+ let result = { tabsRestored: 0, tabIdMap: /* @__PURE__ */ new Map(), groupIdMap: /* @__PURE__ */ new Map() };
980
+ if (window2) {
981
+ result = restoreSessionWindow(model, window2, {
982
+ createTabData: options.createTabData ?? ((tab) => this.deserializeTabData_(tab.data)),
983
+ preserveTabIds: options.preserveTabIds,
984
+ deferLoading: options.deferLoading
985
+ });
986
+ for (const sessionTab of window2.tabs) {
987
+ const liveId = result.tabIdMap.get(sessionTab.tabId);
988
+ if (liveId === void 0) continue;
989
+ if (sessionTab.navigations.length > 0) {
990
+ const position = Math.max(
991
+ 0,
992
+ Math.min(sessionTab.currentNavigationIndex, sessionTab.navigations.length - 1)
993
+ );
994
+ this.navState_.set(liveId, {
995
+ navigations: sessionTab.navigations.map((n) => ({ ...n })),
996
+ currentNavigationIndex: sessionTab.navigations[position].index
997
+ });
998
+ }
999
+ const extra = Object.entries(sessionTab.extraData);
1000
+ if (extra.length > 0) this.tabExtraData_.set(liveId, new Map(extra));
1001
+ }
1002
+ const windowExtra = Object.entries(window2.extraData);
1003
+ if (windowExtra.length > 0) this.windowExtraData_.set(windowId, new Map(windowExtra));
1004
+ }
1005
+ this.attach(model, { windowId });
1006
+ return {
1007
+ ...result,
1008
+ restored: window2 !== void 0,
1009
+ snapshot,
1010
+ ownership: this.ownership_ === "secondary" ? "secondary" : "owner"
1011
+ };
1012
+ }
1013
+ // Persistence control /////////////////////////////////////////////////////
1014
+ /** Flushes buffered commands now (e.g. before an intentional teardown). */
1015
+ saveNow() {
1016
+ return this.manager_.save();
1017
+ }
1018
+ /** True while commands are buffered awaiting the save timer. */
1019
+ get hasPendingSave() {
1020
+ return this.manager_.hasPendingSave;
1021
+ }
1022
+ /**
1023
+ * Discards the log and rewrites it from live state. Mirrors
1024
+ * ScheduleResetCommands (session_service_base.cc:404) +
1025
+ * BuildCommandsFromBrowsers (cc:767).
1026
+ */
1027
+ scheduleResetCommands() {
1028
+ if (!this.savingEnabled_) return;
1029
+ this.manager_.setPendingReset(true);
1030
+ this.manager_.clearPendingCommands();
1031
+ this.rebuildOnNextSave_ = false;
1032
+ for (const [windowId, entry] of this.windows_) {
1033
+ this.buildCommandsForWindow_(windowId, entry);
1034
+ }
1035
+ if (this.activeWindowId_ !== null) {
1036
+ this.manager_.appendRebuildCommand(createSetActiveWindowCommand(this.activeWindowId_));
1037
+ }
1038
+ this.manager_.startSaveTimer();
1039
+ }
1040
+ /** Detaches everything and stops timers. Does not flush — saveNow() first if needed. */
1041
+ dispose() {
1042
+ if (this.disposed_) return;
1043
+ this.disposed_ = true;
1044
+ for (const windowId of [...this.windows_.keys()]) this.detach(windowId);
1045
+ if (this.pageHideListener_ !== null) {
1046
+ window.removeEventListener("pagehide", this.pageHideListener_);
1047
+ this.pageHideListener_ = null;
1048
+ }
1049
+ this.manager_.dispose();
1050
+ this.singleton_?.release();
1051
+ }
1052
+ // Internals ///////////////////////////////////////////////////////////////
1053
+ checkNotDisposed_() {
1054
+ if (this.disposed_) throw new Error("session: service is disposed");
1055
+ }
1056
+ findTab_(tabId) {
1057
+ for (const [windowId, entry] of this.windows_) {
1058
+ const tab = entry.model.getTabById(tabId);
1059
+ if (tab) return { windowId, tab, index: entry.model.indexOfTab(tab) };
1060
+ }
1061
+ return null;
1062
+ }
1063
+ /** Replace-or-insert by navigation index (session_service_commands.cc:664). */
1064
+ insertNavigation_(nav, navigation) {
1065
+ const i = findClosestNavigationWithIndex(nav.navigations, navigation.index);
1066
+ if (i < nav.navigations.length && nav.navigations[i].index === navigation.index) {
1067
+ nav.navigations[i] = navigation;
1068
+ } else {
1069
+ nav.navigations.splice(i, 0, navigation);
1070
+ }
1071
+ }
1072
+ serializeTab_(tab) {
1073
+ try {
1074
+ return this.serializeTabData_(tab.data, tab);
1075
+ } catch (error) {
1076
+ this.onError_(error);
1077
+ return void 0;
1078
+ }
1079
+ }
1080
+ /**
1081
+ * Coalesces a new command with a buffered one when only the latest value
1082
+ * matters. Port of ReplacePendingCommand (session_service_commands.cc:1344)
1083
+ * — Chrome only handles UpdateTabNavigation and SetActiveWindow; the other
1084
+ * last-write-wins commands here are this port's extension, safe because the
1085
+ * rebuild treats each of them as a keyed assignment.
1086
+ */
1087
+ replacePendingCommand_(command) {
1088
+ const pending = this.manager_.pendingCommands;
1089
+ for (let i = pending.length - 1; i >= 0; i--) {
1090
+ const existing = pending[i];
1091
+ if (existing.id !== command.id) continue;
1092
+ switch (command.id) {
1093
+ case SessionCommandId.UPDATE_TAB_NAVIGATION: {
1094
+ const old = existing;
1095
+ if (old.tabId === command.tabId && old.navigation.index === command.navigation.index) {
1096
+ this.manager_.eraseCommand(existing);
1097
+ this.manager_.appendRebuildCommand(command);
1098
+ return true;
1099
+ }
1100
+ continue;
1101
+ }
1102
+ case SessionCommandId.SET_ACTIVE_WINDOW:
1103
+ this.manager_.swapCommand(existing, command);
1104
+ return true;
1105
+ case SessionCommandId.SET_TAB_DATA:
1106
+ case SessionCommandId.SET_TAB_INDEX_IN_WINDOW:
1107
+ case SessionCommandId.LAST_ACTIVE_TIME:
1108
+ case SessionCommandId.SET_PINNED_STATE:
1109
+ case SessionCommandId.SET_TAB_GROUP: {
1110
+ if (existing.tabId === command.tabId) {
1111
+ this.manager_.swapCommand(existing, command);
1112
+ return true;
1113
+ }
1114
+ continue;
1115
+ }
1116
+ case SessionCommandId.SET_SELECTED_TAB_IN_INDEX: {
1117
+ if (existing.windowId === command.windowId) {
1118
+ this.manager_.swapCommand(existing, command);
1119
+ return true;
1120
+ }
1121
+ continue;
1122
+ }
1123
+ case SessionCommandId.SET_TAB_GROUP_METADATA2: {
1124
+ if (existing.groupId === command.groupId) {
1125
+ this.manager_.swapCommand(existing, command);
1126
+ return true;
1127
+ }
1128
+ continue;
1129
+ }
1130
+ default:
1131
+ return false;
1132
+ }
1133
+ }
1134
+ return false;
1135
+ }
1136
+ /** Mirrors SessionServiceBase::ScheduleCommand (session_service_base.cc:788). */
1137
+ scheduleCommand_(command) {
1138
+ if (this.disposed_ || !this.savingEnabled_) return;
1139
+ if (this.replacePendingCommand_(command)) return;
1140
+ const closing = isClosingCommand(command);
1141
+ this.manager_.scheduleCommand(command);
1142
+ if (!this.manager_.pendingReset && this.manager_.commandsSinceReset >= this.writesPerReset_ && !closing) {
1143
+ this.scheduleResetCommands();
1144
+ }
1145
+ }
1146
+ /** SetTabIndexInWindow for every tab in [lo, hi] — see the header comment. */
1147
+ emitIndexRange_(model, lo, hi) {
1148
+ const tabs = model.getTabs();
1149
+ const last = Math.min(hi, tabs.length - 1);
1150
+ for (let i = Math.max(0, lo); i <= last; i++) {
1151
+ this.scheduleCommand_(createSetTabIndexInWindowCommand(tabs[i].id, i));
1152
+ }
1153
+ }
1154
+ /** Dedup-cached SetSelectedTabInWindow, like session_service.cc. */
1155
+ maybeEmitSelectedTab_(windowId, entry) {
1156
+ const index = entry.model.activeIndex;
1157
+ if (index === entry.lastSelectedIndex || index === NO_TAB) return;
1158
+ entry.lastSelectedIndex = index;
1159
+ this.scheduleCommand_(createSetSelectedTabInWindowCommand(windowId, index));
1160
+ }
1161
+ /**
1162
+ * BuildCommandsForBrowser + BuildCommandsForTab
1163
+ * (session_service_base.cc:672, :592), adapted to one TabStripModel.
1164
+ */
1165
+ buildCommandsForWindow_(windowId, entry) {
1166
+ const model = entry.model;
1167
+ const m = this.manager_;
1168
+ const windowExtra = this.windowExtraData_.get(windowId);
1169
+ if (windowExtra) {
1170
+ for (const [key, value] of windowExtra) {
1171
+ m.appendRebuildCommand(createAddWindowExtraDataCommand(windowId, key, value));
1172
+ }
1173
+ }
1174
+ const tabs = model.getTabs();
1175
+ tabs.forEach((tab, index) => {
1176
+ m.appendRebuildCommand(createSetTabWindowCommand(windowId, tab.id));
1177
+ if (Number.isFinite(tab.lastActiveAt)) {
1178
+ m.appendRebuildCommand(createLastActiveTimeCommand(tab.id, tab.lastActiveAt));
1179
+ }
1180
+ const nav = this.navState_.get(tab.id);
1181
+ if (nav && nav.navigations.length > 0) {
1182
+ const position = findClosestNavigationWithIndex(nav.navigations, nav.currentNavigationIndex);
1183
+ const lo = Math.max(position - this.maxPersistedNavigations_, 0);
1184
+ const hi = Math.min(position + this.maxPersistedNavigations_, nav.navigations.length);
1185
+ for (let i = lo; i < hi; i++) {
1186
+ m.appendRebuildCommand(createUpdateTabNavigationCommand(tab.id, { ...nav.navigations[i] }));
1187
+ }
1188
+ m.appendRebuildCommand(createSetSelectedNavigationIndexCommand(tab.id, nav.currentNavigationIndex));
1189
+ }
1190
+ m.appendRebuildCommand(createSetTabIndexInWindowCommand(tab.id, index));
1191
+ if (tab.pinned) m.appendRebuildCommand(createPinnedStateCommand(tab.id, true));
1192
+ if (tab.group !== null) m.appendRebuildCommand(createTabGroupCommand(tab.id, tab.group));
1193
+ m.appendRebuildCommand(createSetTabDataCommand(tab.id, this.serializeTab_(tab)));
1194
+ const extra = this.tabExtraData_.get(tab.id);
1195
+ if (extra) {
1196
+ for (const [key, value] of extra) {
1197
+ m.appendRebuildCommand(createAddTabExtraDataCommand(tab.id, key, value));
1198
+ }
1199
+ }
1200
+ });
1201
+ for (const group of model.getGroups()) {
1202
+ m.appendRebuildCommand(createTabGroupMetadataUpdateCommand(group.id, group.visualData));
1203
+ }
1204
+ if (model.activeIndex !== NO_TAB) {
1205
+ m.appendRebuildCommand(createSetSelectedTabInWindowCommand(windowId, model.activeIndex));
1206
+ }
1207
+ entry.lastSelectedIndex = model.activeIndex;
1208
+ }
1209
+ createObserver_(windowId, entry) {
1210
+ const guarded = (fn) => {
1211
+ return (...args) => {
1212
+ try {
1213
+ fn(...args);
1214
+ } catch (error) {
1215
+ this.onError_(error);
1216
+ }
1217
+ };
1218
+ };
1219
+ return {
1220
+ onTabStripModelChanged: guarded(
1221
+ (change, selection) => {
1222
+ switch (change.type) {
1223
+ case "inserted": {
1224
+ let minIndex = Infinity;
1225
+ for (const { tab, index } of change.contents) {
1226
+ this.scheduleCommand_(createSetTabWindowCommand(windowId, tab.id));
1227
+ this.scheduleCommand_(createSetTabDataCommand(tab.id, this.serializeTab_(tab)));
1228
+ if (tab.pinned) this.scheduleCommand_(createPinnedStateCommand(tab.id, true));
1229
+ if (tab.group !== null) this.scheduleCommand_(createTabGroupCommand(tab.id, tab.group));
1230
+ minIndex = Math.min(minIndex, index);
1231
+ }
1232
+ this.emitIndexRange_(entry.model, minIndex, entry.model.count - 1);
1233
+ break;
1234
+ }
1235
+ case "removed": {
1236
+ let minIndex = Infinity;
1237
+ for (const { tab, index } of change.contents) {
1238
+ this.scheduleCommand_(createTabClosedCommand(tab.id));
1239
+ this.navState_.delete(tab.id);
1240
+ this.tabExtraData_.delete(tab.id);
1241
+ minIndex = Math.min(minIndex, index);
1242
+ }
1243
+ this.emitIndexRange_(entry.model, minIndex, entry.model.count - 1);
1244
+ break;
1245
+ }
1246
+ case "moved":
1247
+ this.emitIndexRange_(
1248
+ entry.model,
1249
+ Math.min(change.fromIndex, change.toIndex),
1250
+ Math.max(change.fromIndex, change.toIndex)
1251
+ );
1252
+ break;
1253
+ case "replaced":
1254
+ this.scheduleCommand_(createSetTabDataCommand(change.tab.id, this.serializeTab_(change.tab)));
1255
+ break;
1256
+ case "selectionOnly":
1257
+ break;
1258
+ }
1259
+ if (selection.activeTabChanged && selection.oldTab && Number.isFinite(selection.oldTab.lastActiveAt)) {
1260
+ this.scheduleCommand_(createLastActiveTimeCommand(selection.oldTab.id, selection.oldTab.lastActiveAt));
1261
+ }
1262
+ this.maybeEmitSelectedTab_(windowId, entry);
1263
+ }
1264
+ ),
1265
+ onTabPinnedStateChanged: guarded((tab) => {
1266
+ this.scheduleCommand_(createPinnedStateCommand(tab.id, tab.pinned));
1267
+ }),
1268
+ onTabGroupedStateChanged: guarded((_old, _new, tab) => {
1269
+ this.scheduleCommand_(createTabGroupCommand(tab.id, tab.group));
1270
+ }),
1271
+ onTabGroupChanged: guarded((change) => {
1272
+ if (change.type === "created" || change.type === "visualsChanged") {
1273
+ const visuals = entry.model.getGroupVisualData(change.groupId);
1274
+ if (visuals) {
1275
+ this.scheduleCommand_(createTabGroupMetadataUpdateCommand(change.groupId, visuals));
1276
+ }
1277
+ }
1278
+ }),
1279
+ onTabChanged: guarded((tab) => {
1280
+ this.scheduleCommand_(createSetTabDataCommand(tab.id, this.serializeTab_(tab)));
1281
+ })
1282
+ };
1283
+ }
1284
+ };
1285
+ export {
1286
+ CommandStorageManager,
1287
+ DEFAULT_WINDOW_ID,
1288
+ InMemoryStorageBackend,
1289
+ MAX_PERSISTED_NAVIGATIONS,
1290
+ SAVE_DELAY_MS,
1291
+ SessionCommandId,
1292
+ SessionService,
1293
+ WRITES_PER_RESET,
1294
+ WebLocksProcessSingleton,
1295
+ WebStorageBackend,
1296
+ createAddTabExtraDataCommand,
1297
+ createAddWindowExtraDataCommand,
1298
+ createDefaultProcessSingleton,
1299
+ createLastActiveTimeCommand,
1300
+ createPinnedStateCommand,
1301
+ createSetActiveWindowCommand,
1302
+ createSetSelectedNavigationIndexCommand,
1303
+ createSetSelectedTabInWindowCommand,
1304
+ createSetTabDataCommand,
1305
+ createSetTabIndexInWindowCommand,
1306
+ createSetTabWindowCommand,
1307
+ createTabClosedCommand,
1308
+ createTabGroupCommand,
1309
+ createTabGroupMetadataUpdateCommand,
1310
+ createTabNavigationPathPrunedCommand,
1311
+ createUpdateTabNavigationCommand,
1312
+ createWindowClosedCommand,
1313
+ currentNavigationEntry,
1314
+ findClosestNavigationWithIndex,
1315
+ isClosingCommand,
1316
+ processTabNavigationPathPruned,
1317
+ restoreSessionFromCommands,
1318
+ restoreSessionWindow
1319
+ };
1320
+ //# sourceMappingURL=index.js.map