Skip to content

Commit 83871d0

Browse files
author
DavidQ
committed
Move browser persistence utilities into engine persistence - PR_26140_047-move-browser-persistence-to-engine
1 parent 3ac0e1d commit 83871d0

16 files changed

Lines changed: 368 additions & 337 deletions
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
05/20/2026
5+
BrowserStorageService.js
6+
*/
7+
export default class BrowserStorageService {
8+
constructor(storage = null) {
9+
this.storage = storage;
10+
}
11+
12+
static resolveGlobalStorage(storageName) {
13+
try {
14+
return globalThis?.[storageName] ?? null;
15+
} catch {
16+
return null;
17+
}
18+
}
19+
20+
isSupported() {
21+
return !!this.storage;
22+
}
23+
24+
length() {
25+
try {
26+
return Number(this.storage?.length || 0);
27+
} catch {
28+
return 0;
29+
}
30+
}
31+
32+
key(index) {
33+
try {
34+
return this.storage?.key(index) ?? null;
35+
} catch {
36+
return null;
37+
}
38+
}
39+
40+
getItem(key, defaultValue = null) {
41+
if (!this.storage || typeof this.storage.getItem !== 'function') {
42+
return defaultValue;
43+
}
44+
45+
try {
46+
const value = this.storage.getItem(key);
47+
return value == null ? defaultValue : value;
48+
} catch {
49+
return defaultValue;
50+
}
51+
}
52+
53+
setItem(key, value) {
54+
if (!this.storage || typeof this.storage.setItem !== 'function') {
55+
return false;
56+
}
57+
58+
try {
59+
this.storage.setItem(key, value);
60+
return true;
61+
} catch {
62+
return false;
63+
}
64+
}
65+
66+
removeItem(key) {
67+
if (!this.storage || typeof this.storage.removeItem !== 'function' || !key) {
68+
return { ok: false, message: 'storage entry does not include a supported storage and key' };
69+
}
70+
71+
try {
72+
this.storage.removeItem(key);
73+
return { ok: true };
74+
} catch (error) {
75+
return { ok: false, message: error.message };
76+
}
77+
}
78+
79+
entries() {
80+
const entries = [];
81+
const length = this.length();
82+
for (let index = 0; index < length; index += 1) {
83+
const key = this.key(index);
84+
if (!key) {
85+
continue;
86+
}
87+
entries.push({
88+
key,
89+
rawValue: this.getItem(key, null)
90+
});
91+
}
92+
return entries;
93+
}
94+
95+
saveJson(key, value) {
96+
const serialized = JSON.stringify(value);
97+
return this.setItem(key, serialized);
98+
}
99+
100+
loadJson(key, defaultValue = null) {
101+
const raw = this.getItem(key, null);
102+
if (!raw) {
103+
return defaultValue;
104+
}
105+
106+
try {
107+
return JSON.parse(raw);
108+
} catch {
109+
return defaultValue;
110+
}
111+
}
112+
}

