@nanostores/logger 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <img align="right" width="92" height="92" title="Nano Stores logo"
4
4
  src="https://nanostores.github.io/nanostores/logo.svg">
5
5
 
6
- Logger of lifecycles and changes for **[Nano Stores]**,
6
+ Logger of lifecycles, changes and actions for **[Nano Stores]**,
7
7
  a tiny state manager with many atomic tree-shakable stores.
8
8
 
9
9
  * **Clean.** All messages are stacked in compact, collapsible nested groups.
@@ -0,0 +1,35 @@
1
+ import type { WritableStore } from 'nanostores'
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>
@@ -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
+ }
@@ -1,6 +1,11 @@
1
1
  import type { AnyStore, Store, StoreValue } from 'nanostores'
2
2
 
3
3
  interface LoggerOptionsMessages {
4
+ /**
5
+ * Disable action logs.
6
+ */
7
+ action?: boolean
8
+
4
9
  /**
5
10
  * Disable change logs.
6
11
  */
@@ -18,6 +23,11 @@ interface LoggerOptionsMessages {
18
23
  }
19
24
 
20
25
  export interface LoggerOptions {
26
+ /**
27
+ * Disable logs of actions with a specific name.
28
+ */
29
+ ignoreActions?: string[]
30
+
21
31
  /**
22
32
  * Disable specific types of logs.
23
33
  */
@@ -29,13 +39,33 @@ interface EventPayloadBase {
29
39
  }
30
40
 
31
41
  interface EventChangePayload extends EventPayloadBase {
42
+ actionId?: number
43
+ actionName?: string
32
44
  changed?: keyof StoreValue<Store>
33
45
  newValue: any
34
46
  oldValue?: any
35
47
  valueMessage?: string
36
48
  }
37
49
 
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
+
38
63
  interface BuildLoggerEvents {
64
+ action?: {
65
+ end?: (payload: EventActionPayload) => void
66
+ error?: (payload: EventActionErrorPayload) => void
67
+ start?: (payload: EventActionStartPayload) => void
68
+ }
39
69
  change?: (payload: EventChangePayload) => void
40
70
  mount?: (payload: EventPayloadBase) => void
41
71
  unmount?: (payload: EventPayloadBase) => void
@@ -57,13 +87,30 @@ interface BuildLoggerEvents {
57
87
  * console.log(`${storeName} was unmounted`)
58
88
  * },
59
89
  *
60
- * change: ({ changed, newValue, oldValue, valueMessage }) => {
90
+ * change: ({ actionName, changed, newValue, oldValue, valueMessage }) => {
61
91
  * let message = `${storeName} was changed`
62
92
  * if (changed) message += `in the ${changed} key`
63
93
  * if (oldValue) message += `from ${oldValue}`
64
94
  * message += `to ${newValue}`
95
+ * if (actionName) message += `by action ${actionName}`
65
96
  * 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
+ * })
67
114
  * ```
68
115
  *
69
116
  * @param store Any Nano Store
@@ -1,7 +1,17 @@
1
- import { onMount, onNotify, onSet } from 'nanostores'
1
+ import { getPath, onMount, onNotify, onSet } from 'nanostores'
2
+
3
+ import { lastActionId, lastActionName, onAction } from '../action/index.js'
2
4
 
3
5
  const isAtom = store => store.setKey === undefined
4
- const isDeepMapKey = key => /.+(\..+|\[\d+\.*])/.test(key)
6
+ const isPrimitive = value => value !== Object(value) || value === null
7
+ /* v8 ignore next 7 */
8
+ const clone = value => {
9
+ try {
10
+ return structuredClone(value)
11
+ } catch {
12
+ return { ...value }
13
+ }
14
+ }
5
15
 
6
16
  function handleMount(store, storeName, messages, events) {
7
17
  return onMount(store, () => {
@@ -16,18 +26,37 @@ function handleMount(store, storeName, messages, events) {
16
26
  })
17
27
  }
18
28
 
19
- function handleSet(store, storeName, events) {
20
- return onSet(store, ({ changed }) => {
21
- let oldValue = isAtom(store) ? store.value : { ...store.value }
22
- oldValue = isDeepMapKey(changed) ? structuredClone(oldValue) : oldValue
23
- let unbindNotify = onNotify(store, () => {
24
- let newValue = store.value
29
+ function handleSet(store, storeName, messages, ignoreActions, events) {
30
+ return onSet(store, () => {
31
+ let currentActionId = store[lastActionId]
32
+ let currentActionName = store[lastActionName]
33
+
34
+ if (messages.action === false && currentActionId) return
35
+ if (ignoreActions.includes(currentActionName)) return
36
+
37
+ let oldValue = clone(store.value)
38
+
39
+ let unbindNotify = onNotify(store, ({ changed }) => {
40
+ let newValue = clone(store.value)
41
+
25
42
  let valueMessage
26
- if (changed && !isDeepMapKey(changed)) {
27
- valueMessage = `${oldValue[changed]} → ${newValue[changed]}`
43
+ if (isAtom(store)) {
44
+ if (isPrimitive(newValue) && isPrimitive(oldValue)) {
45
+ valueMessage = `${oldValue} → ${newValue}`
46
+ oldValue = undefined
47
+ newValue = undefined
48
+ }
49
+ } else if (changed) {
50
+ let oldPrimitiveValue = getPath(oldValue, changed)
51
+ let newPrimitiveValue = getPath(newValue, changed)
52
+ if (isPrimitive(oldPrimitiveValue) && isPrimitive(newPrimitiveValue)) {
53
+ valueMessage = `${oldPrimitiveValue} → ${newPrimitiveValue}`
54
+ }
28
55
  }
29
56
 
30
57
  events.change({
58
+ actionId: currentActionId,
59
+ actionName: currentActionName,
31
60
  changed,
32
61
  newValue,
33
62
  oldValue,
@@ -40,7 +69,24 @@ function handleSet(store, storeName, events) {
40
69
  })
41
70
  }
42
71
 
72
+ function handleAction(store, storeName, ignoreActions, events) {
73
+ return onAction(store, ({ actionName, args, id, onEnd, onError }) => {
74
+ if (ignoreActions.includes(actionName)) return
75
+
76
+ events.action.start({ actionId: id, actionName, args, storeName })
77
+
78
+ onError(({ error }) => {
79
+ events.action.error({ actionId: id, actionName, error, storeName })
80
+ })
81
+
82
+ onEnd(() => {
83
+ events.action.end({ actionId: id, actionName, storeName })
84
+ })
85
+ })
86
+ }
87
+
43
88
  export function buildLogger(store, storeName, events, opts = {}) {
89
+ let ignoreActions = opts.ignoreActions || []
44
90
  let messages = opts.messages || {}
45
91
  let unbind = []
46
92
 
@@ -49,7 +95,11 @@ export function buildLogger(store, storeName, events, opts = {}) {
49
95
  }
50
96
 
51
97
  if (messages.change !== false) {
52
- unbind.push(handleSet(store, storeName, events))
98
+ unbind.push(handleSet(store, storeName, messages, ignoreActions, events))
99
+ }
100
+
101
+ if (messages.action !== false) {
102
+ unbind.push(handleAction(store, storeName, ignoreActions, events))
53
103
  }
54
104
 
55
105
  return () => {
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { action } from './action/index.js'
1
2
  export {
2
3
  buildCreatorLogger,
3
4
  CreatorLoggerOptions
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { action } from './action/index.js'
1
2
  export { buildCreatorLogger } from './build-creator-logger/index.js'
2
3
  export { buildLogger } from './build-logger/index.js'
3
4
  export { creatorLogger } from './creator-logger/index.js'
package/logger/index.js CHANGED
@@ -2,13 +2,66 @@ import { buildLogger } from '../build-logger/index.js'
2
2
  import { group, groupEnd, log } from '../printer/index.js'
3
3
 
4
4
  function createLogger(store, storeName, opts) {
5
+ let queue = {}
6
+
5
7
  return buildLogger(
6
8
  store,
7
9
  storeName,
8
10
  {
9
- change: ({ changed, newValue, oldValue, valueMessage }) => {
11
+ action: {
12
+ end: ({ actionId }) => {
13
+ for (let i of queue[actionId]) i()
14
+ delete queue[actionId]
15
+ groupEnd()
16
+ },
17
+
18
+ error: ({ actionId, actionName, error }) => {
19
+ queue[actionId].push(() =>
20
+ log({
21
+ message: [
22
+ ['bold', storeName],
23
+ ['regular', 'store handled error in action'],
24
+ ['bold', actionName]
25
+ ],
26
+ type: 'error',
27
+ value: {
28
+ message: error.message
29
+ }
30
+ })
31
+ )
32
+ },
33
+
34
+ start: ({ actionId, actionName, args }) => {
35
+ queue[actionId] = []
36
+
37
+ let message = [
38
+ ['bold', storeName],
39
+ ['regular', 'store was changed by action'],
40
+ ['bold', actionName]
41
+ ]
42
+
43
+ queue[actionId].push(() =>
44
+ group({
45
+ logo: true,
46
+ message,
47
+ type: 'action'
48
+ })
49
+ )
50
+ if (args.length > 0) {
51
+ message.push(['regular', 'with arguments'])
52
+ queue[actionId].push(() =>
53
+ log({
54
+ type: 'arguments',
55
+ value: args
56
+ })
57
+ )
58
+ }
59
+ }
60
+ },
61
+
62
+ change: ({ actionId, changed, newValue, oldValue, valueMessage }) => {
10
63
  let groupLog = {
11
- logo: true,
64
+ logo: typeof actionId === 'undefined',
12
65
  message: [
13
66
  ['bold', storeName],
14
67
  ['regular', 'store was changed']
@@ -22,24 +75,35 @@ function createLogger(store, storeName, opts) {
22
75
  ['regular', 'key']
23
76
  )
24
77
  }
25
- group(groupLog)
26
- if (valueMessage) {
27
- log({
28
- message: valueMessage,
29
- type: 'value'
30
- })
78
+
79
+ let run = () => {
80
+ group(groupLog)
81
+ if (valueMessage) {
82
+ log({
83
+ message: valueMessage,
84
+ type: 'value'
85
+ })
86
+ }
87
+ if (newValue) {
88
+ log({
89
+ type: 'new',
90
+ value: newValue
91
+ })
92
+ }
93
+ if (oldValue) {
94
+ log({
95
+ type: 'old',
96
+ value: oldValue
97
+ })
98
+ }
99
+ groupEnd()
31
100
  }
32
- log({
33
- type: 'new',
34
- value: newValue
35
- })
36
- if (oldValue) {
37
- log({
38
- type: 'old',
39
- value: oldValue
40
- })
101
+
102
+ if (actionId) {
103
+ queue[actionId].push(run)
104
+ } else {
105
+ run()
41
106
  }
42
- groupEnd()
43
107
  },
44
108
 
45
109
  mount: () => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nanostores/logger",
3
- "version": "0.4.0",
4
- "description": "Pretty logger of lifecycles and changes for Nano Stores",
3
+ "version": "1.0.0",
4
+ "description": "Pretty logger of lifecycles, changes and actions for Nano Stores",
5
5
  "keywords": [
6
6
  "nanostores",
7
7
  "devtools",
@@ -20,10 +20,17 @@
20
20
  ".": "./index.js",
21
21
  "./package.json": "./package.json"
22
22
  },
23
+ "packageManager": "pnpm@10.8.0",
23
24
  "engines": {
24
- "node": "^18.0.0 || >=20.0.0"
25
+ "node": "^20.0.0 || >=22.0.0"
25
26
  },
27
+ "funding": [
28
+ {
29
+ "type": "buymeacoffee",
30
+ "url": "https://buymeacoffee.com/euaaaio"
31
+ }
32
+ ],
26
33
  "peerDependencies": {
27
- "nanostores": ">=0.10.2"
34
+ "nanostores": "^0.10.0 || ^0.11.0 || ^1.0.0"
28
35
  }
29
36
  }
@@ -1,4 +1,5 @@
1
1
  type LogType =
2
+ | 'action'
2
3
  | 'arguments'
3
4
  | 'build'
4
5
  | 'change'
package/printer/index.js CHANGED
@@ -14,6 +14,7 @@ function borders(full) {
14
14
 
15
15
  const STYLES = {
16
16
  badges: {
17
+ action: badge('#00899A'),
17
18
  arguments: badge('#007281'),
18
19
  build: badge('#BB5100'),
19
20
  change: badge('#0E8A00'),