@e280/strata 0.2.0-0 → 0.2.0-2

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.
Files changed (58) hide show
  1. package/README.md +21 -1
  2. package/package.json +2 -2
  3. package/s/signals/derive.ts +0 -4
  4. package/s/signals/index.ts +1 -1
  5. package/s/signals/parts/hipster.ts +35 -0
  6. package/s/signals/{fns.ts → porcelain.ts} +1 -0
  7. package/s/signals/signal.ts +20 -8
  8. package/s/signals/signals.test.ts +11 -264
  9. package/s/signals/tests/derive.test.ts +89 -0
  10. package/s/signals/tests/effect.test.ts +89 -0
  11. package/s/signals/tests/lazy.test.ts +49 -0
  12. package/s/signals/tests/signal-fn.test.ts +56 -0
  13. package/s/signals/tests/signal.test.ts +63 -0
  14. package/s/signals/types.ts +8 -0
  15. package/s/tests.test.ts +1 -1
  16. package/s/tree/parts/branch.ts +1 -1
  17. package/s/tree/parts/trunk.ts +1 -1
  18. package/x/signals/derive.d.ts +0 -1
  19. package/x/signals/derive.js +0 -3
  20. package/x/signals/derive.js.map +1 -1
  21. package/x/signals/index.d.ts +1 -1
  22. package/x/signals/index.js +1 -1
  23. package/x/signals/index.js.map +1 -1
  24. package/x/signals/parts/hipster.d.ts +3 -0
  25. package/x/signals/parts/hipster.js +24 -0
  26. package/x/signals/parts/hipster.js.map +1 -0
  27. package/x/signals/{fns.d.ts → porcelain.d.ts} +3 -2
  28. package/x/signals/{fns.js → porcelain.js} +2 -1
  29. package/x/signals/porcelain.js.map +1 -0
  30. package/x/signals/signal.d.ts +2 -1
  31. package/x/signals/signal.js +13 -5
  32. package/x/signals/signal.js.map +1 -1
  33. package/x/signals/signals.test.d.ts +35 -19
  34. package/x/signals/signals.test.js +11 -217
  35. package/x/signals/signals.test.js.map +1 -1
  36. package/x/signals/tests/derive.test.d.ts +8 -0
  37. package/x/signals/tests/derive.test.js +73 -0
  38. package/x/signals/tests/derive.test.js.map +1 -0
  39. package/x/signals/tests/effect.test.d.ts +10 -0
  40. package/x/signals/tests/effect.test.js +72 -0
  41. package/x/signals/tests/effect.test.js.map +1 -0
  42. package/x/signals/tests/lazy.test.d.ts +7 -0
  43. package/x/signals/tests/lazy.test.js +39 -0
  44. package/x/signals/tests/lazy.test.js.map +1 -0
  45. package/x/signals/tests/signal-fn.test.d.ts +8 -0
  46. package/x/signals/tests/signal-fn.test.js +43 -0
  47. package/x/signals/tests/signal-fn.test.js.map +1 -0
  48. package/x/signals/tests/signal.test.d.ts +12 -0
  49. package/x/signals/tests/signal.test.js +54 -0
  50. package/x/signals/tests/signal.test.js.map +1 -0
  51. package/x/signals/types.d.ts +6 -0
  52. package/x/tests.test.js +1 -1
  53. package/x/tests.test.js.map +1 -1
  54. package/x/tree/parts/branch.js +1 -1
  55. package/x/tree/parts/branch.js.map +1 -1
  56. package/x/tree/parts/trunk.js +1 -1
  57. package/x/tree/parts/trunk.js.map +1 -1
  58. package/x/signals/fns.js.map +0 -1
package/README.md CHANGED
@@ -54,11 +54,31 @@ import {signal, effect} from "@e280/strata"
54
54
  count.value // get
55
55
  count.value = 2 // set
56
56
  ```
57
- value pattern is nice for this vibe
57
+ value pattern is nice for these vibes
58
58
  ```ts
59
59
  count.value++
60
60
  count.value += 1
