lakutata 3.1.3 → 3.1.5

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 (55) hide show
  1. package/README.md +10 -10
  2. package/README.zh-CN.md +5 -5
  3. package/dist/cjs/exports/NwjsAdapter.d.ts +1 -0
  4. package/dist/cjs/exports/NwjsAdapter.js +20 -0
  5. package/dist/esm/exports/NwjsAdapter.js +1 -0
  6. package/dist/types/exports/NwjsAdapter.d.ts +1 -0
  7. package/doc/en/Application.md +1 -1
  8. package/doc/en/Cache.md +2 -1
  9. package/doc/en/Command.md +10 -4
  10. package/doc/en/ContainerdIntegration.md +1 -1
  11. package/doc/en/DatabaseDrivers.md +3 -2
  12. package/doc/en/Entrypoint.md +3 -2
  13. package/doc/en/Exceptions.md +1 -1
  14. package/doc/en/FFI.md +1 -1
  15. package/doc/en/GRPC.md +9 -3
  16. package/doc/en/GettingStarted.md +15 -2
  17. package/doc/en/HTTP.md +35 -0
  18. package/doc/en/I18n.md +1 -1
  19. package/doc/en/Installers.md +34 -6
  20. package/doc/en/MCP.md +4 -2
  21. package/doc/en/Math.md +1 -1
  22. package/doc/en/NativeAddons.md +9 -2
  23. package/doc/en/Netif.md +261 -0
  24. package/doc/en/Overview.md +10 -8
  25. package/doc/en/Packaging.md +4 -1
  26. package/doc/en/ProjectStructure.md +8 -3
  27. package/doc/en/ServiceCalls.md +69 -6
  28. package/doc/en/UpgradingFrom1.md +2 -2
  29. package/doc/en/UpgradingFrom2.md +4 -4
  30. package/doc/en/WebRTC.md +4 -4
  31. package/doc/zh/Application.md +1 -1
  32. package/doc/zh/Cache.md +1 -1
  33. package/doc/zh/Command.md +7 -3
  34. package/doc/zh/ContainerdIntegration.md +1 -1
  35. package/doc/zh/DatabaseDrivers.md +1 -1
  36. package/doc/zh/Entrypoint.md +2 -1
  37. package/doc/zh/Exceptions.md +1 -1
  38. package/doc/zh/FFI.md +1 -1
  39. package/doc/zh/GRPC.md +6 -3
  40. package/doc/zh/GettingStarted.md +11 -2
  41. package/doc/zh/HTTP.md +30 -0
  42. package/doc/zh/I18n.md +1 -1
  43. package/doc/zh/Installers.md +16 -5
  44. package/doc/zh/MCP.md +3 -2
  45. package/doc/zh/Math.md +1 -1
  46. package/doc/zh/NativeAddons.md +3 -2
  47. package/doc/zh/Netif.md +207 -0
  48. package/doc/zh/Overview.md +5 -4
  49. package/doc/zh/Packaging.md +4 -1
  50. package/doc/zh/ProjectStructure.md +4 -2
  51. package/doc/zh/ServiceCalls.md +55 -5
  52. package/doc/zh/UpgradingFrom1.md +2 -2
  53. package/doc/zh/UpgradingFrom2.md +3 -3
  54. package/doc/zh/WebRTC.md +1 -1
  55. package/package.json +29 -15