src/engine/persistence/CookieStorageService.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,30 @@ David Quesenberry
44
03/22/2026
55
CookieStorageService.js
66
*/
7+
function safeDecodeCookiePart(value) {
8+
try {
9+
return decodeURIComponent(String(value || ''));
10+
} catch {
11+
return String(value || '');
12+
}
13+
}
14+
15+
function parseCookieString(cookieText) {
16+
const text = String(cookieText || '').trim();
17+
if (!text) {
18+
return [];
19+
}
20+
return text.split(';').map((part) => part.trim()).filter(Boolean).map((part) => {
21+
const separatorIndex = part.indexOf('=');
22+
const rawName = separatorIndex >= 0 ? part.slice(0, separatorIndex) : part;
23+
const rawValue = separatorIndex >= 0 ? part.slice(separatorIndex + 1) : '';
24+
return {
25+
key: safeDecodeCookiePart(rawName),
26+
rawValue: safeDecodeCookiePart(rawValue)
27+
};
28+
}).filter((entry) => entry.key);
29+
}
30+
731
export default class CookieStorageService {
832
constructor({ documentRef = globalThis.document ?? null } = {}) {
933
this.documentRef = documentRef;
@@ -47,4 +71,39 @@ export default class CookieStorageService {
4771
this.documentRef.cookie = `${encodeURIComponent(name)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`;
4872
return true;
4973
}
74+
75+
readCookieString() {
76+
try {
77+
return String(this.documentRef?.cookie || '');
78+
} catch {
79+
return '';
80+
}
81+
}
82+
83+
entries() {
84+
return parseCookieString(this.readCookieString());
85+
}
86+
87+
removeEverywhere(name) {
88+
if (!this.documentRef || !name) {
89+
return { ok: false, message: 'cookie entry does not include a supported document and key' };
90+
}
91+
92+
try {
93+
const encodedKey = encodeURIComponent(name);
94+
const expires = 'expires=Thu, 01 Jan 1970 00:00:00 GMT';
95+
const maxAge = 'Max-Age=0';
96+
this.documentRef.cookie = `${encodedKey}=; ${maxAge}; ${expires}; path=/`;
97+
const pathname = String(this.documentRef.location?.pathname || '/');
98+
const pathSegments = pathname.split('/').filter(Boolean);
99+
let currentPath = '';
100+
pathSegments.forEach((segment) => {
101+
currentPath += `/${segment}`;
102+
this.documentRef.cookie = `${encodedKey}=; ${maxAge}; ${expires}; path=${currentPath}`;
103+
});
104+
return { ok: true };
105+
} catch (error) {
106+
return { ok: false, message: error.message };
107+
}
108+
}
50109
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
05/20/2026
5+
LocalStorageService.js
6+
*/
7+
import BrowserStorageService from './BrowserStorageService.js';
8+
9+
export default class LocalStorageService extends BrowserStorageService {
10+
constructor(storage = undefined) {
11+
super(storage === undefined
12+
? BrowserStorageService.resolveGlobalStorage('localStorage')
13+
: storage);
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
05/20/2026
5+
SessionStorageService.js
6+
*/
7+
import BrowserStorageService from './BrowserStorageService.js';
8+
9+
export default class SessionStorageService extends BrowserStorageService {
10+
constructor(storage = undefined) {
11+
super(storage === undefined
12+
? BrowserStorageService.resolveGlobalStorage('sessionStorage')
13+
: storage);
14+
}
15+
}

src/engine/persistence/StorageService.js

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,14 @@ David Quesenberry
44
03/21/2026
55
StorageService.js
66
*/
7-
export default class StorageService {
7+
import BrowserStorageService from './BrowserStorageService.js';
8+
9+
export default class StorageService extends BrowserStorageService {
810
constructor(storage = undefined) {
9-
this.storage = storage === undefined ? StorageService.resolveDefaultStorage() : storage;
11+
super(storage === undefined ? StorageService.resolveDefaultStorage() : storage);
1012
}
1113

1214
static resolveDefaultStorage() {
13-
try {
14-
return globalThis.localStorage ?? null;
15-
} catch {
16-
return null;
17-
}
18-
}
19-
20-
saveJson(key, value) {
21-
if (!this.storage || typeof this.storage.setItem !== 'function') {
22-
return false;
23-
}
24-
25-
const serialized = JSON.stringify(value);
26-
27-
try {
28-
this.storage.setItem(key, serialized);
29-
return true;
30-
} catch {
31-
return false;
32-
}
33-
}
34-
35-
loadJson(key, fallback = null) {
36-
if (!this.storage || typeof this.storage.getItem !== 'function') {
37-
return fallback;
38-
}
39-
40-
let raw;
41-
42-
try {
43-
raw = this.storage.getItem(key);
44-
} catch {
45-
return fallback;
46-
}
47-
48-
if (!raw) {
49-
return fallback;
50-
}
51-
52-
try {
53-
return JSON.parse(raw);
54-
} catch {
55-
return fallback;
56-
}
15+
return BrowserStorageService.resolveGlobalStorage('localStorage');
5716
}
5817
}

src/engine/persistence/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ David Quesenberry
44
03/21/2026
55
index.js
66
*/
7+
export { default as BrowserStorageService } from './BrowserStorageService.js';
78
export { default as StorageService } from './StorageService.js';
9+
export { default as LocalStorageService } from './LocalStorageService.js';
10+
export { default as SessionStorageService } from './SessionStorageService.js';
811
export { default as CookieStorageService } from './CookieStorageService.js';
912
export { default as SaveSlotManager } from './SaveSlotManager.js';
1013
export { compressText, decompressText, compressJson, decompressJson } from './CompressionService.js';

src/shared/utils/debugConfigUtils.js

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { LocalStorageService } from '../../engine/persistence/index.js';
12
import { safeTrim, toLowerSafe } from './stringUtils.js';
23
const BUILD_DEBUG_MODE = 'prod';
34
const BUILD_DEBUG_ENABLED = false;
@@ -25,35 +26,27 @@ export function normalizeDebugMode(value, fallback = 'prod') {
2526
}
2627

2728
export function readStoredBoolean(key) {
28-
if (!key || typeof globalThis.localStorage === 'undefined') {
29+
if (!key) {
2930
return null;
3031
}
3132

32-
try {
33-
const value = globalThis.localStorage.getItem(key);
34-
if (value === '1') {
35-
return true;
36-
}
37-
if (value === '0') {
38-
return false;
39-
}
40-
} catch {
41-
return null;
33+
const value = new LocalStorageService().getItem(key, null);
34+
if (value === '1') {
35+
return true;
36+
}
37+
if (value === '0') {
38+
return false;
4239
}
4340

4441
return null;
4542
}
4643

4744
export function writeStoredBoolean(key, value) {
48-
if (!key || typeof globalThis.localStorage === 'undefined') {
45+
if (!key) {
4946
return;
5047
}
5148

52-
try {
53-
globalThis.localStorage.setItem(key, value ? '1' : '0');
54-
} catch {
55-
// Ignore storage failures to keep startup resilient.
56-
}
49+
new LocalStorageService().setItem(key, value ? '1' : '0');
5750
}
5851

5952
export function isLocalDebugEnvironment(documentRef) {

src/tools/common/GameManifestLoader.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { asPositiveInteger } from "../../shared/number/index.js";
2+
import { SessionStorageService } from "../../engine/persistence/index.js";
23
import { isRecord } from "../../shared/types/typeGuards.js";
34

45
export { isRecord };
@@ -36,7 +37,7 @@ export class GameManifestLoader {
3637
} = {}) {
3738
this.fetch = fetchRef || windowRef.fetch?.bind(windowRef) || null;
3839
this.pathParams = pathParams;
39-
this.sessionStorage = sessionStorageRef || windowRef.sessionStorage || null;
40+
this.sessionStorage = new SessionStorageService(sessionStorageRef || windowRef.sessionStorage || null);
4041
this.window = windowRef;
4142
}
4243

@@ -62,7 +63,7 @@ export class GameManifestLoader {
6263
if (!hostContextId) {
6364
return { ok: false, message: "Workspace launch did not include hostContextId." };
6465
}
65-
const rawValue = this.sessionStorage?.getItem(hostContextId) || "";
66+
const rawValue = this.sessionStorage.getItem(hostContextId, "") || "";
6667
if (!rawValue) {
6768
return { ok: false, message: `Workspace manifest context was not found in sessionStorage: ${hostContextId}.` };
6869
}

0 commit comments

Comments
 (0)