Skip to content

Commit

Permalink
feat(state): Add multiple state types for more use cases
Browse files Browse the repository at this point in the history
  • Loading branch information
snickbit committed Aug 7, 2023
1 parent 7a7453b commit c5defdb
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 158 deletions.
37 changes: 37 additions & 0 deletions packages/state/__tests__/ReactiveState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {ReactiveState} from '../src'

describe('ReactiveState', () => {
it('should get and set state properties', () => {
const state = new ReactiveState({foo: 'bar'})
expect(state.foo).toBe('bar')
state.foo = 'baz'
expect(state.foo).toBe('baz')
})

it('should get and set proxy properties', () => {
const state = new ReactiveState({foo: 'bar'})
state.baz = 'qux'
expect(state.baz).toBe('qux')
})

it('should return undefined for non-existent properties', () => {
const state = new ReactiveState()
expect(state.foo).toBeUndefined()
})

it('should call watchers when state properties change', () => {
const state = new ReactiveState({foo: 'bar'})
const callback = jest.fn()
state.$watch('foo', callback)
state.foo = 'baz'
expect(callback).toHaveBeenCalledWith('baz')
})

it('should emit events when $emit is called', () => {
const state = new ReactiveState()
const callback = jest.fn()
state.$on('test', callback)
state.$emit('test', 'data')
expect(callback).toHaveBeenCalledWith('data')
})
})
21 changes: 21 additions & 0 deletions packages/state/__tests__/SimpleState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {SimpleState} from '../src'

describe('SimpleState', () => {
it('should get and set state properties', () => {
const state = new SimpleState({foo: 'bar'})
expect(state.foo).toBe('bar')
state.foo = 'baz'
expect(state.foo).toBe('baz')
})

it('should get and set proxy properties', () => {
const state = new SimpleState({foo: 'bar'})
state.baz = 'qux'
expect(state.baz).toBe('qux')
})

it('should return undefined for non-existent properties', () => {
const state = new SimpleState()
expect(state.foo).toBeUndefined()
})
})
43 changes: 43 additions & 0 deletions packages/state/__tests__/State.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {State} from '../src'

describe('State', () => {
it('should get and set state properties', () => {
const state = new State({foo: 'bar'})
expect(state.foo).toBe('bar')
state.foo = 'baz'
expect(state.foo).toBe('baz')
})

it('should get and set proxy properties', () => {
const state = new State({foo: 'bar'})
state.baz = 'qux'
expect(state.baz).toBe('qux')
})

it('should return undefined for non-existent properties', () => {
const state = new State()
expect(state.foo).toBeUndefined()
})

it('should have a unique ID', () => {
const state1 = new State()
const state2 = new State()
expect(state1.$id).not.toBe(state2.$id)
})

it('should have a name if provided', () => {
const state = new State('test')
expect(state.$name).toBe('State.test')
})

it('should get state properties with $get', () => {
const state = new State({foo: 'bar'})
expect(state.$get('foo')).toBe('bar')
})

it('should set state properties with $set', () => {
const state = new State({foo: 'bar'})
state.$set('foo', 'baz')
expect(state.foo).toBe('baz')
})
})
27 changes: 0 additions & 27 deletions packages/state/__tests__/index.test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/state/indexer.config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"source": "src/**/*",
"source": "src/*",
"output": "src/index.ts",
"type": "wildcard"
}
67 changes: 67 additions & 0 deletions packages/state/src/ReactiveState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {uuid} from '@snickbit/utilities'
import {State} from './State'
import {makeStateProxy} from './lib/make-state-proxy'
import mitt, {Handler} from 'mitt'

export type WatchCallback = (value: any) => any
export type WatchStop = () => void
export type Watchers = Record<string, WatchCallback>

export class ReactiveState<T extends object = any> extends State<T> {
declare protected proxy: ReactiveState<T> & T

protected emitter = mitt()
protected watchers = {} as Record<keyof T, Watchers>

constructor(data: Partial<T> = {}) {
super(data)

console.log(`ReactiveState ${this.id} constructor`, {data, state: this.$state})

if (!this.proxy) {
throw new Error('No ReactiveState proxy')
}

this.proxy = makeStateProxy.apply(this)
return this.proxy
}

$set(key: keyof T, value: any) {
console.log({key, value})
super.$set(key, value)
this.callWatchers(key, value)
}

$on(event: string, callback: Handler) {
this.emitter.on(this.makeId(event), callback)
}

$off(event: string, callback: Handler) {
this.emitter.off(this.makeId(event), callback)
}

$emit(event: string, data: any) {
this.emitter.emit(this.makeId(event), data)
}

$watch(key: keyof T, callback: WatchCallback) {
const watchers: Watchers = this.watchers[key] || {}

const id = uuid() as string
watchers[id] = callback

this.watchers[key] = watchers

return () => {
delete this.watchers[key][id]
}
}

private callWatchers(key: keyof T, value: any) {
if (this.watchers[key]) {
for (const id in this.watchers[key]) {
this.watchers[key][id](value)
}
}
}
}
23 changes: 23 additions & 0 deletions packages/state/src/SimpleState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {uuid} from '@snickbit/utilities'
import {makeStateProxy} from './lib/make-state-proxy'

export interface SimpleState {
[key: string | symbol]: any
}

export class SimpleState<T extends object = any> {
protected proxy: SimpleState<T> & T
protected state = {} as T
protected id?: string

constructor(data?: Partial<T>) {
this.id = uuid()
this.state = {...data} as T

this.proxy = makeStateProxy.apply(this)

console.log(`SimpleState ${this.id} constructor`, {data, state: this.state})

return this.proxy
}
}
73 changes: 73 additions & 0 deletions packages/state/src/State.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {isString, objectClone} from '@snickbit/utilities'
import {SimpleState} from './SimpleState'
import {makeStateProxy} from './lib/make-state-proxy'

export class State<T extends object = any> extends SimpleState<T> {
declare protected proxy: State<T> & T
protected original: T
protected name?: string

constructor(name?: string)
constructor(data?: Partial<T>)
constructor(name: string, data?: Partial<T>)
constructor(nameOrData: Partial<T> | string, optionalData?: Partial<T>) {
let name: string
let data: Partial<T>
if (isString(nameOrData)) {
name = nameOrData
data = optionalData
} else {
data = nameOrData
}

super(data)

this.proxy = makeStateProxy.apply(this)

this.name = this.constructor.name + (name ? `.${name}` : '')
this.original = objectClone(this.state)
return this.proxy
}

get $id() {
return this.id
}

get $name() {
return this.name || this.constructor.name
}

get $state() {
return this.state
}

$get(key: keyof T) {
return this.state[key]
}

$set(key: keyof T, value: any) {
this.state[key as keyof T] = value
}

$has(key: string) {
return key in this.state
}

$keys() {
return Object.keys(this.state)
}

$patch(data: Partial<T>) {
for (const key in data) {
this.$set(key, data[key])
}
}

$reset() {
this.state = objectClone(this.original)
}

protected makeId(...keys: string[]) {
return [this.$name, ...keys].join('.')
}
}
Loading

0 comments on commit c5defdb

Please sign in to comment.