61
61
  ```
62
+ - **signal hipster fn syntax**
63
+ - turn a signal into a hipster fn
64
+ ```ts
65
+ const count = signal.fn(1)
66
+ ```
67
+ - now you can directly invoke it
68
+ ```ts
69
+ count() // get
70
+ await count(2) // set
71
+ ```
72
+ - it has all the stuff that a signal has
73
+ ```ts
74
+ count.get()
75
+ await count.publish(5)
76
+ count.on(x => console.log(x))
77
+ ```
78
+ - mint a fresh new signal fn
79
+ ```ts
80
+ const count = signal.fn(1)
81
+ ```
62
82
 
63
83
  ### 🚦 effects
64
84
  - **effects run when the relevant signals change**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/strata",
3
- "version": "0.2.0-0",
3
+ "version": "0.2.0-2",
4
4
  "description": "state management",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -32,7 +32,7 @@
32
32
  "@e280/stz": "^0.2.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@e280/science": "^0.1.1",
35
+ "@e280/science": "^0.1.2",
36
36
  "@types/node": "^24.3.0",
37
37
  "npm-run-all": "^4.1.5",
38
38
  "typescript": "^5.9.2"
@@ -25,10 +25,6 @@ export class Derive<V> extends Reactive<V> {
25
25
  this.#dispose = dispose
26
26
  }
27
27
 
28
- get value() {
29
- return this.get()
30
- }
31
-
32
28
  dispose() {
33
29
  super.dispose()
34
30
  this.#dispose()
@@ -4,7 +4,7 @@ export * from "./parts/readable.js"
4
4
 
5
5
  export * from "./derive.js"
6
6
  export * from "./effect.js"
7
- export * from "./fns.js"
7
+ export * from "./porcelain.js"
8
8
  export * from "./lazy.js"
9
9
  export * from "./signal.js"
10
10
  export * from "./types.js"
@@ -0,0 +1,35 @@
1
+
2
+ import {Signal} from "../signal.js"
3
+ import {SignalFn} from "../types.js"
4
+
5
+ export function hipster<V>(sig: Signal<V>) {
6
+ function f(): V
7
+ function f(v: V): Promise<V>
8
+ function f(_v?: V): V | Promise<V> {
9
+ return (arguments.length === 0)
10
+ ? sig.get()
11
+ : sig.set(arguments[0])
12
+ }
13
+
14
+ f.signal = sig
15
+
16
+ f.get = sig.get.bind(sig)
17
+ f.set = sig.set.bind(sig)
18
+ f.on = sig.on
19
+ f.dispose = sig.dispose.bind(sig)
20
+ f.publish = sig.publish.bind(sig)
21
+ f.fn = sig.fn.bind(sig)
22
+
23
+ Object.defineProperty(f, "value", {
24
+ get: () => sig.value,
25
+ set: (v) => sig.value = v,
26
+ })
27
+
28
+ Object.defineProperty(f, "sneak", {
29
+ get: () => sig.sneak,
30
+ set: (v) => sig.sneak = v,
31
+ })
32
+
33
+ return f as SignalFn<V>
34
+ }
35
+
@@ -27,4 +27,5 @@ export function signal<V>(
27
27
 
28
28
  signal.lazy = lazy
29
29
  signal.derive = derive
30
+ signal.fn = <V>(value: V) => signal(value).fn()
30
31
 
@@ -1,5 +1,6 @@
1
1
 
2
2
  import {SignalOptions} from "./types.js"
3
+ import {hipster} from "./parts/hipster.js"
3
4
  import {Reactive} from "./parts/reactive.js"
4
5
  import {tracker} from "../tracker/tracker.js"
5
6
  import {defaultCompare} from "./utils/default-compare.js"
@@ -8,11 +9,15 @@ export class Signal<V> extends Reactive<V> {
8
9
  #lock = false
9
10
  #compare: (a: any, b: any) => boolean
10
11
 
11
- constructor(sneak: V, options?: Partial<SignalOptions>) {
12
- super(sneak)
12
+ constructor(value: V, options?: Partial<SignalOptions>) {
13
+ super(value)
13
14
  this.#compare = options?.compare ?? defaultCompare
14
15
  }
15
16
 
17
+ fn() {
18
+ return hipster(this)
19
+ }
20
+
16
21
  async set(v: V) {
17
22
  const isChanged = !this.#compare(this.sneak, v)
18
23
  if (isChanged) await this.publish(v)
@@ -24,20 +29,27 @@ export class Signal<V> extends Reactive<V> {
24
29
  }
25
30
 
26
31
  set value(v: V) {
27
- this.set(v)
32
+ void this.set(v)
28
33
  }
29
34
 
30
- async publish(v = this.get()) {
31
- if (this.#lock) throw new Error("forbid circularity")
32
- let promise = Promise.resolve()
35
+ async publish(v = this.sneak) {
36
+ // only wizards are allowed beyond this point.
37
+ // - the implementation is subtle
38
+ // - it looks wrong, but it's right
39
+ // - tarnished alchemists, take heed: lock engages only for sync activity of the async fns (think of the value setter!)
40
+
41
+ if (this.#lock)
42
+ throw new Error("forbid circularity")
43
+
44
+ let promise: Promise<any> = Promise.resolve()
33
45
 
34
46
  try {
35
47
  this.#lock = true
36
48
  this.sneak = v
37
49
  promise = Promise.all([
38
50
  tracker.notifyWrite(this),
39
- this.on.pub(v),
40
- ]) as any
51
+ this.on.publish(v),
52
+ ])
41
53
  }
42
54
  finally {
43
55
  this.#lock = false
@@ -1,269 +1,16 @@
1
1
 
2
- import {Science, test, expect, spy} from "@e280/science"
3
- import {effect} from "./effect.js"
4
- import {derive, lazy, signal} from "./fns.js"
2
+ import {Science} from "@e280/science"
3
+ import signalTest from "./tests/signal.test.js"
4
+ import signalFnTest from "./tests/signal-fn.test.js"
5
+ import effectTest from "./tests/effect.test.js"
6
+ import lazyTest from "./tests/lazy.test.js"
7
+ import deriveTest from "./tests/derive.test.js"
5
8
 
6
9
  export default Science.suite({
7
- "signal get/set value": test(async() => {
8
- const count = signal(0)
9
- expect(count.value).is(0)
10
-
11
- count.value++
12
- expect(count.value).is(1)
13
-
14
- count.value = 5
15
- expect(count.value).is(5)
16
- }),
17
-
18
- "signal set and publish returns value": test(async() => {
19
- const count = signal(0)
20
- expect(count.value).is(0)
21
- expect(await count.set(1)).is(1)
22
- expect(await count.publish(2)).is(2)
23
- }),
24
-
25
- "signal syntax interop": test(async() => {
26
- const count = signal(0)
27
- count.value = 1
28
- expect(count.get()).is(1)
29
- }),
30
-
31
- "signal on is not debounced": test(async() => {
32
- const count = signal(1)
33
- let runs = 0
34
- count.on(() => void runs++)
35
- await count.set(2)
36
- await count.set(3)
37
- expect(runs).is(2)
38
- }),
39
-
40
- "signal on only fires on change": test(async() => {
41
- const count = signal(1)
42
- let runs = 0
43
- count.on(() => void runs++)
44
- await count.set(2)
45
- await count.set(2)
46
- expect(runs).is(1)
47
- }),
48
-
49
- "signal on circularity forbidden": test(async() => {
50
- const count = signal(1)
51
- let runs = 0
52
- count.on(async() => {
53
- await count.set(99)
54
- runs++
55
- })
56
- expect(async() => {
57
- await count.set(2)
58
- }).throwsAsync()
59
- expect(runs).is(0)
60
- }),
61
-
62
- "effect tracks signal changes": test(async() => {
63
- const count = signal(1)
64
- let doubled = 0
65
-
66
- effect(() => doubled = count.value * 2)
67
- expect(doubled).is(2)
68
-
69
- await count.set(3)
70
- expect(doubled).is(6)
71
- }),
72
-
73
- "effect is only called when signal actually changes": test(async() => {
74
- const count = signal(1)
75
- let runs = 0
76
- effect(() => {
77
- count.get()
78
- runs++
79
- })
80
- expect(runs).is(1)
81
- await count.set(999)
82
- expect(runs).is(2)
83
- await count.set(999)
84
- expect(runs).is(2)
85
- }),
86
-
87
- "effects are debounced": test(async() => {
88
- const count = signal(1)
89
- let runs = 0
90
- effect(() => {
91
- count.get()
92
- runs++
93
- })
94
- expect(runs).is(1)
95
- count.value++
96
- count.value++
97
- await count.set(count.get() + 1)
98
- expect(runs).is(2)
99
- }),
100
-
101
- "effects can be disposed": test(async() => {
102
- const count = signal(1)
103
- let doubled = 0
104
-
105
- const dispose = effect(() => doubled = count.value * 2)
106
- expect(doubled).is(2)
107
-
108
- await count.set(3)
109
- expect(doubled).is(6)
110
-
111
- dispose()
112
- await count.set(4)
113
- expect(doubled).is(6) // old value
114
- }),
115
-
116
- "signal set promise waits for effects": test(async() => {
117
- const count = signal(1)
118
- let doubled = 0
119
-
120
- effect(() => doubled = count.value * 2)
121
- expect(doubled).is(2)
122
-
123
- await count.set(3)
124
- expect(doubled).is(6)
125
- }),
126
-
127
- "effect only runs on change": test(async() => {
128
- const sig = signal("a")
129
- let runs = 0
130
-
131
- effect(() => {
132
- sig.value
133
- runs++
134
- })
135
- expect(runs).is(1)
136
-
137
- await sig.set("a")
138
- expect(runs).is(1)
139
-
140
- await sig.set("b")
141
- expect(runs).is(2)
142
- }),
143
-
144
- "lazy values": test(async() => {
145
- const a = signal(2)
146
- const b = signal(3)
147
- const sum = lazy(() => a.value + b.value)
148
- expect(sum.value).is(5)
149
-
150
- await a.set(5)
151
- expect(sum.value).is(8)
152
-
153
- await b.set(7)
154
- expect(sum.value).is(12)
155
- }),
156
-
157
- "effect reacts to derived changes": test(async() => {
158
- const a = signal(1)
159
- const b = signal(10)
160
- const product = derive(() => a.value * b.value)
161
-
162
- let mutations = 0
163
- effect(() => {
164
- void product.get()
165
- mutations++
166
- })
167
- expect(product.value).is(10)
168
- expect(mutations).is(1)
169
-
170
- await a.set(2)
171
- expect(product.value).is(20)
172
- expect(mutations).is(2)
173
-
174
- await a.set(3)
175
- expect(product.value).is(30)
176
- expect(mutations).is(3)
177
- }),
178
-
179
- "effect doesn't overreact to derived": test(async() => {
180
- const a = signal(1)
181
- const b = signal(10)
182
- const product = signal.derive(() => a.value * b.value)
183
-
184
- const derivedSpy = spy(() => {})
185
- product.on(derivedSpy)
186
-
187
- let mutations = 0
188
- effect(() => {
189
- a.get()
190
- product.get()
191
- mutations++
192
- })
193
- expect(product.value).is(10)
194
- expect(mutations).is(1)
195
- expect(derivedSpy.spy.calls.length).is(0)
196
-
197
- await a.set(2)
198
- expect(product.value).is(20)
199
- expect(mutations).is(2)
200
- expect(derivedSpy.spy.calls.length).is(1)
201
- }),
202
-
203
- "derived.on": test(async() => {
204
- const a = signal(1)
205
- const b = signal(10)
206
- const product = signal.derive(() => a.value * b.value)
207
- expect(product.value).is(10)
208
-
209
- const mole = spy((_v: number) => {})
210
- product.on(mole)
211
- expect(mole.spy.calls.length).is(0)
212
-
213
- await a.set(2)
214
- expect(product.value).is(20)
215
- expect(mole.spy.calls.length).is(1)
216
- expect(mole.spy.calls[0].args[0]).is(20)
217
- }),
218
-
219
- "derived.on not called if result doesn't change": test(async() => {
220
- const a = signal(1)
221
- const b = signal(10)
222
- const product = signal.derive(() => a.value * b.value)
223
- expect(product.value).is(10)
224
-
225
- const mole = spy((_v: number) => {})
226
- product.on(mole)
227
- expect(mole.spy.calls.length).is(0)
228
-
229
- await a.set(2)
230
- expect(product.value).is(20)
231
- expect(mole.spy.calls.length).is(1)
232
- expect(mole.spy.calls[0].args[0]).is(20)
233
-
234
- await a.set(2)
235
- expect(product.value).is(20)
236
- expect(mole.spy.calls.length).is(1)
237
- }),
238
-
239
- "lazy is lazy": test(async() => {
240
- const a = signal(1)
241
- let runs = 0
242
-
243
- const comp = lazy(() => {
244
- runs++
245
- return a.value * 10
246
- })
247
-
248
- expect(runs).is(0)
249
- expect(comp.value).is(10)
250
- expect(runs).is(1)
251
-
252
- await a.set(2)
253
- expect(runs).is(1)
254
- expect(comp.value).is(20)
255
- expect(runs).is(2)
256
- }),
257
-
258
- "lazy syntax": test(async() => {
259
- const a = signal(2)
260
- const b = signal(3)
261
- const sum = lazy(() => a.value + b.value)
262
- expect(sum.value).is(5)
263
-
264
- await a.set(5)
265
- expect(sum.value).is(8)
266
- expect(sum.get()).is(8)
267
- }),
10
+ "signal": signalTest,
11
+ "signal.fn": signalFnTest,
12
+ "effect": effectTest,
13
+ "lazy": lazyTest,
14
+ "derive": deriveTest,
268
15
  })
269
16
 
@@ -0,0 +1,89 @@
1
+
2
+ import {Science, test, expect, spy} from "@e280/science"
3
+ import {effect} from "../effect.js"
4
+ import {derive, signal} from "../porcelain.js"
5
+
6
+ export default Science.suite({
7
+ "effect reacts to derived changes": test(async() => {
8
+ const a = signal(1)
9
+ const b = signal(10)
10
+ const product = derive(() => a.value * b.value)
11
+
12
+ let mutations = 0
13
+ effect(() => {
14
+ void product.get()
15
+ mutations++
16
+ })
17
+ expect(product.value).is(10)
18
+ expect(mutations).is(1)
19
+
20
+ await a.set(2)
21
+ expect(product.value).is(20)
22
+ expect(mutations).is(2)
23
+
24
+ await a.set(3)
25
+ expect(product.value).is(30)
26
+ expect(mutations).is(3)
27
+ }),
28
+
29
+ "effect doesn't overreact to derived": test(async() => {
30
+ const a = signal(1)
31
+ const b = signal(10)
32
+ const product = signal.derive(() => a.value * b.value)
33
+
34
+ const derivedSpy = spy(() => {})
35
+ product.on(derivedSpy)
36
+
37
+ let mutations = 0
38
+ effect(() => {
39
+ a.get()
40
+ product.get()
41
+ mutations++
42
+ })
43
+ expect(product.value).is(10)
44
+ expect(mutations).is(1)
45
+ expect(derivedSpy.spy.calls.length).is(0)
46
+
47
+ await a.set(2)
48
+ expect(product.value).is(20)
49
+ expect(mutations).is(2)
50
+ expect(derivedSpy.spy.calls.length).is(1)
51
+ }),
52
+
53
+ "derived.on": test(async() => {
54
+ const a = signal(1)
55
+ const b = signal(10)
56
+ const product = signal.derive(() => a.value * b.value)
57
+ expect(product.value).is(10)
58
+
59
+ const mole = spy((_v: number) => {})
60
+ product.on(mole)
61
+ expect(mole.spy.calls.length).is(0)
62
+
63
+ await a.set(2)
64
+ expect(product.value).is(20)
65
+ expect(mole.spy.calls.length).is(1)
66
+ expect(mole.spy.calls[0].args[0]).is(20)
67
+ }),
68
+
69
+ "derived.on not called if result doesn't change": test(async() => {
70
+ const a = signal(1)
71
+ const b = signal(10)
72
+ const product = signal.derive(() => a.value * b.value)
73
+ expect(product.value).is(10)
74
+
75
+ const mole = spy((_v: number) => {})
76
+ product.on(mole)
77
+ expect(mole.spy.calls.length).is(0)
78
+
79
+ await a.set(2)
80
+ expect(product.value).is(20)
81
+ expect(mole.spy.calls.length).is(1)
82
+ expect(mole.spy.calls[0].args[0]).is(20)
83
+
84
+ await a.set(2)
85
+ expect(product.value).is(20)
86
+ expect(mole.spy.calls.length).is(1)
87
+ }),
88
+ })
89
+
@@ -0,0 +1,89 @@
1
+
2
+ import {Science, test, expect} from "@e280/science"
3
+ import {signal} from "../porcelain.js"
4
+ import {effect} from "../effect.js"
5
+
6
+ export default Science.suite({
7
+ "tracks signal changes": test(async() => {
8
+ const count = signal(1)
9
+ let doubled = 0
10
+
11
+ effect(() => doubled = count.value * 2)
12
+ expect(doubled).is(2)
13
+
14
+ await count.set(3)
15
+ expect(doubled).is(6)
16
+ }),
17
+
18
+ "is only called when signal actually changes": test(async() => {
19
+ const count = signal(1)
20
+ let runs = 0
21
+ effect(() => {
22
+ count.get()
23
+ runs++
24
+ })
25
+ expect(runs).is(1)
26
+ await count.set(999)
27
+ expect(runs).is(2)
28
+ await count.set(999)
29
+ expect(runs).is(2)
30
+ }),
31
+
32
+ "debounced": test(async() => {
33
+ const count = signal(1)
34
+ let runs = 0
35
+ effect(() => {
36
+ count.get()
37
+ runs++
38
+ })
39
+ expect(runs).is(1)
40
+ count.value++
41
+ count.value++
42
+ await count.set(count.get() + 1)
43
+ expect(runs).is(2)
44
+ }),
45
+
46
+ "can be disposed": test(async() => {
47
+ const count = signal(1)
48
+ let doubled = 0
49
+
50
+ const dispose = effect(() => doubled = count.value * 2)
51
+ expect(doubled).is(2)
52
+
53
+ await count.set(3)
54
+ expect(doubled).is(6)
55
+
56
+ dispose()
57
+ await count.set(4)
58
+ expect(doubled).is(6) // old value
59
+ }),
60
+
61
+ "signal set promise waits for effects": test(async() => {
62
+ const count = signal(1)
63
+ let doubled = 0
64
+
65
+ effect(() => doubled = count.value * 2)
66
+ expect(doubled).is(2)
67
+
68
+ await count.set(3)
69
+ expect(doubled).is(6)
70
+ }),
71
+
72
+ "only runs on change": test(async() => {
73
+ const sig = signal("a")
74
+ let runs = 0
75
+
76
+ effect(() => {
77
+ sig.value
78
+ runs++
79
+ })
80
+ expect(runs).is(1)
81
+
82
+ await sig.set("a")
83
+ expect(runs).is(1)
84
+
85
+ await sig.set("b")
86
+ expect(runs).is(2)
87
+ }),
88
+ })
89
+
@@ -0,0 +1,49 @@
1
+
2
+ import {Science, test, expect} from "@e280/science"
3
+ import {lazy, signal} from "../porcelain.js"
4
+
5
+ export default Science.suite({
6
+ "lazy values": test(async() => {
7
+ const a = signal(2)
8
+ const b = signal(3)
9
+ const sum = lazy(() => a.value + b.value)
10
+ expect(sum.value).is(5)
11
+
12
+ await a.set(5)
13
+ expect(sum.value).is(8)
14
+
15
+ await b.set(7)
16
+ expect(sum.value).is(12)
17
+ }),
18
+
19
+ "lazy is lazy": test(async() => {
20
+ const a = signal(1)
21
+ let runs = 0
22
+
23
+ const comp = lazy(() => {
24
+ runs++
25
+ return a.value * 10
26
+ })
27
+
28
+ expect(runs).is(0)
29
+ expect(comp.value).is(10)
30
+ expect(runs).is(1)
31
+
32
+ await a.set(2)
33
+ expect(runs).is(1)
34
+ expect(comp.value).is(20)
35
+ expect(runs).is(2)
36
+ }),
37
+
38
+ "lazy syntax": test(async() => {
39
+ const a = signal(2)
40
+ const b = signal(3)
41
+ const sum = lazy(() => a.value + b.value)
42
+ expect(sum.value).is(5)
43
+
44
+ await a.set(5)
45
+ expect(sum.value).is(8)
46
+ expect(sum.get()).is(8)
47
+ }),
48
+ })
49
+