Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions lib/internal/webstorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ const { hasInspector } = internalBinding('config');
const { getOptionValue } = require('internal/options');
const { kConstructorKey, Storage } = internalBinding('webstorage');
const { getValidatedPath } = require('internal/fs/utils');
const {
addAfterUserSerializeCallback,
namespace: { isBuildingSnapshot },
throwIfBuildingSnapshot,
} = require('internal/v8/startup_snapshot');
const kInMemoryPath = ':memory:';

module.exports = { Storage };
Expand All @@ -14,12 +19,19 @@ let lazyLocalStorage;
let lazySessionStorage;
let lazyInspectorStorage;
let localStorageWarned = false;
if (isBuildingSnapshot()) {
// User serialize callbacks can still read localStorage.
addAfterUserSerializeCallback(() => {
localStorageWarned = false;
});
}

// Check at load time if localStorage file is provided to determine enumerability.
// If not provided, localStorage is non-enumerable to avoid breaking {...globalThis}.
const localStorageLocation = getOptionValue('--localstorage-file');
const experimentalStorageInspection =
hasInspector && getOptionValue('--experimental-storage-inspection');
// This module can be loaded while building a startup snapshot and then run
// with different options, so the getters read the options when they are
// called instead of at load time.
function isStorageInspectionEnabled() {
return hasInspector && getOptionValue('--experimental-storage-inspection');
}

function getInspectorStorage() {
if (lazyInspectorStorage === undefined) {
Expand All @@ -33,9 +45,11 @@ ObjectDefineProperties(module.exports, {
localStorage: {
__proto__: null,
configurable: true,
enumerable: localStorageLocation !== '',
// If no file is provided, localStorage is non-enumerable to avoid breaking {...globalThis}.
enumerable: getOptionValue('--localstorage-file') !== '',
get() {
if (lazyLocalStorage === undefined) {
const localStorageLocation = getOptionValue('--localstorage-file');
if (localStorageLocation === '') {
if (!localStorageWarned) {
localStorageWarned = true;
Expand All @@ -47,7 +61,8 @@ ObjectDefineProperties(module.exports, {
return undefined;
}

if (experimentalStorageInspection) {
throwIfBuildingSnapshot('localStorage');
if (isStorageInspectionEnabled()) {
const { InspectorLocalStorage } = getInspectorStorage();
lazyLocalStorage = new InspectorLocalStorage(kConstructorKey, getValidatedPath(localStorageLocation), true);
} else {
Expand All @@ -63,7 +78,8 @@ ObjectDefineProperties(module.exports, {
enumerable: true,
get() {
if (lazySessionStorage === undefined) {
if (experimentalStorageInspection) {
throwIfBuildingSnapshot('sessionStorage');
if (isStorageInspectionEnabled()) {
const { InspectorSessionStorage } = getInspectorStorage();
lazySessionStorage = new InspectorSessionStorage(kConstructorKey, kInMemoryPath, false);
} else {
Expand Down
9 changes: 8 additions & 1 deletion src/node_external_reference.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,19 @@ class ExternalReferenceRegistry {
#define EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V)
#endif

#if HAVE_SQLITE
#define EXTERNAL_REFERENCE_BINDING_LIST_SQLITE(V) V(webstorage)
#else
#define EXTERNAL_REFERENCE_BINDING_LIST_SQLITE(V)
#endif // HAVE_SQLITE

#define EXTERNAL_REFERENCE_BINDING_LIST(V) \
EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \
EXTERNAL_REFERENCE_BINDING_LIST_INSPECTOR(V) \
EXTERNAL_REFERENCE_BINDING_LIST_I18N(V) \
EXTERNAL_REFERENCE_BINDING_LIST_CRYPTO(V) \
EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V)
EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) \
EXTERNAL_REFERENCE_BINDING_LIST_SQLITE(V)

} // namespace node

Expand Down
24 changes: 24 additions & 0 deletions src/node_webstorage.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "memory_tracker-inl.h"
#include "node.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_mem-inl.h"
#include "path.h"
#include "simdutf.h"
Expand Down Expand Up @@ -813,7 +814,30 @@ static void Initialize(Local<Object> target,
.Check();
}

static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(Storage::New);
registry->Register(StorageGetter);
registry->Register(StorageSetter);
registry->Register(StorageQuery);
registry->Register(StorageDeleter);
registry->Register(StorageEnumerator);
registry->Register(StorageDefiner);
registry->Register(IndexedGetter);
registry->Register(IndexedSetter);
registry->Register(IndexedQuery);
registry->Register(IndexedDeleter);
registry->Register(IndexedDefiner);
registry->Register(StorageLengthGetter);
registry->Register(Clear);
registry->Register(GetItem);
registry->Register(Key);
registry->Register(RemoveItem);
registry->Register(SetItem);
}

} // namespace webstorage
} // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(webstorage, node::webstorage::Initialize)
NODE_BINDING_EXTERNAL_REFERENCE(webstorage,
node::webstorage::RegisterExternalReferences)
86 changes: 86 additions & 0 deletions test/parallel/test-snapshot-webstorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

// Tests that web storage can be touched by a snapshot builder script: the
// binding can be serialized, creating a Storage is rejected with an error, and
// the options passed after deserialization are honored.

const common = require('../common');
common.skipIfSQLiteMissing();
const assert = require('assert');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');
const {
spawnSyncAndAssert,
spawnSyncAndExitWithoutError,
} = require('../common/child_process');

tmpdir.refresh();
const blobPath = tmpdir.resolve('snapshot.blob');

for (const [name, args] of [
['localStorage', [`--localstorage-file=${tmpdir.resolve('build.db')}`]],
['sessionStorage', []],
]) {
const entry = tmpdir.resolve(`${name}.js`);
fs.writeFileSync(entry, `globalThis.${name};\n`);
spawnSyncAndAssert(process.execPath, [
...args,
'--snapshot-blob',
blobPath,
'--build-snapshot',
entry,
], {
cwd: tmpdir.path,
}, {
status: 1,
stderr: new RegExp(
`ERR_NOT_SUPPORTED_IN_SNAPSHOT.*${name} is not supported in startup snapshot`, 's'),
});
assert(!fs.existsSync(blobPath));
}

{
const entry = tmpdir.resolve('no-file.js');
fs.writeFileSync(entry, `
const { startupSnapshot } = require('v8');
if (globalThis.localStorage !== undefined) throw new Error('unexpected');
// Registered after the web storage module has registered its own.
startupSnapshot.addSerializeCallback(() => globalThis.localStorage);
startupSnapshot.setDeserializeMainFunction(() => {
localStorage?.setItem('local', 'a');
sessionStorage.setItem('session', 'b');
console.log(localStorage?.getItem('local'), sessionStorage.getItem('session'));
});
`);
spawnSyncAndExitWithoutError(process.execPath, [
'--snapshot-blob',
blobPath,
'--build-snapshot',
entry,
], {
cwd: tmpdir.path,
});

spawnSyncAndAssert(process.execPath, [
`--localstorage-file=${tmpdir.resolve('run.db')}`,
'--snapshot-blob',
blobPath,
], {
cwd: tmpdir.path,
}, {
stdout: 'a b',
trim: true,
});

// The warning from the build is not carried over into the snapshot.
spawnSyncAndAssert(process.execPath, [
'--snapshot-blob',
blobPath,
], {
cwd: tmpdir.path,
}, {
stdout: 'undefined b',
stderr: /localStorage is not available because --localstorage-file was not provided/,
trim: true,
});
}
Loading