@venizia/ignis-docs 0.2.1-0 → 0.2.1-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/content/best-practices/architectural-patterns.md +3 -3
- package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
- package/content/best-practices/contribution-workflow.md +2 -2
- package/content/best-practices/error-handling.md +94 -89
- package/content/best-practices/security-guidelines.md +5 -5
- package/content/extensions/components/api-reference.md +22 -21
- package/content/extensions/components/authentication/api.md +64 -28
- package/content/extensions/components/authentication/errors.md +19 -5
- package/content/extensions/components/authentication/index.md +19 -18
- package/content/extensions/components/authentication/usage.md +55 -30
- package/content/extensions/components/authorization/api.md +351 -84
- package/content/extensions/components/authorization/errors.md +51 -17
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +24 -16
- package/content/extensions/components/authorization/usage.md +45 -21
- package/content/extensions/components/health-check.md +15 -9
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +105 -54
- package/content/extensions/components/mail/errors.md +12 -10
- package/content/extensions/components/mail/index.md +32 -13
- package/content/extensions/components/mail/usage.md +26 -18
- package/content/extensions/components/request-tracker.md +18 -14
- package/content/extensions/components/socket-io/api.md +377 -882
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +72 -88
- package/content/extensions/components/socket-io/usage.md +107 -117
- package/content/extensions/components/static-asset/api.md +83 -31
- package/content/extensions/components/static-asset/errors.md +18 -7
- package/content/extensions/components/static-asset/index.md +18 -11
- package/content/extensions/components/static-asset/usage.md +11 -6
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +58 -27
- package/content/extensions/components/websocket/errors.md +3 -3
- package/content/extensions/components/websocket/index.md +8 -7
- package/content/extensions/components/websocket/usage.md +21 -8
- package/content/extensions/helpers/cron/index.md +8 -7
- package/content/extensions/helpers/crypto/index.md +16 -8
- package/content/extensions/helpers/crypto/reference.md +96 -24
- package/content/extensions/helpers/env/index.md +14 -10
- package/content/extensions/helpers/error/index.md +99 -23
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +23 -6
- package/content/extensions/helpers/inversion/reference.md +30 -22
- package/content/extensions/helpers/kafka/admin.md +3 -2
- package/content/extensions/helpers/kafka/compile-binary.md +69 -44
- package/content/extensions/helpers/kafka/consumer.md +24 -21
- package/content/extensions/helpers/kafka/examples.md +22 -234
- package/content/extensions/helpers/kafka/index.md +30 -62
- package/content/extensions/helpers/kafka/producer.md +31 -26
- package/content/extensions/helpers/kafka/schema-registry.md +19 -14
- package/content/extensions/helpers/logger/hf-logger.md +49 -22
- package/content/extensions/helpers/logger/index.md +37 -12
- package/content/extensions/helpers/logger/pino.md +31 -11
- package/content/extensions/helpers/logger/reference.md +243 -52
- package/content/extensions/helpers/network/api.md +65 -30
- package/content/extensions/helpers/network/index.md +32 -11
- package/content/extensions/helpers/queue/index.md +17 -6
- package/content/extensions/helpers/queue/reference.md +52 -25
- package/content/extensions/helpers/redis/index.md +29 -11
- package/content/extensions/helpers/redis/reference.md +85 -28
- package/content/extensions/helpers/secrets/index.md +82 -12
- package/content/extensions/helpers/socket-io/api.md +40 -21
- package/content/extensions/helpers/socket-io/index.md +26 -10
- package/content/extensions/helpers/storage/api.md +42 -24
- package/content/extensions/helpers/storage/index.md +10 -7
- package/content/extensions/helpers/types/index.md +20 -7
- package/content/extensions/helpers/types/reference.md +53 -14
- package/content/extensions/helpers/uid/index.md +166 -10
- package/content/extensions/helpers/websocket/api.md +52 -13
- package/content/extensions/helpers/websocket/index.md +7 -4
- package/content/extensions/helpers/worker-thread/index.md +9 -5
- package/content/extensions/helpers/worker-thread/reference.md +20 -20
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +4 -3
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +7 -6
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +63 -21
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +14 -14
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +32 -31
- package/content/references/base/datasources.md +6 -6
- package/content/references/base/dependency-injection.md +12 -11
- package/content/references/base/filter-system/application-usage.md +54 -30
- package/content/references/base/filter-system/array-operators.md +24 -46
- package/content/references/base/filter-system/comparison-operators.md +47 -67
- package/content/references/base/filter-system/default-filter.md +59 -53
- package/content/references/base/filter-system/fields-order-pagination.md +92 -146
- package/content/references/base/filter-system/index.md +25 -13
- package/content/references/base/filter-system/json-filtering.md +45 -184
- package/content/references/base/filter-system/list-operators.md +23 -53
- package/content/references/base/filter-system/logical-operators.md +63 -121
- package/content/references/base/filter-system/null-operators.md +34 -104
- package/content/references/base/filter-system/pattern-matching.md +40 -55
- package/content/references/base/filter-system/quick-reference.md +86 -198
- package/content/references/base/filter-system/range-operators.md +18 -46
- package/content/references/base/filter-system/tips.md +6 -6
- package/content/references/base/filter-system/use-cases.md +33 -15
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +11 -10
- package/content/references/base/models-reference.md +17 -17
- package/content/references/base/models.md +4 -3
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +224 -326
- package/content/references/base/repositories/index.md +19 -6
- package/content/references/base/repositories/mixins.md +5 -5
- package/content/references/base/repositories/relations.md +160 -293
- package/content/references/base/repositories/soft-deletable.md +16 -6
- package/content/references/base/secrets.md +17 -13
- package/content/references/base/services.md +6 -4
- package/content/references/configuration/environment-variables.md +31 -23
- package/content/references/configuration/index.md +6 -4
- package/content/references/index.md +1 -1
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +5 -1
- package/content/references/utilities/jsx-reference.md +11 -11
- package/content/references/utilities/jsx.md +2 -2
- package/content/references/utilities/module.md +78 -25
- package/content/references/utilities/request.md +2 -1
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +2 -2
- package/content/references/utilities/statuses-reference.md +4 -4
- package/content/references/utilities/statuses.md +5 -5
- package/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +17 -16
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: UID
|
|
3
|
-
description:
|
|
3
|
+
description: Two ID generators - time-sortable Snowflake IDs, and short random IDs a human can read back
|
|
4
4
|
difficulty: beginner
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# UID
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Two generators, and they answer different questions.
|
|
10
|
+
|
|
11
|
+
| Helper | Reads back as | Pick it when |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `SnowflakeUidHelper` | Timestamp, worker ID, sequence | The ID is a primary key and you want insert order for free |
|
|
14
|
+
| `OpaqueUidHelper` | Nothing | The ID leaves your system - a human reads it, or a stranger sees it |
|
|
15
|
+
|
|
16
|
+
A Snowflake ID is transparent by design. `parseId()` gives back the exact millisecond it was minted and the worker that minted it. That is what you want in a log, and what you do not want printed on an invoice a customer keeps.
|
|
17
|
+
|
|
18
|
+
An opaque ID gives nothing back. You trade the ordering for that, and for a short ID a human can read out loud.
|
|
10
19
|
|
|
11
20
|
## In one example
|
|
12
21
|
|
|
@@ -18,15 +27,19 @@ const id = generator.nextId();
|
|
|
18
27
|
// => e.g. "9du1sJXO88"
|
|
19
28
|
```
|
|
20
29
|
|
|
21
|
-
Use one `SnowflakeUidHelper` instance per worker process
|
|
30
|
+
Use one `SnowflakeUidHelper` instance per worker process. `nextId()` returns a URL-safe Base62 string, and it sorts in the same order the IDs were generated.
|
|
22
31
|
|
|
23
32
|
## How it works
|
|
24
33
|
|
|
25
|
-
- **The ID packs three fields into 70 bits.**
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
-
|
|
29
|
-
- **
|
|
34
|
+
- **The ID packs three fields into 70 bits.** `nextSnowflake()` shifts and OR's together a timestamp, a worker ID, and a sequence - see the bit layout below.
|
|
35
|
+
- **Sorting the ID sorts by generation time.** This holds for the raw `bigint`, and for the Base62 string.
|
|
36
|
+
- **The sequence resets every millisecond, per worker.** It increments within the millisecond and wraps at 4096.
|
|
37
|
+
- **A wrap forces a busy-wait for the next millisecond.** That caps throughput at 4,096,000 IDs per second per worker.
|
|
38
|
+
- **A small backward clock drift busy-waits.** Up to 100ms (`MAX_CLOCK_BACKWARD_MS`), the generator waits for the clock to catch up, and logs a warning.
|
|
39
|
+
- **A larger drift throws instead.** IGNIS refuses to risk generating a duplicate ID.
|
|
40
|
+
- **`encodeBase62` / `decodeBase62` are a bigint <-> string codec.** They don't know about the Snowflake layout.
|
|
41
|
+
- **`parseId()` layers on top of the codec.** It calls `decodeBase62()`, then the three `extract*` calls, using the same instance's `epoch` and bit-shift constants.
|
|
42
|
+
- **An expiry warning logs once the epoch nears its 48-bit limit.** The timestamp field overflows about 8,919 years after the configured epoch. A warning starts logging 10 years before that.
|
|
30
43
|
|
|
31
44
|
**Snowflake layout (70 bits)**
|
|
32
45
|
|
|
@@ -47,7 +60,7 @@ Use one `SnowflakeUidHelper` instance per worker process - `nextId()` returns a
|
|
|
47
60
|
|
|
48
61
|
### Generate an ID
|
|
49
62
|
|
|
50
|
-
`nextId()` is the common case - a Base62 string, 10-12 characters. `nextSnowflake()` returns the raw `bigint
|
|
63
|
+
`nextId()` is the common case - a Base62 string, 10-12 characters. `nextSnowflake()` returns the raw `bigint`, for arithmetic or bit-level access.
|
|
51
64
|
|
|
52
65
|
```typescript
|
|
53
66
|
const id = generator.nextId(); // "9du1sJXO88"
|
|
@@ -71,7 +84,7 @@ const sequence = generator.extractSequence(raw);
|
|
|
71
84
|
|
|
72
85
|
### Run one generator per worker in a distributed deployment
|
|
73
86
|
|
|
74
|
-
Give each process a unique `workerId
|
|
87
|
+
Give each process a unique `workerId`. Keep `epoch` identical across every process, so ID ordering stays meaningful.
|
|
75
88
|
|
|
76
89
|
```typescript
|
|
77
90
|
const generator = new SnowflakeUidHelper({ workerId: Number(process.env.WORKER_ID) });
|
|
@@ -89,6 +102,147 @@ try {
|
|
|
89
102
|
}
|
|
90
103
|
```
|
|
91
104
|
|
|
105
|
+
## OpaqueUidHelper
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { OpaqueUidHelper } from '@venizia/ignis-helpers';
|
|
109
|
+
|
|
110
|
+
const generator = new OpaqueUidHelper({
|
|
111
|
+
prefix: { enable: true, value: 'INV' },
|
|
112
|
+
delimiter: { enable: true, value: '-' },
|
|
113
|
+
length: 6,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
generator.nextId();
|
|
117
|
+
// => e.g. "INV-7K2MQ9"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`length` counts the body only. The prefix and the delimiter sit in front of it, so that ID is 10 characters.
|
|
121
|
+
|
|
122
|
+
### Options
|
|
123
|
+
|
|
124
|
+
| Option | Type | Default | Meaning |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| `prefix` | `{ enable, value? }` | disabled | Leads the ID. `value` survives being disabled, so you can toggle it back on |
|
|
127
|
+
| `delimiter` | `{ enable, value? }` | disabled, `_` | Separates prefix from body. Requires a prefix |
|
|
128
|
+
| `length` | `number` | `6` | Characters in the body |
|
|
129
|
+
| `caseForm` | `'upper' \| 'lower' \| 'mixed'` | `'upper'` | Folds the alphabet before sampling |
|
|
130
|
+
| `alphabet` | `string` | `UidAlphabets.CROCKFORD` | The characters to draw from |
|
|
131
|
+
| `exclude` | `string` | `''` | Removed from `alphabet` |
|
|
132
|
+
|
|
133
|
+
Four alphabets ship with it:
|
|
134
|
+
|
|
135
|
+
| Alphabet | Size | Drops | Use for |
|
|
136
|
+
|---|---|---|---|
|
|
137
|
+
| `CROCKFORD` | 32 | Lowercase, `I` `L` `O` `U` | IDs a human reads, types, or says aloud |
|
|
138
|
+
| `BASE58` | 58 | `0` `O` `I` `l` | IDs in a URL or a log |
|
|
139
|
+
| `NO_VOWEL` | 50 | `BASE58` plus every vowel | Customer-facing IDs - no word can form |
|
|
140
|
+
| `BASE62` | 62 | Nothing | Machines only |
|
|
141
|
+
|
|
142
|
+
`getAlphabet()` returns the set actually in use, after the case fold and every exclusion. That is what `length` is measured against, so read it before calculating how many IDs a length affords you.
|
|
143
|
+
|
|
144
|
+
### Six characters needs a unique index
|
|
145
|
+
|
|
146
|
+
Six Crockford characters is 2^30 combinations, the size of an airline record locator. That works for airlines because of three things, not because 2^30 is large:
|
|
147
|
+
|
|
148
|
+
1. **Scoped.** A record locator is unique per carrier, not worldwide.
|
|
149
|
+
2. **Recycled.** The airline reuses it once the trip is over.
|
|
150
|
+
3. **Retried.** The reservation system regenerates on conflict.
|
|
151
|
+
|
|
152
|
+
> [!WARNING]
|
|
153
|
+
> At the default length, a 1% chance of at least one collision arrives at roughly 4,600 IDs in one space, and 50% at roughly 38,000.
|
|
154
|
+
|
|
155
|
+
A `prefix` PARTITIONS the space. `INV-7K2MQ9` can never collide with `CUS-7K2MQ9`, so ten entity types get ten separate spaces instead of sharing one. It does not make an ID unique inside its own space - within `INV`, the numbers above still hold.
|
|
156
|
+
|
|
157
|
+
So do both. Give each entity type its own `prefix`, and put the column behind a unique index.
|
|
158
|
+
|
|
159
|
+
### Reject an ID you already hold
|
|
160
|
+
|
|
161
|
+
`nextId()` takes a callback. Return `true` to accept the ID, `false` to draw another.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const issued = new Set<string>();
|
|
165
|
+
const generator = new OpaqueUidHelper({ length: 8 });
|
|
166
|
+
|
|
167
|
+
const id = generator.nextId({
|
|
168
|
+
isAvailable: candidate => !issued.has(candidate),
|
|
169
|
+
});
|
|
170
|
+
issued.add(id);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Every argument is optional, including the options object itself - `nextId()` alone is the common case.
|
|
174
|
+
|
|
175
|
+
**`nextId()` options**
|
|
176
|
+
|
|
177
|
+
| Option | Type | Default | Meaning |
|
|
178
|
+
|---|---|---|---|
|
|
179
|
+
| `prefix` | `string` | The configured prefix | Replaces it for this call |
|
|
180
|
+
| `isAvailable` | `(id: string) => ValueOrPromise<boolean>` | none | `true` accepts, `false` draws again |
|
|
181
|
+
| `maxAttempts` | `number` | `10` | Draws before it throws. Requires `isAvailable` |
|
|
182
|
+
|
|
183
|
+
The callback sees the finished ID, prefix and delimiter included - the string the column stores.
|
|
184
|
+
|
|
185
|
+
**The callback's return type decides `nextId`'s.** A synchronous check keeps the call synchronous, so no promise is allocated. An asynchronous one makes it a promise:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const id = generator.nextId({ isAvailable: candidate => !issued.has(candidate) });
|
|
189
|
+
// string
|
|
190
|
+
|
|
191
|
+
const id = await generator.nextId({
|
|
192
|
+
isAvailable: async candidate => !(await invoiceRepository.exists(candidate)),
|
|
193
|
+
});
|
|
194
|
+
// Promise<string>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
> [!WARNING]
|
|
198
|
+
> An asynchronous check does not make an ID safe. Against a database it is a read and then a write with a gap between them, and another request can take the ID inside that gap. `isAvailable` lowers the collision rate; the unique index is what removes it.
|
|
199
|
+
|
|
200
|
+
`maxAttempts` bounds the redraws. Repeated rejection means the space is full, and the helper throws with the length and alphabet size rather than looping.
|
|
201
|
+
|
|
202
|
+
### Regenerate on conflict, never check first
|
|
203
|
+
|
|
204
|
+
Insert and catch the violation. Checking whether an ID exists and then inserting it leaves a window where another request takes the same ID between the two statements.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { executeWithRetry, OpaqueUidHelper } from '@venizia/ignis-helpers';
|
|
208
|
+
|
|
209
|
+
const generator = new OpaqueUidHelper({
|
|
210
|
+
prefix: { enable: true, value: 'INV' },
|
|
211
|
+
delimiter: { enable: true, value: '-' },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const invoice = await executeWithRetry({
|
|
215
|
+
operation: 'createInvoice',
|
|
216
|
+
maxAttempts: 5,
|
|
217
|
+
// A fresh ID per attempt - retrying with the same one would collide forever.
|
|
218
|
+
execution: () => invoiceRepository.create({ data: { id: generator.nextId(), total } }),
|
|
219
|
+
shouldRetry: context => isUniqueViolation(context.error),
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Five attempts is generous. At the default length a second collision on the same insert has a probability in the millionths.
|
|
224
|
+
|
|
225
|
+
Or raise `length` - every extra character multiplies the space by 32.
|
|
226
|
+
|
|
227
|
+
### What the constructor refuses
|
|
228
|
+
|
|
229
|
+
Each of these fails loudly at construction, because each one fails silently at runtime:
|
|
230
|
+
|
|
231
|
+
| Configuration | Why it is refused |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `delimiter` enabled, `prefix` disabled | The delimiter would lead the ID and separate nothing |
|
|
234
|
+
| A delimiter character that is in the alphabet | The ID could not be split back into prefix and body |
|
|
235
|
+
| A `prefix` that disagrees with `caseForm` | A lowercase prefix on an uppercase ID defeats the reason for choosing one case |
|
|
236
|
+
| `caseForm` folding a two-case alphabet | Uppercasing `BASE58` brings `I` and `O` back - the pair it drops so `1` and `0` stay readable |
|
|
237
|
+
| An alphabet under 16 characters | `exclude` has eaten it |
|
|
238
|
+
| `maxAttempts` passed without `isAvailable` | Nothing rejects a draw, so the retry it configures cannot happen |
|
|
239
|
+
|
|
240
|
+
### Randomness
|
|
241
|
+
|
|
242
|
+
`OpaqueUidHelper` draws from `crypto.getRandomValues`, and rejects samples that fall outside the alphabet rather than wrapping them. A `byte % 58` would favour the first 24 characters by about 1.5%, forever, and no caller could see it.
|
|
243
|
+
|
|
244
|
+
Unlike `crypto.randomUUID`, `getRandomValues` works outside a secure context. The generator runs on a plain-http origin and inside a browser Worker.
|
|
245
|
+
|
|
92
246
|
## See also
|
|
93
247
|
|
|
94
248
|
- [Models](/guides/core-concepts/persistent/models) - using UIDs as primary keys
|
|
@@ -100,4 +254,6 @@ try {
|
|
|
100
254
|
**Files:**
|
|
101
255
|
|
|
102
256
|
- [`packages/helpers/src/modules/uid/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/uid/helper.ts) - `SnowflakeUidHelper`, `SnowflakeConfig`
|
|
257
|
+
- [`packages/helpers/src/modules/uid/opaque.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/uid/opaque.ts) - `OpaqueUidHelper`
|
|
258
|
+
- [`packages/helpers/src/modules/uid/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/uid/common/constants.ts) - `UidAlphabets`, `UidCaseForms`
|
|
103
259
|
- [`packages/helpers/src/modules/uid/index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/uid/index.ts) - module barrel
|
|
@@ -15,6 +15,19 @@ Exhaustive reference for `WebSocketServerHelper` and `WebSocketEmitter`. For a r
|
|
|
15
15
|
- [`packages/helpers/src/modules/socket/websocket/common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/common/types.ts) - option, callback, and wire types
|
|
16
16
|
- [`packages/helpers/src/modules/socket/websocket/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/common/constants.ts) - `WebSocketEvents`, `WebSocketChannels`, `WebSocketDefaults`, `WebSocketMessageTypes`, `WebSocketClientStates`
|
|
17
17
|
|
|
18
|
+
## Find what you need
|
|
19
|
+
|
|
20
|
+
| You want to | Go to |
|
|
21
|
+
|---|---|
|
|
22
|
+
| See every class and its role at a glance | [Quick Reference](#quick-reference) |
|
|
23
|
+
| Follow a connection from upgrade to disconnect | [Client connection lifecycle](#client-connection-lifecycle) |
|
|
24
|
+
| Look up every server constructor option | [`IWebSocketServerOptions`](#iwebsocketserveroptions) |
|
|
25
|
+
| Send to one client, a user, a room, or everyone (same process) | [Server API](#server-api) |
|
|
26
|
+
| Send to clients on any server instance, or publish from a process with no server of its own | [Server API](#server-api) / [Emitter API](#emitter-api) |
|
|
27
|
+
| Look up a wire type, option type, or callback signature | [Types Reference](#types-reference) |
|
|
28
|
+
| Look up an event name, channel, default, or close code | [Constants](#constants) |
|
|
29
|
+
| Debug a stuck, rejected, or cross-instance delivery problem | [Troubleshooting](#troubleshooting) |
|
|
30
|
+
|
|
18
31
|
## Quick Reference
|
|
19
32
|
|
|
20
33
|
| Class | Extends | Role |
|
|
@@ -157,7 +170,7 @@ Server A Redis Server B
|
|
|
157
170
|
|
|
158
171
|
- **Duplication.** `WebSocketServerHelper` duplicates its `redisConnection` twice (`redisPub`, `redisSub`); `WebSocketEmitter` duplicates it once (`redisPub` only).
|
|
159
172
|
- **Connection types.** Both single-instance `Redis` and `Cluster` connections from ioredis are supported - the parent helper connection stays independent and unconsumed.
|
|
160
|
-
- **Dedup on receipt.** Every server instance generates a unique `serverId` (UUID) at construction
|
|
173
|
+
- **Dedup on receipt.** Every server instance generates a unique `serverId` (UUID) at construction. A message carrying that same `serverId` on receipt is skipped, so the originating instance never delivers to itself twice.
|
|
161
174
|
|
|
162
175
|
## Server API
|
|
163
176
|
|
|
@@ -169,11 +182,20 @@ Server A Redis Server B
|
|
|
169
182
|
constructor(opts: IWebSocketServerOptions<AuthDataType, MetadataType>)
|
|
170
183
|
```
|
|
171
184
|
|
|
172
|
-
|
|
185
|
+
On construction:
|
|
186
|
+
|
|
187
|
+
- Generates a unique `serverId` (UUID).
|
|
188
|
+
- Stores all options, with defaults applied.
|
|
189
|
+
- Initializes two duplicated Redis clients (`redisPub`, `redisSub`).
|
|
190
|
+
|
|
191
|
+
Throws `getError({ statusCode: 500, message: '[WebSocketServerHelper] Invalid redis connection!' })` if `redisConnection` is falsy.
|
|
173
192
|
|
|
174
193
|
#### Generic type parameters
|
|
175
194
|
|
|
176
|
-
|
|
195
|
+
| Type parameter | Types | Default |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `AuthDataType` | The payload passed to `authenticateFn`/`handshakeFn` | `Record<string, unknown>` |
|
|
198
|
+
| `MetadataType` | The value returned as `metadata` and stored on `IWebSocketClient` | `Record<string, unknown>` |
|
|
177
199
|
|
|
178
200
|
```typescript
|
|
179
201
|
interface AuthPayload { type: string; token: string; publicKey?: string }
|
|
@@ -219,7 +241,7 @@ const helper = new WebSocketServerHelper<AuthPayload, UserMetadata>({
|
|
|
219
241
|
| `handshakeFn` | `TWebSocketHandshakeFn` | No | - | Required when `requireEncryption` is `true`. Returns `{ serverPublicKey, salt }` to accept, `null`/`false` to reject |
|
|
220
242
|
|
|
221
243
|
- **All callbacks run through `invokeHook()`.** Applies to `authenticateFn`, `validateRoomFn`, `clientConnectedFn`, `clientDisconnectedFn`, `messageHandler`, and `handshakeFn`.
|
|
222
|
-
- **Failures do not crash the process.** A synchronous throw inside a Bun socket handler is caught and logged
|
|
244
|
+
- **Failures do not crash the process.** A synchronous throw inside a Bun socket handler is caught and logged. A rejected promise is logged the same way, via `voidExecution`.
|
|
223
245
|
|
|
224
246
|
### `configure()`
|
|
225
247
|
|
|
@@ -253,7 +275,7 @@ Returns the Bun WebSocket handler object. Pass it to `server.reload({ websocket
|
|
|
253
275
|
#### Bun native configuration
|
|
254
276
|
|
|
255
277
|
- **`serverOptions` is spread into the returned handler.** `WebSocketServerHelper` only applies its own default for `sendPings`, `idleTimeout`, and `maxPayloadLength`.
|
|
256
|
-
- **Everything else falls through to Bun.** The remaining fields are `undefined` unless you set them
|
|
278
|
+
- **Everything else falls through to Bun.** The remaining fields are `undefined` unless you set them. Bun applies its own runtime default in that case.
|
|
257
279
|
|
|
258
280
|
| Option | Type | Helper default | Description |
|
|
259
281
|
|--------|------|-----------------|--------------|
|
|
@@ -306,7 +328,13 @@ Returns every client in `room`; `[]` if the room does not exist or is empty.
|
|
|
306
328
|
onClientConnect(opts: { clientId: string; socket: IWebSocket }): void
|
|
307
329
|
```
|
|
308
330
|
|
|
309
|
-
Handles a new WebSocket connection:
|
|
331
|
+
Handles a new WebSocket connection:
|
|
332
|
+
|
|
333
|
+
- Creates an `IWebSocketClient` entry with state `UNAUTHORIZED`.
|
|
334
|
+
- Subscribes the socket to its own `clientId` topic (Bun native pub/sub).
|
|
335
|
+
- Starts the authentication timeout.
|
|
336
|
+
|
|
337
|
+
If the client ID already exists, it returns early and logs `'Client already existed'`.
|
|
310
338
|
|
|
311
339
|
### `onClientMessage()`
|
|
312
340
|
|
|
@@ -316,7 +344,8 @@ onClientMessage(opts: { clientId: string; raw: string }): void
|
|
|
316
344
|
|
|
317
345
|
Routes an incoming message:
|
|
318
346
|
|
|
319
|
-
- Parses `raw` as JSON. On failure, sends `{ event: 'error', data: { message: 'Invalid message format' } }` and returns.
|
|
347
|
+
- Parses `raw` as JSON. On failure, sends `{ event: 'error', data: { message: 'Invalid message format' } }` and returns.
|
|
348
|
+
- A parsed message with no `event` field is logged and dropped.
|
|
320
349
|
- `heartbeat`: consumed silently (`lastActivity` is already updated by the `message` callback before this runs).
|
|
321
350
|
- `authenticate`: delegates to `handleAuthenticate()`.
|
|
322
351
|
- Any other event, when the client is not `AUTHENTICATED`: sends `{ event: 'error', data: { message: 'Not authenticated' } }`.
|
|
@@ -329,7 +358,14 @@ Routes an incoming message:
|
|
|
329
358
|
onClientDisconnect(opts: { clientId: string }): void
|
|
330
359
|
```
|
|
331
360
|
|
|
332
|
-
Cleans up a disconnected client:
|
|
361
|
+
Cleans up a disconnected client:
|
|
362
|
+
|
|
363
|
+
- Clears the pending auth timer.
|
|
364
|
+
- Removes the client from its user's index.
|
|
365
|
+
- Removes it from every joined room.
|
|
366
|
+
- Removes it from the clients map.
|
|
367
|
+
|
|
368
|
+
It then invokes `clientDisconnectedFn`.
|
|
333
369
|
|
|
334
370
|
### `joinRoom()`
|
|
335
371
|
|
|
@@ -337,7 +373,7 @@ Cleans up a disconnected client: clears the pending auth timer, removes the clie
|
|
|
337
373
|
joinRoom(opts: { clientId: string; room: string }): void
|
|
338
374
|
```
|
|
339
375
|
|
|
340
|
-
Adds `clientId` to the room index and to `client.rooms`, and subscribes the socket to the room's Bun native pub/sub topic
|
|
376
|
+
Adds `clientId` to the room index and to `client.rooms`, and subscribes the socket to the room's Bun native pub/sub topic. An encrypted client skips the subscribe step - delivery goes through the transformer instead. No-op if the client does not exist.
|
|
341
377
|
|
|
342
378
|
### `leaveRoom()`
|
|
343
379
|
|
|
@@ -355,7 +391,10 @@ Removes `clientId` from the room index and from `client.rooms`, and unsubscribes
|
|
|
355
391
|
- Maximum 256 characters.
|
|
356
392
|
- Cannot start with `ws:` (reserved for the internal Redis channel prefix).
|
|
357
393
|
|
|
358
|
-
- **Rejection is silent.**
|
|
394
|
+
- **Rejection is silent.** No error goes back to the client - the join is only rejected and logged. That happens when:
|
|
395
|
+
- every requested room is filtered out,
|
|
396
|
+
- `validateRoomFn` is not configured, or
|
|
397
|
+
- it resolves to an empty array.
|
|
359
398
|
- **`leave` does not re-filter.** `{ event: 'leave' }` only processes rooms the client is actually a member of (`client.rooms.has(room)`).
|
|
360
399
|
- **Programmatic calls bypass validation.** `joinRoom()`/`leaveRoom()` called directly skip all of the above.
|
|
361
400
|
|
|
@@ -365,7 +404,7 @@ Removes `clientId` from the room index and from `client.rooms`, and unsubscribes
|
|
|
365
404
|
enableClientEncryption(opts: { clientId: string }): void
|
|
366
405
|
```
|
|
367
406
|
|
|
368
|
-
Unsubscribes the client from every Bun native pub/sub topic (the broadcast topic plus all joined rooms) so `server.publish()` no longer reaches it
|
|
407
|
+
Unsubscribes the client from every Bun native pub/sub topic (the broadcast topic plus all joined rooms), so `server.publish()` no longer reaches it. Messages are instead delivered individually through `outboundTransformer`. No-op if the client is already encrypted or does not exist.
|
|
369
408
|
|
|
370
409
|
> [!WARNING]
|
|
371
410
|
> Irreversible for the lifetime of the connection. Once a client is encrypted it cannot be switched back to Bun native pub/sub delivery.
|
|
@@ -377,7 +416,7 @@ sendToClient(opts: { clientId: string; event: string; data: unknown; doLog?: boo
|
|
|
377
416
|
```
|
|
378
417
|
|
|
379
418
|
- **Local delivery only.**
|
|
380
|
-
- **Encrypted client, `outboundTransformer` configured.** The transformer runs (async) before `socket.send()
|
|
419
|
+
- **Encrypted client, `outboundTransformer` configured.** The transformer runs (async) before `socket.send()`. A transformer error is logged, and the message is dropped.
|
|
381
420
|
- **Otherwise.** Sends `JSON.stringify({ event, data })` directly.
|
|
382
421
|
- **`doLog: true`** emits an info log after delivery.
|
|
383
422
|
|
|
@@ -450,7 +489,7 @@ shutdown(): Promise<void>
|
|
|
450
489
|
`Source ->` [`emitter/helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/socket/websocket/emitter/helper.ts)
|
|
451
490
|
|
|
452
491
|
- **Role.** A Redis-only publisher for processes with no WebSocket server of their own - background workers, other microservices, cron jobs.
|
|
453
|
-
- **No self-skip.** It always publishes with `serverId: 'emitter'`, so every server instance processes the message
|
|
492
|
+
- **No self-skip.** It always publishes with `serverId: 'emitter'`, so every server instance processes the message. There is no dedup skip on the sending side.
|
|
454
493
|
|
|
455
494
|
### Constructor
|
|
456
495
|
|
|
@@ -6,7 +6,9 @@ difficulty: intermediate
|
|
|
6
6
|
|
|
7
7
|
# WebSocket
|
|
8
8
|
|
|
9
|
-
A Bun-native WebSocket server with post-connection authentication, rooms, heartbeat, and Redis Pub/Sub for horizontal scaling
|
|
9
|
+
A Bun-native WebSocket server with post-connection authentication, rooms, heartbeat, and Redis Pub/Sub for horizontal scaling. A Redis-only emitter sends to it from other processes that run no server of their own.
|
|
10
|
+
|
|
11
|
+
This helper is the raw class: construct it and wire it into your own server yourself. Need it inside an IGNIS app instead? [`WebSocketComponent`](/extensions/components/websocket/) does that wiring for you. It creates and binds the helper through DI once your app's Bun server is listening.
|
|
10
12
|
|
|
11
13
|
> [!IMPORTANT]
|
|
12
14
|
> **Bun only.** `WebSocketServerHelper` uses Bun's native WebSocket API and will not run on Node.js. For Node.js, use the [Socket.IO Helper](../socket-io/) instead.
|
|
@@ -46,7 +48,7 @@ ws.onopen = () => ws.send(JSON.stringify({ event: 'authenticate', data: { token:
|
|
|
46
48
|
|
|
47
49
|
## How it works
|
|
48
50
|
|
|
49
|
-
- **Every connection starts unauthenticated.** The WebSocket upgrade always succeeds
|
|
51
|
+
- **Every connection starts unauthenticated.** The WebSocket upgrade always succeeds, and the client starts as `UNAUTHORIZED`. It must send `{ event: 'authenticate' }` within `authTimeout` (5s default) or get closed with code `4001`.
|
|
50
52
|
- **`authenticateFn` decides accept or reject.** On success the client is indexed by `userId`, joins `defaultRooms`, and moves to `AUTHENTICATED`.
|
|
51
53
|
- **Delivery has two tiers** - local-only fan-out on this process, or a Redis Pub/Sub layer that also reaches other server instances:
|
|
52
54
|
|
|
@@ -56,7 +58,8 @@ ws.onopen = () => ws.send(JSON.stringify({ event: 'authenticate', data: { token:
|
|
|
56
58
|
| Cross-instance | `send()`, `WebSocketEmitter` | Clients on any server instance | Redis Pub/Sub, via the server's duplicated `redisPub`/`redisSub` connections |
|
|
57
59
|
|
|
58
60
|
- **Liveness is passive.** The server never pings clients - they must periodically send `{ event: 'heartbeat' }`. A sweep every `heartbeatInterval` (30s) closes anyone silent for longer than `heartbeatTimeout` (90s) with code `4002`.
|
|
59
|
-
- **Encryption is opt-in per client.** An `outboundTransformer` callback intercepts every outbound message before `socket.send()`.
|
|
61
|
+
- **Encryption is opt-in per client.** An `outboundTransformer` callback intercepts every outbound message before `socket.send()`.
|
|
62
|
+
- **Once encrypted, delivery changes.** Bun native pub/sub is bypassed for that client. `sendToRoom`/`broadcast` then fall back to iterating encrypted clients individually.
|
|
60
63
|
|
|
61
64
|
## Common tasks
|
|
62
65
|
|
|
@@ -96,7 +99,7 @@ helper.broadcast({ event: 'system:announcement', data: { text: 'Maintenance in 5
|
|
|
96
99
|
|
|
97
100
|
### Let clients join rooms
|
|
98
101
|
|
|
99
|
-
Clients can only join custom rooms when you supply `validateRoomFn
|
|
102
|
+
Clients can only join custom rooms when you supply `validateRoomFn`. Without it, every join request is rejected by default:
|
|
100
103
|
|
|
101
104
|
```typescript
|
|
102
105
|
const helper = new WebSocketServerHelper({
|
|
@@ -6,7 +6,7 @@ difficulty: intermediate
|
|
|
6
6
|
|
|
7
7
|
# Worker Thread
|
|
8
8
|
|
|
9
|
-
The worker-thread helper wraps Node's `worker_threads
|
|
9
|
+
The worker-thread helper wraps Node's `worker_threads`. It adds a pooled registry, lifecycle-event helpers, and a `MessagePort` bus for two-way communication between the main thread and a worker.
|
|
10
10
|
|
|
11
11
|
## In one example
|
|
12
12
|
|
|
@@ -30,16 +30,20 @@ pool.register({ key: 'image-resizer', worker });
|
|
|
30
30
|
|
|
31
31
|
## How it works
|
|
32
32
|
|
|
33
|
-
- **Main thread vs worker thread.** Two class families split by side
|
|
33
|
+
- **Main thread vs worker thread.** Two class families split by side.
|
|
34
|
+
- `BaseWorkerHelper` runs on the main thread and wraps a `Worker` instance.
|
|
35
|
+
- `BaseWorkerThreadHelper` runs inside the spawned worker script. It throws `[BaseWorker] Cannot start worker in MAIN_THREAD` if you construct it on the main thread instead.
|
|
34
36
|
- **Pool caps concurrency.** `WorkerPoolHelper` is a lazy singleton (`getInstance()`) that limits registrations to `os.cpus().length`. Past the limit, `register()` returns `false` and logs a warning - it never throws.
|
|
35
|
-
- **Lifecycle hooks, not raw events.**
|
|
36
|
-
-
|
|
37
|
+
- **Lifecycle hooks, not raw events.**
|
|
38
|
+
- `BaseWorkerHelper` binds `online`, `exit`, `error`, `message`, and `messageerror` once in its constructor. Each has a default logging behavior, overridable per instance via `eventHandlers`.
|
|
39
|
+
- A synchronous throw inside a handler is caught and logged, not left to crash the process.
|
|
40
|
+
- **Two-way messaging via buses.** Inside a worker script, `BaseWorkerThreadHelper` manages named `BaseWorkerBusHelper` instances, each wrapping one `MessagePort`. A single worker can multiplex several independent channels this way, one per key.
|
|
37
41
|
|
|
38
42
|
## Common tasks
|
|
39
43
|
|
|
40
44
|
### Look up and message a registered worker
|
|
41
45
|
|
|
42
|
-
`get()` and `has()` read the pool by key
|
|
46
|
+
`get()` and `has()` read the pool by key. `size()` reports how many workers are registered.
|
|
43
47
|
|
|
44
48
|
```typescript
|
|
45
49
|
const worker = pool.get<string>({ key: 'image-resizer' });
|
|
@@ -18,13 +18,13 @@ Exhaustive reference for the worker-thread classes, every constructor option, ev
|
|
|
18
18
|
|
|
19
19
|
## Quick Reference
|
|
20
20
|
|
|
21
|
-
| Class | Extends | Use
|
|
21
|
+
| Class | Extends | Use case |
|
|
22
22
|
|-------|---------|----------|
|
|
23
|
-
| `WorkerPoolHelper` | `BaseHelper` | Singleton registry that tracks and limits active worker instances |
|
|
24
|
-
| `BaseWorkerHelper<MessageType>` | `AbstractWorkerHelper<MessageType>` | Wraps a `Worker` with event lifecycle hooks
|
|
25
|
-
| `BaseWorkerThreadHelper` | `AbstractWorkerThreadHelper` | Runs inside a worker thread
|
|
26
|
-
| `BaseWorkerBusHelper<
|
|
27
|
-
| `BaseWorkerMessageBusHandlerHelper<
|
|
23
|
+
| [`WorkerPoolHelper`](#workerpoolhelper) | `BaseHelper` | Singleton registry that tracks and limits active worker instances |
|
|
24
|
+
| [`BaseWorkerHelper<MessageType>`](#baseworkerhelper-main-thread-worker-wrapper) | `AbstractWorkerHelper<MessageType>` | Wraps a `Worker` with event lifecycle hooks - online, exit, error, message |
|
|
25
|
+
| [`BaseWorkerThreadHelper`](#baseworkerthreadhelper-inside-a-worker-thread) | `AbstractWorkerThreadHelper` | Runs inside a worker thread. Manages named `WorkerBus` channels |
|
|
26
|
+
| [`BaseWorkerBusHelper<IConsumePayload, IPublishPayload>`](#baseworkerbushelper-messageport-communication) | `AbstractWorkerBusHelper<IConsumePayload, IPublishPayload>` | Bidirectional `MessagePort` communication with pre/post hooks |
|
|
27
|
+
| [`BaseWorkerMessageBusHandlerHelper<IConsumePayload>`](#baseworkermessagebushandlerhelper) | `AbstractWorkerMessageBusHandlerHelper<IConsumePayload>` | Defines event handlers for a worker bus - message, close, error, exit |
|
|
28
28
|
|
|
29
29
|
| Item | Value |
|
|
30
30
|
|------|-------|
|
|
@@ -72,7 +72,7 @@ const customPool = new WorkerPoolHelper({ ignoreMaxWarning: true });
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
> [!NOTE]
|
|
75
|
-
> `WorkerPoolHelper.getInstance()` always creates the singleton with `ignoreMaxWarning: false`. To override this
|
|
75
|
+
> `WorkerPoolHelper.getInstance()` always creates the singleton with `ignoreMaxWarning: false`. To override this, construct a new instance manually. The singleton and a manually-constructed instance are independent registries.
|
|
76
76
|
|
|
77
77
|
### Constructor options
|
|
78
78
|
|
|
@@ -85,7 +85,7 @@ const customPool = new WorkerPoolHelper({ ignoreMaxWarning: true });
|
|
|
85
85
|
| Method | Signature | Description |
|
|
86
86
|
|--------|-----------|-------------|
|
|
87
87
|
| `getInstance` | `static getInstance(): WorkerPoolHelper` | Returns the singleton pool instance (creates one with `ignoreMaxWarning: false` if needed) |
|
|
88
|
-
| `register` | `register<MessageType>(opts: { key: string; worker: IWorker<MessageType> }): boolean` | Adds a worker to the pool. Returns `false` and logs
|
|
88
|
+
| `register` | `register<MessageType>(opts: { key: string; worker: IWorker<MessageType> }): boolean` | Adds a worker to the pool. Returns `false` and logs, without registering, if the key exists or the pool is full |
|
|
89
89
|
| `unregister` | `async unregister(opts: { key: string }): Promise<void>` | Terminates the worker (`worker.terminate()`) and removes it from the pool. The registry entry is deleted even if termination throws |
|
|
90
90
|
| `get` | `get<MessageType>(opts: { key: string }): IWorker<MessageType> \| undefined` | Retrieves a registered worker by key |
|
|
91
91
|
| `has` | `has(opts: { key: string }): boolean` | Checks if a worker is registered under the given key |
|
|
@@ -138,9 +138,9 @@ const worker = new BaseWorkerHelper<MyMessageType>({
|
|
|
138
138
|
|
|
139
139
|
| Method | Signature | Description |
|
|
140
140
|
|--------|-----------|-------------|
|
|
141
|
-
| `binding` | `binding(): void` | Binds all five event handlers
|
|
141
|
+
| `binding` | `binding(): void` | Binds all five event handlers to the internal `Worker` instance. Called automatically by the constructor. Throws `[binding] Invalid worker instance to bind event handlers` if `this.worker` is falsy |
|
|
142
142
|
|
|
143
|
-
Every handler runs through `invokeHook`, which wraps the call in a `try/catch
|
|
143
|
+
Every handler runs through `invokeHook`, which wraps the call in a `try/catch`. A synchronous throw inside a user handler is caught and logged as `"Hook execution FAILED | Error: %s"`, rather than crashing the process. Async rejections are settled the same way, via `voidExecution`.
|
|
144
144
|
|
|
145
145
|
### AbstractWorkerHelper (subclassing)
|
|
146
146
|
|
|
@@ -155,7 +155,7 @@ class CustomWorker extends AbstractWorkerHelper<MyMessage> {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
onExit(opts: { code: string | number }) {
|
|
158
|
-
// Custom exit handling,
|
|
158
|
+
// Custom exit handling - for example, restart logic
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
onError(opts: { error: Error }) {
|
|
@@ -172,13 +172,13 @@ class CustomWorker extends AbstractWorkerHelper<MyMessage> {
|
|
|
172
172
|
}
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
`AbstractWorkerHelper<MessageType>` declares `worker: Worker`, `options: WorkerOptions`, and the five abstract lifecycle methods above
|
|
175
|
+
`AbstractWorkerHelper<MessageType>` declares `worker: Worker`, `options: WorkerOptions`, and the five abstract lifecycle methods above. It does not implement `binding()` itself.
|
|
176
176
|
|
|
177
177
|
## BaseWorkerThreadHelper (inside a worker thread)
|
|
178
178
|
|
|
179
179
|
`Source ->` [`base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/worker-thread/base.ts)
|
|
180
180
|
|
|
181
|
-
Used inside a worker script to manage named communication buses. Must be instantiated from within a worker thread
|
|
181
|
+
Used inside a worker script to manage named communication buses. Must be instantiated from within a worker thread. Constructing it on the main thread throws.
|
|
182
182
|
|
|
183
183
|
```typescript
|
|
184
184
|
// Inside worker-script.js
|
|
@@ -203,7 +203,7 @@ The constructor checks `isMainThread` (from `node:worker_threads`) and throws `[
|
|
|
203
203
|
| `unbindWorkerBus` | `unbindWorkerBus(opts: { key: string }): void` | Calls `port.removeAllListeners()` on the bus's port, then deletes it from the registry. Logs a warning if the key is not found |
|
|
204
204
|
| `getWorkerBus` | `getWorkerBus<IC, IP>(opts: { key: string }): IWorkerBus<IC, IP>` | Returns the bus for the given key. Throws `[getWorkerBus] Not found worker bus | key: {key}` if not found |
|
|
205
205
|
|
|
206
|
-
`unbindWorkerBus` is defined only on `BaseWorkerThreadHelper`, not on the `AbstractWorkerThreadHelper` interface
|
|
206
|
+
`unbindWorkerBus` is defined only on `BaseWorkerThreadHelper`, not on the `AbstractWorkerThreadHelper` interface. `AbstractWorkerThreadHelper` declares `buses`, `bindWorkerBus`, and `getWorkerBus` as abstract members only.
|
|
207
207
|
|
|
208
208
|
## BaseWorkerMessageBusHandlerHelper
|
|
209
209
|
|
|
@@ -259,17 +259,17 @@ const bus = new BaseWorkerBusHelper<IncomingMessage, OutgoingMessage>({
|
|
|
259
259
|
| `port` | `MessagePort` | - | The `MessagePort` to bind for communication. Required. |
|
|
260
260
|
| `busHandler` | `IWorkerMessageBusHandler<IConsumePayload>` | - | Handler that receives incoming messages and lifecycle events. Required. |
|
|
261
261
|
|
|
262
|
-
The constructor binds `message`, `error`, `messageerror`, `exit`, and `close` listeners on `port
|
|
262
|
+
The constructor binds `message`, `error`, `messageerror`, `exit`, and `close` listeners on `port`. Each routes to the handler's matching callback. `error` and `messageerror` both route to `handler.onError`.
|
|
263
263
|
|
|
264
264
|
### Methods
|
|
265
265
|
|
|
266
266
|
| Method | Signature | Description |
|
|
267
267
|
|--------|-----------|-------------|
|
|
268
|
-
| `postMessage` | `postMessage(opts: { message: IPublishPayload; transferList: readonly Transferable[] \| undefined }): ValueOrPromise<void>` | Sends a message through the port.
|
|
268
|
+
| `postMessage` | `postMessage(opts: { message: IPublishPayload; transferList: readonly Transferable[] \| undefined }): ValueOrPromise<void>` | Sends a message through the port. Calls `port.postMessage(message, [...transferList])` if `transferList` is set, otherwise `port.postMessage(message)`. Logs and returns, without throwing, if `port` is falsy |
|
|
269
269
|
|
|
270
270
|
### Pre/post message hooks
|
|
271
271
|
|
|
272
|
-
`onBeforePostMessage` and `onAfterPostMessage` are optional properties declared on the class
|
|
272
|
+
`onBeforePostMessage` and `onAfterPostMessage` are optional properties declared on the class. The constructor leaves them `undefined`. Assign them yourself, after construction:
|
|
273
273
|
|
|
274
274
|
```typescript
|
|
275
275
|
bus.onBeforePostMessage = opts => {
|
|
@@ -281,7 +281,7 @@ bus.onAfterPostMessage = opts => {
|
|
|
281
281
|
};
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
`postMessage` invokes `onBeforePostMessage`
|
|
284
|
+
`postMessage` invokes `onBeforePostMessage` before the port write, if set. It invokes `onAfterPostMessage` after the write, if set. Both hooks run through the same `try/catch`-wrapped `invokeHook` used for the port event listeners.
|
|
285
285
|
|
|
286
286
|
### Sending transferable objects
|
|
287
287
|
|
|
@@ -390,9 +390,9 @@ const bus = thread.getWorkerBus({ key: 'my-bus' });
|
|
|
390
390
|
|
|
391
391
|
### "Failed to post message to main | Invalid parentPort!"
|
|
392
392
|
|
|
393
|
-
**Cause:** `BaseWorkerBusHelper.postMessage()` was called but the `port` property is falsy. This typically means the bus was constructed with an invalid `MessagePort`. This is logged at error level, not thrown
|
|
393
|
+
**Cause:** `BaseWorkerBusHelper.postMessage()` was called but the `port` property is falsy. This typically means the bus was constructed with an invalid `MessagePort`. This is logged at error level, not thrown. The message is silently dropped.
|
|
394
394
|
|
|
395
|
-
**Fix:** Ensure a valid `MessagePort`
|
|
395
|
+
**Fix:** Ensure a valid `MessagePort` is passed to the constructor - for example `parentPort` from `node:worker_threads`, or a port from `new MessageChannel()`:
|
|
396
396
|
|
|
397
397
|
```typescript
|
|
398
398
|
import { parentPort } from 'node:worker_threads';
|