Skip to content

Commit 8ffbfdb

Browse files
committed
Migrate preferences from writable stores to runes
1 parent ed50b8d commit 8ffbfdb

File tree

7 files changed

+145
-119
lines changed

7 files changed

+145
-119
lines changed

_locales/en/messages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@
172172
"message": "Error opening tab.",
173173
"description": "Message shown when an error occurs when opening a tab."
174174
},
175+
"errorRefreshingHistoryManager": {
176+
"message": "Error refreshing history manager.",
177+
"description": "Message shown when an error occurs when refreshing the history manager."
178+
},
175179
"openSourceAttributions": {
176180
"message": "Open Source Attributions",
177181
"description": "Text for link to open source attributions."

src/treetop/Bookmark.svelte

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script lang="ts">
22
import { getContext } from 'svelte';
33
import type { SvelteDate } from 'svelte/reactivity';
4-
import type { Writable } from 'svelte/store';
54
import lodashTruncate from 'lodash-es/truncate';
65
76
import type * as Treetop from './types';
@@ -17,8 +16,8 @@
1716
1817
const lastVisitTimeMap: Treetop.LastVisitTimeMap =
1918
getContext('lastVisitTimeMap');
20-
const truncate = getContext<Writable<boolean>>('truncate');
21-
const tooltips = getContext<Writable<boolean>>('tooltips');
19+
const truncate = getContext<() => boolean>('truncate');
20+
const tooltips = getContext<() => boolean>('tooltips');
2221
const clock = getContext<SvelteDate>('clock');
2322
2423
const lastVisitTime = $derived(lastVisitTimeMap.get(nodeId)!);
@@ -29,7 +28,7 @@
2928
// Set name, truncating based on preference setting.
3029
// Fall back to URL if title is blank.
3130
const name = $derived(
32-
$truncate
31+
truncate()
3332
? lodashTruncate(title || url, {
3433
length: maxLength,
3534
separator: ' ',
@@ -40,7 +39,7 @@
4039
// Set tooltip if preference is enabled.
4140
// Display title and URL on separate lines, truncating long URLs in the middle.
4241
const tooltip = $derived(
43-
$tooltips ? `${title}\n${truncateMiddle(url, maxLength)}` : undefined,
42+
tooltips() ? `${title}\n${truncateMiddle(url, maxLength)}` : undefined,
4443
);
4544
4645
// Number of milliseconds in a day

src/treetop/Bookmark.svelte.test.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { SvelteDate, SvelteMap } from 'svelte/reactivity';
2-
import { type Writable, writable } from 'svelte/store';
32
import { faker } from '@faker-js/faker';
43
import { render, screen } from '@testing-library/svelte';
54
import escapeRegExp from 'lodash-es/escapeRegExp';
@@ -11,8 +10,10 @@ import type * as Treetop from '@Treetop/treetop/types';
1110
import ContextWrapper from '../../test/utils/ContextWrapper.svelte';
1211

1312
let lastVisitTimeMap: Treetop.LastVisitTimeMap;
14-
let truncate: Writable<boolean>;
15-
let tooltips: Writable<boolean>;
13+
let currentTruncate: boolean;
14+
const truncate = () => currentTruncate;
15+
let currentTooltips: boolean;
16+
const tooltips = () => currentTooltips;
1617
let clock: SvelteDate;
1718
let nodeId: string;
1819
let title: string;
@@ -48,8 +49,8 @@ beforeEach(() => {
4849
lastVisitTimeMap = new SvelteMap();
4950
lastVisitTimeMap.set(nodeId, 0);
5051

51-
truncate = writable(false);
52-
tooltips = writable(false);
52+
currentTruncate = false;
53+
currentTooltips = false;
5354

5455
clock = new SvelteDate();
5556
});
@@ -81,7 +82,7 @@ it('uses URL as content when title is empty', () => {
8182

8283
describe('truncate option', () => {
8384
beforeEach(() => {
84-
truncate.set(true);
85+
currentTruncate = true;
8586
});
8687

8788
it('truncates long title', () => {
@@ -133,7 +134,7 @@ describe('truncate option', () => {
133134

134135
describe('tooltips option', () => {
135136
beforeEach(() => {
136-
tooltips.set(true);
137+
currentTooltips = true;
137138
});
138139

139140
it('title attribute contains title and URL', () => {

src/treetop/Folder.svelte.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint no-irregular-whitespace: ["error", { "skipComments": true }] */
22

33
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
4-
import { type Writable, writable } from 'svelte/store';
54
import { render, screen } from '@testing-library/svelte';
65
import type { MockInstance } from 'vitest';
76
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -25,8 +24,8 @@ let nodeId: string;
2524

2625
// Bookmark component requirements
2726
let lastVisitTimeMap: Treetop.LastVisitTimeMap;
28-
let truncate: Writable<boolean>;
29-
let tooltips: Writable<boolean>;
27+
const truncate = () => false;
28+
const tooltips = () => false;
3029
let clock: SvelteDate;
3130

3231
let rootNode: Treetop.FolderNode;
@@ -115,8 +114,6 @@ beforeEach(() => {
115114
lastVisitTimeMap = new SvelteMap();
116115
currentFilterActive = false;
117116
filterSet = new SvelteSet();
118-
truncate = writable(false);
119-
tooltips = writable(false);
120117
clock = new SvelteDate();
121118
});
122119

src/treetop/PreferencesManager.test.ts

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { get, type Writable } from 'svelte/store';
21
import { faker } from '@faker-js/faker';
32
import EventEmitter from 'node:events';
43
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -21,7 +20,7 @@ describe('constructor', () => {
2120
});
2221
});
2322

24-
describe('createStore', () => {
23+
describe('createPreference', () => {
2524
beforeEach(() => {
2625
const addListener = vi.fn();
2726
vi.spyOn(chrome.storage.onChanged, 'addListener').mockImplementation(
@@ -32,23 +31,23 @@ describe('createStore', () => {
3231
});
3332

3433
it.each(['value', 2, false, true, ['value'], [2], [false], [true]])(
35-
'creates and returns a store for value: %p',
34+
'creates and returns a preference for value: %p',
3635
(value: Treetop.PreferenceValue) => {
3736
const name = faker.word.sample();
38-
const store = preferencesManager.createStore(name, value);
37+
const preference = preferencesManager.createPreference(name, value);
3938

40-
expect(get(store)).toBe(value);
39+
expect(preference()).toBe(value);
4140
},
4241
);
4342
});
4443

4544
describe('loadPreferences', () => {
46-
let stringStore: Writable<Treetop.PreferenceValue>;
47-
let numberStore: Writable<Treetop.PreferenceValue>;
48-
let booleanStore: Writable<Treetop.PreferenceValue>;
49-
let stringArrayStore: Writable<Treetop.PreferenceValue>;
50-
let numberArrayStore: Writable<Treetop.PreferenceValue>;
51-
let booleanArrayStore: Writable<Treetop.PreferenceValue>;
45+
let stringPreference: () => Treetop.PreferenceValue;
46+
let numberPreference: () => Treetop.PreferenceValue;
47+
let booleanPreference: () => Treetop.PreferenceValue;
48+
let stringArrayPreference: () => Treetop.PreferenceValue;
49+
let numberArrayPreference: () => Treetop.PreferenceValue;
50+
let booleanArrayPreference: () => Treetop.PreferenceValue;
5251

5352
beforeEach(() => {
5453
const addListener = vi.fn();
@@ -57,17 +56,24 @@ describe('loadPreferences', () => {
5756
);
5857

5958
preferencesManager = new PreferencesManager();
60-
stringStore = preferencesManager.createStore('string', 'value');
61-
numberStore = preferencesManager.createStore('number', 2);
62-
booleanStore = preferencesManager.createStore('boolean', true);
63-
stringArrayStore = preferencesManager.createStore('string_array', [
64-
'value',
65-
]);
66-
numberArrayStore = preferencesManager.createStore('number_array', [2]);
67-
booleanArrayStore = preferencesManager.createStore('boolean_array', [true]);
59+
stringPreference = preferencesManager.createPreference('string', 'value');
60+
numberPreference = preferencesManager.createPreference('number', 2);
61+
booleanPreference = preferencesManager.createPreference('boolean', true);
62+
stringArrayPreference = preferencesManager.createPreference(
63+
'string_array',
64+
['value'],
65+
);
66+
numberArrayPreference = preferencesManager.createPreference(
67+
'number_array',
68+
[2],
69+
);
70+
booleanArrayPreference = preferencesManager.createPreference(
71+
'boolean_array',
72+
[true],
73+
);
6874
});
6975

70-
it('loads preferences from storage and initializes stores', async () => {
76+
it('loads preferences from storage and initializes preference state', async () => {
7177
const values = {
7278
string: 'value2',
7379
number: 3,
@@ -83,15 +89,15 @@ describe('loadPreferences', () => {
8389

8490
await preferencesManager.loadPreferences();
8591

86-
expect(get(stringStore)).toBe('value2');
87-
expect(get(numberStore)).toBe(3);
88-
expect(get(booleanStore)).toBe(false);
89-
expect(get(stringArrayStore)).toStrictEqual(['value2']);
90-
expect(get(numberArrayStore)).toStrictEqual([3]);
91-
expect(get(booleanArrayStore)).toStrictEqual([false]);
92+
expect(stringPreference()).toBe('value2');
93+
expect(numberPreference()).toBe(3);
94+
expect(booleanPreference()).toBe(false);
95+
expect(stringArrayPreference()).toStrictEqual(['value2']);
96+
expect(numberArrayPreference()).toStrictEqual([3]);
97+
expect(booleanArrayPreference()).toStrictEqual([false]);
9298
});
9399

94-
it('ignores preferences from storage without a corresponding store', async () => {
100+
it('ignores preferences from storage without a corresponding preference', async () => {
95101
const values = { other: true };
96102

97103
vi.spyOn(chrome.storage.local, 'get').mockImplementation(
@@ -102,14 +108,14 @@ describe('loadPreferences', () => {
102108
});
103109
});
104110

105-
describe('handleStoreChanged', () => {
111+
describe('handleStorageChanged', () => {
106112
let emitter: EventEmitter;
107-
let stringStore: Writable<Treetop.PreferenceValue>;
108-
let numberStore: Writable<Treetop.PreferenceValue>;
109-
let booleanStore: Writable<Treetop.PreferenceValue>;
110-
let stringArrayStore: Writable<Treetop.PreferenceValue>;
111-
let numberArrayStore: Writable<Treetop.PreferenceValue>;
112-
let booleanArrayStore: Writable<Treetop.PreferenceValue>;
113+
let stringPreference: () => Treetop.PreferenceValue;
114+
let numberPreference: () => Treetop.PreferenceValue;
115+
let booleanPreference: () => Treetop.PreferenceValue;
116+
let stringArrayPreference: () => Treetop.PreferenceValue;
117+
let numberArrayPreference: () => Treetop.PreferenceValue;
118+
let booleanArrayPreference: () => Treetop.PreferenceValue;
113119

114120
beforeEach(() => {
115121
emitter = new EventEmitter();
@@ -120,17 +126,24 @@ describe('handleStoreChanged', () => {
120126
);
121127

122128
preferencesManager = new PreferencesManager();
123-
stringStore = preferencesManager.createStore('string', 'value');
124-
numberStore = preferencesManager.createStore('number', 3);
125-
booleanStore = preferencesManager.createStore('boolean', true);
126-
stringArrayStore = preferencesManager.createStore('string_array', [
127-
'value',
128-
]);
129-
numberArrayStore = preferencesManager.createStore('number_array', [2]);
130-
booleanArrayStore = preferencesManager.createStore('boolean_array', [true]);
129+
stringPreference = preferencesManager.createPreference('string', 'value');
130+
numberPreference = preferencesManager.createPreference('number', 3);
131+
booleanPreference = preferencesManager.createPreference('boolean', true);
132+
stringArrayPreference = preferencesManager.createPreference(
133+
'string_array',
134+
['value'],
135+
);
136+
numberArrayPreference = preferencesManager.createPreference(
137+
'number_array',
138+
[2],
139+
);
140+
booleanArrayPreference = preferencesManager.createPreference(
141+
'boolean_array',
142+
[true],
143+
);
131144
});
132145

133-
it('updates stores when store values change', () => {
146+
it('updates preferences when storage values change', () => {
134147
const changes = {
135148
string: {
136149
newValue: 'value2',
@@ -154,15 +167,15 @@ describe('handleStoreChanged', () => {
154167

155168
emitter.emit('onChanged', changes, 'local');
156169

157-
expect(get(stringStore)).toBe('value2');
158-
expect(get(numberStore)).toBe(4);
159-
expect(get(booleanStore)).toBe(false);
160-
expect(get(stringArrayStore)).toStrictEqual(['value2']);
161-
expect(get(numberArrayStore)).toStrictEqual([3]);
162-
expect(get(booleanArrayStore)).toStrictEqual([false]);
170+
expect(stringPreference()).toBe('value2');
171+
expect(numberPreference()).toBe(4);
172+
expect(booleanPreference()).toBe(false);
173+
expect(stringArrayPreference()).toStrictEqual(['value2']);
174+
expect(numberArrayPreference()).toStrictEqual([3]);
175+
expect(booleanArrayPreference()).toStrictEqual([false]);
163176
});
164177

165-
it('ignores store changes without a corresponding store', () => {
178+
it('ignores storage changes without a corresponding preference', () => {
166179
const changes = { other: { newValue: true } };
167180

168181
emitter.emit('onChanged', changes, 'local');

src/treetop/PreferencesManager.ts

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Writable, writable } from 'svelte/store';
1+
import { SvelteMap } from 'svelte/reactivity';
22

33
import type * as Treetop from './types';
44

@@ -7,12 +7,12 @@ type StorageChangedCallback = Parameters<
77
>[0];
88

99
/**
10-
* Class to create and manage updating preference stores.
10+
* Class to create and manage updating preferences.
1111
*/
1212
export class PreferencesManager {
13-
private readonly stores = new Map<
13+
private readonly preferences = new SvelteMap<
1414
string,
15-
Writable<Treetop.PreferenceValue>
15+
Treetop.PreferenceValue
1616
>();
1717

1818
// Bound event handler
@@ -26,37 +26,36 @@ export class PreferencesManager {
2626
}
2727

2828
/**
29-
* Create and register a store.
29+
* Create and register a preference.
3030
*/
31-
createStore(
31+
createPreference(
3232
name: string,
3333
value: Treetop.PreferenceValue,
34-
): Writable<Treetop.PreferenceValue> {
35-
const store = writable(value);
36-
this.stores.set(name, store);
37-
return store;
34+
): () => Treetop.PreferenceValue {
35+
this.preferences.set(name, value);
36+
37+
return () => this.preferences.get(name)!;
3838
}
3939

4040
/**
41-
* Load preferences from storage and initialize stores.
41+
* Load preferences from storage and initialize preference state.
4242
*/
4343
async loadPreferences(): Promise<void> {
4444
// Get preferences from storage
4545
const results = await chrome.storage.local.get();
4646

47-
// Initialize stores
47+
// Initialize preference state
4848
for (const key of Object.keys(results)) {
4949
const value = results[key] as Treetop.PreferenceValue;
5050

51-
const store = this.stores.get(key);
52-
if (store !== undefined) {
53-
store.set(value);
51+
if (this.preferences.has(key)) {
52+
this.preferences.set(key, value);
5453
}
5554
}
5655
}
5756

5857
/**
59-
* Update preferences stores when storage values change.
58+
* Update preference state when storage values change.
6059
*/
6160
private handleStorageChanged(
6261
changes: Record<string, chrome.storage.StorageChange>,
@@ -65,9 +64,8 @@ export class PreferencesManager {
6564
for (const key of Object.keys(changes)) {
6665
const value = changes[key].newValue as Treetop.PreferenceValue;
6766

68-
const store = this.stores.get(key);
69-
if (store !== undefined) {
70-
store.set(value);
67+
if (this.preferences.has(key)) {
68+
this.preferences.set(key, value);
7169
}
7270
}
7371
}

0 commit comments

Comments
 (0)