Skip to content

Commit ad20943

Browse files
authored
Merge pull request #22 from nanostores/feat/actions-api
feat: actions api
2 parents 10f2842 + b709bde commit ad20943

File tree

22 files changed

+664
-171
lines changed

22 files changed

+664
-171
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ on:
77
- next
88
pull_request:
99

10+
permissions:
11+
contents: read
12+
1013
jobs:
1114
full:
1215
name: Node.js Latest Full

README.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<img align="right" width="92" height="92" title="Nano Stores logo"
44
src="https://nanostores.github.io/nanostores/logo.svg">
55

6-
Logger of lifecycles and changes for **[Nano Stores]**,
6+
Logger of lifecycles, changes and actions for **[Nano Stores]**,
77
a tiny state manager with many atomic tree-shakable stores.
88

99
* **Clean.** All messages are stacked in compact, collapsible nested groups.
@@ -46,7 +46,7 @@ let destroy = logger({
4646
#### Disable specific types of logs
4747

4848
Using `messages` option you can disable
49-
**mount**, **unmount** or **change** log messages.
49+
**mount**, **unmount**, **change** or **action** log messages.
5050

5151
```js
5252
import { logger } from '@nanostores/logger'
@@ -61,12 +61,30 @@ let destroy = logger({ $users }, {
6161
})
6262
```
6363

64+
#### Disable logs of actions with a specific name
65+
66+
Using the `ignoreActions` option, you can specify the names of actions
67+
that will not be logged.
68+
69+
```js
70+
import { logger } from '@nanostores/logger'
71+
72+
import { $users } from './stores/index.js'
73+
74+
let destroy = logger({ $users }, {
75+
ignoreActions: [
76+
'Change Username',
77+
'Fetch User Profile'
78+
]
79+
})
80+
```
81+
6482
### Custom messages
6583

6684
You can create custom log messages and collapsible nested groups of messages
6785
with your own name and badge color or with any predefined types.
6886

69-
Available types: `arguments`, `build`, `change`, `error`, `mount`,
87+
Available types: `action`, `arguments`, `build`, `change`, `error`, `mount`,
7088
`new`, `old`, `unmount`, `value`.
7189

7290
```js
@@ -121,12 +139,29 @@ let destroy = buildLogger($profile, 'Profile', {
121139
console.log(`${storeName} was unmounted`)
122140
},
123141

124-
change: ({ storeName, changed, newValue, oldValue, valueMessage }) => {
142+
change: ({ storeName, actionName, changed, newValue, oldValue, valueMessage }) => {
125143
let message = `${storeName} was changed`
126144
if (changed) message += `in the ${changed} key`
127145
if (oldValue) message += `from ${oldValue}`
128146
message += `to ${newValue}`
147+
if (actionName) message += `by action ${actionName}`
129148
console.log(message, valueMessage)
149+
},
150+
151+
action: {
152+
start: ({ actionName, args }) => {
153+
let message = `${actionName} was started`
154+
if (args.length) message += 'with arguments'
155+
console.log(message, args)
156+
},
157+
158+
error: ({ actionName, error }) => {
159+
console.log(`${actionName} was failed`, error)
160+
},
161+
162+
end: ({ actionName }) => {
163+
console.log(`${actionName} was ended`)
164+
}
130165
}
131166
})
132167
```

action/index.d.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { WritableStore } from '../map/index.js'
2+
3+
type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => infer R
4+
? (...args: P) => R
5+
: never
6+
7+
export const lastActionName: unique symbol
8+
export const lastActionId: unique symbol
9+
10+
/**
11+
* Action is a function which changes the store.
12+
*
13+
* This wrap allows DevTools to see the name of action, which changes the store.
14+
*
15+
* ```js
16+
* export const increase = action($counter, 'increase', ($store, value = 1) => {
17+
* if (validateMax($store.get() + value)) {
18+
* $store.set($store.get() + value)
19+
* }
20+
* return $store.get()
21+
* })
22+
*
23+
* increase() //=> 1
24+
* increase(5) //=> 6
25+
* ```
26+
*
27+
* @param store Store instance.
28+
* @param actionName Action name for logs.
29+
* @param cb Function changing the store.
30+
* @returns Wrapped function with the same arguments.
31+
*/
32+
export function action<
33+
SomeStore extends WritableStore,
34+
Callback extends ($store: SomeStore, ...args: any[]) => any
35+
>(store: SomeStore, actionName: string, cb: Callback): OmitFirstArg<Callback>

action/index.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { startTask } from 'nanostores'
2+
3+
export let lastActionId = Symbol('last-action-id')
4+
export let lastActionName = Symbol('last-action-name')
5+
6+
let uid = 0
7+
let actionHook = Symbol('action-hook')
8+
9+
export function onAction($store, listener) {
10+
$store[actionHook] = (id, actionName, args) => {
11+
let errorListeners = {}
12+
let endListeners = {}
13+
listener({
14+
actionName,
15+
args,
16+
id,
17+
onEnd: l => {
18+
;(endListeners[id] || (endListeners[id] = [])).push(l)
19+
},
20+
onError: l => {
21+
;(errorListeners[id] || (errorListeners[id] = [])).push(l)
22+
}
23+
})
24+
return [
25+
error => {
26+
if (errorListeners[id]) {
27+
for (let l of errorListeners[id]) l({ error })
28+
}
29+
},
30+
() => {
31+
if (endListeners[id]) {
32+
for (let l of endListeners[id]) l()
33+
delete errorListeners[id]
34+
delete endListeners[id]
35+
}
36+
}
37+
]
38+
}
39+
40+
return () => {
41+
delete $store[actionHook]
42+
}
43+
}
44+
45+
export function action($store, actionName, cb) {
46+
return (...args) => {
47+
let id = ++uid
48+
let tracker = { ...$store }
49+
tracker.set = (...setArgs) => {
50+
$store[lastActionName] = actionName
51+
$store[lastActionId] = id
52+
$store.set(...setArgs)
53+
delete $store[lastActionName]
54+
delete $store[lastActionId]
55+
}
56+
if ($store.setKey) {
57+
tracker.setKey = (...setArgs) => {
58+
$store[lastActionName] = actionName
59+
$store[lastActionId] = id
60+
$store.setKey(...setArgs)
61+
delete $store[lastActionName]
62+
delete $store[lastActionId]
63+
}
64+
}
65+
let onEnd, onError
66+
if ($store[actionHook]) {
67+
;[onError, onEnd] = $store[actionHook](id, actionName, args)
68+
}
69+
let result = cb(tracker, ...args)
70+
if (result instanceof Promise) {
71+
let endTask = startTask()
72+
return result
73+
.catch(error => {
74+
if (onError) onError(error)
75+
throw error
76+
})
77+
.finally(() => {
78+
endTask()
79+
if (onEnd) onEnd()
80+
})
81+
}
82+
if (onEnd) onEnd()
83+
return result
84+
}
85+
}

build-logger/index.d.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { AnyStore, Store, StoreValue } from 'nanostores'
22

33
interface LoggerOptionsMessages {
4+
/**
5+
* Disable action logs.
6+
*/
7+
action?: boolean
8+
49
/**
510
* Disable change logs.
611
*/
@@ -18,6 +23,11 @@ interface LoggerOptionsMessages {
1823
}
1924

2025
export interface LoggerOptions {
26+
/**
27+
* Disable logs of actions with a specific name.
28+
*/
29+
ignoreActions?: string[]
30+
2131
/**
2232
* Disable specific types of logs.
2333
*/
@@ -29,13 +39,33 @@ interface EventPayloadBase {
2939
}
3040

3141
interface EventChangePayload extends EventPayloadBase {
42+
actionId?: number
43+
actionName?: string
3244
changed?: keyof StoreValue<Store>
3345
newValue: any
3446
oldValue?: any
3547
valueMessage?: string
3648
}
3749

50+
interface EventActionPayload extends EventPayloadBase {
51+
actionId: number
52+
actionName: string
53+
}
54+
55+
interface EventActionStartPayload extends EventActionPayload {
56+
args: any[]
57+
}
58+
59+
interface EventActionErrorPayload extends EventActionPayload {
60+
error: Error
61+
}
62+
3863
interface BuildLoggerEvents {
64+
action?: {
65+
end?: (payload: EventActionPayload) => void
66+
error?: (payload: EventActionErrorPayload) => void
67+
start?: (payload: EventActionStartPayload) => void
68+
}
3969
change?: (payload: EventChangePayload) => void
4070
mount?: (payload: EventPayloadBase) => void
4171
unmount?: (payload: EventPayloadBase) => void
@@ -57,13 +87,30 @@ interface BuildLoggerEvents {
5787
* console.log(`${storeName} was unmounted`)
5888
* },
5989
*
60-
* change: ({ changed, newValue, oldValue, valueMessage }) => {
90+
* change: ({ actionName, changed, newValue, oldValue, valueMessage }) => {
6191
* let message = `${storeName} was changed`
6292
* if (changed) message += `in the ${changed} key`
6393
* if (oldValue) message += `from ${oldValue}`
6494
* message += `to ${newValue}`
95+
* if (actionName) message += `by action ${actionName}`
6596
* console.log(message, valueMessage)
66-
* }
97+
* },
98+
*
99+
* action: {
100+
* start: ({ actionName, args }) => {
101+
* let message = `${actionName} was started`
102+
* if (args.length) message += 'with arguments'
103+
* console.log(message, args)
104+
* },
105+
*
106+
* error: ({ actionName, error }) => {
107+
* console.log(`${actionName} was failed`, error)
108+
* },
109+
*
110+
* end: ({ actionName }) => {
111+
* console.log(`${actionName} was ended`)
112+
* }
113+
* })
67114
* ```
68115
*
69116
* @param store Any Nano Store

0 commit comments

Comments
 (0)