@@ -0,0 +1,261 @@
1
+ # Lakutata Netif
2
+
3
+ The package `@lakutata/netif` lets a server of Node.js listen on a network interface instead of an address. A device
4
+ with several interfaces often serves its management pages on one of them only: the address of that port comes from DHCP
5
+ and changes, or is not there yet when the device starts, and listening on `0.0.0.0` would expose the pages on every
6
+ interface. With the package the server listens on "eth1", whatever its addresses, and keeps listening through their
7
+ changes and through the interface unplugged and plugged back. It is not part of `lakutata`, and it needs no
8
+ application. What it is like:
9
+
10
+ - **Any server**: `http`, `https`, `http2`, `tls`, Express and Fastify take the connections of the interface as their
11
+ own; UDP sockets too, for the services that work on one interface (DHCP, mDNS, SSDP).
12
+ - **The entrypoints of lakutata** take an `interface` option, which serves the controllers of an application on one
13
+ interface (section 5).
14
+ - **The system refuses the other interfaces where it can**: on Linux the sockets are bound to the interface, and a
15
+ connection that reaches its address through another interface is refused. On Windows and macOS the addresses of the
16
+ interface are listened on, and `enforced` tells whether the system refuses those connections.
17
+ - **The interfaces and their changes**: every interface of the machine with its state, and events when its addresses,
18
+ its link or its presence change, from the notifications of the system.
19
+ - **A native addon**, compiled when the package is installed, for Linux, macOS and Windows; it also runs in Bun and in
20
+ the main process of Electron.
21
+
22
+ The README of `@lakutata/netif` describes every function and its options; this guide goes through what the package is
23
+ for, with an example of each use.
24
+
25
+ ```bash
26
+ npm install @lakutata/netif
27
+ ```
28
+
29
+ The addon is compiled when the package is installed, so the machine needs Python 3 and a C++17 compiler (on Alpine Linux
30
+ the headers of the kernel too: `apk add python3 make g++ linux-headers`). The package manager has to run the install
31
+ script of the package: npm runs it, and may warn that the script is "not yet covered by allowScripts" (`npm
32
+ install-scripts approve @lakutata/netif` records that you allow it); Bun and pnpm run it only for the packages they are
33
+ told to trust (`trustedDependencies` in the package.json for Bun, `pnpm approve-builds` for pnpm). Its types refer to
34
+ the ones of `@lakutata/core`, a peer dependency that the package managers install with it; it is not loaded when the
35
+ package runs.
36
+
37
+ ## 1. Listening on an interface
38
+
39
+ `listenOn()` listens on an interface for a server. The interface is given by its name, its hardware address
40
+ (`{mac: '52:54:00:12:34:56'}`, the same whatever the system names it) or its index; on Windows the name is the one of
41
+ the network connection (`'以太网 2'`, `'Ethernet 2'`).
42
+
43
+ ```typescript
44
+ import {createServer} from 'node:http'
45
+ import express from 'express'
46
+ import {type InterfaceListener, listenOn} from '@lakutata/netif'
47
+
48
+ const app = express()
49
+ app.get('/', (request, response) => response.send(`served on ${request.socket.localAddress}`))
50
+
51
+ const listener: InterfaceListener = await listenOn('eth1', createServer(app), {port: 8080})
52
+ listener.on('error', error => console.error(error))
53
+ console.log(listener.port, listener.addresses)
54
+ ```
55
+
56
+ - The listener accepts the connections and gives them to the server, which handles them as its own. The server itself
57
+ does not listen: its `address()` is `null`, and `server.close()` does not stop the listener; use `listener.port`,
58
+ `listener.addresses` and `listener.close()`.
59
+ - The interface may not exist yet or have no address yet: `listenOn()` returns at once, and the interface is listened
60
+ on when it can be. When it is unplugged and plugged back, or when its addresses change, it is listened on again by
61
+ itself; the connections established are left to the server.
62
+ - The first listening throws its errors (the port taken); the later ones are `error` events, retried a little later.
63
+ Handle the `error` event of the listener, as for a `net.Server`.
64
+ - `family` chooses `'IPv4'`, `'IPv6'` or `'both'` (by default); `port: 0` lets the system choose a port, kept when the
65
+ interface is listened on again.
66
+
67
+ ## 2. Which connections are refused
68
+
69
+ A client of another network can send packets for the address of the management port through another interface of the
70
+ device, with a route of its own through it. Whether the system refuses such a connection is what `enforced` tells:
71
+
72
+ | | `enforced` |
73
+ |---|---|
74
+ | Linux | `true`: the sockets are bound to the interface (`SO_BINDTODEVICE`); before Linux 5.7 this needs the capability `CAP_NET_RAW`, without which the addresses are listened on and `enforced` is `false` |
75
+ | Windows | `true` while every other interface receives with the strong host model and does not forward, which is the default |
76
+ | macOS | `false` unless the system is set to refuse those packets (`net.inet.ip.check_interface` set to 2, and `net.inet6.ip6.check_interface` for IPv6) and does not forward |
77
+
78
+ By default the interface is listened on in the best way the system allows, never less than `listen(port, host)` on its
79
+ addresses; `reason` tells why there is no guarantee and how to get it. Where the management port is a boundary of
80
+ security, `strict: true` refuses to listen without the guarantee, and stops the listening while it is lost:
81
+
82
+ ```typescript
83
+ import {createServer} from 'node:http'
84
+ import {type InterfaceListener, NetIfNotEnforcedException, NetworkInterface} from '@lakutata/netif'
85
+
86
+ const management: NetworkInterface = new NetworkInterface({mac: '52:54:00:12:34:56'})
87
+ try {
88
+ const listener: InterfaceListener = await management.listen(createServer(), {port: 443, strict: true})
89
+ listener.on('error', error => console.error(error))
90
+ listener.on('enforced', enforced => console.log(enforced ? 'guaranteed again' : 'stopped: no longer guaranteed'))
91
+ } catch (error) {
92
+ if (error instanceof NetIfNotEnforcedException) console.error(error.message)
93
+ throw error
94
+ }
95
+ ```
96
+
97
+ ## 3. Following an interface
98
+
99
+ A `NetworkInterface` gives the state of an interface now (`present`, `running`, `addresses`…) and an event at each
100
+ change. The events come from the notifications of the system, not from polling.
101
+
102
+ ```typescript
103
+ import {type InterfaceAddress, listInterfaces, NetworkInterface} from '@lakutata/netif'
104
+
105
+ console.log(listInterfaces().map(entry => `${entry.name}: ${entry.running ? 'connected' : 'not connected'}`))
106
+
107
+ const wan: NetworkInterface = new NetworkInterface('eth0')
108
+ wan.on('change', ({added, removed}) => {
109
+ for (const address of added) console.log(`new address ${address.cidr}`)
110
+ for (const address of removed) console.log(`address gone ${address.cidr}`)
111
+ })
112
+ wan.on('down', () => console.log('the cable of eth0 is unplugged'))
113
+ wan.on('up', () => console.log('the cable of eth0 is plugged in'))
114
+ const address: InterfaceAddress = await wan.waitForAddress({family: 'IPv4', timeout: 60000})
115
+ console.log(`reachable at ${address.address}`)
116
+ ```
117
+
118
+ - `listInterfaces()` lists the interfaces without an address too, with their state; `os.networkInterfaces()` lists
119
+ only the ones with an address.
120
+ - An IPv6 address counts once it is usable: about a second after it is added, when its duplicate address detection is
121
+ over.
122
+ - `close()` stops following the interface (and closes its listeners).
123
+
124
+ ## 4. UDP on an interface
125
+
126
+ `createUDPSocket()` gives a UDP socket of the interface, with the methods and the events of a `dgram` socket. A device
127
+ that answers the discovery broadcasts of its management network only:
128
+
129
+ ```typescript
130
+ import {type InterfaceUDPSocket, NetworkInterface} from '@lakutata/netif'
131
+
132
+ const management: NetworkInterface = new NetworkInterface('eth1')
133
+ const socket: InterfaceUDPSocket = await management.createUDPSocket({port: 30303, family: 'IPv4'})
134
+ socket.setBroadcast(true)
135
+ socket.on('message', (message, remote) => {
136
+ const answer: string = `device at ${management.addresses[0]?.address}`
137
+ if (message.toString() === 'who is there') socket.send(answer, remote.port, remote.address)
138
+ })
139
+ socket.on('error', error => console.error(error))
140
+ ```
141
+
142
+ On Linux the socket receives what comes through the interface, its broadcasts and the multicast of the groups joined
143
+ with `addMembership()` included, and nothing of the other interfaces. On Windows and macOS a socket is bound to each
144
+ address of the interface; on macOS such sockets receive no broadcast and no multicast.
145
+
146
+ ## 5. In an application
147
+
148
+ The entrypoints of lakutata that listen on a port take an `interface` option: `expressHTTPEntrypoint()`,
149
+ `fastifyHTTPEntrypoint()`, `httpServiceEntrypoint()`, `socketIOServiceEntrypoint()`, `jsonRPCServiceEntrypoint()`,
150
+ `grpcServiceEntrypoint()` of `@lakutata/grpc` and `httpMCPEntrypoint()` of `@lakutata/mcp`. The actions of the
151
+ controllers are then served on that interface, whatever its addresses:
152
+
153
+ ```typescript
154
+ import {Application} from 'lakutata'
155
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
156
+ import {GET} from 'lakutata/decorator/ctrl'
157
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
158
+
159
+ class ManagementController extends Controller {
160
+ @GET('/status')
161
+ public async status(): Promise<{ ok: boolean }> {
162
+ return {ok: true}
163
+ }
164
+ }
165
+
166
+ Application.run(() => ({
167
+ id: 'device.app',
168
+ name: 'Device',
169
+ components: {
170
+ entrypoint: buildEntrypoints({
171
+ controllers: [ManagementController],
172
+ http: expressHTTPEntrypoint({port: 8080, interface: 'eth1'})
173
+ })
174
+ },
175
+ bootstrap: ['entrypoint']
176
+ }))
177
+ ```
178
+
179
+ - `family`, `strict` and `onError` go with `interface`. Without `onError`, the errors of listening again are written to
180
+ the log of the application. The `listening` callback of the adapter gets the listeners. [HTTP](HTTP.md), section 2,
181
+ describes these options.
182
+ - The application starts while the interface does not exist yet, and its actions can be reached once it appears.
183
+ - The entrypoints listen through `IInterfaceListening`, a contract of `@lakutata/core` that this package implements
184
+ and that they load by default. Another implementation is given in their `interfaceListening` option; the
185
+ `InterfaceListener` of the package is an `IInterfaceListener`.
186
+
187
+ A server that is not an entrypoint belongs to a component: it listens in `startup()` and stops in `destroy()`. The
188
+ interface and the port are the options of the component.
189
+
190
+ ```typescript
191
+ import {createServer} from 'node:http'
192
+ import express from 'express'
193
+ import {Application, Component, DTO, type ILogger} from 'lakutata'
194
+ import {Configurable, Inject} from 'lakutata/decorator/di'
195
+ import {type InterfaceListener, listenOn} from '@lakutata/netif'
196
+
197
+ class ManagementPages extends Component {
198
+ @Inject('log')
199
+ protected readonly log: ILogger
200
+
201
+ @Configurable(DTO.String().required())
202
+ protected readonly interfaceName: string
203
+
204
+ @Configurable(DTO.Number().port().default(8080))
205
+ protected readonly port: number
206
+
207
+ protected listener: InterfaceListener
208
+
209
+ protected async startup(): Promise<void> {
210
+ const app = express()
211
+ app.get('/', (request, response) => response.send('management'))
212
+ this.listener = await listenOn(this.interfaceName, createServer(app), {port: this.port})
213
+ this.listener.on('error', (error: Error): void => this.log.error(error.message))
214
+ this.listener.on('listening', (): void => this.log.info('listening on %o', this.listener.addresses))
215
+ this.log.info('enforced: %s', this.listener.enforced)
216
+ }
217
+
218
+ protected async destroy(): Promise<void> {
219
+ await this.listener?.close()
220
+ }
221
+ }
222
+
223
+ Application.run(() => ({
224
+ id: 'device.app',
225
+ name: 'Device',
226
+ components: {
227
+ management: {class: ManagementPages, interfaceName: 'eth1', port: 8080}
228
+ },
229
+ bootstrap: ['management']
230
+ }))
231
+ ```
232
+
233
+ ## 6. Systems, runtimes and packaging
234
+
235
+ - **Linux** binds the sockets to the interface: the system refuses the other interfaces, an address added is served at
236
+ once, and UDP gets the broadcasts and the multicast of the interface. Before Linux 5.7 give the process `CAP_NET_RAW`
237
+ (`setcap cap_net_raw+ep`, or `AmbientCapabilities=CAP_NET_RAW` in its systemd unit).
238
+ - **Windows** listens on the addresses of the interface; its strong host model refuses the other interfaces unless an
239
+ interface is set to the weak host model (`netsh interface ipv4 set interface "<name>" weakhostreceive=enabled`, which
240
+ some load balancers do) or forwards. `reason` names that interface.
241
+ - **macOS** listens on the addresses of the interface, without the guarantee unless the system is set to it. macOS 27
242
+ hides the hardware addresses from the processes (it gives `02:00:00:00:00:00`): give an interface by its name there.
243
+ - **Bun**: its `https` server answers the connections given to it without TLS, so `listen()` refuses it in Bun (a
244
+ Fastify instance created with `https` too); use `http2.createSecureServer({key, cert, allowHTTP1: true})` or
245
+ `tls.createServer()` there. Its servers do not close the idle keep-alive connections given to them, which the
246
+ entrypoints close themselves. A connection given to a server cannot be upgraded to a WebSocket (seen with Bun 1.4), so
247
+ the entrypoints refuse WebSocket on an interface in Bun.
248
+ - **Electron**: in the main process.
249
+ - The bundles, the executables and the installers made by `lakutata package` carry the compiled addon
250
+ (`build/Release/netif.node`): build them on each target platform, after installing the package there (see
251
+ [Packaging](Packaging.md) and [NativeAddons](NativeAddons.md)).
252
+
253
+ ## 7. Exceptions
254
+
255
+ | Exception | When |
256
+ |---|---|
257
+ | `NetIfNotFoundException` | An index no interface has, a hardware address several interfaces have |
258
+ | `NetIfNotEnforcedException` | `strict` where the system cannot refuse the other interfaces, or the guarantee lost (an `error` event) |
259
+ | `NetIfNotSupportedException` | What the system or the runtime cannot do: `bindSocket()` outside Linux, multicast on a socket bound to an address on macOS, an `https` server in Bun |
260
+ | `NetIfTimeoutException` | `waitForAddress()` past its `timeout` |
261
+ | `NetIfException` | The base of all of them; a refusal of the system has its code as `errno` (`'EADDRINUSE'`, `'EACCES'`…) |
@@ -1,10 +1,10 @@
1
1
  # Lakutata Overview
