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