Skip to content

Commit 5daecec

Browse files
committed
feat(kit): shared state utils
1 parent 50c65f5 commit 5daecec

File tree

9 files changed

+237
-42
lines changed

9 files changed

+237
-42
lines changed

alias.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export const alias = {
1111
'@vitejs/devtools-rpc/presets/ws/client': r('rpc/src/presets/ws/client.ts'),
1212
'@vitejs/devtools-kit/client': r('kit/src/client/index.ts'),
1313
'@vitejs/devtools-kit/utils/events': r('kit/src/utils/events.ts'),
14+
'@vitejs/devtools-kit/utils/nanoid': r('kit/src/utils/nanoid.ts'),
15+
'@vitejs/devtools-kit/utils/shared-state': r('kit/src/utils/shared-state.ts'),
1416
'@vitejs/devtools-kit': r('kit/src/index.ts'),
1517
'@vitejs/devtools-vite': r('vite/src/index.ts'),
1618
'@vitejs/devtools/client/inject': r('core/src/client/inject/index.ts'),

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"birpc-x": "catalog:deps",
6161
"cac": "catalog:deps",
6262
"debug": "catalog:deps",
63+
"immer": "catalog:deps",
6364
"launch-editor": "catalog:deps",
6465
"mlly": "catalog:deps",
6566
"open": "catalog:deps",

packages/kit/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"./client": "./dist/client.mjs",
2424
"./utils/events": "./dist/utils/events.mjs",
2525
"./utils/nanoid": "./dist/utils/nanoid.mjs",
26+
"./utils/shared-state": "./dist/utils/shared-state.mjs",
2627
"./package.json": "./package.json"
2728
},
2829
"main": "./dist/index.mjs",
@@ -42,7 +43,8 @@
4243
"dependencies": {
4344
"@vitejs/devtools-rpc": "workspace:*",
4445
"birpc": "catalog:deps",
45-
"birpc-x": "catalog:deps"
46+
"birpc-x": "catalog:deps",
47+
"immer": "catalog:deps"
4648
},
4749
"devDependencies": {
4850
"my-ua-parser": "catalog:frontend",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { Objectish, Patch } from 'immer'
2+
import type { EventEmitter } from '../types/events'
3+
import { applyPatches, produce, produceWithPatches } from 'immer'
4+
import { createEventEmitter } from './events'
5+
6+
// eslint-disable-next-line ts/no-unsafe-function-type
7+
type ImmutablePrimitive = undefined | null | boolean | string | number | Function
8+
9+
export type Immutable<T>
10+
= T extends ImmutablePrimitive ? T
11+
: T extends Array<infer U> ? ImmutableArray<U>
12+
: T extends Map<infer K, infer V> ? ImmutableMap<K, V>
13+
: T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>
14+
15+
export type ImmutableArray<T> = ReadonlyArray<Immutable<T>>
16+
export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>
17+
export type ImmutableSet<T> = ReadonlySet<Immutable<T>>
18+
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> }
19+
20+
/**
21+
* State host that is immutable by default with explicit mutate.
22+
*/
23+
export interface SharedState<T> {
24+
/**
25+
* Get the current state. Immutable.
26+
*/
27+
get: () => Immutable<T>
28+
/**
29+
* Subscribe to state changes.
30+
*/
31+
on: EventEmitter<SharedStateEvents<T>>['on']
32+
/**
33+
* Mutate the state.
34+
*/
35+
mutate: (fn: (state: T) => void) => void
36+
/**
37+
* Apply patches to the state.
38+
*/
39+
patch: (patches: Patch[]) => void
40+
}
41+
42+
export interface SharedStateEvents<T> {
43+
updated: (state: T) => void
44+
patches: (patches: Patch[]) => void
45+
}
46+
47+
export interface SharedStateOptions<T> {
48+
/**
49+
* Initial state.
50+
*/
51+
initialState: T
52+
/**
53+
* Enable patches.
54+
*
55+
* @default false
56+
*/
57+
enablePatches?: boolean
58+
}
59+
60+
export function createSharedState<T extends Objectish>(
61+
options: SharedStateOptions<T>,
62+
): SharedState<T> {
63+
const {
64+
enablePatches = false,
65+
} = options
66+
67+
const events = createEventEmitter<SharedStateEvents<T>>()
68+
let state = options.initialState
69+
70+
return {
71+
on: events.on,
72+
get: () => state as Immutable<T>,
73+
patch: (patches: Patch[]) => {
74+
state = applyPatches<T>(state, patches)
75+
events.emit('updated', state)
76+
},
77+
mutate: (fn) => {
78+
if (enablePatches) {
79+
const [newState, patches] = produceWithPatches(state, fn)
80+
state = newState
81+
events.emit('patches', patches)
82+
}
83+
else {
84+
state = produce(state, fn)
85+
}
86+
events.emit('updated', state)
87+
},
88+
}
89+
}