2
2
 
3
3
  **Lakutata** is an IoC application framework for Node.js and TypeScript. It gives web services, CLI tools, desktop
4
- (Electron) applications and embedded devices one programming model: an application is a tree of objects (components,
5
- providers, controllers) that a container creates. DTOs validate their options and inputs, and transports expose
6
- the controllers: HTTP (Express, Fastify), CLI (Commander), service RPC (HTTP, Socket.IO, JSON-RPC 2.0, gRPC, NATS,
7
- Electron IPC), schedules and message queues. One controller method can serve all of them at once.
4
+ (Electron, NW.js) applications and embedded devices one programming model: an application is a tree of objects
5
+ (components, providers, controllers) that a container creates. DTOs validate their options and inputs, and transports
6
+ expose the controllers: HTTP (Express, Fastify), CLI (Commander), service RPC (HTTP, Socket.IO, JSON-RPC 2.0, gRPC,
7
+ NATS, Electron IPC, NW.js), schedules and message queues. One controller method can serve all of them at once.
8
8
 
9
9
  The package `lakutata` re-exports the `@lakutata/*` packages under its subpaths (see the table below) and presets two
10
10
  components in every application: `log` (the `Logger`, which writes pino-compatible JSON lines) and `entrypoint` (the
@@ -30,7 +30,7 @@ tool, a web and a desktop application, calls between services, background jobs,
30
30
  | `lakutata/decorator/ctrl` | `@HTTPAction`, `@GET`…`@OPTIONS`, `@CLIAction`, `@ServiceAction`, `@Schedule`, `@Prefix`, `@UseMiddlewares` |
31
31
  | `lakutata/adapter/express`, `lakutata/adapter/fastify` | The HTTP transports |
32
32
  | `lakutata/adapter/commander` | The CLI transport |
33
- | `lakutata/adapter/socket.io`, `lakutata/adapter/jsonrpc`, `lakutata/adapter/electron` | The service transports |
33
+ | `lakutata/adapter/socket.io`, `lakutata/adapter/jsonrpc`, `lakutata/adapter/electron`, `lakutata/adapter/nwjs` | The service transports |
34
34
  | `lakutata/com/logger` | The `Logger` component, preset as `log` (pino-compatible JSON lines) |
35
35
  | `lakutata/orm`, `lakutata/decorator/orm` | TypeORM and its decorators |
36
36
  | `lakutata/com/database`, `lakutata/provider/database` | The `Database` component and provider |
@@ -48,8 +48,9 @@ Not part of this package: `@lakutata/cli` (the `lakutata` command for creating p
48
48
  applications, see [Command](Command.md)), `@lakutata/grpc` (a gRPC client, server and service transport),
49
49
  `@lakutata/nats` (a NATS component and service transport), `@lakutata/dbus` (D-Bus), `@lakutata/mcp` (the Model
50
50
  Context Protocol), `@lakutata/webrtc` (WebRTC: the signaling, a TURN server, a media server, peer connections),
51
- `@lakutata/ffi` (the C functions of native libraries) and `@lakutata/math` (symbolic and numeric mathematics, as
52
- functions that need no lakutata application).
51
+ `@lakutata/ffi` (the C functions of native libraries), `@lakutata/math` (symbolic and numeric mathematics, as functions
52
+ that need no lakutata application) and `@lakutata/netif` (servers and UDP sockets listening on a network interface
53
+ whatever its addresses, the interfaces of the machine and their changes).
53
54
 
54
55
  ## The guides
55
56
 
@@ -73,7 +74,7 @@ functions that need no lakutata application).
73
74
  | [Middlewares](Middlewares.md) | The middlewares and the access control of the actions |
