memorio 4.6.9 → 4.7.1
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/FUNDING.yml +12 -0
- package/README.md +106 -199
- package/index.cjs +31 -2192
- package/index.js +9 -2167
- package/llms.txt +75 -91
- package/package.json +13 -2
package/FUNDING.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# These are supported funding model platforms
|
|
2
|
+
|
|
3
|
+
github: passariello
|
|
4
|
+
patreon: passariello
|
|
5
|
+
ko_fi: passariello
|
|
6
|
+
liberapay: passariello
|
|
7
|
+
issuehunt: passariello
|
|
8
|
+
custom:
|
|
9
|
+
[
|
|
10
|
+
"https://dario.passariello.ca",
|
|
11
|
+
"https://www.indiegogo.com/individuals/28513718",
|
|
12
|
+
]
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🧠 memorio
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
[](https://npmjs.com/package/memorio)
|
|
6
6
|
[](https://npmjs.com/package/memorio)
|
|
@@ -13,197 +13,141 @@
|
|
|
13
13
|

|
|
14
14
|

|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
### One import. Global state, persistence, and a IndexedDB layer — done.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
```javascript
|
|
19
|
+
import 'memorio'
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
state.user = { name: 'Sara' } // reactive, everywhere, instantly
|
|
22
|
+
```
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
No provider tree. No reducers. No actions to dispatch before you can change a number. If you've ever set up a global store for an app that just needed *"a value, shared, that updates the UI when it changes,"* memorio is the toolkit for that thirty-second job — and it keeps growing with you: session storage, a cache, typed IndexedDB tables, an observer system, a React hook, devtools, and a change logger, all sharing one mental model.
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| **Dependencies** | ✅ **Zero** | ❌ Many | ⚠️ Few |
|
|
28
|
-
| **TypeScript** | ✅ Native | ✅ Yes | ✅ Yes |
|
|
29
|
-
| **Binary storage** | ✅ Built-in IDB | ❌ Add-on | ❌ Add-on |
|
|
30
|
-
| **Observer** | ✅ Built-in | ❌ Add-on | ❌ Add-on |
|
|
31
|
-
| **DevTools** | ✅ Built-in + dphelper-manager | ⚠️ Extension | ⚠️ Extension |
|
|
32
|
-
| **Edge runtime** | ✅ Workers, Deno | ❌ Limited | ❌ Limited |
|
|
33
|
-
| **Learning curve** | ✅ **5 minutes** | ❌ Hours | ⚠️ 30 min |
|
|
34
|
-
| **Boilerplate** | ✅ **None** | ❌ Tons | ⚠️ Some |
|
|
35
|
-
| **React support** | ✅ `useObserver` hook | ✅ `connect` | ✅ `useSyncExternalStore` |
|
|
36
|
-
| **Context isolation** | ✅ Multi-tenant | ⚠️ Manual | ⚠️ Manual |
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Table of Contents
|
|
37
29
|
|
|
38
|
-
|
|
30
|
+
1. [Is this for you?](#is-this-for-you)
|
|
31
|
+
2. [Installation](#installation)
|
|
32
|
+
3. [Quick Start](#quick-start)
|
|
33
|
+
4. [API Reference](#api-reference)
|
|
34
|
+
5. [Cross-Platform Behavior](#cross-platform-behavior)
|
|
35
|
+
6. [Context Isolation (multi-tenant)](#context-isolation-multi-tenant)
|
|
36
|
+
7. [Security](#security)
|
|
37
|
+
8. [License](#license)
|
|
39
38
|
|
|
40
39
|
---
|
|
41
40
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| **Platform detection** | `isBrowser`, `isNode`, `isDeno`, `isEdge` |
|
|
58
|
-
|
|
59
|
-
No Zustand. No Redux. No provider boilerplate.
|
|
60
|
-
**Import → assign → done.**
|
|
41
|
+
## Is this for you?
|
|
42
|
+
|
|
43
|
+
memorio is built for speed of thought: you think "I need this value everywhere," you write one line, it works. That's the entire pitch — and it's a real one, not a rewritten complaint about how much Redux boilerplate you've had to write.
|
|
44
|
+
|
|
45
|
+
**Reach for memorio when:**
|
|
46
|
+
- You want state that's simply *there* — no store setup, no provider wrapping your app.
|
|
47
|
+
- You want `localStorage`, `sessionStorage`, and `IndexedDB` behind one consistent API instead of learning three.
|
|
48
|
+
- You're prototyping, building an internal tool, or shipping something small-to-medium where iteration speed matters more than architectural ceremony.
|
|
49
|
+
|
|
50
|
+
**Reach for something else when:**
|
|
51
|
+
- You need every state path statically type-checked against every observer — memorio's `state.foo.bar` and `observer('state.foo.bar', ...)` are connected by a string, not by the compiler, so a rename won't be caught for you.
|
|
52
|
+
- You need Redux-style middleware, action logs, or time-travel debugging as a hard requirement for a large team — those tools exist because that discipline solves real problems at scale, not because their authors enjoy boilerplate.
|
|
53
|
+
- Your isolation requirements are a security boundary, not a convenience — see [Context Isolation](#context-isolation-multi-tenant) before you rely on it for that.
|
|
54
|
+
|
|
55
|
+
Pick the right tool. memorio is at its best when the answer to "how much state architecture do I need here?" is genuinely "not much."
|
|
61
56
|
|
|
62
57
|
---
|
|
63
58
|
|
|
64
|
-
##
|
|
59
|
+
## Installation
|
|
65
60
|
|
|
66
61
|
```bash
|
|
67
|
-
# npm
|
|
68
62
|
npm i memorio
|
|
63
|
+
# pnpm add memorio
|
|
64
|
+
# yarn add memorio
|
|
69
65
|
|
|
70
|
-
#
|
|
71
|
-
pnpm add memorio
|
|
72
|
-
|
|
73
|
-
# yarn
|
|
74
|
-
yarn add memorio
|
|
75
|
-
|
|
76
|
-
# React peer dep (optional, React >= 16.8)
|
|
66
|
+
# Optional — only if you use the React hook
|
|
77
67
|
npm i react react-dom
|
|
78
68
|
```
|
|
79
69
|
|
|
80
70
|
---
|
|
81
71
|
|
|
82
|
-
##
|
|
72
|
+
## Quick Start
|
|
83
73
|
|
|
84
|
-
### Global style
|
|
74
|
+
### Global style — the whole point of memorio
|
|
85
75
|
|
|
86
76
|
```typescript
|
|
87
|
-
|
|
88
|
-
import 'memorio'
|
|
77
|
+
import 'memorio' // once, at your entry point
|
|
89
78
|
|
|
90
|
-
// state is now available everywhere
|
|
91
79
|
state.user = { name: 'Sara', role: 'admin' }
|
|
92
80
|
state.counter++
|
|
93
|
-
state.settings = { theme: 'dark', lang: 'it' }
|
|
94
81
|
|
|
95
|
-
// React - automatic dependency discovery
|
|
96
82
|
useObserver(
|
|
97
|
-
() =>
|
|
83
|
+
() => console.debug('user changed:', state.user),
|
|
98
84
|
[state.user]
|
|
99
85
|
)
|
|
100
|
-
|
|
101
|
-
// Vanilla JS - event system
|
|
102
|
-
memorio.dispatch.listen('state.user', (event) => {
|
|
103
|
-
console.debug('user changed:', event.detail)
|
|
104
|
-
})
|
|
105
86
|
```
|
|
106
87
|
|
|
107
|
-
###
|
|
108
|
-
|
|
109
|
-
Every module is also a named export. Same instances, explicit dependencies.
|
|
88
|
+
### Named imports — same instances, explicit about it
|
|
110
89
|
|
|
111
90
|
```typescript
|
|
112
|
-
|
|
113
|
-
import {
|
|
114
|
-
state,
|
|
115
|
-
store,
|
|
116
|
-
session,
|
|
117
|
-
cache,
|
|
118
|
-
idb,
|
|
119
|
-
observer,
|
|
120
|
-
useObserver,
|
|
121
|
-
dispatch,
|
|
122
|
-
memorio
|
|
123
|
-
} from 'memorio'
|
|
91
|
+
import { state, store, session, cache, idb, observer, useObserver, dispatch, memorio } from 'memorio'
|
|
124
92
|
|
|
125
93
|
state.user = { name: 'Sara' }
|
|
126
94
|
store.set('theme', 'dark')
|
|
127
|
-
|
|
128
|
-
// CJS
|
|
129
|
-
const { state, store, memorio } = require('memorio')
|
|
130
95
|
```
|
|
131
96
|
|
|
132
97
|
```tsx
|
|
133
|
-
// React with named imports
|
|
134
|
-
import { useObserver, state } from 'memorio'
|
|
135
|
-
|
|
136
98
|
function Counter() {
|
|
137
99
|
const [, forceUpdate] = useReducer(x => x + 1, 0)
|
|
138
|
-
|
|
139
100
|
useObserver(forceUpdate, [state.counter])
|
|
140
|
-
|
|
141
101
|
return <div>Count: {state.counter}</div>
|
|
142
102
|
}
|
|
143
103
|
```
|
|
144
104
|
|
|
145
|
-
|
|
105
|
+
Two styles, one engine underneath — use whichever reads better in your codebase.
|
|
146
106
|
|
|
147
107
|
---
|
|
148
108
|
|
|
149
|
-
##
|
|
150
|
-
|
|
151
|
-
### `state` — Reactive volatile state
|
|
109
|
+
## API Reference
|
|
152
110
|
|
|
153
|
-
|
|
111
|
+
### `state` — reactive, volatile, Proxy-based
|
|
154
112
|
|
|
155
113
|
```javascript
|
|
156
|
-
// Set
|
|
157
114
|
state.user = { name: 'Sara', role: 'admin' }
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// Get
|
|
161
|
-
const name = state.user.name // 'Sara'
|
|
162
|
-
|
|
163
|
-
// List all keys
|
|
164
|
-
console.debug(state.list) // ['user', 'items']
|
|
115
|
+
const name = state.user.name
|
|
165
116
|
|
|
166
|
-
//
|
|
117
|
+
state.list // ['user', 'items', ...]
|
|
167
118
|
state.remove('items')
|
|
168
|
-
|
|
169
|
-
// Clear all
|
|
170
119
|
state.removeAll()
|
|
171
120
|
|
|
172
|
-
//
|
|
121
|
+
// Freeze a slice of state when you need to stop guessing who mutated it
|
|
173
122
|
state.config = { maxUsers: 100 }
|
|
174
123
|
state.config.lock()
|
|
175
|
-
state.config.maxUsers = 200
|
|
124
|
+
state.config.maxUsers = 200 // throws: state 'config' is locked
|
|
176
125
|
state.config.unlock()
|
|
177
126
|
```
|
|
178
127
|
|
|
179
|
-
### `store` —
|
|
128
|
+
### `store` — the value that survives a refresh
|
|
180
129
|
|
|
181
130
|
```javascript
|
|
182
131
|
store.set('preferences', { theme: 'dark' })
|
|
183
|
-
|
|
184
|
-
store.
|
|
185
|
-
store.removeAll()
|
|
186
|
-
console.debug(store.size(), 'chars stored')
|
|
187
|
-
console.debug(store.isPersistent) // true -> real localStorage
|
|
132
|
+
store.get('preferences') // { theme: 'dark' } or null
|
|
133
|
+
store.isPersistent // true when backed by real localStorage
|
|
188
134
|
```
|
|
189
135
|
|
|
190
|
-
### `session` —
|
|
136
|
+
### `session` — lives as long as the tab does
|
|
191
137
|
|
|
192
138
|
```javascript
|
|
193
139
|
session.set('token', 'user-abc-123')
|
|
194
|
-
|
|
195
|
-
session.removeAll()
|
|
140
|
+
session.get('token')
|
|
196
141
|
```
|
|
197
142
|
|
|
198
|
-
### `cache` —
|
|
143
|
+
### `cache` — the fastest thing you own, gone on refresh
|
|
199
144
|
|
|
200
145
|
```javascript
|
|
201
146
|
cache.set('temp', computeExpensiveResult())
|
|
202
|
-
|
|
203
|
-
cache.clear() // empty it all
|
|
147
|
+
cache.get('temp')
|
|
204
148
|
```
|
|
205
149
|
|
|
206
|
-
### `idb` —
|
|
150
|
+
### `idb` — typed, async, structured tables without the ceremony
|
|
207
151
|
|
|
208
152
|
```javascript
|
|
209
153
|
await idb.db.create('my-db')
|
|
@@ -212,7 +156,9 @@ await idb.data.set('my-db', 'users', { id: 1, name: 'Sara' })
|
|
|
212
156
|
const user = await idb.data.get('my-db', 'users', 1)
|
|
213
157
|
```
|
|
214
158
|
|
|
215
|
-
|
|
159
|
+
> IndexedDB is a browser-only primitive — see [Cross-Platform Behavior](#cross-platform-behavior) for what happens off the browser.
|
|
160
|
+
|
|
161
|
+
### `observer` / `useObserver` — watch a path, react to it
|
|
216
162
|
|
|
217
163
|
```javascript
|
|
218
164
|
observer('state.user', (newVal, oldVal) => {
|
|
@@ -220,133 +166,94 @@ observer('state.user', (newVal, oldVal) => {
|
|
|
220
166
|
})
|
|
221
167
|
```
|
|
222
168
|
|
|
223
|
-
|
|
169
|
+
> Keys here are plain strings, not statically checked against `state`'s shape. Keep the observer near the code that shapes that state, and grep before you rename.
|
|
224
170
|
|
|
225
|
-
|
|
226
|
-
import { useObserver, state } from 'memorio'
|
|
227
|
-
|
|
228
|
-
function Counter() {
|
|
229
|
-
const [, forceUpdate] = useReducer(x => x + 1, 0)
|
|
230
|
-
|
|
231
|
-
useObserver(forceUpdate, [state.counter])
|
|
232
|
-
|
|
233
|
-
return <div>Count: {state.counter}</div>
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### `dispatch` — Event system
|
|
171
|
+
### `dispatch` — the event bus underneath it all
|
|
238
172
|
|
|
239
173
|
```javascript
|
|
240
|
-
|
|
241
|
-
memorio.dispatch.
|
|
242
|
-
console.debug('user changed:', event.detail)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
// Emit
|
|
246
|
-
memorio.dispatch.set('state.user', { detail: { name: 'state.user' } })
|
|
247
|
-
|
|
248
|
-
// Remove
|
|
249
|
-
memorio.dispatch.remove('state.user')
|
|
174
|
+
memorio.dispatch.listen('state.user', (event) => console.debug(event.detail))
|
|
175
|
+
memorio.dispatch.set('state.user', { detail: { name: 'Sara' } })
|
|
250
176
|
```
|
|
251
177
|
|
|
252
|
-
### `devtools` —
|
|
178
|
+
### `devtools` — see everything, instantly
|
|
253
179
|
|
|
254
180
|
```javascript
|
|
255
|
-
memorio.devtools.inspect()
|
|
256
|
-
memorio.devtools.stats()
|
|
257
|
-
memorio.devtools.
|
|
258
|
-
memorio.devtools.exportData() // JSON snapshot
|
|
259
|
-
$state // console shortcut -> globalThis.state
|
|
181
|
+
memorio.devtools.inspect()
|
|
182
|
+
memorio.devtools.stats()
|
|
183
|
+
memorio.devtools.exportData()
|
|
260
184
|
```
|
|
261
185
|
|
|
262
|
-
|
|
186
|
+
Pairs with [dphelper-manager](https://chrome.google.com/webstore/detail/dphelper-manager-dev-tool/oppppldaoknfddeikfloonnialijngbk) for visual, time-travel inspection of your global state straight from devtools.
|
|
263
187
|
|
|
264
|
-
### `logger` —
|
|
188
|
+
### `logger` — a black box for every change
|
|
265
189
|
|
|
266
190
|
```javascript
|
|
267
191
|
memorio.logger.configure({ enabled: true, logToConsole: true })
|
|
268
|
-
memorio.logger.getHistory()
|
|
269
|
-
memorio.logger.getStats() // { total, state, set, get, ... }
|
|
270
|
-
memorio.logger.exportLogs() // JSON string of all history
|
|
192
|
+
memorio.logger.getHistory()
|
|
271
193
|
```
|
|
272
194
|
|
|
195
|
+
> It logs *everything* written to `state`/`store`/`session` — including anything sensitive you put there. Great for debugging, but don't leave it on unconditionally in production if secrets or PII pass through your state. See [Security](#security).
|
|
196
|
+
|
|
273
197
|
---
|
|
274
198
|
|
|
275
|
-
##
|
|
199
|
+
## Cross-Platform Behavior
|
|
200
|
+
|
|
201
|
+
memorio runs everywhere JavaScript does — but "everywhere" means different guarantees in different places, and we'd rather tell you now than have you find out at 2am:
|
|
202
|
+
|
|
203
|
+
| API | Browser | Node.js | Deno | Edge / Workers |
|
|
204
|
+
|---|---|---|---|---|
|
|
205
|
+
| `state` | Proxy in memory | Proxy in memory | Proxy in memory | Proxy in memory |
|
|
206
|
+
| `observer` / `useObserver` | ✅ | ✅ | ✅ | ✅ |
|
|
207
|
+
| `cache` | ✅ in memory | ✅ in memory | ✅ in memory | ✅ in memory |
|
|
208
|
+
| `store` | `localStorage` | `Map` fallback — **not durable across restarts** | `Map` fallback — not durable | `localStorage` where available, else `Map` |
|
|
209
|
+
| `session` | `sessionStorage` | `Map` fallback — not durable | `Map` fallback — not durable | `sessionStorage` where available, else `Map` |
|
|
210
|
+
| `idb` | ✅ `IndexedDB` | ❌ not available | ❌ not available | ⚠️ check `getCapabilities()` |
|
|
211
|
+
| `devtools` | ✅ | ❌ | ❌ | ⚠️ |
|
|
212
|
+
|
|
213
|
+
Same API top to bottom — that's the promise. But if your server code leans on `store.get(...)` surviving a redeploy, know that on Node/Deno it won't; the fallback is an in-memory cache with the same shape, not durable storage.
|
|
276
214
|
|
|
277
215
|
```javascript
|
|
278
|
-
memorio.isBrowser()
|
|
279
|
-
memorio.isNode()
|
|
280
|
-
memorio.isDeno()
|
|
281
|
-
memorio.isEdge()
|
|
216
|
+
memorio.isBrowser()
|
|
217
|
+
memorio.isNode()
|
|
218
|
+
memorio.isDeno()
|
|
219
|
+
memorio.isEdge()
|
|
282
220
|
|
|
283
|
-
|
|
221
|
+
memorio.getCapabilities()
|
|
284
222
|
// { platform: 'browser', hasLocalStorage: true, hasIndexedDB: true, ... }
|
|
285
223
|
```
|
|
286
224
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
import { isBrowser, isNode, getCapabilities } from 'memorio'
|
|
291
|
-
```
|
|
225
|
+
Check `getCapabilities()` before leaning on `idb` or persistent `store`/`session` in code that might run on more than one platform.
|
|
292
226
|
|
|
293
227
|
---
|
|
294
228
|
|
|
295
|
-
##
|
|
229
|
+
## Context Isolation (multi-tenant)
|
|
296
230
|
|
|
297
231
|
```javascript
|
|
298
|
-
// Create isolated context
|
|
299
232
|
const ctx = memorio.createContext('tenant-123')
|
|
300
233
|
|
|
301
|
-
// Use context storage (prefix: 'tenant-123-key')
|
|
302
234
|
ctx.state.user = { name: 'Isolated' }
|
|
303
235
|
ctx.store.set('settings', { theme: 'dark' })
|
|
304
|
-
ctx.session.set('token', 'abc123')
|
|
305
|
-
|
|
306
|
-
// Context is completely isolated from global state
|
|
307
|
-
console.debug(state.user) // undefined
|
|
308
236
|
|
|
309
|
-
//
|
|
310
|
-
memorio.listContexts() // ['tenant-123']
|
|
311
|
-
memorio.deleteContext('tenant-123')
|
|
237
|
+
console.debug(state.user) // undefined — separate namespace
|
|
312
238
|
```
|
|
313
239
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
import { createContext, listContexts, deleteContext, isolate } from 'memorio'
|
|
318
|
-
```
|
|
240
|
+
Handy for keeping per-tenant or per-request state from colliding in the same process. Under the hood it's a key prefix (`tenant-123-key`) inside the same storage — a naming convention, not a hard memory boundary. Great for a browser tab; if you're running this in a shared Node.js process or an edge isolate that might be reused across requests, make sure tenant IDs can't collide or be forged, and don't treat this as your only isolation layer if the stakes are real.
|
|
319
241
|
|
|
320
242
|
---
|
|
321
243
|
|
|
322
|
-
##
|
|
244
|
+
## Security
|
|
323
245
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
| `state` | ✅ | ✅ | ✅ | ✅ |
|
|
329
|
-
| `observer` / `useObserver` | ✅ | ✅ | ✅ | ✅ |
|
|
330
|
-
| `cache` | ✅ | ✅ | ✅ | ✅ |
|
|
331
|
-
| `store` | localStorage | memory | memory | localStorage |
|
|
332
|
-
| `session` | sessionStorage | memory | memory | sessionStorage |
|
|
333
|
-
| `idb` | IndexedDB | ❌ | ❌ | ⚠️ |
|
|
334
|
-
| `devtools` | ✅ | ❌ | ❌ | ⚠️ |
|
|
335
|
-
|
|
336
|
-
> **Why memory fallbacks on the server?** There is no browser. `store` and `session` gracefully fall back to `Map`. You still get the same API. Same `state`, same `cache`, same `useObserver`. No extra config required.
|
|
337
|
-
|
|
338
|
-
---
|
|
246
|
+
- Zero production dependencies.
|
|
247
|
+
- No `eval`, no dynamic code execution, no bundled telemetry.
|
|
248
|
+
- Session IDs generated via `crypto.randomUUID`.
|
|
249
|
+
- Inputs validated, keys sanitized, errors caught at module boundaries.
|
|
339
250
|
|
|
340
|
-
|
|
251
|
+
memorio does **not** encrypt what you put into `store`, `session`, or `idb` — they're thin, fast wrappers over `localStorage`/`sessionStorage`/`IndexedDB`, which store data in the clear on the user's device. Bring your own encryption layer for tokens, secrets, or regulated personal data.
|
|
341
252
|
|
|
342
|
-
-
|
|
343
|
-
- NIST & NSA aligned — enterprise-grade security standards
|
|
344
|
-
- No `eval`, no obfuscation, no hardcoded secrets
|
|
345
|
-
- All inputs validated, keys sanitized, errors caught
|
|
346
|
-
- Secure random session IDs via `crypto.randomUUID`
|
|
253
|
+
We build with recognized engineering guidance (NIST SP 800-53 practices) as an input to how we write code — that's a design discipline, not a compliance certificate, and no third-party audit has been performed. Found an issue? Report it privately — see `SECURITY.md` — rather than opening a public issue.
|
|
347
254
|
|
|
348
255
|
---
|
|
349
256
|
|
|
350
|
-
##
|
|
257
|
+
## License
|
|
351
258
|
|
|
352
259
|
MIT © [Dario Passariello](https://dario.passariello.ca)
|