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.
Files changed (117) hide show
  1. package/README.md +50 -30
  2. package/README.zh-CN.md +130 -0
  3. package/dist/cjs/exports/QueueComponent.d.ts +1 -0
  4. package/dist/cjs/exports/QueueComponent.js +20 -0
  5. package/dist/esm/exports/QueueComponent.js +1 -0
  6. package/dist/types/exports/QueueComponent.d.ts +1 -0
  7. package/doc/en/Application.md +107 -0
  8. package/doc/en/CLI.md +274 -0
  9. package/doc/en/Cache.md +379 -0
  10. package/doc/en/Command.md +110 -0
  11. package/doc/en/ContainerdIntegration.md +17 -16
  12. package/doc/en/Controllers.md +256 -0
  13. package/doc/en/DBus.md +291 -0
  14. package/doc/en/DTOValidation.md +72 -43
  15. package/doc/en/Database.md +188 -0
  16. package/doc/en/DatabaseDrivers.md +287 -0
  17. package/doc/en/Decorator.md +52 -40
  18. package/doc/en/DockerIntegration.md +7 -6
  19. package/doc/en/Entities.md +240 -0
  20. package/doc/en/Entrypoint.md +216 -0
  21. package/doc/en/Exceptions.md +174 -0
  22. package/doc/en/GRPC.md +251 -0
  23. package/doc/en/GettingStarted.md +165 -0
  24. package/doc/en/HTTP.md +507 -0
  25. package/doc/en/HelperFunctions.md +11 -11
  26. package/doc/en/I18n.md +119 -115
  27. package/doc/en/Injection.md +144 -0
  28. package/doc/en/Installers.md +280 -0
  29. package/doc/en/Logger.md +206 -0
  30. package/doc/en/MCP.md +293 -0
  31. package/doc/en/Middlewares.md +213 -0
  32. package/doc/en/Migrations.md +202 -0
  33. package/doc/en/MongoEmbedded.md +149 -0
  34. package/doc/en/Monitor.md +164 -0
  35. package/doc/en/Mutex.md +27 -25
  36. package/doc/en/NATS.md +246 -0
  37. package/doc/en/NativeAddons.md +212 -0
  38. package/doc/en/Objects.md +143 -0
  39. package/doc/en/Overview.md +62 -85
  40. package/doc/en/Packaging.md +193 -0
  41. package/doc/en/ProjectStructure.md +124 -0
  42. package/doc/en/Queue.md +269 -0
  43. package/doc/en/Schedules.md +217 -0
  44. package/doc/en/Security.md +10 -9
  45. package/doc/en/ServiceCalls.md +504 -0
  46. package/doc/en/Time.md +19 -20
  47. package/doc/en/UpgradingFrom1.md +435 -0
  48. package/doc/en/UpgradingFrom2.md +315 -0
  49. package/doc/en/Workspaces.md +189 -0
  50. package/doc/en/examples/BackgroundJobs.md +677 -0
  51. package/doc/en/examples/CommandLineTool.md +664 -0
  52. package/doc/en/examples/DesktopApp.md +809 -0
  53. package/doc/en/examples/DeviceService.md +607 -0
  54. package/doc/en/examples/MCPServer.md +653 -0
  55. package/doc/en/examples/README.md +16 -0
  56. package/doc/en/examples/RestAPI.md +490 -0
  57. package/doc/en/examples/ServiceCalls.md +829 -0
  58. package/doc/en/examples/WebApp.md +568 -0
  59. package/doc/zh/Application.md +75 -0
  60. package/doc/zh/CLI.md +250 -0
  61. package/doc/zh/Cache.md +328 -0
  62. package/doc/zh/Command.md +78 -0
  63. package/doc/zh/ContainerdIntegration.md +9 -9
  64. package/doc/zh/Controllers.md +228 -0
  65. package/doc/zh/DBus.md +257 -0
  66. package/doc/zh/DTOValidation.md +49 -19
  67. package/doc/zh/Database.md +175 -0
  68. package/doc/zh/DatabaseDrivers.md +261 -0
  69. package/doc/zh/Decorator.md +25 -18
  70. package/doc/zh/DockerIntegration.md +6 -6
  71. package/doc/zh/Entities.md +224 -0
  72. package/doc/zh/Entrypoint.md +193 -0
  73. package/doc/zh/Exceptions.md +158 -0
  74. package/doc/zh/GRPC.md +226 -0
  75. package/doc/zh/GettingStarted.md +148 -0
  76. package/doc/zh/HTTP.md +445 -0
  77. package/doc/zh/HelperFunctions.md +2 -2
  78. package/doc/zh/I18n.md +26 -26
  79. package/doc/zh/Injection.md +129 -0
  80. package/doc/zh/Installers.md +232 -0
  81. package/doc/zh/Logger.md +188 -0
  82. package/doc/zh/MCP.md +265 -0
  83. package/doc/zh/Middlewares.md +198 -0
  84. package/doc/zh/Migrations.md +178 -0
  85. package/doc/zh/MongoEmbedded.md +118 -0
  86. package/doc/zh/Monitor.md +145 -0
  87. package/doc/zh/Mutex.md +10 -10
  88. package/doc/zh/NATS.md +207 -0
  89. package/doc/zh/NativeAddons.md +180 -0
  90. package/doc/zh/Objects.md +119 -0
  91. package/doc/zh/Overview.md +46 -71
  92. package/doc/zh/Packaging.md +154 -0
  93. package/doc/zh/ProjectStructure.md +102 -0
  94. package/doc/zh/Queue.md +214 -0
  95. package/doc/zh/Schedules.md +193 -0
  96. package/doc/zh/Security.md +6 -6
  97. package/doc/zh/ServiceCalls.md +459 -0
  98. package/doc/zh/Time.md +2 -2
  99. package/doc/zh/UpgradingFrom1.md +347 -0
  100. package/doc/zh/UpgradingFrom2.md +230 -0
  101. package/doc/zh/Workspaces.md +150 -0
  102. package/doc/zh/examples/BackgroundJobs.md +618 -0
  103. package/doc/zh/examples/CommandLineTool.md +618 -0
  104. package/doc/zh/examples/DesktopApp.md +748 -0
  105. package/doc/zh/examples/DeviceService.md +546 -0
  106. package/doc/zh/examples/MCPServer.md +604 -0
  107. package/doc/zh/examples/README.md +14 -0
  108. package/doc/zh/examples/RestAPI.md +458 -0
  109. package/doc/zh/examples/ServiceCalls.md +765 -0
  110. package/doc/zh/examples/WebApp.md +520 -0
  111. package/package.json +46 -17
  112. package/doc/en/ActionPattern.md +0 -408
  113. package/doc/en/Core.md +0 -312
  114. package/doc/en/ORMIntegration.md +0 -341
  115. package/doc/zh/ActionPattern.md +0 -334
  116. package/doc/zh/Core.md +0 -256
  117. 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.