74
75
  | [HTTP](HTTP.md) | The HTTP adapters: routes, input, responses, errors, the features of Express and Fastify |
75
76
  | [CLI](CLI.md) | Command line tools: commands, options, prompts, output and exit codes |
76
- | [ServiceCalls](ServiceCalls.md) | The service actions over HTTP, Socket.IO, JSON-RPC 2.0 and Electron: patterns, responses, clients and their events |
77
+ | [ServiceCalls](ServiceCalls.md) | The service actions over HTTP, Socket.IO, JSON-RPC 2.0, Electron and NW.js: patterns, responses, clients and their events |
77
78
  | [GRPC](GRPC.md) | The service actions over gRPC (`@lakutata/grpc`), and calling them from other applications and languages |
78
79
  | [NATS](NATS.md) | The NATS component, the service actions over NATS (`@lakutata/nats`), the service proxies and their events |
79
80
  | [Schedules](Schedules.md) | Scheduled jobs: intervals, cron expressions, jobs run by one instance |
@@ -95,6 +96,7 @@ functions that need no lakutata application).
95
96
  | [WebRTC](WebRTC.md) | Peer-to-peer connections and a media server (`@lakutata/webrtc`): the signaling, the TURN server, browsers and applications, the actions of an application called through data channels, WHIP and WHEP |
96
97
  | [FFI](FFI.md) | The C functions of native libraries (`@lakutata/ffi`): declarations copied from headers, structs, callbacks, memory, packaging |
97
98
  | [Math](Math.md) | Mathematics (`@lakutata/math`): formulas and exact results, equations, decimals of any precision, matrices, statistics, numeric solutions, units |
99
+ | [Netif](Netif.md) | Network interfaces (`@lakutata/netif`): a server listening on an interface whatever its addresses, the connections of the other interfaces refused, the interfaces and their changes, UDP on one interface |
98
100
  | [Command](Command.md) | The `lakutata` command: creating projects, migrations, translations, environment variables |
99
101
  | [Workspaces](Workspaces.md) | Projects of several packages: running their scripts, giving them a version, adding packages |