packages/kit/src/utils/state.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { Objectish, Patch } from 'immer'
2+
import type { EventEmitter } from '../types/events'
3+
import { applyPatches, produce, produceWithPatches } from 'immer'
4+
import { createEventEmitter } from './events'
5+
6+
// eslint-disable-next-line ts/no-unsafe-function-type
7+
type ImmutablePrimitive = undefined | null | boolean | string | number | Function
8+
9+
export type Immutable<T>
10+
= T extends ImmutablePrimitive ? T
11+
: T extends Array<infer U> ? ImmutableArray<U>
12+
: T extends Map<infer K, infer V> ? ImmutableMap<K, V>
13+
: T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>
14+
15+
export type ImmutableArray<T> = ReadonlyArray<Immutable<T>>
16+
export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>
17+
export type ImmutableSet<T> = ReadonlySet<Immutable<T>>
18+
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> }
19+
20+
/**
21+
* State host that is immutable by default with explicit mutate.
22+
*/
23+
export interface SharedState<T> {
24+
/**
25+
* Get the current state. Immutable.
26+
*/
27+
get: () => Immutable<T>
28+
/**
29+
* Subscribe to state changes.
30+
*/
31+
on: EventEmitter<SharedStateEvents<T>>['on']
32+
/**
33+
* Mutate the state.
34+
*/
35+
mutate: (fn: (state: T) => void) => void
36+
/**
37+
* Apply patches to the state.
38+
*/
39+
patch: (patches: Patch[]) => void
40+
}
41+
42+
export interface SharedStateEvents<T> {
43+
updated: (state: T) => void
44+
patches: (patches: Patch[]) => void
45+
}
46+
47+
export interface SharedStateOptions<T> {
48+
/**
49+
* Initial state.
50+
*/
51+
initialState: T
52+
/**
53+
* Enable patches.
54+
*
55+
* @default false
56+
*/
57+
enablePatches?: boolean
58+
}
59+
60+
export function createSharedState<T extends Objectish>(
61+
options: SharedStateOptions<T>,
62+
): SharedState<T> {
63+
const {
64+
enablePatches = false,
65+
} = options
66+
67+
const events = createEventEmitter<SharedStateEvents<T>>()
68+
let state = options.initialState
69+
70+
return {
71+
on: events.on,
72+
get: () => state as Immutable<T>,
73+
patch: (patches: Patch[]) => {
74+
state = applyPatches<T>(state, patches)
75+
events.emit('updated', state)
76+
},
77+
mutate: (fn) => {
78+
if (enablePatches) {
79+
const [newState, patches] = produceWithPatches(state, fn)
80+
state = newState
81+
events.emit('patches', patches)
82+
}
83+
else {
84+
state = produce(state, fn)
85+
}
86+
events.emit('updated', state)
87+
},
88+
}
89+
}

packages/kit/tsdown.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export default defineConfig({
55
'index': 'src/index.ts',
66
'utils/events': 'src/utils/events.ts',
77
'utils/nanoid': 'src/utils/nanoid.ts',
8+
'utils/shared-state': 'src/utils/shared-state.ts',
89
'client': 'src/client/index.ts',
910
},
1011
exports: true,

0 commit comments

Comments
 (0)