lakutata 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -30
- package/README.zh-CN.md +130 -0
- package/dist/cjs/exports/QueueComponent.d.ts +1 -0
- package/dist/cjs/exports/QueueComponent.js +20 -0
- package/dist/esm/exports/QueueComponent.js +1 -0
- package/dist/types/exports/QueueComponent.d.ts +1 -0
- package/doc/en/Application.md +107 -0
- package/doc/en/CLI.md +274 -0
- package/doc/en/Cache.md +379 -0
- package/doc/en/Command.md +110 -0
- package/doc/en/ContainerdIntegration.md +17 -16
- package/doc/en/Controllers.md +256 -0
- package/doc/en/DBus.md +291 -0
- package/doc/en/DTOValidation.md +72 -43
- package/doc/en/Database.md +188 -0
- package/doc/en/DatabaseDrivers.md +287 -0
- package/doc/en/Decorator.md +52 -40
- package/doc/en/DockerIntegration.md +7 -6
- package/doc/en/Entities.md +240 -0
- package/doc/en/Entrypoint.md +216 -0
- package/doc/en/Exceptions.md +174 -0
- package/doc/en/GRPC.md +251 -0
- package/doc/en/GettingStarted.md +165 -0
- package/doc/en/HTTP.md +507 -0
- package/doc/en/HelperFunctions.md +11 -11
- package/doc/en/I18n.md +119 -115
- package/doc/en/Injection.md +144 -0
- package/doc/en/Installers.md +280 -0
- package/doc/en/Logger.md +206 -0
- package/doc/en/MCP.md +293 -0
- package/doc/en/Middlewares.md +213 -0
- package/doc/en/Migrations.md +202 -0
- package/doc/en/MongoEmbedded.md +149 -0
- package/doc/en/Monitor.md +164 -0
- package/doc/en/Mutex.md +27 -25
- package/doc/en/NATS.md +246 -0
- package/doc/en/NativeAddons.md +212 -0
- package/doc/en/Objects.md +143 -0
- package/doc/en/Overview.md +62 -85
- package/doc/en/Packaging.md +193 -0
- package/doc/en/ProjectStructure.md +124 -0
- package/doc/en/Queue.md +269 -0
- package/doc/en/Schedules.md +217 -0
- package/doc/en/Security.md +10 -9
- package/doc/en/ServiceCalls.md +504 -0
- package/doc/en/Time.md +19 -20
- package/doc/en/UpgradingFrom1.md +435 -0
- package/doc/en/UpgradingFrom2.md +315 -0
- package/doc/en/Workspaces.md +189 -0
- package/doc/en/examples/BackgroundJobs.md +677 -0
- package/doc/en/examples/CommandLineTool.md +664 -0
- package/doc/en/examples/DesktopApp.md +809 -0
- package/doc/en/examples/DeviceService.md +607 -0
- package/doc/en/examples/MCPServer.md +653 -0
- package/doc/en/examples/README.md +16 -0
- package/doc/en/examples/RestAPI.md +490 -0
- package/doc/en/examples/ServiceCalls.md +829 -0
- package/doc/en/examples/WebApp.md +568 -0
- package/doc/zh/Application.md +75 -0
- package/doc/zh/CLI.md +250 -0
- package/doc/zh/Cache.md +328 -0
- package/doc/zh/Command.md +78 -0
- package/doc/zh/ContainerdIntegration.md +9 -9
- package/doc/zh/Controllers.md +228 -0
- package/doc/zh/DBus.md +257 -0
- package/doc/zh/DTOValidation.md +49 -19
- package/doc/zh/Database.md +175 -0
- package/doc/zh/DatabaseDrivers.md +261 -0
- package/doc/zh/Decorator.md +25 -18
- package/doc/zh/DockerIntegration.md +6 -6
- package/doc/zh/Entities.md +224 -0
- package/doc/zh/Entrypoint.md +193 -0
- package/doc/zh/Exceptions.md +158 -0
- package/doc/zh/GRPC.md +226 -0
- package/doc/zh/GettingStarted.md +148 -0
- package/doc/zh/HTTP.md +445 -0
- package/doc/zh/HelperFunctions.md +2 -2
- package/doc/zh/I18n.md +26 -26
- package/doc/zh/Injection.md +129 -0
- package/doc/zh/Installers.md +232 -0
- package/doc/zh/Logger.md +188 -0
- package/doc/zh/MCP.md +265 -0
- package/doc/zh/Middlewares.md +198 -0
- package/doc/zh/Migrations.md +178 -0
- package/doc/zh/MongoEmbedded.md +118 -0
- package/doc/zh/Monitor.md +145 -0
- package/doc/zh/Mutex.md +10 -10
- package/doc/zh/NATS.md +207 -0
- package/doc/zh/NativeAddons.md +180 -0
- package/doc/zh/Objects.md +119 -0
- package/doc/zh/Overview.md +46 -71
- package/doc/zh/Packaging.md +154 -0
- package/doc/zh/ProjectStructure.md +102 -0
- package/doc/zh/Queue.md +214 -0
- package/doc/zh/Schedules.md +193 -0
- package/doc/zh/Security.md +6 -6
- package/doc/zh/ServiceCalls.md +459 -0
- package/doc/zh/Time.md +2 -2
- package/doc/zh/UpgradingFrom1.md +347 -0
- package/doc/zh/UpgradingFrom2.md +230 -0
- package/doc/zh/Workspaces.md +150 -0
- package/doc/zh/examples/BackgroundJobs.md +618 -0
- package/doc/zh/examples/CommandLineTool.md +618 -0
- package/doc/zh/examples/DesktopApp.md +748 -0
- package/doc/zh/examples/DeviceService.md +546 -0
- package/doc/zh/examples/MCPServer.md +604 -0
- package/doc/zh/examples/README.md +14 -0
- package/doc/zh/examples/RestAPI.md +458 -0
- package/doc/zh/examples/ServiceCalls.md +765 -0
- package/doc/zh/examples/WebApp.md +520 -0
- package/package.json +46 -17
- package/doc/en/ActionPattern.md +0 -408
- package/doc/en/Core.md +0 -312
- package/doc/en/ORMIntegration.md +0 -341
- package/doc/zh/ActionPattern.md +0 -334
- package/doc/zh/Core.md +0 -256
- package/doc/zh/ORMIntegration.md +0 -319
package/doc/en/NATS.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Lakutata NATS
|
|
2
|
+
|
|
3
|
+
The package `@lakutata/nats` connects an application to a [NATS](https://nats.io) server. It provides a component to
|
|
4
|
+
publish, subscribe and send requests; a service adapter that serves the service actions of the application over NATS;
|
|
5
|
+
and a service proxy that calls them from other applications. Several instances of a service share its requests, without
|
|
6
|
+
a load balancer. `lakutata` does not include the package, so install it next to it:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install lakutata @lakutata/nats
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
It needs a NATS server 2.2 or later, with JetStream enabled (`nats-server -js`) for the payloads larger than the
|
|
13
|
+
server's limit (section 7):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
docker run -d --name nats -p 4222:4222 nats:2.12-alpine -js
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 1. The NATS component
|
|
20
|
+
|
|
21
|
+
Declare the component with `buildNatsClientOptions`. It connects when it is created. When the server cannot be reached,
|
|
22
|
+
the creation fails after `timeout` (20 seconds by default), and so does the launch of the application if the component
|
|
23
|
+
is created during the launch (listed in `bootstrap`, or needed by an object that is).
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import {Application} from 'lakutata'
|
|
27
|
+
import {buildNatsClientOptions, type NATS} from '@lakutata/nats'
|
|
28
|
+
|
|
29
|
+
Application
|
|
30
|
+
.run(() => ({
|
|
31
|
+
id: 'pricing.app',
|
|
32
|
+
name: 'Pricing',
|
|
33
|
+
components: {
|
|
34
|
+
nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222', name: 'pricing'})
|
|
35
|
+
},
|
|
36
|
+
bootstrap: ['nats']
|
|
37
|
+
}))
|
|
38
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
39
|
+
const nats: NATS = await app.getObject<NATS>('nats')
|
|
40
|
+
//A responder: the value returned answers the request; the instances in the queue group share the requests
|
|
41
|
+
nats.subscribe('price.get', async (input: { sku: string }): Promise<number> => input.sku.length * 10, {queue: 'pricing'})
|
|
42
|
+
//A subscriber with a wildcard: the second argument is the subject of the message
|
|
43
|
+
nats.subscribe('price.changed.*', (price: number, subject: string): void => console.log(subject, price))
|
|
44
|
+
nats.publish('price.changed.eu', 42)
|
|
45
|
+
//The response, within 5 seconds
|
|
46
|
+
console.log(await nats.request('price.get', {sku: 'ABC'}, 5000)) //30
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `publish(subject, payload)` sends a message to every subscription of the subject (to one subscription per queue
|
|
51
|
+
group). When nobody is listening, the message is lost.
|
|
52
|
+
- `subscribe(subject, callback, options?)` accepts the wildcards `*` (one token) and `>` (all the remaining tokens).
|
|
53
|
+
The options are `queue` (a queue group: each message goes to one member), `max` (end after that many messages),
|
|
54
|
+
`iterator` (handle the messages one after the other) and `errorResponse` (see below). It returns the NATS
|
|
55
|
+
subscription (`unsubscribe()`, `drain()`).
|
|
56
|
+
- `request(subject, payload, timeout?)` returns the first response. Its timeout is in milliseconds, 10 minutes by
|
|
57
|
+
default. When the callback of the responder throws, the request is not answered and times out, unless the
|
|
58
|
+
subscription has `errorResponse: (error) => value` to answer it.
|
|
59
|
+
- A lost connection is opened again in the background (10 attempts per server, `maxReconnectAttempts: -1` to retry
|
|
60
|
+
forever).
|
|
61
|
+
- When the application stops, the component drains its connections: the messages being handled are finished, and the
|
|
62
|
+
outgoing ones sent.
|
|
63
|
+
- The component emits `'error'` for the failures it cannot throw to a caller (a callback that throws, a message that
|
|
64
|
+
cannot be decoded): `nats.on('error', listener)`.
|
|
65
|
+
|
|
66
|
+
The options of `buildNatsClientOptions`:
|
|
67
|
+
|
|
68
|
+
| Option | Default | Meaning |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `servers` | Required | A server URL or a list of them (`'nats://127.0.0.1:4222'`) |
|
|
71
|
+
| `user`, `pass` / `token` | | The credentials |
|
|
72
|
+
| `name` | | The name of the connection, shown by the server |
|
|
73
|
+
| `codec` | `messagePackCodec()` | How the payloads are encoded (section 6) |
|
|
74
|
+
| `timeout` | `20000` | The milliseconds to wait for the first connection |
|
|
75
|
+
| `reconnect`, `maxReconnectAttempts` | `true`, `10` | Whether, and how many times per server, to reconnect |
|
|
76
|
+
| `pingInterval`, `maxPingOut` | `120000`, `2` | How often to ping the server, and how many pings may stay unanswered |
|
|
77
|
+
| `noEcho` | `false` | Do not receive the messages this connection publishes |
|
|
78
|
+
| `noRandomize` | `false` | Try the servers in their order, not at random |
|
|
79
|
+
| `debug` | `false` | Print the protocol |
|
|
80
|
+
| `bulk`, `bulkBucket`, `bulkTTL`, `bulkReplicas` | | The large payloads (section 7) |
|
|
81
|
+
|
|
82
|
+
TLS and the NKey or JWT credentials are not supported by the component.
|
|
83
|
+
|
|
84
|
+
## 2. Serving the service actions
|
|
85
|
+
|
|
86
|
+
`natsServiceEntrypoint(natsComponentName, options?)` is a `service` adapter of the entrypoint: it receives the requests
|
|
87
|
+
sent to the application and calls its `@ServiceAction` methods.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import {Application, DTO} from 'lakutata'
|
|
91
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
92
|
+
import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
93
|
+
import {Expect} from 'lakutata/decorator/dto'
|
|
94
|
+
import {buildNatsClientOptions, natsServiceEntrypoint} from '@lakutata/nats'
|
|
95
|
+
|
|
96
|
+
class GetOrderDTO extends DTO {
|
|
97
|
+
@Expect(DTO.Number().integer().required())
|
|
98
|
+
public id: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class OrderController extends Controller {
|
|
102
|
+
@ServiceAction({cmd: 'order.get'}, GetOrderDTO)
|
|
103
|
+
public async get(input: GetOrderDTO): Promise<{ id: number, total: number }> {
|
|
104
|
+
return {id: input.id, total: 42}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Application.run(() => ({
|
|
109
|
+
//The subject of the requests, and the serviceId the proxies use
|
|
110
|
+
id: 'order.service',
|
|
111
|
+
name: 'OrderService',
|
|
112
|
+
components: {
|
|
113
|
+
nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'}),
|
|
114
|
+
entrypoint: buildEntrypoints({
|
|
115
|
+
controllers: [OrderController],
|
|
116
|
+
service: natsServiceEntrypoint('nats')
|
|
117
|
+
})
|
|
118
|
+
},
|
|
119
|
+
bootstrap: ['entrypoint']
|
|
120
|
+
}))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- The first argument is the name of the NATS component.
|
|
124
|
+
- The requests arrive on a subject named after the application id (`order.service`); the `subject` option sets
|
|
125
|
+
another one.
|
|
126
|
+
- The instances of the service join one queue group (`order.service.serviceQueue`, or the `queue` option), so each
|
|
127
|
+
request is handled by one instance.
|
|
128
|
+
- A message published without waiting for a response still runs its action, but nothing is answered.
|
|
129
|
+
- When the application stops, the adapter stops receiving new requests, and finishes the ones in progress. Callers of a
|
|
130
|
+
restarting service retry their requests (section 3).
|
|
131
|
+
|
|
132
|
+
The adapter also takes `middlewares`, run after the ones of the entrypoint, and `name`.
|
|
133
|
+
|
|
134
|
+
## 3. Calling a service: the service proxy
|
|
135
|
+
|
|
136
|
+
A `ServiceProxy` calls the actions of one remote service, and listens to its events. It is a singleton provider: declare
|
|
137
|
+
one per service with `buildServiceProxy`, under `providers`.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import {Application} from 'lakutata'
|
|
141
|
+
import {
|
|
142
|
+
buildNatsClientOptions,
|
|
143
|
+
buildServiceProxy,
|
|
144
|
+
NatsRequestTimeoutException,
|
|
145
|
+
ServiceInvokeException,
|
|
146
|
+
type ServiceProxy
|
|
147
|
+
} from '@lakutata/nats'
|
|
148
|
+
|
|
149
|
+
Application
|
|
150
|
+
.run(() => ({
|
|
151
|
+
id: 'billing.app',
|
|
152
|
+
name: 'Billing',
|
|
153
|
+
components: {
|
|
154
|
+
nats: buildNatsClientOptions({servers: 'nats://127.0.0.1:4222'})
|
|
155
|
+
},
|
|
156
|
+
providers: {
|
|
157
|
+
orderService: buildServiceProxy({natsComponentName: 'nats', serviceId: 'order.service'})
|
|
158
|
+
}
|
|
159
|
+
}))
|
|
160
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
161
|
+
const orderService: ServiceProxy = await app.getObject<ServiceProxy>('orderService')
|
|
162
|
+
try {
|
|
163
|
+
//The pattern of the action and its input in one object, and a timeout of 30 seconds
|
|
164
|
+
console.log(await orderService.invoke({cmd: 'order.get', id: 1}, 30000))
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error instanceof ServiceInvokeException) console.error(error.service, error.errno, error.message)
|
|
167
|
+
else if (error instanceof NatsRequestTimeoutException) console.error('No response in time')
|
|
168
|
+
else throw error
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`invoke(input, timeout)` returns the result of the action. The timeout is 10 minutes by default; a service that is not
|
|
174
|
+
running fails at once, so the timeout only limits an action that is too slow.
|
|
175
|
+
|
|
176
|
+
- When no instance of the service received the request (it is not running, or is restarting), the proxy sends the
|
|
177
|
+
request again, `maxRetries` times (3 by default) every `retryDelay` milliseconds (100 by default), then throws a
|
|
178
|
+
`NatsNoRespondersAvailableException`.
|
|
179
|
+
- A timeout and a failed action are never retried: the action may have run, and must not run twice.
|
|
180
|
+
- **A timeout does not stop the action on the service**: the caller stops waiting, but the action runs to its end.
|
|
181
|
+
This differs from gRPC, where the deadline aborts the action.
|
|
182
|
+
|
|
183
|
+
The options of `buildServiceProxy` are `natsComponentName`, `serviceId`, `maxRetries` and `retryDelay`.
|
|
184
|
+
|
|
185
|
+
## 4. Events
|
|
186
|
+
|
|
187
|
+
A service sends events to the proxies listening to it:
|
|
188
|
+
|
|
189
|
+
- `nats.emitServiceEvent(event, ...args)`, from the NATS component, under the application id;
|
|
190
|
+
- `entrypoint.broadcast(event, ...args)`, under the subject of the adapter. It returns 0: NATS does not tell how many
|
|
191
|
+
receive a message. When the `subject` option of the adapter is not the application id, the proxies whose `serviceId`
|
|
192
|
+
is the application id receive only the events of `emitServiceEvent`.
|
|
193
|
+
|
|
194
|
+
`entrypoint.notify(clientId, ...)` does not reach the NATS callers, which have no client id.
|
|
195
|
+
|
|
196
|
+
On the other side, `orderService.on(event, listener)`, `once` and `off` listen to the events of the service. With
|
|
197
|
+
`on(event, listener, true)`, each event goes to only one of the listeners registered this way, across all the
|
|
198
|
+
applications (a queue group). The events are delivered at most once: an application that is not connected when one is
|
|
199
|
+
sent misses it. The listeners of a proxy are removed when it is destroyed.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import type {ServiceProxy} from '@lakutata/nats'
|
|
203
|
+
|
|
204
|
+
export function watchPayments(orderService: ServiceProxy): void {
|
|
205
|
+
orderService.on('orderPaid', (id: number, total: number): void => console.log(`Order ${id} paid: ${total}`))
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## 5. Errors
|
|
210
|
+
|
|
211
|
+
| Exception | errno | statusCode | When |
|
|
212
|
+
|---|---|---|---|
|
|
213
|
+
| `ServiceInvokeException` | The errno of the remote error | The one of the remote error | The action failed. Its message and properties are the remote error's, its `service` is the service id |
|
|
214
|
+
| `NatsNoRespondersAvailableException` | `E_NATS_NO_RESPONDERS_AVAILABLE` | 503 | No instance received the request, after the retries |
|
|
215
|
+
| `NatsRequestTimeoutException` | `E_NATS_REQUEST_TIMEOUT` | 408 | No response before the timeout |
|
|
216
|
+
| `NatsBulkException` | `E_NATS_BULK` | 502 | A large payload could not be stored or read (section 7) |
|
|
217
|
+
|
|
218
|
+
All of them are exported by `@lakutata/nats`. The remote error carries all its properties, its stack included.
|
|
219
|
+
|
|
220
|
+
## 6. Codecs
|
|
221
|
+
|
|
222
|
+
Every payload is encoded with the `codec` of the component, which must be the same on both sides of a subject:
|
|
223
|
+
|
|
224
|
+
- `messagePackCodec()`, the default: MessagePack, compact and fast;
|
|
225
|
+
- `jsonCodec()`: JSON text, easier for the clients written in other languages.
|
|
226
|
+
|
|
227
|
+
Both send the value through JSON first, so that every client receives plain JSON data: a `Date` arrives as its ISO
|
|
228
|
+
string, a `Buffer` as `{type: 'Buffer', data: [...]}`, a class instance as a plain object, `undefined` as `null`. A
|
|
229
|
+
`bigint` cannot be sent. Any object with `encode(value): Uint8Array` and `decode(bytes)` can be the codec, such as
|
|
230
|
+
`StringCodec()` for text payloads.
|
|
231
|
+
|
|
232
|
+
## 7. Large payloads
|
|
233
|
+
|
|
234
|
+
A NATS message cannot be larger than the `max_payload` of the server, 1 MiB by default. The requests and the responses
|
|
235
|
+
of 90% of that size or more are stored in a JetStream object store, and the message only carries a reference to them;
|
|
236
|
+
the receiver reads the object, then deletes it. The callbacks, the actions and `invoke()` always see the whole value.
|
|
237
|
+
|
|
238
|
+
- This channel is on by default (`bulk: true`) and needs JetStream. Without JetStream, the component prints a warning
|
|
239
|
+
and starts without it: then only the payloads larger than `max_payload` fail.
|
|
240
|
+
- The applications exchanging large payloads must use the same object store: set the same `bulkBucket` on all of them.
|
|
241
|
+
By default it is named after the `name` option (`lkt-bulk-<name>`, or `lkt-bulk-default`), so two applications with
|
|
242
|
+
different names cannot read each other's objects.
|
|
243
|
+
- The objects nobody read expire after `bulkTTL` (5 minutes by default). `bulkReplicas` sets the number of copies in a
|
|
244
|
+
cluster. Both apply when the store is created.
|
|
245
|
+
- Published messages and events are never stored: larger than `max_payload`, they fail with the NATS error
|
|
246
|
+
`MAX_PAYLOAD_EXCEEDED`.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Lakutata Native Addons
|
|
2
|
+
|
|
3
|
+
A native addon is a library written in C++ or Rust that Node.js loads as a `.node` file: for drivers and devices,
|
|
4
|
+
codecs, system APIs, or code that must run fast. `lakutata add --native` creates an addon package in a workspace,
|
|
5
|
+
ready to build and test; `lakutata native` links libraries to it; and `lakutata package` ships it with the application.
|
|
6
|
+
The addons use Node-API, so one build runs in all the supported versions of Node.js and in Electron.
|
|
7
|
+
|
|
8
|
+
## 1. Creating an addon
|
|
9
|
+
|
|
10
|
+
In a workspace ([Workspaces](Workspaces.md)):
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# One of:
|
|
14
|
+
lakutata add @acme/capture --native --to app # C++, built by node-gyp
|
|
15
|
+
lakutata add @acme/capture --native cmake --to app # C++, built by cmake-js
|
|
16
|
+
lakutata add @acme/capture --native rust --to app # Rust, built by napi-rs (cargo)
|
|
17
|
+
# Then:
|
|
18
|
+
npm install # links the package and builds the addon
|
|
19
|
+
npm run rebuild -w @acme/capture
|
|
20
|
+
npm test -w @acme/capture
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Build system | Its files | Building it needs |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `gyp` (the default) | `binding.gyp`, `cpp/binding.cc` | Python and a C++ toolchain: the Xcode command line tools on macOS, build-essential on Linux, the Visual Studio Build Tools on Windows |
|
|
26
|
+
| `cmake` | `CMakeLists.txt`, `cpp/binding.cc` | CMake and a C++ toolchain |
|
|
27
|
+
| `rust` | `Cargo.toml`, `build.rs`, `rust/lib.rs` | Rust (rustup) |
|
|
28
|
+
|
|
29
|
+
Every addon package also has `src/index.ts`, which loads the addon and gives it a typed API, a test
|
|
30
|
+
(`src/tests/index.spec.ts`), a `tsconfig.json` that compiles `src` into `dist`, and a `README.md`. The C++ addon:
|
|
31
|
+
|
|
32
|
+
```cpp
|
|
33
|
+
#include <napi.h>
|
|
34
|
+
|
|
35
|
+
static Napi::Value Greet(const Napi::CallbackInfo &info) {
|
|
36
|
+
Napi::Env env = info.Env();
|
|
37
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
38
|
+
throw Napi::TypeError::New(env, "The name must be a string");
|
|
39
|
+
}
|
|
40
|
+
return Napi::String::New(env, "Hello, " + info[0].As<Napi::String>().Utf8Value() + " (native)");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
44
|
+
exports.Set("greet", Napi::Function::New(env, Greet));
|
|
45
|
+
return exports;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
NODE_API_MODULE(capture, Init)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The Rust addon:
|
|
52
|
+
|
|
53
|
+
```rust
|
|
54
|
+
use napi_derive::napi;
|
|
55
|
+
|
|
56
|
+
/// Greet someone: greet(name)
|
|
57
|
+
#[napi]
|
|
58
|
+
pub fn greet(name: String) -> String {
|
|
59
|
+
format!("Hello, {} (native)", name)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
And `src/index.ts`, the same for all three:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import {createRequire} from 'node:module'
|
|
67
|
+
import path from 'node:path'
|
|
68
|
+
import {fileURLToPath} from 'node:url'
|
|
69
|
+
|
|
70
|
+
type Binding = {
|
|
71
|
+
greet(name: string): string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The addon built into build/Release, loaded from the directory of the package */
|
|
75
|
+
const binding: Binding = createRequire(import.meta.url)('node-gyp-build')(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'))
|
|
76
|
+
|
|
77
|
+
export function greet(name: string): string {
|
|
78
|
+
return binding.greet(name)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Script | What it does |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `install` | Builds the addon when the package is installed (`npm install`) |
|
|
85
|
+
| `build:native` | Builds the addon into `build/Release/<name>.node` |
|
|
86
|
+
| `build` | Compiles the TypeScript into `dist` |
|
|
87
|
+
| `rebuild` | `clean`, then `build:native` and `build` |
|
|
88
|
+
| `test` | Runs the tests (`node --test`) |
|
|
89
|
+
| `clean`, `clean:native` | Remove `dist`, or the build of the addon (`build`, and `target` for Rust) |
|
|
90
|
+
|
|
91
|
+
- **Errors**: a C++ exception of node-addon-api reaches JavaScript as an error of the same kind
|
|
92
|
+
(`Napi::TypeError` becomes a `TypeError`). In Rust, an argument of the wrong type is refused by napi-rs with an
|
|
93
|
+
`Error` whose `code` says why (`StringExpected`).
|
|
94
|
+
- **macOS**: the C++ addons are built for the Macs with Intel and with Apple silicon in one file; a Rust addon is built
|
|
95
|
+
for the architecture of the machine (`rustup target add` adds the other ones).
|
|
96
|
+
- **Node-API**: the addons are built for Node-API version 8, which every supported version of Node.js and of Electron
|
|
97
|
+
provides, so they are not rebuilt for each of them.
|
|
98
|
+
- The package is `"private": true`, like every package that `lakutata add` creates: an addon is usually shipped with
|
|
99
|
+
its application rather than published.
|
|
100
|
+
|
|
101
|
+
## 2. Linking libraries
|
|
102
|
+
|
|
103
|
+
`lakutata native <link|include|define> <value>`, run in the directory of the addon, changes its build file
|
|
104
|
+
(`binding.gyp`, `CMakeLists.txt`, or for Rust `.cargo/config.toml`). The file is edited in place, its comments and its
|
|
105
|
+
layout kept, and the sources of the addon are not touched. Running the same change again changes nothing.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
lakutata native link z # a library of the system (-lz)
|
|
109
|
+
lakutata native link ws2_32 --platform win # a library of the system, on Windows only
|
|
110
|
+
lakutata native link zlib --pkg-config # a library found by pkg-config (not on Windows)
|
|
111
|
+
lakutata native link CoreFoundation --framework # a framework of macOS
|
|
112
|
+
lakutata native link deps/lib/libfoo.a # a static library of the package, linked into the addon
|
|
113
|
+
lakutata native link deps/lib/libbar.dylib # a dynamic library of the package, shipped with the addon
|
|
114
|
+
lakutata native include deps/include # a directory of headers (C++)
|
|
115
|
+
lakutata native define TRACE_LEVEL=3 # a definition of the preprocessor (C++)
|
|
116
|
+
lakutata native link libc --crate # a crate (Rust: cargo add)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
✔ Linked deps/lib/libbar.dylib (gyp): binding.gyp
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- **A file or a system library**: a value with a `/`, or ending in `.a`, `.lib`, `.so`, `.dylib` or `.dll`, is a file of
|
|
124
|
+
the package; any other value is the name of a library of the system. The files must be inside the package, since they
|
|
125
|
+
are shipped with it; a directory of headers is one of the package, or an absolute directory of the system.
|
|
126
|
+
- **The platform** of a change: a library file gives its own (`.dylib` macOS, `.so` Linux, `.dll` and `.lib` Windows,
|
|
127
|
+
`.a` every platform but Windows), `--framework` means macOS, `--pkg-config` every platform but Windows, and
|
|
128
|
+
`--platform mac|linux|win` chooses one. A change for a platform other than the machine's is noted
|
|
129
|
+
(`! The change is for win: it is built and tried there`).
|
|
130
|
+
- **Dynamic libraries** are copied next to the addon when it is built, and the addon finds them there at run time
|
|
131
|
+
through its rpath (`@loader_path` on macOS, `$ORIGIN` on Linux). On macOS, the install name of the library must be
|
|
132
|
+
`@rpath/libbar.dylib` (`install_name_tool -id @rpath/libbar.dylib deps/lib/libbar.dylib`): the command tells you when
|
|
133
|
+
it is not. On Windows, a DLL is linked through its import library, which must be next to it (`deps/lib/bar.dll` and
|
|
134
|
+
`deps/lib/bar.lib`).
|
|
135
|
+
- **Rust addons** link their native libraries through `build.rs`, which reads them from `.cargo/config.toml` (where
|
|
136
|
+
`lakutata native link` writes them); `include`, `define` and `--pkg-config` are refused, since Rust uses crates for
|
|
137
|
+
these (`--crate` adds one with `cargo add`, which looks it up in the registry of crates).
|
|
138
|
+
- **Several targets**: when the build file declares several, `--target <name>` chooses the one changed.
|
|
139
|
+
|
|
140
|
+
What each change writes, by build system:
|
|
141
|
+
|
|
142
|
+
| | gyp | CMake | Rust |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| File | `binding.gyp`, or the `.gypi` it includes that declares the target | `CMakeLists.txt` | `.cargo/config.toml` (`[env]`), read by `build.rs` |
|
|
145
|
+
| Library | `libraries` | `target_link_libraries` | `rustc-link-lib` |
|
|
146
|
+
| pkg-config | `<!@(pkg-config ...)` | `pkg_check_modules(... IMPORTED_TARGET ...)` | the `-sys` crates |
|
|
147
|
+
| Framework | `xcode_settings.OTHER_LDFLAGS` | `-framework` in `if(APPLE)` | `rustc-link-lib=framework=` |
|
|
148
|
+
| Dynamic library | `copies`, the rpath | `INSTALL_RPATH` (`BUILD_WITH_INSTALL_RPATH`), a `POST_BUILD` copy | `rustc-cdylib-link-arg`, a copy in `build:native` |
|
|
149
|
+
| Headers, definitions | `include_dirs`, `defines` | `target_include_directories`, `target_compile_definitions` | none (crates) |
|
|
150
|
+
| Platform | `conditions` (`OS=="mac"`, `OS!="win"`) | `if(APPLE)`, `if(WIN32)`, `if(NOT WIN32)`, `if(CMAKE_SYSTEM_NAME STREQUAL "Linux")` | `LAKUTATA_NATIVE_MACOS`, `_LINUX`, `_WINDOWS`, `_UNIX` |
|
|
151
|
+
|
|
152
|
+
For example, `link deps/lib/libbar.dylib` writes the rpath, the library and its copy into the macOS condition of
|
|
153
|
+
`binding.gyp`:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
['OS=="mac"', {
|
|
157
|
+
'xcode_settings': {
|
|
158
|
+
'MACOSX_DEPLOYMENT_TARGET': '11.0',
|
|
159
|
+
'OTHER_CFLAGS': ['-arch x86_64', '-arch arm64'],
|
|
160
|
+
'OTHER_LDFLAGS': [
|
|
161
|
+
'-arch x86_64', '-arch arm64',
|
|
162
|
+
'-Wl,-rpath,@loader_path',
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
'libraries': ['<(module_root_dir)/deps/lib/libbar.dylib'],
|
|
166
|
+
'copies': [
|
|
167
|
+
{
|
|
168
|
+
'destination': '<(PRODUCT_DIR)',
|
|
169
|
+
'files': ['<(module_root_dir)/deps/lib/libbar.dylib']
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
}]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
(The comments of the generated file are left out here.)
|
|
176
|
+
|
|
177
|
+
## 3. Other build options
|
|
178
|
+
|
|
179
|
+
The comments of the generated `binding.gyp` and `CMakeLists.txt` show the forms that `lakutata native` does not write:
|
|
180
|
+
|
|
181
|
+
- the standard of C++ (`binding.gyp`: `CLANG_CXX_LANGUAGE_STANDARD` on macOS, `cflags_cc` on Linux, `/std:` on Windows;
|
|
182
|
+
`CMakeLists.txt` sets C++20, which the headers of Node.js need);
|
|
183
|
+
- the options of one platform, in the `conditions` of `binding.gyp`;
|
|
184
|
+
- in `CMakeLists.txt`, a package of CMake (`find_package(ZLIB REQUIRED)`) or a library built from its own sources
|
|
185
|
+
(`add_subdirectory(deps/foo)`).
|
|
186
|
+
|
|
187
|
+
A library built by its own Makefile is built before the addon by a script of the package:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"scripts": {
|
|
192
|
+
"build:deps": "make -C deps/foo",
|
|
193
|
+
"build:native": "npm run build:deps && node-gyp rebuild"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 4. Shipping an addon
|
|
199
|
+
|
|
200
|
+
The application depends on the addon package, and imports it as any package (`import {greet} from '@acme/capture'`).
|
|
201
|
+
|
|
202
|
+
- **Bundles and executables**: `lakutata package bundle` leaves the addon packages out of the bundle and writes them,
|
|
203
|
+
with their files (their JavaScript, the addon, its dynamic libraries) but without their sources and what built them,
|
|
204
|
+
into `dist/<name>.native/`, which is shipped with the bundle; an executable embeds them, or holds them next to it in
|
|
205
|
+
the directory layout ([Packaging](Packaging.md)). The addon is built for the platform of the machine that packs the
|
|
206
|
+
application, so pack the application of each platform on that platform (a CI matrix, or a container of the
|
|
207
|
+
architecture for Linux).
|
|
208
|
+
- **Electron**: the addons are copied as they are installed. The addons that `lakutata add` creates run in Electron
|
|
209
|
+
without being rebuilt, as do all the addons built on Node-API; an addon built on the V8 API of Node.js must be
|
|
210
|
+
installed in its build for Electron before packing (`electron-rebuild`).
|
|
211
|
+
- **Bun**: Bun loads the addons of Node-API; an executable of Bun warns of the addons built on the V8 API of Node.js.
|
|
212
|
+
- **Alpine (musl)**: the executables of Node.js do not load native addons there: make them with Bun.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Lakutata Objects and Lifetimes
|
|
2
|
+
|
|
3
|
+
Every object of an application (components, providers, controllers) is created by an IoC container; you never call
|
|
4
|
+
`new`. This page covers the classes of these objects, their lifetimes and scopes, how they are created and destroyed,
|
|
5
|
+
and how to use them without an application. They come from the package `@lakutata/core`, which `lakutata`,
|
|
6
|
+
`lakutata/decorator/di`, `lakutata/decorator/dto` and `lakutata/decorator/asst` re-export; its README and the type
|
|
7
|
+
declarations of each export give the details.
|
|
8
|
+
|
|
9
|
+
## 1. The classes
|
|
10
|
+
|
|
11
|
+
| Class | Role | Default lifetime |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `BaseObject` | The root class: asynchronous construction, `init()`/`destroy()`, `getObject()` | `@Transient` |
|
|
14
|
+
| `Provider` | A unit of work that lives in a scope such as a request (a repository, a password hasher) | `@Scoped` |
|
|
15
|
+
| `Component` | A long-lived service shared by the whole application (a logger, a database, an entrypoint); also an event emitter | `@Singleton` (locked) |
|
|
16
|
+
| `Application` | The application: it declares the components, providers and objects, and is created by `Application.run()` | |
|
|
17
|
+
| `Controller` (`lakutata/com/entrypoint`) | A provider that handles the requests of an entrypoint | `@Scoped` (locked) |
|
|
18
|
+
|
|
19
|
+
## 2. Lifetimes
|
|
20
|
+
|
|
21
|
+
| Decorator (`lakutata/decorator/di`) | One instance… |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `@Singleton()` | per container that registers the class, shared by everything that uses it, including its scopes |
|
|
24
|
+
| `@Scoped()` | per scope (`container.createScope()`; each request of an entrypoint is a scope, the root container is one too) |
|
|
25
|
+
| `@Transient()` | per resolution, never cached |
|
|
26
|
+
|
|
27
|
+
Subclasses inherit the lifetime. `Component` locks its lifetime: a subclass that declares a different one throws a
|
|
28
|
+
`LifetimeLockedException`. A `Provider` is `@Scoped` by default but may be declared `@Transient` or `@Singleton` (as in
|
|
29
|
+
2.x, one instance shared by all scopes, like the `ServiceProxy` of `@lakutata/nats`). A long-lived object that owns
|
|
30
|
+
resources (connections, subscriptions, servers) is better written as a `Component`. When a singleton injects a scoped
|
|
31
|
+
object, it keeps the instance from the scope that created it. `@Lifetime('SINGLETON' | 'SCOPED' | 'TRANSIENT', lock)` is
|
|
32
|
+
the long form of the three decorators.
|
|
33
|
+
|
|
34
|
+
## 3. Scopes
|
|
35
|
+
|
|
36
|
+
A scope is a child container that holds its own instances of the scoped objects and shares the singletons and
|
|
37
|
+
registrations of its parent. The entrypoint creates a scope for each request: the request's controller and the providers
|
|
38
|
+
it injects are created in it and destroyed when the request ends. A component can create its own scope with its
|
|
39
|
+
protected `createScope()`.
|
|
40
|
+
|
|
41
|
+
## 4. Creation and teardown
|
|
42
|
+
|
|
43
|
+
The container creates objects asynchronously: the promise returned by `getObject()` (or resolved for `@Inject`) settles
|
|
44
|
+
once the object is ready. Creation goes through these steps:
|
|
45
|
+
|
|
46
|
+
1. its `@Inject` properties are resolved;
|
|
47
|
+
2. its `@Configurable` properties receive their configured values, validated (and converted) by their schemas; a
|
|
48
|
+
property that is not configured keeps its initializer, or the default of its schema when it has none;
|
|
49
|
+
3. its `init()` runs (an error thrown here fails the creation);
|
|
50
|
+
4. its `startup()` runs. By then other objects can already obtain it, but they wait for `startup()` to finish (and fail
|
|
51
|
+
with its error if it throws). The exception is anything requested from inside `startup()`, directly or through the
|
|
52
|
+
objects it creates: those get the object at once, which is how an object requested there can inject it back (an
|
|
53
|
+
entrypoint starts its adapters in `startup()`, and their hooks obtain an authenticator that injects the entrypoint).
|
|
54
|
+
The method is named `startup` rather than `start` so that classes with a `start()` method of their own, such as the
|
|
55
|
+
containers of `@lakutata/docker`, are not started by the framework;
|
|
56
|
+
5. once the application has launched (and the object has started), its `launched()` runs; it is not awaited, and its
|
|
57
|
+
errors are logged.
|
|
58
|
+
|
|
59
|
+
An object is torn down by `dispose()`, or along with the object, scope or application that created it. Teardown waits
|
|
60
|
+
for `startup()` to end, then runs `beforeDestroy()`, destroys the objects it created, and finally runs `destroy()`. Each
|
|
61
|
+
object owns a container, so whatever it creates with `this.getObject()` is destroyed with it.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import {Application, Component, type ILogger} from 'lakutata'
|
|
65
|
+
import {Inject} from 'lakutata/decorator/di'
|
|
66
|
+
|
|
67
|
+
class Poller extends Component {
|
|
68
|
+
@Inject('log')
|
|
69
|
+
protected readonly log: ILogger
|
|
70
|
+
|
|
71
|
+
protected timer: NodeJS.Timeout
|
|
72
|
+
|
|
73
|
+
//Its dependencies are injected and its options set
|
|
74
|
+
protected async init(): Promise<void> {
|
|
75
|
+
this.log.info('init')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//Other objects can already get it, but they wait until this ends
|
|
79
|
+
protected async startup(): Promise<void> {
|
|
80
|
+
this.timer = setInterval((): void => this.log.info('polling'), 10000)
|
|
81
|
+
this.log.info('startup')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//The application has launched (not awaited)
|
|
85
|
+
protected async launched(): Promise<void> {
|
|
86
|
+
this.log.info('launched')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//Teardown begins: the objects it created still exist
|
|
90
|
+
protected async beforeDestroy(): Promise<void> {
|
|
91
|
+
this.log.info('beforeDestroy')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//Teardown ends: the objects it created are destroyed
|
|
95
|
+
protected async destroy(): Promise<void> {
|
|
96
|
+
clearInterval(this.timer)
|
|
97
|
+
this.log.info('destroy')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Application
|
|
102
|
+
.run(() => ({
|
|
103
|
+
id: 'poller.app',
|
|
104
|
+
name: 'Poller',
|
|
105
|
+
components: {
|
|
106
|
+
poller: {class: Poller}
|
|
107
|
+
},
|
|
108
|
+
bootstrap: ['poller']
|
|
109
|
+
}))
|
|
110
|
+
.onLaunched((app: Application, log: ILogger): void => log.info('onLaunched'))
|
|
111
|
+
.onDone((app: Application, log: ILogger): void => log.info('onDone'))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It logs `init`, `startup`, `launched` and `onLaunched`. On Ctrl+C, it logs `onDone`, `beforeDestroy` and `destroy`,
|
|
115
|
+
then the process exits.
|
|
116
|
+
|
|
117
|
+
## 5. A container without an application
|
|
118
|
+
|
|
119
|
+
Tests, scripts and libraries can use these objects in a bare container, without an application:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import {Component, Container, DTO} from 'lakutata'
|
|
123
|
+
import {Configurable} from 'lakutata/decorator/di'
|
|
124
|
+
|
|
125
|
+
class Mailer extends Component {
|
|
126
|
+
@Configurable(DTO.String().required())
|
|
127
|
+
protected readonly host: string
|
|
128
|
+
|
|
129
|
+
public async send(to: string, text: string): Promise<string> {
|
|
130
|
+
return `${this.host} -> ${to}: ${text}`
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const container: Container = new Container()
|
|
135
|
+
//Created with its options; its init() is awaited
|
|
136
|
+
const mailer: Mailer = await container.set(Mailer, {host: 'localhost'})
|
|
137
|
+
await mailer.send('someone@example.com', 'Hello')
|
|
138
|
+
//Destroys the objects it created
|
|
139
|
+
await container.destroy()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`container.register()` and `registerNamed()` register a class without creating it, `get()` resolves an object, `build()`
|
|
143
|
+
always creates a new instance whatever the lifetime, and `createScope()` creates a scope.
|