100
102
  | [NativeAddons](NativeAddons.md) | Native addons in C++ or Rust: creating them, linking libraries, shipping them |
@@ -49,6 +49,7 @@ directory, once moved with them.
49
49
  | `--outfile <file>` | The bundle, `dist/<name>.cjs` by default |
50
50
  | `--no-minify` | Do not minify the bundle (the names of the classes are kept either way) |
51
51
  | `--external <modules...>` | Modules left out of the bundle, loaded at run time from a `node_modules` next to it |
52
+ | `--node <version>` | The version of Node.js running the bundle (`18.10` for NW.js 0.70): the bundle is written in the syntax that version reads; the one running the tool by default |
52
53
 
53
54
  ## 3. Native addons
54
55
 
@@ -124,11 +125,12 @@ architecture for Linux (`docker run --platform linux/arm64`); the README of the
124
125
 
125
126
  | Option | Meaning |
126
127
  |---|---|
127
- | `--runtime node` (default) | The bundle is embedded in a copy of the Node.js running the tool (a single executable application of Node.js): about 110 MB on macOS arm64 |
128
+ | `--runtime node` (default) | The bundle is embedded in a copy of the Node.js running the tool, or of `--node-program` (a single executable application of Node.js): about 110 MB on macOS arm64 |
128
129
  | `--runtime bun` | The bundle is compiled by Bun (the `bun` package of the project, else the `bun` of the system): about 70 MB on macOS arm64, and it starts faster; the runtime is then Bun |
129
130
  | `--layout file` (default) | One file: the native addons, the files read by path and the resources are embedded, and extracted when it starts |
130
131
  | `--layout directory` | `dist/<name>/` holds the executable, `native/` and `resources/`, loaded from there without extraction (the installers use it) |
131
132
  | `--outdir <directory>` | Where the executable is written, `dist` by default |
133
+ | `--node-program <path>` | The node embedded instead of the one running the tool: a Node.js 20.12, 21.7 or later (`$(nvm which 22)`), so that an older Node.js can run the tool; the bundle follows its syntax |
132
134
  | `--entry`, `--no-minify`, `--external` | As for a bundle |
133
135
 
134
136
  - **Extraction**: an executable of the file layout extracts what it embeds at its first run, into its cache directory,
@@ -171,6 +173,7 @@ of the project (it may be outside the package, such as `../ui/dist`), `to` its p
171
173
  | An executable, file layout | `true` | The directory the resources were extracted to |
172
174
  | An executable, directory layout, or an installed application | `true` | `resources` next to the executable |
173
175
  | A packaged Electron application | `true` | `main.resources` in the resources of Electron |
176
+ | An NW.js application (run by `lakutata nwjs run`, or packaged) | `true` | `resources` in the directory of the application |
174
177
 
175
178
  `resourcesPath()` returns the path even when the application declares no resources, so the directory may not exist.
176
179
  The application chooses where to read its files from:
@@ -53,8 +53,8 @@ templates other than `cli` do.
53
53
  ## 2. The workspace templates
54
54
 
55
55
  The other templates are npm workspaces: the application is the package `packages/app`, and the templates with an
56
- interface or with installers add `packages/ui` (the web pages, or the renderer of Electron) and `packages/resources`
57
- (the icon). The `service` template:
56
+ interface or with installers add `packages/ui` (the web pages, the renderer of Electron, or the user interface of
57
+ NW.js) and `packages/resources` (the icon). The `service` template:
58
58
 
59
59
  ```
60
60
  shop/
@@ -93,6 +93,10 @@ The other templates add to this layout:
93
93
  it into `dist/` ([Packaging](Packaging.md)).
94
94
  - `electron-react` and `electron-vue`: the main process of Electron in `packages/app` (its window and its service
95
95
  actions over IPC), the renderer in `packages/ui`, and the scripts that make the installers.
96
+ - `nwjs-react` and `nwjs-vue`: the background script of NW.js in `packages/app` (its window, whose host page in
97
+ `packages/app/host` holds the user interface in a webview, and its service actions), the user interface in
98
+ `packages/ui`, the version of NW.js in `packages/app/nwjs.json` (written by `npm run nwjs:use -- <version>`), and the
99
+ scripts that make the installers.
96
100
 
97
101
  The packed forms are written to `dist/` at the root of the workspace; `.gitignore` leaves out `build`, `dist` and the
98
102
  outputs of the interfaces.
@@ -105,8 +109,9 @@ outputs of the interfaces.
105
109
  |---|---|---|
106
110
  | `i18n` | all | The messages of the languages in `src/locales` (`en`, `zh-CN`), the `i18n` component, the language middleware, and the scripts `i18n:check` and `i18n:types` ([I18n](I18n.md)) |
107
111
  | `installer` | `cli`, `service`, `web-*` | The `lakutata.installer` of the application, the scripts that make the installers (on the machine, and for Linux in Docker), and for the services a system service ([Installers](Installers.md)) |
112
+ | `win7` | `nwjs-*` | NW.js 0.72.0 instead of the release the template was published with: the last release running on Windows 7, 8 and 8.1, and the script `package:ia32` that makes the installers of 32-bit Windows |
108
113
 
109
- The Electron templates make installers without this feature.
114
+ The Electron and NW.js templates make installers without the `installer` feature.
110
115
 
111
116
  ## 4. Templates of your own
112
117
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  A service action (`@ServiceAction`) is called with a message rather than a URL: an object whose keys select the action,
4
4
  its **pattern**, and whose other keys are the input. The same actions can be served over several transports at once:
5
- plain HTTP, Socket.IO, JSON-RPC 2.0 (over HTTP, WebSocket or the standard input and output) and the IPC of Electron, as
6
- this guide describes, and gRPC, NATS and the data channels of WebRTC, which have their own guides. The controllers and
5
+ plain HTTP, Socket.IO, JSON-RPC 2.0 (over HTTP, WebSocket or the standard input and output), the IPC of Electron and the
6
+ windows of NW.js, as this guide describes, and gRPC, NATS and the data channels of WebRTC, which have their own guides. The controllers and
7
7
  their actions are described in [Controllers](Controllers.md), the entrypoint and its adapters in
8
8
  [Entrypoint](Entrypoint.md).
9
9
 
@@ -136,7 +136,7 @@ class OrderController extends Controller {
136
136
 
137
137
  ## 3. The response
138
138
 
139
- The HTTP service, Socket.IO and Electron answer with an envelope; JSON-RPC answers with its own responses (section 4).
139
+ The HTTP service, Socket.IO, Electron and NW.js answer with an envelope; JSON-RPC answers with its own responses (section 4).
140
140
 
141
141
  ```text
