-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement localStorage #1657
Comments
In #1181 we discussed that these could be external modules added to the Deno registry and there wasn't a need for them to be part of the core of Deno. |
I changed my mind. I think local storage would be really useful for serverless situations - imagine you have a server and you want to store a simple chat log or counter - with localStorage we can circumvent using a database nor expose the disk. Let's do it! |
Wont it be better to support sqlite by default, Something like android does. localStorage would be too limited. |
|
https://developer.mozilla.org/en-US/docs/Web/API/Cache Also- should be noted that the reason we want this in core is that Cache/localStorage should be accessible even when --allow-read and --allow-write are off. |
What's the policy for the cache? Is it persistent if the runtime turns off or not? |
@zekth it's persistent |
So if we use something like indexDB it will be shared by all scripts using the Deno runtime? If so, have to ensure there is no overlapping. Also do we add any maximum cache size or a permission for the max size? |
I believe they are part of the spec or defacto standards in browser implementations. |
In Chrome, LocalStorage uses Maybe implement
|
I am trying to do this with https://docs.rs/rusty-leveldb/0.3.0/rusty_leveldb/index.html to implement localStorage
Does
same with sessionStorage and easy implement in Typescript side
How to isolate files?localStorage isolate files by Different domain names have different But in Deno, it is common sense to isolate files with Because it is unique, but each time the pid is different when start a Deno process, it cannot be persisted So is there a better isolation solution? eg. run command line with this If files are not isolatedall Deno processes use the same file Writing files so frequently may cause errors and is not safe This is the problem I have to resolve. |
in the browser, |
This gave me an idea.... maybe it should be scoped to the file that is being executed? Though if you change the location of the file that would break the |
The app could probably request access for origins, then the user will allow/disallow that access. |
Right, that would basically be exposing this: |
From @rektide
|
Copy-pasting my comment from #4671 since this thread is more appropriate: Please make it possible to disable access to localStorage and indexDB and anything else that has side-effects that gets added to Deno in the future. The reason is that things get a lot more complicated for those of us using Deno as the sandbox component of an application platform when apps can have side-effects that we can't control. Example: I might use Deno to run code on demand on any of a large number of VMs. If an app stashed some data in locaStorage during one run, it's going to expect that to be there on a subsequent run. But that might happen on a totally different VM, so the data won't be there. At least make it possible to export all localStorage/IndexDB data for an origin from the command line, and provide an import function as well. One more concern is that it creates the need for an additional layer of control around the amount of data that an origin/whatever can store in their local storage. So long as deno only allowed access to the underlying OS file system, a host could could use the OS's facilities to track and control the user's storage usage. Once you start throwing localStorage data from every app into some internal db, the burden falls on deno to track, report, and limit storage usage. Thanks |
@teleclimber anything that writes/persists data, outside of normal imports, would/should require some sort of allow flag. Security definitely needs to be a consideration for this. |
I'm pretty sure the "persistent-storage" permission exists on the web, and would dictate whether or not the |
The browser doesn't require permission to use I'm not sure why Deno would need permission to store data in a See https://developer.mozilla.org/en-US/docs/Web/API/Storage_API for more details. With the |
Local Storage in a browser can be removed by the user and therefore the web application cannot rely on it. The same states for Deno. The program may be be copied to another host or the local DB cache is cleared. A web app should be a able to handle such cases. |
Something that I have not seen mentioned in this debate yet is that There's an async version called Then there's the argument that "with Also, I'm not sure I'd like I actually think a third party, or even standard module, with a transparent API might be better, for example: import { initLocalStorage } from "https://deno.land/std@v0.41.0/web-compat/localstorage.ts";
async function main() {
let localStorage = await initLocalStorage("my_local_storage.db");
localStorage.setItem("a", "b");
// ...
}
main(); On second thought, I see no reason for providing |
Also it is not available on web workers.
|
So i started working an a localStorage implementation, but there is an issue: which DB to use. |
|
Just a JSON file =)? |
@kryptish I actually implemented something in this direction a while ago, it tries to mirror the Browser localStorage interface as closely as possible and just saves (by default, can be configured via constructor) a .json file with the data in the working directory (it creates a import {
copySync,
ensureFileSync,
existsSync
} from 'https://deno.land/std/fs/mod.ts';
// maybe a Deno flag like --persistent-storage would be useful in the future
// https://storage.spec.whatwg.org/#persistence
// interface: https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
export default class Storage {
// Can't use privat fields and Proxy(), things will crash hard, see
// https://disq.us/url?url=https%3A%2F%2Fgit.luolix.top%2Ftc39%2Fproposal-class-fields%2Fissues%2F106%3ACgK5-2pGsZhNCXXqGKGy2OO0PwI&cuid=611304
//#entries;
constructor(path) {
// should this even be configurable?
Reflect.defineProperty(this, 'path', {
configurable: false,
enumerable: false,
writable: false,
value: path || `${Deno.cwd()}/.tmp/localStorage.json`,
});
Reflect.defineProperty(this, 'entries', {
configurable: false,
enumerable: true,
writable: true,
value: null,
});
Reflect.defineProperty(this, 'read', {
configurable: false,
enumerable: false,
writable: false,
value: function() {
if ( existsSync(this.path) ) {
try {
return Object.entries(JSON.parse(Deno.readTextFileSync(this.path)));
} catch(err) {
// check for backup
if ( existsSync(`${this.path}.backup`) ) {
try {
return Object.entries(JSON.parse(Deno.readTextFileSync(`${this.path}.backup`)));
} catch(err) {
return [];
}
}
}
} else {
return [];
}
},
});
Reflect.defineProperty(this, 'write', {
configurable: false,
enumerable: false,
writable: false,
value: function() {
ensureFileSync(this.path);
// create backup in case something goes wrong while writing the file
// Deno crashing mid-write or something similar can cause corrupted JSON!
copySync(this.path, `${this.path}.backup`, {
overwrite: true,
preserveTimestamps: true,
});
// persist to disk...
Deno.writeTextFileSync(this.path, JSON.stringify(Object.fromEntries(this.entries)));
},
});
Reflect.defineProperty(this, 'delete', {
configurable: false,
enumerable: false,
writable: false,
value: function() {
[this.path, `${this.path}.backup`].forEach((path) => {
existsSync(path) && Deno.removeSync(path);
});
},
});
/**
* Returns the number of key/value pairs. In the Browser this is enumerable!
* Plus setting length on Browser localStorage with `localStorage['length'] = x`
* creates an actual entry with key 'length'...but then the console.log() representation
* changes and contains an entries property.
*
*
* @returns {Number}
*/
Reflect.defineProperty(this, 'length', {
configurable: true,
enumerable: true,
writeable: true,
get: function() {
return this.entries.size;
},
});
this.entries = new Map(this.read());
return new Proxy(this, {
get(target, key, receiver) {
if ( !target[key] ) {
return target.getItem(key);
}
return Reflect.get(target, key, receiver);
},
set(target, key, value) {
// redirect setting any properties on the Storage object itself to the Map
target.setItem(key, value);
return true;
},
deleteProperty(target, key) {
target.removeItem(key);
return true;
}
});
}
/**
* Returns the name of the nth key, or null if n is greater than or equal to
* the number of key/value pairs.
*
* @param {Number} index
*
* @returns {String}
*/
key(index) {
return index >= this.entries.length ? null : [...this.entries.keys()][index];
};
/**
* Returns the current value associated with the given key,
* or null if the given key does not exist.
*
* @param {String} key
*
* @returns {String}
*/
getItem(key) {
if ( this.entries.has(key) ) {
return String(this.entries.get(key));
}
return null;
};
/**
* Sets the value of the pair identified by key to value, creating a new key/value pair
* if none existed for key previously.
* TODO: Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set.
* (Setting could fail if, e.g., the user has disabled storage for the site,
* or if the quota has been exceeded.)
* TODO: Dispatches a storage event on Window objects holding an equivalent Storage object.
*
* @note Browser's behaviour is a bit strange, with localStorage[<key>] = <value> it returns
* the value, but when using localStorage.setItem(<key>) it always returns undefined.
*
* @param {String} key
* @param {any} value
*
* @returns undefined
*/
setItem(key, value) {
this.entries.set(key, String(value));
this.write();
};
/**
* Removes the key/value pair with the given key, if a key/value pair with the given key exists.
* TODO: Dispatches a storage event on Window objects holding an equivalent Storage object.
*
* @note Browser's behaviour is a bit strange, with delete localStorage[<key>] it returns true
* for known and unknown keys, but when using localStorage.removeItem(<key>) it always
* returns undefined, regardless if the key was known or not.
*
* @param {String} key
*
* @returns undefined
*/
removeItem(key) {
this.entries.delete(key);
this.write()
};
/**
* Removes all key/value pairs, if there are any.
* TODO: Dispatches a storage event on Window objects holding an equivalent Storage object.
*
* @returns undefined
*/
clear() {
this.entries.clear();
this.delete();
};
} some primitive tests: import { default as Storage } from './Storage.js';
const localStorage = new Storage();
console.log(localStorage);
localStorage.setItem('int', 0);
localStorage.setItem('float', 1/7);
localStorage.setItem('array', [1, 'two', 3, 'four']);
localStorage.setItem('json', JSON.stringify({'some': 'object', 'with': 1}));
localStorage['some'] = 'value';
// some strange edge cases
localStorage['length'] = 0;
console.log('localStorage["set"] = "a value" =>', localStorage['set'] = 'a value');
console.log('localStorage.key(2) =>', localStorage.key(2));
console.log('getItem("array") =>', localStorage.getItem('array'));
console.log('localStorage["array"] =>', localStorage['array']);
console.log(localStorage);
console.log('delete known key with delete =>', delete localStorage['json']);
console.log('delete unknown key with delete =>', delete localStorage['unknown']);
console.log('delete known key that exists on the Storage object as well with delete =>', delete localStorage['length']);
console.log('delete unknown key with removeItem() =>', localStorage.removeItem('unknown'));
localStorage.removeItem('set');
console.log(localStorage);
// localStorage.clear();
// console.log('clear() =>', localStorage); |
Do you suggest limitations like that of the browser? Ex: 2MB - 5MB memory cap? |
@00ff0000red yes, limit is going to be 5MB, just like in the spec. |
Implement
localStorage
andsessionStorage
following the MDN spec, sessionStorageThe text was updated successfully, but these errors were encountered: