@zeix/cause-effect 0.16.1 → 0.17.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/.ai-context.md +71 -21
- package/.cursorrules +3 -2
- package/.github/copilot-instructions.md +59 -13
- package/CLAUDE.md +170 -24
- package/LICENSE +1 -1
- package/README.md +156 -52
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +19 -19
- package/archive/list.ts +551 -0
- package/archive/memo.ts +138 -0
- package/{src → archive}/state.ts +13 -11
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +899 -503
- package/index.js +1 -1
- package/index.ts +41 -22
- package/package.json +1 -1
- package/src/classes/collection.ts +272 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +304 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +26 -53
- package/src/effect.ts +9 -9
- package/src/errors.ts +50 -25
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -30
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +796 -0
- package/test/computed.test.ts +138 -130
- package/test/diff.test.ts +2 -2
- package/test/effect.test.ts +36 -35
- package/test/list.test.ts +754 -0
- package/test/match.test.ts +25 -25
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +70 -119
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +253 -929
- package/types/index.d.ts +10 -8
- package/types/src/classes/collection.d.ts +32 -0
- package/types/src/classes/composite.d.ts +15 -0
- package/types/src/classes/computed.d.ts +97 -0
- package/types/src/classes/list.d.ts +41 -0
- package/types/src/classes/state.d.ts +52 -0
- package/types/src/classes/store.d.ts +51 -0
- package/types/src/diff.d.ts +8 -12
- package/types/src/errors.d.ts +12 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -3
- package/src/store.ts +0 -474
- package/types/src/collection.d.ts +0 -26
- package/types/src/computed.d.ts +0 -33
- package/types/src/scheduler.d.ts +0 -55
- package/types/src/state.d.ts +0 -24
- package/types/src/store.d.ts +0 -65
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.
|
|
3
|
+
Version 0.17.0
|
|
4
4
|
|
|
5
5
|
**Cause & Effect** is a lightweight, reactive state management library for JavaScript applications. It uses fine-grained reactivity with signals to create predictable and efficient data flow in your app.
|
|
6
6
|
|
|
@@ -10,9 +10,12 @@ Version 0.16.1
|
|
|
10
10
|
|
|
11
11
|
### Core Concepts
|
|
12
12
|
|
|
13
|
-
- **State signals**: Hold values that can be directly modified: `
|
|
13
|
+
- **State signals**: Hold values that can be directly modified: `new State()`
|
|
14
|
+
- **Memo signals**: Derive memoized values from other signals: `new Memo()`
|
|
15
|
+
- **Task signals**: Execute asynchronous functions of other signals: `new Task()`
|
|
14
16
|
- **Store signals**: Hold objects of nested reactive properties: `createStore()`
|
|
15
|
-
- **
|
|
17
|
+
- **List signals**: Create keyed lists with reactive items: `new List()`
|
|
18
|
+
- **Collection signals**: Read-only derived array transformations: `new Collection()`
|
|
16
19
|
- **Effects**: Run side effects when signals change: `createEffect()`
|
|
17
20
|
|
|
18
21
|
## Key Features
|
|
@@ -28,13 +31,13 @@ Version 0.16.1
|
|
|
28
31
|
## Quick Start
|
|
29
32
|
|
|
30
33
|
```js
|
|
31
|
-
import {
|
|
34
|
+
import { createEffect, Memo, State } from '@zeix/cause-effect'
|
|
32
35
|
|
|
33
36
|
// 1. Create state
|
|
34
|
-
const user =
|
|
37
|
+
const user = new State({ name: 'Alice', age: 30 })
|
|
35
38
|
|
|
36
39
|
// 2. Create computed values
|
|
37
|
-
const greeting =
|
|
40
|
+
const greeting = Memo(() => `Hello ${user.get().name}!`)
|
|
38
41
|
|
|
39
42
|
// 3. React to changes
|
|
40
43
|
createEffect(() => {
|
|
@@ -59,12 +62,12 @@ bun add @zeix/cause-effect
|
|
|
59
62
|
|
|
60
63
|
### State Signals
|
|
61
64
|
|
|
62
|
-
`
|
|
65
|
+
`new State()` creates a mutable signal. Every signal has a `.get()` method to access its current value. State signals also provide `.set()` to directly assign a new value and `.update()` to modify the value with a function.
|
|
63
66
|
|
|
64
67
|
```js
|
|
65
|
-
import {
|
|
68
|
+
import { createEffect, State } from '@zeix/cause-effect'
|
|
66
69
|
|
|
67
|
-
const count =
|
|
70
|
+
const count = new State(42)
|
|
68
71
|
createEffect(() => {
|
|
69
72
|
console.log(count.get()) // logs '42'
|
|
70
73
|
})
|
|
@@ -116,7 +119,7 @@ createEffect(() => {
|
|
|
116
119
|
**When to use stores vs states:**
|
|
117
120
|
|
|
118
121
|
- **Use `createStore()`** for objects with properties that you want to access and modify individually.
|
|
119
|
-
- **Use `
|
|
122
|
+
- **Use `new State()`** for primitive values (numbers, strings, booleans) or objects you access and replace entirely.
|
|
120
123
|
|
|
121
124
|
#### Dynamic Properties
|
|
122
125
|
|
|
@@ -147,14 +150,14 @@ The `add()` and `remove()` methods are optimized for performance:
|
|
|
147
150
|
- They're perfect for frequent single-property additions/removals
|
|
148
151
|
- They trigger the same events and reactivity as other store operations
|
|
149
152
|
|
|
150
|
-
|
|
153
|
+
### List Signals
|
|
151
154
|
|
|
152
|
-
|
|
155
|
+
`new List()` creates a mutable signal for arrays with individually reactive items and stable keys. Each item becomes its own signal while maintaining persistent identity through sorting and reordering:
|
|
153
156
|
|
|
154
157
|
```js
|
|
155
|
-
import {
|
|
158
|
+
import { List, createEffect } from '@zeix/cause-effect'
|
|
156
159
|
|
|
157
|
-
const items =
|
|
160
|
+
const items = new List(['banana', 'apple', 'cherry'])
|
|
158
161
|
|
|
159
162
|
// Duck-typing: behaves like an array
|
|
160
163
|
console.log(items.length) // 3
|
|
@@ -169,15 +172,116 @@ createEffect(() => {
|
|
|
169
172
|
items.add('date') // Adds at index 3
|
|
170
173
|
console.log(items.get()) // ['banana', 'apple', 'cherry', 'date']
|
|
171
174
|
|
|
175
|
+
// Splice allows removal and insertion at specific indices
|
|
176
|
+
items.splice(1, 1, 'orange') // Removes 'apple' and inserts 'orange' at index 1
|
|
177
|
+
console.log(items.get()) // ['banana', 'orange', 'cherry', 'date']
|
|
178
|
+
|
|
172
179
|
// Efficient sorting preserves signal references
|
|
173
180
|
items.sort() // Default: string comparison
|
|
174
|
-
console.log(items.get()) // ['apple', 'banana', 'cherry', 'date']
|
|
181
|
+
console.log(items.get()) // ['apple', 'banana', 'cherry', 'date', 'orange']
|
|
175
182
|
|
|
176
183
|
// Custom sorting
|
|
177
184
|
items.sort((a, b) => b.localeCompare(a)) // Reverse alphabetical
|
|
178
|
-
console.log(items.get()) // ['date', 'cherry', 'banana', 'apple']
|
|
185
|
+
console.log(items.get()) // ['orange', 'date', 'cherry', 'banana', 'apple']
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
List signals have stable unique keys for entries. This means that the keys for each item in the list will not change even if the items are reordered. Keys default to a string representation of an auto-incrementing number. You can customize keys by passing a prefix string or a function to derive the key from the entry value as the second argument to `new List()`:
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
const items = new List(['banana', 'apple', 'cherry', 'date'], 'item-')
|
|
192
|
+
|
|
193
|
+
// Add returns the key of the added item
|
|
194
|
+
const orangeKey = items.add('orange')
|
|
195
|
+
|
|
196
|
+
// Sort preserves signal references
|
|
197
|
+
items.sort()
|
|
198
|
+
console.log(items.get()) // ['apple', 'banana', 'cherry', 'date', 'orange']
|
|
199
|
+
|
|
200
|
+
// Access items by key
|
|
201
|
+
console.log(items.byKey(orangeKey)) // 'orange'
|
|
202
|
+
|
|
203
|
+
const users = new List(
|
|
204
|
+
[{ id: 'bob', name: 'Bob' }, { id: 'alice', name: 'Alice' }],
|
|
205
|
+
user => user.id
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// Sort preserves signal references
|
|
209
|
+
users.sort((a, b) => a.name.localeCompare(b.name)) // Alphabetical by name
|
|
210
|
+
console.log(users.get()) // [{ id: 'alice', name: 'Alice' }, { id: 'bob', name: 'Bob' }]
|
|
211
|
+
|
|
212
|
+
// Get current positional index for an item
|
|
213
|
+
console.log(users.indexOfKey('alice')) // 0
|
|
214
|
+
|
|
215
|
+
// Get key at index
|
|
216
|
+
console.log(users.keyAt(1)) // 'bob'
|
|
179
217
|
```
|
|
180
218
|
|
|
219
|
+
### Collection Signals
|
|
220
|
+
|
|
221
|
+
`new Collection()` creates read-only derived arrays that transform items from Lists with automatic memoization and async support:
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
import { List, Collection, createEffect } from '@zeix/cause-effect'
|
|
225
|
+
|
|
226
|
+
// Source list
|
|
227
|
+
const users = new List([
|
|
228
|
+
{ id: 1, name: 'Alice', role: 'admin' },
|
|
229
|
+
{ id: 2, name: 'Bob', role: 'user' }
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
// Derived collection - transforms each user
|
|
233
|
+
const userProfiles = new Collection(users, user => ({
|
|
234
|
+
...user,
|
|
235
|
+
displayName: `${user.name} (${user.role})`
|
|
236
|
+
}))
|
|
237
|
+
|
|
238
|
+
// Collections are reactive and memoized
|
|
239
|
+
createEffect(() => {
|
|
240
|
+
console.log('Profiles:', userProfiles.get())
|
|
241
|
+
// [{ id: 1, name: 'Alice', role: 'admin', displayName: 'Alice (admin)' }, ...]
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// Individual items are computed signals
|
|
245
|
+
console.log(userProfiles.at(0).get().displayName) // 'Alice (admin)'
|
|
246
|
+
|
|
247
|
+
// Collections support async transformations
|
|
248
|
+
const userDetails = new Collection(users, async (user, abort) => {
|
|
249
|
+
const response = await fetch(`/users/${user.id}`, { signal: abort })
|
|
250
|
+
return { ...user, details: await response.json() }
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Collections can be chained
|
|
254
|
+
const adminProfiles = new Collection(userProfiles, profile =>
|
|
255
|
+
profile.role === 'admin' ? profile : null
|
|
256
|
+
).filter(Boolean) // Remove null values
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Collections support access by index or key:
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
// Access by index or key (read-only)
|
|
263
|
+
const firstProfile = userProfiles.at(0) // Returns computed signal
|
|
264
|
+
const profileByKey = userProfiles.byKey('user1') // Access by stable key
|
|
265
|
+
|
|
266
|
+
// Array methods work
|
|
267
|
+
console.log(userProfiles.length) // Reactive length
|
|
268
|
+
for (const profile of userProfiles) {
|
|
269
|
+
console.log(profile.get()) // Each item is a computed signal
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Lists can derive collections directly
|
|
273
|
+
const userSummaries = users.deriveCollection(user => ({
|
|
274
|
+
id: user.id,
|
|
275
|
+
summary: `${user.name} is a ${user.role}`
|
|
276
|
+
}))
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### When to Use Collections vs Lists
|
|
280
|
+
|
|
281
|
+
- **Use `new List()`** for mutable arrays where you add, remove, sort, or modify items
|
|
282
|
+
- **Use `new Collection()`** for read-only transformations, filtering, or async processing of Lists
|
|
283
|
+
- **Chain Collections** to create multi-step data pipelines with automatic memoization
|
|
284
|
+
|
|
181
285
|
#### Store Change Notifications
|
|
182
286
|
|
|
183
287
|
Stores emit notifications (sort of light-weight events) when properties are added, changed, or removed. You can listen to these notications using the `.on()` method:
|
|
@@ -208,14 +312,14 @@ user.age.set(31) // Logs: "Changed properties: { age: 31 }
|
|
|
208
312
|
user.remove('email') // Logs: "Removed properties: { email: UNSET }"
|
|
209
313
|
|
|
210
314
|
// Listen for sort notifications (useful for UI animations)
|
|
211
|
-
const items =
|
|
315
|
+
const items = new List(['banana', 'apple', 'cherry'])
|
|
212
316
|
items.sort((a, b) => b.localeCompare(a)) // Reverse alphabetical
|
|
213
317
|
const offSort = items.on('sort', (newOrder) => {
|
|
214
318
|
console.log('Items reordered:', newOrder) // ['2', '1', '0']
|
|
215
319
|
})
|
|
216
320
|
```
|
|
217
321
|
|
|
218
|
-
Notifications are also fired when using `set()` or `
|
|
322
|
+
Notifications are also fired when using `set()`, `update()`, or `splice()` methods:
|
|
219
323
|
|
|
220
324
|
```js
|
|
221
325
|
// This will fire multiple notifications based on what changed
|
|
@@ -235,13 +339,13 @@ offSort() // Stops listening to sort notifications
|
|
|
235
339
|
|
|
236
340
|
### Computed Signals
|
|
237
341
|
|
|
238
|
-
`
|
|
342
|
+
`new Memo()` creates a memoized read-only signal that automatically tracks dependencies and updates only when those dependencies change.
|
|
239
343
|
|
|
240
344
|
```js
|
|
241
|
-
import {
|
|
345
|
+
import { createEffect, Memo, State } from '@zeix/cause-effect'
|
|
242
346
|
|
|
243
|
-
const count =
|
|
244
|
-
const isEven =
|
|
347
|
+
const count = new State(42)
|
|
348
|
+
const isEven = new Memo(() => !(count.get() % 2))
|
|
245
349
|
createEffect(() => console.log(isEven.get())) // logs 'true'
|
|
246
350
|
count.set(24) // logs nothing because 24 is also an even number
|
|
247
351
|
document.querySelector('button.increment').addEventListener('click', () => {
|
|
@@ -262,21 +366,21 @@ const isEven = () => !(count.get() % 2)
|
|
|
262
366
|
**When to use which approach:**
|
|
263
367
|
|
|
264
368
|
- **Use functions when**: The calculation is simple, inexpensive, or called infrequently.
|
|
265
|
-
- **Use
|
|
369
|
+
- **Use new Memo() when**:
|
|
266
370
|
- The calculation is expensive
|
|
267
371
|
- You need to share the result between multiple consumers
|
|
268
372
|
- You're working with asynchronous operations
|
|
269
373
|
- You need to track specific error states
|
|
270
374
|
|
|
271
|
-
#### Reducer
|
|
375
|
+
#### Reducer Capabilities
|
|
272
376
|
|
|
273
|
-
`
|
|
377
|
+
`new Memo()` supports reducer patterns by accepting an initial value and providing access to the previous value in the callback:
|
|
274
378
|
|
|
275
379
|
```js
|
|
276
|
-
import {
|
|
380
|
+
import { createEffect, Memo, State } from '@zeix/cause-effect'
|
|
277
381
|
|
|
278
|
-
const actions =
|
|
279
|
-
const counter =
|
|
382
|
+
const actions = new State('increment')
|
|
383
|
+
const counter = new Memo((prev) => {
|
|
280
384
|
const action = actions.get()
|
|
281
385
|
switch (action) {
|
|
282
386
|
case 'increment':
|
|
@@ -307,7 +411,7 @@ This pattern is particularly useful for:
|
|
|
307
411
|
|
|
308
412
|
#### Asynchronous Computations with Automatic Cancellation
|
|
309
413
|
|
|
310
|
-
`
|
|
414
|
+
`new Task()` seamlessly handles asynchronous operations with built-in cancellation support. When used with an async function, it:
|
|
311
415
|
|
|
312
416
|
1. Provides an `abort` signal parameter you can pass to fetch or other cancelable APIs
|
|
313
417
|
2. Automatically cancels pending operations when dependencies change
|
|
@@ -315,10 +419,10 @@ This pattern is particularly useful for:
|
|
|
315
419
|
4. Properly handles errors from failed requests
|
|
316
420
|
|
|
317
421
|
```js
|
|
318
|
-
import {
|
|
422
|
+
import { createEffect, match, resolve, State, Task } from '@zeix/cause-effect'
|
|
319
423
|
|
|
320
|
-
const id =
|
|
321
|
-
const data =
|
|
424
|
+
const id = new State(42)
|
|
425
|
+
const data = new Task(async (_, abort) => {
|
|
322
426
|
// The abort signal is automatically managed by the computed signal
|
|
323
427
|
const response = await fetch(`/api/entries/${id.get()}`, { signal: abort })
|
|
324
428
|
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`)
|
|
@@ -340,7 +444,7 @@ document.querySelector('button.next').addEventListener('click', () => {
|
|
|
340
444
|
})
|
|
341
445
|
```
|
|
342
446
|
|
|
343
|
-
**Note**: Always use `
|
|
447
|
+
**Note**: Always use `new Task()` (not plain functions) for async operations to benefit from automatic cancellation, memoization, and error handling.
|
|
344
448
|
|
|
345
449
|
## Effects and Error Handling
|
|
346
450
|
|
|
@@ -349,9 +453,9 @@ The `createEffect()` function supports both synchronous and asynchronous callbac
|
|
|
349
453
|
### Synchronous Effects
|
|
350
454
|
|
|
351
455
|
```js
|
|
352
|
-
import {
|
|
456
|
+
import { createEffect, State } from '@zeix/cause-effect'
|
|
353
457
|
|
|
354
|
-
const count =
|
|
458
|
+
const count = new State(42)
|
|
355
459
|
createEffect(() => {
|
|
356
460
|
console.log('Count changed:', count.get())
|
|
357
461
|
})
|
|
@@ -362,9 +466,9 @@ createEffect(() => {
|
|
|
362
466
|
Async effect callbacks receive an `AbortSignal` parameter that automatically cancels when the effect re-runs or is cleaned up:
|
|
363
467
|
|
|
364
468
|
```js
|
|
365
|
-
import {
|
|
469
|
+
import { createEffect, State } from '@zeix/cause-effect'
|
|
366
470
|
|
|
367
|
-
const userId =
|
|
471
|
+
const userId = new State(1)
|
|
368
472
|
createEffect(async (abort) => {
|
|
369
473
|
try {
|
|
370
474
|
const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
@@ -383,9 +487,9 @@ createEffect(async (abort) => {
|
|
|
383
487
|
For more sophisticated error handling, use the `resolve()` and `match()` helper functions:
|
|
384
488
|
|
|
385
489
|
```js
|
|
386
|
-
import {
|
|
490
|
+
import { createEffect, resolve, match, State } from '@zeix/cause-effect'
|
|
387
491
|
|
|
388
|
-
const userId =
|
|
492
|
+
const userId = new State(1)
|
|
389
493
|
const userData = createEffect(async (abort) => {
|
|
390
494
|
const response = await fetch(`/api/users/${userId.get()}`, { signal: abort })
|
|
391
495
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
@@ -407,23 +511,23 @@ The `resolve()` function extracts values from signals and returns a discriminate
|
|
|
407
511
|
|
|
408
512
|
### Batching Updates
|
|
409
513
|
|
|
410
|
-
Use `
|
|
514
|
+
Use `batchSignalWrites()` to group multiple signal updates, ensuring effects run only once after all changes are applied:
|
|
411
515
|
|
|
412
516
|
```js
|
|
413
517
|
import {
|
|
414
|
-
createState,
|
|
415
|
-
createComputed,
|
|
416
518
|
createEffect,
|
|
417
|
-
|
|
519
|
+
batchSignalWrites,
|
|
418
520
|
resolve,
|
|
419
|
-
match
|
|
521
|
+
match,
|
|
522
|
+
Memo
|
|
523
|
+
State
|
|
420
524
|
} from '@zeix/cause-effect'
|
|
421
525
|
|
|
422
526
|
// State: define an Array<State<number>>
|
|
423
|
-
const signals = [
|
|
527
|
+
const signals = [new State(2), new State(3), new State(5)]
|
|
424
528
|
|
|
425
529
|
// Compute the sum of all signals
|
|
426
|
-
const sum =
|
|
530
|
+
const sum = new Memo(() => {
|
|
427
531
|
const v = signals.reduce((total, signal) => total + signal.get(), 0)
|
|
428
532
|
// Validate the result
|
|
429
533
|
if (!Number.isFinite(v)) throw new Error('Invalid value')
|
|
@@ -458,9 +562,9 @@ signals[0].set(NaN)
|
|
|
458
562
|
Effects return a cleanup function. When executed, it will unsubscribe from signals and run cleanup functions returned by effect callbacks, for example to remove event listeners.
|
|
459
563
|
|
|
460
564
|
```js
|
|
461
|
-
import {
|
|
565
|
+
import { createEffect, State } from '@zeix/cause-effect'
|
|
462
566
|
|
|
463
|
-
const user =
|
|
567
|
+
const user = new State({ name: 'Alice', age: 30 })
|
|
464
568
|
const greeting = () => `Hello ${user.get().name}!`
|
|
465
569
|
const cleanup = createEffect(() => {
|
|
466
570
|
console.log(`${greeting()} You are ${user.get().age} years old`)
|
|
@@ -480,10 +584,10 @@ user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
|
480
584
|
The `resolve()` function extracts values from multiple signals and returns a discriminated union result:
|
|
481
585
|
|
|
482
586
|
```js
|
|
483
|
-
import {
|
|
587
|
+
import { Memo, resolve, State } from '@zeix/cause-effect'
|
|
484
588
|
|
|
485
|
-
const name =
|
|
486
|
-
const age =
|
|
589
|
+
const name = new State('Alice')
|
|
590
|
+
const age = new Memo(() => 30)
|
|
487
591
|
const result = resolve({ name, age })
|
|
488
592
|
|
|
489
593
|
if (result.ok) {
|
|
@@ -556,4 +660,4 @@ Feel free to contribute, report issues, or suggest improvements.
|
|
|
556
660
|
|
|
557
661
|
License: [MIT](LICENSE)
|
|
558
662
|
|
|
559
|
-
(c)
|
|
663
|
+
(c) 2024 – 2026 [Zeix AG](https://zeix.com)
|