142
142
  {"success": true, "error": null, "payload": <the result>}
@@ -179,6 +179,7 @@ export async function call<T>(message: Record<string, unknown>): Promise<T> {
179
179
  | `socketIOServiceEntrypoint()` | `lakutata/adapter/socket.io` | `socket.io` (`socket.io-client` on the clients) | An emit with an acknowledgement | Socket.IO events |
180
180
  | `jsonRPCServiceEntrypoint()` | `lakutata/adapter/jsonrpc` | `ws`, for WebSocket | A JSON-RPC 2.0 request | Notifications (WebSocket, stdio) |
181
181
  | `electronIPCServiceEntrypoint()` | `lakutata/adapter/electron` | (Electron) | `ipcRenderer.invoke()` | IPC messages |
182
+ | `nwjsServiceEntrypoint()` | `lakutata/adapter/nwjs` | (NW.js) | A call of its bridge, by the host page of a window | Calls of the function the host page gives |
182
183
  | `grpcServiceEntrypoint()` | `@lakutata/grpc/service` | `@lakutata/grpc` | See [GRPC](GRPC.md) | gRPC streams |
183
184
  | `natsServiceEntrypoint()` | `@lakutata/nats` | `@lakutata/nats` | See [NATS](NATS.md) | NATS messages |
184
185
  | `webrtcServiceEntrypoint()` | `@lakutata/webrtc` | `@lakutata/webrtc` | A JSON-RPC 2.0 request on a data channel, see [WebRTC](WebRTC.md) | Notifications |
@@ -197,7 +198,7 @@ pages of a web UI during its development. The Electron templates add it in devel
197
198
  | `host` | all the interfaces | The address to listen on |
198
199
  | `path` | `'/'` | The path of the requests; the events are under it, at `<path>/events` |
199
200
  | `limit` | `10485760` (10 MiB) | The largest body, in bytes |
200
- | `listening` | | `(server) => …`, called once the server listens |
201
+ | `listening` | | `(server, listeners) => …`, called once the server listens (`listeners`, with `interface`: see below) |
201
202
 
202
203
  - A request is a `POST` to `path`, its body the message as JSON. It is answered with 200 and the envelope, whatever the
203
204
  result of the action. A body that is not JSON is answered with 400, a body over `limit` with 413, another method or
@@ -284,7 +285,7 @@ connected.
284
285
  | `options` | | The options of the Socket.IO server: `cors`, `path`, `maxHttpBufferSize`… |
285
286
  | `server` | | An HTTP server to share, or a function returning it (below) |
286
287
  | `io` | | An existing Socket.IO server, used as it is |
287
- | `listening` | | `(io, server) => …`, called once the adapter is ready |
288
+ | `listening` | | `(io, server, listeners) => …`, called once the adapter is ready |
288
289
 
289
290
  ```typescript
290
291
  import {io, type Socket} from 'socket.io-client'
@@ -340,7 +341,7 @@ the tools that speak the protocol, and the parent processes over the standard in
340
341
  | `input`, `output` | `process.stdin`, `process.stdout` | The streams of stdio |
341
342
  | `end` | `'application'` | When the input of stdio ends: `'application'` exits the application once the requests in progress are answered, `'none'` does nothing, or a function |
342
343
  | `heartbeat` | `30000` | The interval of the pings of WebSocket, in milliseconds; `0` sends none |
343
- | `listening` | | `(server) => …`, called once the server of HTTP or WebSocket listens |
344
+ | `listening` | | `(server, listeners) => …`, called once the server of HTTP or WebSocket listens |
344
345
 
345
346
  The method and the params of a request make the message: `{"method": "order.get", "params": {"id": 1}}` is the message
346
347
  `{method: 'order.get', id: 1}`, so the action declares the method in its pattern:
@@ -417,6 +418,25 @@ $ echo '{"jsonrpc": "2.0", "id": 1, "method": "sum", "params": {"values": [1, 2,
417
418
 
418
419
  The application exits once its input ends and its requests are answered (`end`).
419
420
 
421
+ ### On a network interface
422
+
423
+ `httpServiceEntrypoint()`, `socketIOServiceEntrypoint()` with its own server, and `jsonRPCServiceEntrypoint()` over HTTP
424
+ or WebSocket can listen on a network interface instead of an address, whatever its addresses, with the package
425
+ `@lakutata/netif` installed apart. They take the options `interface`, `family`, `strict`, `onError` and
426
+ `interfaceListening` of the HTTP adapters (see [HTTP](HTTP.md), section 2), and their `listening` callback gets the
427
+ listeners of the interface as its last argument:
428
+
429
+ ```typescript
430
+ import {httpServiceEntrypoint} from 'lakutata/com/entrypoint'
431
+
432
+ //The service on eth1 only, whatever its address
433
+ export const service = httpServiceEntrypoint({port: 3000, interface: 'eth1'})
434
+ ```
435
+
436
+ In Bun, a connection given to a server cannot be upgraded to a WebSocket (seen with Bun 1.4). There the start fails for
437
+ JSON-RPC over WebSocket, and for Socket.IO unless it uses polling only (`options: {transports: ['polling']}`, and the
438
+ same on the clients).
439
+
420
440
  ### Electron
421
441
 
422
442
  `electronIPCServiceEntrypoint()` serves the actions to the windows of an Electron application, in its main process.
@@ -449,6 +469,47 @@ contextBridge.exposeInMainWorld('service', {
449
469
  - The Electron templates of `lakutata create` contain a preload script of this shape, and a `window.ipc` for the pages,
450
470
  whose `invoke()` returns the payload or throws the error.
451
471
 
472
+ ### NW.js
473
+
474
+ `nwjsServiceEntrypoint()` serves the actions to the windows of an NW.js application, in its background script (its
475
+ Node.js context). NW.js gives Node.js to the pages of its windows, and an iframe of such a page does not keep Node.js
476
+ from its content (its sandbox is ignored): the user interface runs in a `<webview>`, without Node.js, held by a trusted
477
+ page of the window (its host page). The adapter puts a bridge on the global object that NW.js shares with its windows,
478
+ `global[Symbol.for(key)]`; the host page forwards the requests of the interface to it, and gives it the function that
479
+ pushes the events to the interface.
480
+
481
+ | Option | Default | Meaning |
482
+ |---|---|---|
483
+ | `key` | `'lakutata:service'` (`NWJS_SERVICE_BRIDGE`) | The key of the bridge on the global object |
484
+ | `global` | the global object of NW.js | The object the bridge is put on, or a stand-in in the tests |
485
+
486
+ | Method of the bridge | Meaning |
487
+ |---|---|
488
+ | `invoke(message, clientId)` | Invokes an action: resolves to the envelope, never rejects |
489
+ | `subscribe(clientId, push)` | Makes a page a client: the events pushed to it are given to `push(event, args)` |
490
+ | `unsubscribe(clientId)` | Removes a client, when its page is loaded again or its window closed: its requests in progress are aborted |
491
+
492
+ The host page forwards the messages of the interface, from the page loaded in its `<webview>` only:
493
+
494
+ ```javascript
495
+ const view = document.querySelector('webview')
496
+ const origin = new URL(view.src).origin
497
+ const bridge = global[Symbol.for('lakutata:service')]
498
+ const send = (message) => view.contentWindow.postMessage(message, origin)
499
+ window.addEventListener('message', async (event) => {
500
+ if (event.source !== view.contentWindow || event.origin !== origin) return
501
+ const {type, id, message, clientId} = event.data
502
+ if (type === 'invoke') send({type: 'response', id: id, response: await bridge.invoke(message, clientId)})
503
+ if (type === 'subscribe') bridge.subscribe(clientId, (name, args) => send({type: 'event', name: name, args: args}))
504
+ })
505
+ ```
506
+
507
+ - The `<webview>` loads the files of the application when its partition is declared in the manifest of NW.js:
508
+ `"webview": {"partitions": [{"name": "trusted", "accessible_resources": ["<all_urls>"]}]}` (the builder writes it).
509
+ - A page loaded again in the `<webview>` is a new client: the host page unsubscribes the previous one.
510
+ - The NW.js templates of `lakutata create` contain such a host page (`packages/app/host`), and a `window.ipc` for the
511
+ interface like the one of the Electron templates.
512
+
452
513
  ## 5. Clients and events
453
514
 
454
515
  The transports that keep their clients connected can push events to them. A client is identified by the id it gives:
@@ -461,6 +522,7 @@ The transports that keep their clients connected can push events to them. A clie
461
522
  | JSON-RPC over WebSocket | The `id` header or `id` query parameter of the connection, else a random id |
462
523
  | JSON-RPC over stdio | `'stdio'` |
463
524
  | Electron | The id the page gives when it subscribes or invokes |
525
+ | NW.js | The id the host page gives to the bridge for its page |
464
526
  | WebRTC | The member id of the caller in its room |
465
527
 
466
528
  An action reads the id of its client in `this.context.clientId`. The `entrypoint` component pushes the events:
@@ -496,6 +558,7 @@ A client receives `notify(clientId, 'progress', 50, 'half')` as:
496
558
  | Socket.IO | `socket.on('progress', (percent, label) => …)` |
497
559
  | JSON-RPC (WebSocket, stdio) | `{"jsonrpc": "2.0", "method": "progress", "params": [50, "half"]}` |
498
560
  | Electron | `('progress', [50, 'half'])`, through the `subscribe` of the preload script above |
561
+ | NW.js | `('progress', [50, 'half'])`, given to the function of the `subscribe` of the bridge |
499
562
  | WebRTC | `proxy.on('progress', (percent, label) => …)` of a `PeerServiceProxy` |
500
563
 
501
564
  ## 6. Stopping and leaving
@@ -20,7 +20,7 @@ packages obtained elsewhere may have APIs that this page does not cover.
20
20
  | Modules | CommonJS: imports without extensions, `__dirname` | ES modules: relative imports ending in `.js` |
21
21
  | Service actions | `@Controller({_ctrl: …})` and `@PATTERN({_act: …})` from `@lakutata-module/service` | `@ServiceAction({…})` from `lakutata/decorator/ctrl` |
22
22
  | Validation | `Validator` schemas (yup) given to `@Accept` | DTO classes with `@Expect` |
23
- | Node.js | `>=14.15.1` (`@lakutata/core` 1.0.60) | `^22.13.0 \|\| >=24.11.0` |
23
+ | Node.js | `>=14.15.1` (`@lakutata/core` 1.0.60) | `>=18.10.0` |
24
24
 
25
25
  **`@lakutata/core` 3 is not a new version of the 1.x library.** It is the core of Lakutata 3, a different API under the
26
26
  same name. `npm install @lakutata/core@latest`, or a range such as `*`, in a 1.x project puts it in place of the 1.x
@@ -392,7 +392,7 @@ are built differently, so the entries that 1.x wrote are not found: start with a
392
392
  - **The `lakutata` field of `package.json`** told the 1.x tool the kind of the project (`type`), its directories and
393
393
  its configuration file. In 3.x the field has another meaning (the `resources` and the `installer` of a packed
394
394
  application, [Packaging](Packaging.md)): remove the 1.x one.
395
- - **Node.js, TypeScript and modules.** Node.js `^22.13.0 || >=24.11.0`, TypeScript 6 as in the templates, ES modules
395
+ - **Node.js, TypeScript and modules.** Node.js `>=18.10.0`, TypeScript 6 as in the templates, ES modules
396
396
  (`"type": "module"`, relative imports ending in `.js`), `tsconfig.json` extending `lakutata/tsconfig.json`.
397
397
  - **Ports.** The service module listened on a port of its own (6912 by default) and the registry on another (6911).
398
398
  The 3.x adapters listen on the ports of their options, and a service on NATS only connects out to the NATS server:
@@ -17,15 +17,15 @@ It is for the projects that depend on the package `lakutata` at version 2. A pro
17
17
  | Exported functions | PascalCase: `BuildEntrypoints`, `BuildDatabaseOptions`, `MD5` | camelCase: `buildEntrypoints`, `buildDatabaseOptions`, `md5` |
18
18
  | Transports | Written by hand: `BuildHTTPEntrypoint((module, routeMap, handler, registerDestroy) => …)` | Adapters: `expressHTTPEntrypoint()` from `lakutata/adapter/express`… |
19
19
  | Subpaths of 3.x only | | `lakutata/utils`, `lakutata/security`, `lakutata/adapter/*`, `lakutata/com/{containerd,mutex,queue,i18n}` |
20
- | Node.js | `>=20` | `^22.13.0 \|\| >=24.11.0` |
20
+ | Node.js | `>=20` | `>=18.10.0` |
21
21
 
22
22
  `npx @lakutata/cli info` prints the versions of the tool, of the framework and of Node.js, and the `lakutata` version
23
23
  that the project declares and the one installed. There is no upgrade command: the steps below are done by hand.
24
24
 
25
25
  ## 2. The steps
26
26
 
27
- - **Node.js** `^22.13.0 || >=24.11.0` (22.13 or a later 22.x, or 24.11 and later; not 23). Update `engines`, the CI
28
- matrix and the base images of the Dockerfiles.
27
+ - **Node.js** `>=18.10.0` (3.0 to 3.1 needed `^22.13.0 || >=24.11.0`). The versions 2.x needs (`>=20`) are among them:
28
+ `engines`, the CI matrix and the Dockerfiles can stay.
29
29
  - **A TypeScript that knows `"module": "Node20"`** (the templates use TypeScript 6): `lakutata/tsconfig.json` now sets
30
30
  it (2.x: `Node16`), and older compilers reject it.
31
31
  - **`package.json`**: `"type": "module"`, `lakutata` `^3.0.0`, the `@lakutata/*` packages at `^3.0.0` too, and
@@ -281,7 +281,7 @@ Application
281
281
  ## 6. Checking the upgrade
282
282
 
283
283
  ```bash
284
- node --version # ^22.13.0 || >=24.11.0
284
+ node --version # >=18.10.0
285
285
  npm ls lakutata @lakutata/core # one version of each
286
286
  npm run build # tsc without errors: no 2.x import left
287
287
  grep -rnE "process\.on\('SIG|\|\| ''|(startup|launched|beforeDestroy|acceptsOption)\(|AccessDenyException|=== 500" src
package/doc/en/WebRTC.md CHANGED
@@ -224,10 +224,10 @@ export class Telemetry extends Component {
224
224
  ```
225
225
 
226
226
  An `http:` or `https:` URL uses Socket.IO (install `socket.io-client`), a `ws:` or `wss:` URL uses JSON-RPC over the
227
- `WebSocket` of Node.js 22 and Bun. Its options: `url`, `id` (a random UUID by default), `path` (of Socket.IO), `auth`
228
- (more fields of the Socket.IO handshake, a token), `timeout`, and the options of its connections (`iceTransportPolicy`,
229
- `icePortRange`, `iceAdditionalHostAddresses`, `codecs`, `threads`). It is `@Transient`: each `getObject()` gives a new
230
- client, and `close()` leaves its rooms and disconnects.
227
+ global `WebSocket` of Node.js 22 and Bun, else over the `WebSocket` of the package `ws` (install `ws`). Its options: `url`, `id`
228
+ (a random UUID by default), `path` (of Socket.IO), `auth` (more fields of the Socket.IO handshake, a token), `timeout`,
229
+ and the options of its connections (`iceTransportPolicy`, `icePortRange`, `iceAdditionalHostAddresses`, `codecs`,
230
+ `threads`). It is `@Transient`: each `getObject()` gives a new client, and `close()` leaves its rooms and disconnects.
231
231
 
232
232
  ### Calling the actions of an application
233
233
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  | 选项 | 含义 |
10
10
  |---|---|
11
- | `id`、`name` | 必填。应用的 ID(同时作为 `process.title`)和名称(`app.appId`、`app.appName`) |
11
+ | `id`、`name` | 必填。应用的 ID(同时作为 `process.title`,NW.js 中除外)和名称(`app.appId`、`app.appName`) |
12
12
  | `timezone` | IANA 时区名,或 `'auto'`(默认)表示使用系统时区;写入 `TZ` |
13
13
  | `mode` | `'development'` 或 `'production'`,在环境变量未指定时生效(见第 4 节) |
14
14
  | `shutdownSignals` | 使应用优雅退出的信号(默认 `['SIGTERM', 'SIGINT']`),或 `false` |
package/doc/zh/Cache.md CHANGED
@@ -250,7 +250,7 @@ Application.run(() => ({
250
250
 
251
251
  - `table` 中字母、数字和 `_` 以外的字符会被去掉。
252
252
  - 语句同步执行:等待被锁住的数据库时,语句会阻塞事件循环,最长 `busyTimeout`。
253
- - `better-sqlite3` 是原生插件,针对一种运行时构建:Electron 应用需要使用针对其 Electron 版本构建的 `better-sqlite3`。在 Bun 上,存储改用 `bun:sqlite`。
253
+ - `better-sqlite3` 是原生插件,针对一种运行时构建:Electron 应用需要使用针对其 Electron 版本构建的 `better-sqlite3`;NW.js 应用会用 NW.js 的头文件重新构建它(需要 0.111.3 及以后的版本:更早的版本无法加载它)。在 Bun 上,存储改用 `bun:sqlite`。
254
254
 
255
255
  ### PostgreSQL
256
256