🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

statez

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

statez - npm Package Compare versions

Comparing version
1.0.1
to
2.0.0
+6
-2
dist/statez.js

@@ -1,2 +0,6 @@

/* statez 1.0.1, by Craig Buckler, 2022-06-17T11:11 */
var c={},f={page:null,session:"sessionStorage",permanent:"localStorage"},l=class extends EventTarget{#s=null;#e=null;#t=0;#n=null;constructor(e,s,n,i=0){super(),this.#s=s,this.#e=e&&window[e],this.#t=i,this.#e&&(n=JSON.parse(this.#e.getItem(s))||n);for(let o in n)this[o]=n[o];this.#t&&this.syncState()}dispatchEvent(e={}){e.state=c[this.#s],super.dispatchEvent(new CustomEvent("change",{detail:e,bubbles:!1,cancelable:!1})),this.#t&&(clearTimeout(this.#n),this.#n=setTimeout(()=>this.syncState(),this.#t))}cleanState(){Object.getOwnPropertyNames(this).forEach(e=>{let s={property:e,valueOld:this[e]};delete this[e],this.dispatchEvent(s)})}syncState(){this.#e&&this.#e.setItem(this.#s,JSON.stringify(this))}},h={set:(t,e,s)=>{let n=Reflect.get(t,e),i=!0;return n!==s&&typeof t[e]!="function"&&(i=Reflect.set(t,e,s),i&&t.dispatchEvent({property:e,value:s,valueOld:n}),typeof s>"u"&&Reflect.deleteProperty(t,e)),i},deleteProperty:(t,e)=>{let s=Reflect.get(t,e);return Reflect.getOwnPropertyDescriptor(t,e)&&Reflect.deleteProperty(t,e)&&t.dispatchEvent({property:e,valueOld:s}),!0},get:(t,e)=>{let s=t[e];return typeof s=="function"?function(...n){return s.apply(t,n)}:Reflect.get(t,e)}};function d({type:t=f.permanent,name:e=null,init:s=null,sync:n=0}={}){return e=e||"stateZ"+(t||"page"),c[e]||(c[e]=new Proxy(new l(t,e,s,n),h)),c[e]}window.addEventListener("storage",t=>{let e=c[t.key];if(e){let s=JSON.parse(t.newValue);new Set([...Object.getOwnPropertyNames(e),...Object.getOwnPropertyNames(s)]).forEach(i=>{JSON.stringify(e[i])!=JSON.stringify(s[i])&&(typeof s[i]>"u"?delete e[i]:e[i]=s[i])})}});window.addEventListener("beforeunload",()=>{Object.values(c).forEach(t=>t.syncState())});export{d as stateZ,f as stateZtype};
/*
statez 2.0.0 - Simple client-side state manager with events, synchronization, and local storage
https://github.com/craigbuckler/statez#readme
Craig Buckler, 2023-08-26
*/
var c=new Map,u=new Map,y="statez",i=window.localStorage,a,v=window.requestIdleCallback||(t=>setTimeout(t,20)),d=class extends EventTarget{#e=null;constructor(e){super(),this.#e=e}get stateId(){return this.#e}set(e,n){return typeof n>"u"?Reflect.deleteProperty(this,e):Reflect.set(this,e,n)}addEventListener(){super.addEventListener.apply(this,arguments)}removeEventListener(){super.removeEventListener.apply(this,arguments)}dispatchEvent(e){let n={detail:e,bubbles:!1,cancelable:!1};super.dispatchEvent(new CustomEvent(e.property,n)),super.dispatchEvent(new CustomEvent("*",n))}},g={set:(t,e,n)=>p(t,e,n),deleteProperty:(t,e)=>p(t,e),get:(t,e)=>{let n=t[e];return typeof n=="function"?function(...s){return n.apply(t,s)}:Reflect.get(t,e)}};function E(t,e){t=t||y,c.has(t)||c.set(t,new Proxy(new d(t),g));let n=c.get(t);return h(n,{...e}),n}function h(t,e){let n=t.stateId;for(let s=0;s<i.length;s++){let r=i.key(s),o=f(r);if(o&&o.store===n){let l=o.property;t.set(l,JSON.parse(i.getItem(r))||void 0),delete e[l]}}for(let s in e)t[s]=e[s]}function p(t,e,n){let s=Reflect.get(t,e),r=!0;if(s===n)return r;if(typeof n>"u"?r=Reflect.deleteProperty(t,e):r=Reflect.set(t,e,n),r){let o=t.stateId+"."+e,l=u.get(o)||{state:t,property:e,valueOld:s};l.value=n,u.set(o,l),a=a||v(w)}return r}async function w(){let t=u.values(),e;do{if(e=t.next().value,!e||e.value===e.valueOld)continue;let{state:n,property:s,value:r}=e;n.dispatchEvent(e);let o="sZ."+n.stateId+"."+s;typeof r>"u"?i.removeItem(o):i.setItem(o,JSON.stringify(r))}while(e);u.clear(),a=null}window.addEventListener("storage",t=>{let e=f(t.key),n=e&&c.get(e.store);if(n){let s=t.newValue===null?void 0:JSON.parse(t.newValue),r={state:n,property:e.property,value:s,valueOld:n[e.property]};n.set(e.property,s),n.dispatchEvent(r)}});function f(t){let e=t.split(".");return e.length===3&&e[0]==="sZ"?{store:e[1],property:e[2]}:null}export{E as stateZ};
{
"name": "statez",
"version": "1.0.1",
"description": "Simple client-side state manager",
"version": "2.0.0",
"description": "Simple client-side state manager with events, synchronization, and local storage",
"type": "module",

@@ -11,4 +11,4 @@ "browser": "./dist/statez.js",

"scripts": {
"build": "node ./esbuild.config.js production",
"start": "node ./esbuild.config.js development"
"build": "node ./esbuild.config.js",
"prepublishOnly": "npm run build"
},

@@ -27,3 +27,4 @@ "repository": {

"state",
"manager"
"manager",
"localStorage"
],

@@ -33,4 +34,4 @@ "author": "Craig Buckler",

"devDependencies": {
"esbuild": "^0.14.45"
"esbuild": "^0.19.2"
}
}
+191
-132
# stateZ
**stateZ** (state-easy) is a simple client-side state manager used to save values. Features:
stateZ (state-easy) is a simple client-side state manager. Features:
* simple to use, e.g. `myState.x = 1; console.log( myState.x );`
* can store data for the [current page, current session, or permanently](#advanced-initialization)
* [triggers events](#state-change-events) when any state changes
* [synchronizes data](#advanced-initialization) across browser tabs
* triggers events when a state changes
* synchronizes data across browser tabs and windows on the same domain
* vanilla JavaScript compatible with all frameworks
* fast and lightweight - less than 2KB of code
The stateZ version 2 API differs to version 1. Refer to [UPGRADE.md](https://github.com/craigbuckler/statez/blob/main/UPGRADE.md) for migration details.
## Compatibility
**stateZ** works in modern browsers which support ES modules.
stateZ works in modern browsers which support ES modules.
## Basic use
## stateZ comparison
stateZ 2.0 works in a similar way to [stateZx](https://github.com/craigbuckler/statezx):
| feature | stateZ | stateZx |
|-|-|-|
| code size| 2Kb | 4.5Kb |
| storage | localStorage | indexedDB |
| storage limit | typically 5MB | typically 1GB |
| data types | stringified values | values, objects, blobs |
| data lifetime | permanent | permanent |
| performance | good, but synchronous storage | good with asynchronous storage |
stateZ is a good option for web sites with minimal storage requirements. [stateZx](https://github.com/craigbuckler/statezx) may be preferable for complex web apps storing large amounts of data.
## Installation
Load the module from a CDN:
```js
import { stateZ, stateZtype } from 'https://cdn.jsdelivr.net/npm/statez/dist/statez.js'
import { stateZ } from 'https://cdn.jsdelivr.net/npm/statez/dist/statez.js';
```
If using npm and a bundler, install the package:
If using `npm` and a bundler, install with:

@@ -32,15 +49,19 @@ ```sh

then import the module locally (path resolution will depend on the bundler):
then import the module locally *(path resolution will depend on the bundler)*:
```js
import { stateZ, stateZtype } from './node_modules/statez/dist/statez.js';
import { stateZ } from './node_modules/statez/dist/statez.js';
```
Create a named permanent state store:
## Examples
Create/access a named state store by passing an optional ID and initialization object:
```js
// create state object
const myState = stateZ({ name: 'myState' });
const state = stateZ('myState', { a: 1, b: 2, c: 3 });
```
Any state object on any page in any tab or window on the same domain which accesses the same `"myState"` store has access to the same properties. Previously-stored values initialize the properties. If `a` is not defined, the initialization object sets `state.a` to `1`.
Set and retrieve values:

@@ -50,217 +71,255 @@

// set state
myState.x = 123;
myState.y = 'abc';
state.x = 123;
state.y = 'abc';
// get state
console.log( myState.x, myState.y ); // 123 abc
console.log( state.x, state.y ); // 123 abc
// output all properties
for (let p in myState) {
console.log(`${ p }: ${ myState[p] }`);
for (let p in state) {
console.log(`${ p }: ${ state[p] }`);
}
// delete state
delete myState.x; // or
myState.x = undefined;
delete state.x; // or
state.x = undefined;
```
You can use the same state in any other script on the same domain to access the same values:
Get the store name:
```js
// another script
function showState() {
const s = stateZ({ name: 'myState' });
console.log( s.y ); // abc
}
showState();
console.log( state.stateId ); // myState
```
Run an event handler when any property changes:
## Advanced initialization
```js
// event handler function
function stateEventHandler(evt) {
Pass an initialization object to the `stateZ()` function with the following optional properties:
const d = evt.detail;
console.log(`
${ d.property } has changed
from ${ d.valueOld } to ${ d.value }
in store ${ d.store.stateId }
(event type "${ evt.type }")
`);
* `type`: the storage type.
* `name`: a store name. All objects with the same `name` and `type` share the same data.
* `init`: an initial object when no previous state is available.
* `sync`: cross-tab [auto-synchronization](#state-synchronization) time in milliseconds.
}
The storage `type` can be:
// handle any state change
state.addEventListener('*', stateEventHandler);
```
* `stateZtype.permanent` (default): permanent data shared across all browser tabs on the same domain which persists until the user wipes their browser cache.
* `stateZtype.session`: temporary data which persists in the current tab and is wiped when it is closed.
* `stateZtype.page`: temporary data which persists in the current page view and is wiped on a page refresh or tab close.
or when an individual property changes:
When no `name` is specified, a default store is created for each `type`. The following stores are the same:
```js
const s1 = stateZ();
const s2 = stateZ();
// handle changes to state.a property
state.addEventListener('a', stateEventHandler);
```
The following stores are different:
Example:
```js
const s3 = stateZ({ type: stateZtype.permanent }); // identical to s1 and s2
const s4 = stateZ({ type: stateZtype.session });
const s4 = stateZ({ type: stateZtype.page });
```
state.a = 'one';
The `init` object initializes the store when no previous data is available:
/*
both the "a" and "*" events trigger - ouput:
a has changed from 1 to one in store myStore (event type "a")
a has changed from 1 to one in store myStore (event type "*")
*/
```js
const s = stateZ({ name: 'myState', init: { a: 1, b: 2, c: 3 } });
state.b = 'two';
console.log( s.a ); // 1 unless state was previously stored or set elsewhere
/*
the "*" event triggers - ouput:
b has changed from 2 to two in store myStore (event type "*")
*/
```
## State synchronization
## API reference
Permanent and session state is automatically saved when the page unloads. Permanent state is synchronized to other tabs at that point but this may never occur on some long-running web pages.
Create/access a named store using the `stateZ` constructor with optional parameters:
Setting `sync` to a non-zero value automatically synchronises **permanent** data stores to other browser tabs on the same domain where the same **stateZ** object is loaded:
| name | type | description |
|-|-|-|
| `stateId` | string | state identifier (`stateZ` if not defined) |
| `stateDefault` | object | initialization object |
The `stateId` can be any string, but do not use space or `.` characters.
The initialization object can contain any number of key/value pairs, e.g.
```js
const s = stateZ({ name: 'myState', sync: 3000 });
const state = stateZ('myState', {
a: 1,
b: 'two',
c: false,
xArray: [1,2,3],
yObject: { p1: 'prop1', p2: 'prop2' }
});
```
Synchronizing larger states is an expensive operation. The `sync` value sets a (debounce) number of milliseconds to wait before synchronization occurs. The example above will synchronize no more than once every three seconds regardless of how many state values are updated.
stateZ uses previously-stored database values by default. Therefore, `state.a` is only set to `1` if it's initially `undefined` (or was stored as `1`). Setting a new value triggers events, stores it in localStorage, and synchronizes with other tabs/windows using stateZ on the same domain (which trigger their own events).
Alternatively, you can set `sync` to `0` (the default) and manually synchronize to other tabs using the `.syncState()` method:
### .stateId
Returns the state identifier (read-only):
```js
const s = stateZ({ name: 'myState' });
s.a = 1;
s.b = 2;
// synchronize now
s.syncState();
console.log( state.stateId ); // myState
```
## Deleting state
### set, get, and delete properties
You can delete individual properties using:
Set and get any property using a valid name and value:
```js
delete myState.x
state.prop1 = 'my first property';
console.log( state.prop1 ); // my first property
console.log( state['prop1'] ); // my first property
```
Setting a property to `undefined` is identical:
Delete a property:
```js
myState.x = undefined;
delete state.prop1;
// or: delete state['prop1'];
// or: state.prop1 = undefined;
// or: state['prop1'] = undefined;
console.log( state.prop1 ); // undefined
```
Delete all properties using the `.cleanState()` method:
Delete all properties:
```js
myState.cleanState();
for (let p in state) delete state[p];
```
An empty state still overrides any `init` object set when initializing.
Property:
* *names* can contain letters in any case, numbers, or hyphens - but must start with a letter
* *values* can be any value which can be [serialized](https://developer.mozilla.org/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) using `JSON.stringify()` - anything except for Symbol and functions. [Date() objects](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date) are stringified so you may need to re-initialize after calling the stateZ constructor, e.g. `state.myDate = new Date( state.myDate );`
## State change events
Values are checked to ensure they've changed before triggering events, storage, and tab/window synchronization. Setting `state.a = 1` only has an effect when it's not already `1`.
You can trigger an event handler when a property value changes. This could be useful for data binding or similar activities.
The event handler function receives an object with a `details` property that has the following child properties:
### Setting properties to objects or arrays
* `.detail.property`: the name of the updated property
* `.detail.value`: the new value (`undefined` when a property has been deleted)
* `.detail.valueOld`: the previous value before the update
* `.detail.state`: the **stateZ** object
Setting a property to an object or array will **always** trigger events, storage, and tab/window synchronization. This occurs because objects are passed by reference. Two objects or arrays are not the same even when their values are identical:
This example function logs state updates:
```js
console.log( state.myArray ); // [1,2,3]
state.myArray = [1,2,3]; // triggers event, store, sync
console.log( state.myObject ); // {a:1,b:2}
state.myObject = {a:1,b:2}; // triggers event, store, sync
```
Setting a child property or array element will **not** trigger events, storage, and synchronization:
```js
// event handler
function stateUpdate(e) {
console.log(`STATE UPDATE EVENT`);
console.log(`property name : ${ e.detail.property }`);
console.log(`updated value : ${ JSON.stringify(e.detail.value) }`);
console.log(`previous value: ${ JSON.stringify(e.detail.valueOld) }`);
}
state.myArray.push[4]; // not handled
state.myObject.a = 99; // not handled
state.myObject.c = 100; // not handled
```
It can be set as an event listener for any `statez` object using the `.addEventListener() ` method and a `'change'` trigger, e.g.
It may be preferable to update the whole object or create separate stateZ stores with native values rather than use nested arrays and objects.
### .set(property, value)
Sets temporary session-like values in the current tab. It does not trigger events, storage, and synchronization:
```js
const myState = stateZ({ name: 'myState', init: { x: 1 } });
// set value
state.set('temp', 'temporary value');
console.log(state.temp); // temporary value
// add change listener
myState.addEventListener('change', stateUpdate);
// delete value
state.set('temp');
console.log(state.temp); // undefined
```
The handler runs every time a property value is updated:
```js
myState.x = 1; // no change - no event triggered
### State change events
myState.x = 2; // value changed
// STATE UPDATE EVENT
// property name : x
// updated value : 2
// previous value: 1
You can trigger event handler functions when **any** property changes:
myState.y = 'abc'; // new value
// STATE UPDATE EVENT
// property name : y
// updated value : 'abc'
// previous value: undefined
delete myState.y; // value deleted
// STATE UPDATE EVENT
// property name : y
// updated value : undefined
// previous value: 'abc'
```js
// handle any state change
state.addEventListener('*', stateEventHandler);
```
Remove a handler with `.removeEventListener()`:
or when an individual property changes:
```js
myState.removeEventListener('change', stateUpdate);
// handle changes to state.myProp property
state.addEventListener('myProp', stateEventHandler);
```
Handler events are also triggered on **other** tabs when [synchronizing state](#state-synchronization).
Changes to `state.myProp` triggers both event handlers (the more specific `'myProp'` handler runs first).
Be wary about unconditionally changing states in an event handler. You could trigger an infinite cascade of change and synchronization events.
The handler function receives a single object containing information about the event. Its `.detail` property defines an object with the following properties:
| property | description |
|-|-|
| `.property` | name of the updated property |
| `.value` | the new value |
| `.valueOld` | the old value |
| `.state` | the state object |
## Nested objects and arrays
Example:
You can set a **stateZ** property to an array or object:
```js
myState.arr = [1, 2, 3];
// event handler function
function stateEventHandler(evt) {
myState.obj = {
x: 1,
y: [2,2]
z: { a: 3, b: 4 }
};
const d = evt.detail;
console.log(`stateId : ${ d.store.stateId }`);
console.log(`property name : ${ d.property }`);
console.log(`new value : ${ d.value }`);
console.log(`previous value: ${ d.valueOld }`);
}
```
This triggers change events. Setting the same value again will also trigger an event: the values may be identical but the object is different:
A state change also triggers events on other tabs and windows that use stateZ with the same store on the same domain.
Remove event handlers with the `.removeEventListener()` method:
```js
myState.arr = [1, 2, 3]; // change event triggered
state.removeEventListener('*', stateEventHandler);
state.removeEventListener('myProp', stateEventHandler);
```
However, setting a child property or array item will **not** trigger a change event:
### Event and synchronization lifecycle
You can synchronously change and examine any stateZ object's properties in real time. There are no asynchronous operations.
stateZ records all property changes. A later iteration of the JavaScript event loop triggers events, updates storage, and synchronizes across tabs/windows when the CPU is idle. Consider the following code:
```js
myState.obj.x = 2; // no change event
myState.obj.y.push(3); // no change event
myState.obj.z.b = 5; // no change event
let counter = state.counter;
for (let i = 0; i < 1000; i++) {
counter++;
state.counter = counter;
}
```
It may be preferable to create a new named **stateZ** object with native values rather than use nested arrays or objects.
The code will **not** trigger 1,000 event, storage, and synchronization processes. If `state.counter` is initially stored as `0`, the synchronous loop will complete and it's value changes to `1000`. The update process runs at some future point which:
1. triggers local events where the `details` object has `.property` set to `'counter'`, `.oldValue` set to `0`, and `.value` set to `1000`
1. updates `counter` in localStorage to change the value from `0` to `1000`. This triggers a `localStorage` event on all tabs/windows on the same domain using stateZ which triggers identical events.
Intensive state changes do not have a significant impact on performance because stateZ makes background updates when the program is idle. Nothing would run if `state.counter = 0;` was added after the loop!
## Usage policy

@@ -270,2 +329,2 @@

Please consider [sponsorship](https://github.com/sponsors/craigbuckler) if you use **stateZ** commercially, require support, or need new features.
Please consider [sponsorship](https://github.com/sponsors/craigbuckler) if you use **stateZ** commercially, require support, or want new features.