lakutata 3.1.2 → 3.1.4

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/doc/zh/Math.md ADDED
@@ -0,0 +1,312 @@
1
+ # Lakutata Math
2
+
3
+ `@lakutata/math` 包在 Node.js 里做数学计算,符号计算和数值计算都有:公式的代数和微积分运算、方程和不等式、任意精度的十进制数、矩阵、统计和概率分布、数值解法、单位、数论。它不属于 `lakutata`,也不需要应用:在任何地方导入它的函数就能调用。它的特点是:
4
+
5
+ - **公式按纸上的写法书写**:`x^2 - 5x + 6`、`2(x + 1)`、`5!`、`sin(pi/6)`。结果也用同样的写法给出,所以能再读回来,也能转成 TeX 用于显示。
6
+ - **结果是精确的**:`0.1 + 0.2` 是 `3/10`,`sqrt(8)` 是 `2sqrt(2)`,`x^2 - 2` 的根是 `-sqrt(2)` 和 `sqrt(2)`。只有需要数值时才计算数值:可以是双精度数,也可以是指定位数的十进制数,每一位都正确。
7
+ - **符号计算和数值计算在同一个包里**:一个运算没有精确结果时(某些积分、五次多项式的根),数值方法可以直接用同一个公式。
8
+ - **由原生插件完成计算**:插件基于 SymEngine、Boost、Eigen 和 mpdecimal,是编译好的代码,所以很快。耗时的运算在插件自己的线程里执行,可以设时限、可以取消,不会占住事件循环。
9
+
10
+ `@lakutata/math` 的 README 介绍了每个函数及其选项;这篇指南按计算的种类,逐一说明这个包能算什么,每类都给出一个例子。
11
+
12
+ ```bash
13
+ npm install @lakutata/math
14
+ ```
15
+
16
+ 原生插件在安装这个包时编译,所以机器上要有 Python 3 和支持 C++17 的编译器(README 列出了各系统需要安装什么),编译要半分钟到几分钟。包管理器必须运行这个包的安装脚本:npm 会运行它,可能会提示这个脚本“not yet covered by allowScripts”(运行 `npm install-scripts approve @lakutata/math` 记录你允许它运行);Bun 和 pnpm 只为被告知可以信任的包运行安装脚本(Bun 在 package.json 的 `trustedDependencies` 里设置,pnpm 用 `pnpm approve-builds`)。没有编译好的插件时,加载这个包会抛出错误,并说明怎样编译。
17
+
18
+ ## 1. 公式和结果
19
+
20
+ `expression()` 读入一个公式。数字紧挨着名字或括号表示相乘(`2x`、`3(x + 1)`),`^` 表示乘方,常数(`pi`、`e`、`i`、`inf`)和函数(`sin`、`ln`、`sqrt`、`gamma`、`erf`、`zeta` 等)都用通常的名字;完整的列表见 README。表达式本身不会改变:每个运算都得到一个新的表达式。
21
+
22
+ ```typescript
23
+ import {createMath, expression} from '@lakutata/math'
24
+
25
+ console.log(expression('0.1 + 0.2').toString()) //3/10
26
+ console.log(expression('sqrt(8) + sin(pi/6)').toString()) //2sqrt(2) + 1/2
27
+ console.log(expression('3(x + 1)^2 - x').expand().toString()) //3x^2 + 5x + 3
28
+ console.log(expression('e^(-x^2/2)/sqrt(2pi)').toTeX()) //\frac{e^{-\frac{x^{2}}{2}} \sqrt{2}}{2 \sqrt{\pi}}
29
+ console.log(expression('cos(10^30)').toNumber(), Math.cos(1e30)) //-0.9959311944053957 -0.9999564608959665
30
+ console.log(expression('pi').toDecimal(50)) //3.1415926535897932384626433832795028841971693993751
31
+ const finance = createMath({definitions: {'rate': '0.13', 'net(x)': 'x/(1 + rate)'}})
32
+ console.log(finance.expression('net(113)').toString()) //100
33
+ console.log(finance.expression('p - net(p)').diff('p').toString()) //13/113
34
+ ```
35
+
36
+ - `toString()` 给出 `expression()` 能读回的公式,`toTeX()` 给出它的 TeX(供 KaTeX 或 MathJax 显示)。
37
+ - `toNumber()` 给出最接近的双精度数。它从精确值的十进制数字算出,所以即使双精度计算已经没有一位有效数字,结果的每一位也都是对的:作为双精度数,`1e30` 并不正好是 10^30,`Math.cos(1e30)` 算的其实是另一个数的余弦。`toDecimal(digits)` 以字符串给出指定位数的有效数字,每一位都是对的。
38
+ - `substitute({x: 2})` 把变量换成给定的值,结果仍然是精确的。
39
+ - `createMath({definitions})` 创建一个范围,其中的函数和常数用公式写出,可用的函数和包本身相同。公式会展开其中的定义:求导和解方程时都会用到它们。包直接导出的函数属于 `math`,这个范围没有任何定义。
40
+
41
+ ## 2. 代数和微积分
42
+
43
+ ```typescript
44
+ import {expression} from '@lakutata/math'
45
+
46
+ console.log(expression('x^6 - y^6').factor().toString()) //(x^2 - x*y + y^2)(x + y)(x - y)(x^2 + x*y + y^2)
47
+ console.log(expression('(x^2 - 1)/(x^2 + 2x + 1)').cancel().toString()) //(x - 1)/(x + 1)
48
+ console.log(expression('sin(x)^4 - cos(x)^4').simplify().toString()) //-cos(2x)
49
+ console.log(expression('x^2*sin(x)').diff('x').toString()) //2x*sin(x) + x^2*cos(x)
50
+ console.log(expression('x*e^x').integrate('x').toString()) //e^x*(x - 1)
51
+ console.log(expression('1/(x^2 + 1)').integrate('x', '-inf', 'inf').toString()) //pi
52
+ console.log(expression('(1 + a/x)^x').limit('x', 'inf').toString()) //e^a
53
+ console.log(expression('e^x').series('x', 0, 4).toString()) //x^3/6 + x^2/2 + x + 1
54
+ console.log(expression('sum(k^2, k, 1, n)').toString()) //n^3/3 + n^2/2 + n/6
55
+ console.log(expression('t^2*e^(-3t)').laplace('t', 's').toString()) //2/(s + 3)^3
56
+ ```
57
+
58
+ - **多项式和分式**:`expand`、`factor`(在有理数上分解,变量个数不限)、`cancel`、`together`、`apart`(部分分式)和 `completeSquare`。
59
+ - **化简**:`simplify()` 用对变量的所有取值都成立的恒等式(三角、双曲、指数、嵌套根式)寻找更短的形式;取决于变量符号的化简一律不做,所以 `sqrt(x^2)` 保持原样。
60
+ - **积分**:用部分分式、积分表、换元、分部积分,以及积分产生的特殊函数(`sin(x)/x` 的积分是 `Si(x)`)。每个原函数返回前都会求导验证。定积分在区间内的极点处分段计算;积分无限增大时结果是 `inf`。
61
+ - **极限**用 Gruntz 算法:可以取两侧或一侧的极限,点可以是有限点或无穷远,能处理各种不定式(`0/0`、`1^inf`、`inf - inf` 等)。
62
+ - **级数**(`series(variable, point, order)`)、**求和与求积**的闭式(`sum(1/k^2, k, 1, inf)` 是 `pi^2/6`)、**分段函数**(`piecewise(x^2, x < 0, x)`),以及**拉普拉斯变换**及其逆变换。
63
+
64
+ 这些运算大多有异步版本(第 11 节)。
65
+
66
+ ## 3. 方程和不等式
67
+
68
+ ```typescript
69
+ import {equation, type Inequality, parse, solve} from '@lakutata/math'
70
+
71
+ console.log(equation('x^3 - 6x^2 + 11x - 6 = 0').solve('x').map(String)) //['1', '2', '3']
72
+ console.log(equation('x^4 = 16').solve('x').map(String)) //['-2', '2', '-2i', '2i']
73
+ console.log(equation('x^4 = 16').solve('x', {domain: 'real'}).map(String)) //['-2', '2']
74
+ console.log(equation('x^2 - (a + b)x + a*b = 0').solve('x').map(String)) //['a', 'b']
75
+ const [solution] = solve(['x + y = 3', 'x - y = 1'], ['x', 'y'])
76
+ console.log(solution.x.toString(), solution.y.toString()) //2 1
77
+ console.log((parse('(x + 1)/(x - 2) >= 0') as Inequality).solve('x').set.toString()) //(-inf, -1] ∪ (2, inf)
78
+ ```
79
+
80
+ - 多项式方程先做因式分解,所以根的形式很简单(得到 1、2、3,而不是卡尔达诺公式的根式);各个因式在四次以内都精确求解,`a x^n + b` 不论几次都能精确求解。不给 `{domain: 'real'}` 时,解在复数范围内。
81
+ - `solve(equations, variables)` 解线性方程组;有无穷多解时,用自由变量表示其他变量。
82
+ - 由多项式分式构成的不等式给出它的实数解,形式是区间和值组成的集合(`MathSet`),集合可以求并、交、补,也可以判断某个值是否属于它。
83
+
84
+ ## 4. 十进制数
85
+
86
+ `decimal()` 得到一个指定精度的十进制数(默认 32 位有效数字,最多 100000 位):用于金额,或者需要很多位数字的值。每个运算都把结果正确舍入,恰好在中间时舍入到偶数。
87
+
88
+ ```typescript
89
+ import {decimal, expression} from '@lakutata/math'
90
+
91
+ console.log(decimal(0.1).add(0.2).toString()) //0.3
92
+ console.log(decimal('19.99').mul(3).toFixed(2)) //59.97
93
+ console.log(decimal(1, {precision: 50}).div(7).toString()) //0.14285714285714285714285714285714285714285714285714
94
+ console.log(decimal(2, {precision: 40}).sqrt().toString()) //1.41421356237309504880168872420969807857
95
+ console.log(expression('gamma(1/3)').toDecimal(40)) //2.678938534707747633655692940974677644129
96
+ console.log(expression('e^(pi*sqrt(163))').toDecimal(40)) //262537412640768743.9999999999992500725972
97
+ ```
98
+
99
+ - 数字按它的写法读入(`0.1` 就是十分之一,而不是双精度数的二进制值),字符串按十进制数读入(`'19.99'`),bigint 精确读入。
100
+ - 除了四则运算,十进制数还有开方、`exp` 和对数、三角和双曲函数、`gamma`、`erf`、`zeta`、`lambertw` 以及取整函数。表达式的 `toDecimal()` 能计算这些函数,还能计算更多特殊函数(`polygamma`、`beta`、`Si`、`Ei` 等)。
101
+ - `toDecimal()` 会用更多位数重新计算,直到两次结果一致,所以它给出的每一位都是对的:`e^(pi*sqrt(163))` 不是整数,尽管它小数点后的前十二位都是 9。
102
+
103
+ ## 5. 矩阵
104
+
105
+ 矩阵有两种:数值矩阵用于计算,表达式矩阵用于精确的线性代数。
106
+
107
+ ```typescript
108
+ import {expressionMatrix, linalg, matrix} from '@lakutata/math'
109
+
110
+ const a = matrix([[4, 1], [2, 3]])
111
+ console.log(linalg.solve(a, [5, 5])) //Float64Array(2) [1, 1]
112
+ console.log(linalg.inverse(a).toString(), linalg.determinant(a)) //[0.3, -0.1; -0.2, 0.4] 10
113
+ const m = expressionMatrix('[a, b; c, d]')
114
+ console.log(m.determinant().toString()) //a*d - b*c
115
+ console.log(m.inverse().toString()) //[d/(a*d - b*c), -b/(a*d - b*c); -c/(a*d - b*c), a/(a*d - b*c)]
116
+ console.log(expressionMatrix('[2, 1; 1, 2]').eigenvalues().map(({value, multiplicity}) => [value.toString(), multiplicity])) //[['1', 1], ['3', 1]]
117
+ ```
118
+
119
+ - `matrix()` 把双精度数存放在一个 `Float64Array` 里,`linalg` 用 Eigen 计算:线性方程组、最小二乘、逆矩阵、行列式、各种分解(LU、QR、SVD、Cholesky、特征值、Schur)、矩阵的指数和平方根、Sylvester 方程和 Lyapunov 方程,以及稀疏矩阵和它的直接解法、迭代解法。
120
+ - `expressionMatrix()` 的元素是表达式:行列式、逆矩阵、方程组、简化行阶梯形、零空间、特征多项式、特征值和特征向量,结果都是精确的,含变量时也一样。`jacobian()` 给出一组表达式的雅可比矩阵。
121
+
122
+ ## 6. 统计和概率
123
+
124
+ ```typescript
125
+ import {stats} from '@lakutata/math'
126
+
127
+ const values = [2, 4, 4, 4, 5, 5, 7, 9]
128
+ console.log(stats.mean(values), stats.median(values), stats.standardDeviation(values)) //5 4.5 2
129
+ console.log(stats.quantile(values, [0.25, 0.75])) //Float64Array(2) [4, 5.5]
130
+ const iq = stats.normal({mean: 100, standardDeviation: 15})
131
+ console.log(iq.cdf(130) - iq.cdf(70)) //0.9544997361036416
132
+ console.log(iq.quantile(0.975)) //129.3994597681008
133
+ console.log(stats.binomial({trials: 10, probability: 0.3}).pdf(3)) //0.2668279319999998
134
+ console.log(stats.random(42).integers(5, 1, 7)) //Float64Array(5) [1, 1, 6, 6, 5]
135
+ ```
136
+
137
+ - 一组值的统计量:和(补偿了舍入误差)、均值、中位数、众数、方差(默认按总体计算,`{normalization: 'unbiased'}` 按样本计算)、分位数、协方差、相关系数、z 分数。
138
+ - 24 种概率分布,由 Boost.Math 计算,有密度、累积分布函数、分位数、各阶矩和抽样。各个库为分布选用的参数不同,所以这里的参数都用名字给出(`{shape, scale}`);README 列出了它们在 SciPy 中对应的写法。
139
+ - `stats.random(seed)` 得到一个随机数生成器(xoshiro256**):种子相同时,在任何平台上抽出的均匀随机数和整数都相同。它不能用于密码学。
140
+
141
+ ## 7. 数值解法
142
+
143
+ 方程或积分没有精确解,或者只需要数值时,`numeric` 可以直接用同样的公式:
144
+
145
+ ```typescript
146
+ import {numeric} from '@lakutata/math'
147
+
148
+ console.log(numeric.findRoot('cos(x) = x', 'x', {bracket: [0, 1]}).root) //0.7390851332151606
149
+ console.log(numeric.findRoot('x^5 - x - 1', 'x', {bracket: [1, 2]}).root) //1.167303978261426
150
+ const {solution, converged} = numeric.solveSystem(['x^2 + y^2 = 4', 'x*y = 1'], ['x', 'y'], [2, 0])
151
+ console.log(solution, converged) //Float64Array(2) [1.9318516525781366, 0.5176380902050416] true
152
+ console.log(numeric.integrate('e^(-x^2)*ln(1 + x)', 'x', [0, Infinity]).value) //0.36623762882311717
153
+ const decay = numeric.solveODE({derivatives: '-2y', states: 'y', span: [0, 1], initial: 1}, {points: [0.5, 1]})
154
+ console.log(decay.y.data) //Float64Array(2) [0.3678795037443847, 0.13533535227865426]
155
+ ```
156
+
157
+ - `polynomialRoots` 给出多项式的全部根,包括复根;`findRoot` 在一个有根区间里或者从一个初始猜测出发求根;`solveSystem` 求非线性方程组的一个解。
158
+ - `integrate` 用 QUADPACK 的方法计算定积分(和 SciPy 的 `quad` 相同),区间可以是无穷的,端点或指定的点上可以有奇点。
159
+ - `solveODE` 解常微分方程和方程组,用显式 Runge-Kutta 方法(和 SciPy 的 RK45 相同),刚性方程可以用 Radau IIA。
160
+ - 方法不收敛时不会抛出异常,而是在结果里说明(`converged`、`completed`)。
161
+
162
+ ## 8. 单位和物理常数
163
+
164
+ ```typescript
165
+ import {constant, unit} from '@lakutata/math'
166
+
167
+ console.log(unit('5 cm + 2 inch').toString()) //10.08 cm
168
+ console.log(unit('100 km/h').to('m/s').toString()) //(250/9) m / s
169
+ console.log(unit('20 degC').to('degF').toString()) //68 degF
170
+ console.log(unit('1.5 h').split(['h', 'min']).map(String)) //['1 h', '30 min']
171
+ console.log(constant('boltzmann').mul('300 K').to('eV').toDecimal(6)) //0.025852 eV
172
+ ```
173
+
174
+ 单位表来自 mathjs(251 个单位,可以带前缀),单位的值都是精确的:`1 inch` 正好是 `2.54 cm`。量纲不同的值不能相加,也不能互相换算。`constant()` 给出 mathjs 的 52 个物理常数,取 CODATA 2022 的值;`createMath({units})` 可以添加自己的单位。
175
+
176
+ ## 9. 数论和多项式
177
+
178
+ ```typescript
179
+ import {ntheory, poly} from '@lakutata/math'
180
+
181
+ console.log(ntheory.factor(360)) //[{prime: 2n, exponent: 3}, {prime: 3n, exponent: 2}, {prime: 5n, exponent: 1}]
182
+ console.log(ntheory.isPrime(2n ** 61n - 1n)) //true
183
+ console.log(ntheory.convergents(ntheory.continuedFraction('pi', {terms: 4})).map(String)) //['3', '22/7', '333/106', '355/113']
184
+ console.log(poly.gcd('x^4 - 1', 'x^6 - 1').toString()) //x^2 - 1
185
+ console.log(poly.discriminant('a*x^2 + b*x + c', 'x').toString()) //-4a*c + b^2
186
+ ```
187
+
188
+ `ntheory` 处理整数(bigint):整除、模运算、中国剩余定理、素数和因数分解、数列和计数、连分数。`poly` 处理多项式,变量个数不限:最大公因式和最小公倍式、结式、判别式和因式分解。
189
+
190
+ ## 10. 把公式变成函数
191
+
192
+ 需要多次求值的表达式,可以编译成数值函数,或者写成代码:
193
+
194
+ ```typescript
195
+ import {expression} from '@lakutata/math'
196
+
197
+ const f = expression('x^2 + sin(y)').compile(['x', 'y'])
198
+ console.log(f(2, 0)) //4
199
+ console.log(f.batch(Float64Array.of(1, 0, 2, 0, 3, 0))) //Float64Array(3) [1, 4, 9]
200
+ console.log(expression('x^2 - 5x + 6').toJavaScript()) //Math.pow(x, 2) - 5*x + 6
201
+ console.log(expression('sqrt(x^2 + y^2)').toC()) //sqrt(pow(x, 2.0) + pow(y, 2.0))
202
+ ```
203
+
204
+ `batch()` 一次调用就算出很多行的值。`cse()` 找出几个表达式共有的部分,这样由它们写出的代码中,每个部分只计算一次。
205
+
206
+ ## 11. 耗时的运算
207
+
208
+ 运算在 JavaScript 线程里执行,其中一些可能很耗时:大的乘方展开、积分、几千位数字、大数的因数分解。运算期间事件循环只能等着。可能耗时的运算都有异步版本(`expandAsync()`、`simplifyAsync()`、`integrateAsync()`、`solveAsync()`、`toDecimalAsync()` 等,以及 `linalg`、`ntheory`、`poly` 和 `numeric` 里的异步版本),它们在原生插件的线程里执行:
209
+
210
+ ```typescript
211
+ import {createMath, MathTimeoutException} from '@lakutata/math'
212
+
213
+ const scope = createMath({timeout: 2000, concurrency: 4})
214
+ const expanded = await scope.expression('(x + y + 1)^30').expandAsync()
215
+ console.log(expanded.variables()) //['x', 'y']
216
+ try {
217
+ await scope.expression('(x + y + z + 1)^500').expandAsync({timeout: 100})
218
+ } catch (error) {
219
+ console.log(error instanceof MathTimeoutException) //true
220
+ }
221
+ const controller: AbortController = new AbortController()
222
+ setTimeout((): void => controller.abort(), 50)
223
+ await scope.expression('(x + y + z + 1)^500').expandAsync({signal: controller.signal}).catch((error: Error): void => console.log(error.name)) //MathCancelledException
224
+ ```
225
+
226
+ - 原生插件的线程数是处理器核数减一,或者环境变量 `LAKUTATA_MATH_THREADS` 给出的数。它们不是 libuv 的线程池,所以程序的文件读写和 DNS 查询不用等它们。
227
+ - `timeout`(`createMath()` 的选项,也可以在单次调用时给出;默认不限时)到时后,运算以 `MathTimeoutException` 拒绝;`signal` 取消时,以 `MathCancelledException` 拒绝。等待空闲线程的时间不计入时限。
228
+ - 被取消的运算会在下一个检查点停下,检查点在运算的各个步骤之间。单个步骤(Eigen 对矩阵的一次分解、SymEngine 的一次求导)会执行到底,线程要到那时才空出来:Promise 在时限到时就被拒绝,但计算可能还要继续一会儿。
229
+ - `concurrency`(`createMath()` 的选项)限制一个范围里同时执行的运算数,其余的运算排队等待。
230
+
231
+ ## 12. 在应用中使用
232
+
233
+ 控制器调用这个包,和其他代码调用它没有区别。处理客户端发来的公式时,请用异步运算,并传入请求的 `signal`:客户端断开时,运算会被取消。
234
+
235
+ ```typescript
236
+ import {Application, DTO} from 'lakutata'
237
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
238
+ import {POST} from 'lakutata/decorator/ctrl'
239
+ import {Expect} from 'lakutata/decorator/dto'
240
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
241
+ import {createMath, type MathScope, type Solutions} from '@lakutata/math'
242
+
243
+ const scope: MathScope = createMath({timeout: 2000})
244
+
245
+ class SolveDTO extends DTO {
246
+ @Expect(DTO.String().max(1000).required())
247
+ public equation: string
248
+ }
249
+
250
+ class SolveController extends Controller {
251
+ @POST('/solve', SolveDTO)
252
+ public async solve(input: SolveDTO): Promise<{ solutions: Solutions, set: string }> {
253
+ const solutions: Solutions = await scope.equation(input.equation).solveAsync('x', {signal: this.signal})
254
+ return {solutions: solutions, set: solutions.set.toString()}
255
+ }
256
+ }
257
+
258
+ Application.run(() => ({
259
+ id: 'solver.app',
260
+ name: 'Solver',
261
+ components: {
262
+ entrypoint: buildEntrypoints({
263
+ controllers: [SolveController],
264
+ http: expressHTTPEntrypoint({port: 8080})
265
+ })
266
+ },
267
+ bootstrap: ['entrypoint']
268
+ }))
269
+ ```
270
+
271
+ ```text
272
+ $ curl -X POST localhost:8080/solve -H 'Content-Type: application/json' -d '{"equation":"x^2 - 5x + 6 = 0"}'
273
+ {"solutions":["2","3"],"set":"{2, 3}"}
274
+ $ curl -X POST localhost:8080/solve -H 'Content-Type: application/json' -d '{"equation":"x^2 - 5x +* 6 = 0"}'
275
+ {"errno":"E_MATH_SYNTAX","errMsg":"Unexpected *"}
276
+ ```
277
+
278
+ - 这个包的异常没有 `statusCode`,所以上面写错的公式得到的响应是状态码 500 和 `{"errno": "E_MATH_SYNTAX", …}`。如果要用 400 回应客户端公式里的错误,请在动作里捕获 `MathException`,再抛出应用自己的异常(见 [Exceptions](Exceptions.md));`E_MATH_INTERNAL` 是原生插件的内部错误,保持原样。
279
+ - 公式的长度限制不了它的计算量:`(x + 1)^100000` 很短。能限制计算量的是时限。
280
+ - 这个包的值通过 `toJSON()` 转成 JSON:表达式、方程、十进制数和带单位的值转成它们的文本,解转成文本数组,矩阵转成它的各行。向量是 `Float64Array`,`JSON.stringify()` 会把它写成对象(`{"0": 1, "1": 1}`):请发送 `Array.from(vector)`。
281
+ - `lakutata package` 打出的单文件包、可执行文件和安装包都带着编译好的原生插件(`build/Release/math.node`)。请在每个目标平台上安装这个包之后再打包;原生插件的规则同样适用(见 [Packaging](Packaging.md) 和 [NativeAddons](NativeAddons.md))。
282
+ - 这个包可以在 Node.js `^22.13.0 || >=24.11.0`、Bun 和 Electron 的主进程中运行,后两者直接加载为 Node.js 编译的插件;不能在 Electron 的渲染进程和浏览器中运行。在 Bun 里,worker 线程里的插件运算还没结束就终止这个线程的话,整个进程会因为 Node-API 的致命错误退出(这是 Bun 1.4 的问题)。
283
+
284
+ ## 13. 验证方式,以及不支持的情况
285
+
286
+ 这个包的测试用大约 2500 个用例,把它的结果和 SymPy、mpmath(公式、十进制数、单位),NumPy、SciPy(矩阵、统计、概率分布、数值解法),以及 mathjs 和 nerdamer 的结果比较;测试在 macOS(arm64:Node.js 24、Bun 1.4、Electron 44)、Linux(arm64,glibc 和 musl:Node.js 24、Bun 1.4)和 Windows 11(arm64:Node.js 24)上运行。
287
+
288
+ 它不支持的情况,有的是有意为之,有的是还没做:
289
+
290
+ - 分母要借助代数数才能分解的分式的积分(`1/(x^4 + 1)`),以及原函数既不是初等函数、也不是积分产生的特殊函数的积分,会抛出 `MathNotSupportedException`;求不出的积分不会保留成未计算的积分形式。定积分可以用 `numeric.integrate()` 计算。
291
+ - `solve()` 解单个方程或线性方程组;非线性方程组请用 `numeric.solveSystem()`。有无穷多个解的方程(如 `sin(x) = 0`)不给出解的数组:它的 `set` 只描述这些解(`{2pi*n + pi | n integer} or {2pi*n | n integer}`),集合的运算也不接受它。
292
+ - 不等式两边必须是有理系数的多项式分式。
293
+ - 表达式矩阵的精确特征值只能求到四次;数值特征值用 `linalg.eigen()` 求。
294
+ - 不做任何取决于变量符号的处理:`sqrt(x^2)` 不等于 `abs(x)`,取决于参数符号的极限(`exp(a*x)` 在 `inf` 处)求不出来。
295
+ - 精确数最多约 315000 位:更大的乘方保持乘方的形式(`2^(10^7)`)。
296
+
297
+ ## 14. 异常
298
+
299
+ 所有异常都继承 `MathException`。公式里的错误会给出出错字符的位置 `position`。每种异常的全部情况见这个包的 README。
300
+
301
+ | 异常 | `errno` | 什么时候 |
302
+ |---|---|---|
303
+ | `MathSyntaxException` | `E_MATH_SYNTAX` | 公式写得不对;需要表达式的地方给了方程,或者反过来 |
304
+ | `MathUnknownFunctionException` | `E_MATH_UNKNOWN_FUNCTION` | 公式调用了一个既不是内置、也没有定义的函数 |
305
+ | `MathDefinitionException` | `E_MATH_DEFINITION` | `createMath()` 的定义或单位无效 |
306
+ | `MathUnitException` | `E_MATH_UNIT` | 量纲不同的单位相加、比较或换算;未知的单位 |
307
+ | `MathDomainException` | `E_MATH_DOMAIN` | 值超出函数的定义域:把复数值当作数取用、对奇异矩阵求逆、极限不存在 |
308
+ | `MathDivisionByZeroException` | `E_MATH_DIVISION_BY_ZERO` | 计算十进制值时除以零、模为 0 |
309
+ | `MathNotSupportedException` | `E_MATH_NOT_SUPPORTED` | 这个包对这个公式不做的运算(第 13 节) |
310
+ | `MathArgumentException` | `E_MATH_ARGUMENT` | 参数无效:需要数的地方还剩下变量、矩阵的维数不对 |
311
+ | `MathTimeoutException`、`MathCancelledException` | `E_MATH_TIMEOUT`、`E_MATH_CANCELLED` | 异步运算超过时限,或者被取消 |
312
+ | `MathException` | `E_MATH_INTERNAL` | 原生插件的内部错误 |
@@ -175,6 +175,7 @@ lakutata native link libc --crate # 一个 crate(Rust:carg
175
175
  应用依赖这个插件包,并像导入其他包一样导入它(`import {greet} from '@acme/capture'`)。
176
176
 
177
177
  - **单文件包和可执行文件**:`lakutata package bundle` 把插件包排除在单文件包之外,连同它们的文件(JavaScript、插件本身、它的动态库,但不包括源码和构建时产生的文件)写入 `dist/<name>.native/`,这个目录随单文件包一起发布;可执行文件则把它们嵌入其中,或者在目录布局中放在自己旁边(见 [Packaging](Packaging.md))。插件是为打包所在机器的平台构建的,所以每个平台的应用要在那个平台上打包(用 CI 的矩阵,Linux 也可以用对应架构的容器)。
178
+ - **基于框架的插件包**(它依赖 `lakutata` 或 `@lakutata/core`,比如为了提供一个组件)则和应用一起打进单文件包,这样框架只有一份,只有插件和它旁边的文件放进这个目录。插件要用下面几种方式之一加载,才会被放进去:像 `lakutata add` 生成的插件包那样,把包的目录交给 `node-gyp-build`;直接 `require` 插件文件;或者用从模块位置构造的路径(`path.join(__dirname, 'prebuilds', ...)`)。用其他方式加载的插件,`lakutata package` 会发出警告。
178
179
  - **Electron**:插件按安装时的样子复制。`lakutata add` 创建的插件不需要重新构建就能在 Electron 中运行,所有基于 Node-API 的插件都是如此;基于 Node.js V8 API 的插件,打包前要安装它为 Electron 构建的版本(`electron-rebuild`)。
179
180
  - **Bun**:Bun 能加载 Node-API 的插件;Bun 可执行文件会对基于 Node.js V8 API 的插件发出警告。
180
181
  - **Alpine(musl)**:Node.js 的可执行文件在那里不能加载原生插件,请用 Bun 制作。
@@ -0,0 +1,207 @@
1
+ # Lakutata Netif
2
+
3
+ `@lakutata/netif` 包让 Node.js 的服务器按网口监听,而不是按地址监听。有多个网口的设备,常常只在其中一个网口上提供管理页面:这个网口的地址来自 DHCP,会变,设备开机时可能还没有;而监听 `0.0.0.0` 又会让管理页面出现在所有网口上。用这个包,服务器监听的是“eth1 这个网口”,不管它现在是什么地址,地址变化、网口拔掉再插回,都会继续监听。它不属于 `lakutata`,也不需要应用。它的特点是:
4
+
5
+ - **任何服务器都能用**:`http`、`https`、`http2`、`tls`、Express 和 Fastify 都把这个网口的连接当成自己的连接处理;UDP 套接字也可以,适合只在一个网口上工作的服务(DHCP、mDNS、SSDP)。
6
+ - **lakutata 的入口**有 `interface` 选项,可以让应用的控制器只在一个网口上提供服务(第 5 节)。
7
+ - **系统能挡的,就让系统挡住别的网口**:在 Linux 上,套接字绑定在网口上,从别的网口发往这个网口地址的连接会被拒绝。在 Windows 和 macOS 上,监听的是这个网口的地址,`enforced` 告诉你系统会不会挡住那样的连接。
8
+ - **网口和它们的变化**:本机每个网口的状态,以及地址、连通状态、网口出现和消失时的事件,都来自系统的通知。
9
+ - **原生插件**:在安装这个包时编译,支持 Linux、macOS 和 Windows;也能在 Bun 和 Electron 的主进程里运行。
10
+
11
+ `@lakutata/netif` 的 README 介绍了每个函数及其选项;这篇指南按用途逐一说明这个包能做什么,每种用途都给出一个例子。
12
+
13
+ ```bash
14
+ npm install @lakutata/netif
15
+ ```
16
+
17
+ 原生插件在安装这个包时编译,所以机器上要有 Python 3 和支持 C++17 的编译器(Alpine Linux 还要内核的头文件:`apk add python3 make g++ linux-headers`)。包管理器必须运行这个包的安装脚本:npm 会运行它,可能会提示这个脚本“not yet covered by allowScripts”(运行 `npm install-scripts approve @lakutata/netif` 记录你允许它运行);Bun 和 pnpm 只为被告知可以信任的包运行安装脚本(Bun 在 package.json 的 `trustedDependencies` 里设置,pnpm 用 `pnpm approve-builds`)。它的类型引用了 `@lakutata/core` 的类型,这是一个 peer 依赖,包管理器会一起安装;这个包运行时不会加载它。
18
+
19
+ ## 1. 在网口上监听
20
+
21
+ `listenOn()` 让一个服务器在网口上监听。网口可以按名字、MAC 地址(`{mac: '52:54:00:12:34:56'}`,不随系统给网口起的名字变化)或序号指定;Windows 上的名字是网络连接的名字(`'以太网 2'`、`'Ethernet 2'`)。
22
+
23
+ ```typescript
24
+ import {createServer} from 'node:http'
25
+ import express from 'express'
26
+ import {type InterfaceListener, listenOn} from '@lakutata/netif'
27
+
28
+ const app = express()
29
+ app.get('/', (request, response) => response.send(`served on ${request.socket.localAddress}`))
30
+
31
+ const listener: InterfaceListener = await listenOn('eth1', createServer(app), {port: 8080})
32
+ listener.on('error', error => console.error(error))
33
+ console.log(listener.port, listener.addresses)
34
+ ```
35
+
36
+ - 监听器接受连接后交给服务器,服务器像处理自己的连接一样处理它们。服务器自己并没有在监听:它的 `address()` 是 `null`,`server.close()` 也不能让监听器停下;请用 `listener.port`、`listener.addresses` 和 `listener.close()`。
37
+ - 网口可以暂时不存在,也可以还没有地址:`listenOn()` 会立即返回,等网口能监听了就开始监听。网口拔掉再插回、地址变化时,会自动重新监听;已经建立的连接交给服务器处理。
38
+ - 第一次监听的错误(比如端口被占用)会直接抛出;之后的错误是 `error` 事件,过一会儿会重试。请给监听器加上 `error` 的监听函数,就像对 `net.Server` 那样。
39
+ - `family` 选择 `'IPv4'`、`'IPv6'` 或 `'both'`(默认);`port: 0` 让系统选一个端口,重新监听时保持同一个端口。
40
+
41
+ ## 2. 哪些连接会被挡住
42
+
43
+ 另一个网络的客户端,可以自己加一条经过设备的路由,把发往管理网口地址的包从设备的另一个网口送进来。系统会不会拒绝这样的连接,就是 `enforced` 回答的问题:
44
+
45
+ | | `enforced` |
46
+ |---|---|
47
+ | Linux | `true`:套接字绑定在网口上(`SO_BINDTODEVICE`);Linux 5.7 以前这需要 `CAP_NET_RAW` 能力,没有的话就按地址监听,`enforced` 为 `false` |
48
+ | Windows | 其他网口都按强主机模型收包、都没开转发时为 `true`,默认就是这样 |
49
+ | macOS | 默认为 `false`;只有在系统设置为拒绝这样的包(`net.inet.ip.check_interface` 设为 2,IPv6 还有 `net.inet6.ip6.check_interface`)并且没开转发时才为 `true` |
50
+
51
+ 默认按系统允许的最好方式监听,无论如何都不会比用 `listen(port, host)` 按地址监听差;`reason` 说明为什么没有保证,以及怎样才能有。如果管理网口是一道安全边界,用 `strict: true`:没有保证就拒绝监听,运行中保证丢了就停止监听:
52
+
53
+ ```typescript
54
+ import {createServer} from 'node:http'
55
+ import {type InterfaceListener, NetIfNotEnforcedException, NetworkInterface} from '@lakutata/netif'
56
+
57
+ const management: NetworkInterface = new NetworkInterface({mac: '52:54:00:12:34:56'})
58
+ try {
59
+ const listener: InterfaceListener = await management.listen(createServer(), {port: 443, strict: true})
60
+ listener.on('error', error => console.error(error))
61
+ listener.on('enforced', enforced => console.log(enforced ? 'guaranteed again' : 'stopped: no longer guaranteed'))
62
+ } catch (error) {
63
+ if (error instanceof NetIfNotEnforcedException) console.error(error.message)
64
+ throw error
65
+ }
66
+ ```
67
+
68
+ ## 3. 跟踪一个网口
69
+
70
+ `NetworkInterface` 给出一个网口现在的状态(`present`、`running`、`addresses` 等),并在每次变化时发出事件。事件来自系统的通知,不靠定时轮询。
71
+
72
+ ```typescript
73
+ import {type InterfaceAddress, listInterfaces, NetworkInterface} from '@lakutata/netif'
74
+
75
+ console.log(listInterfaces().map(entry => `${entry.name}: ${entry.running ? 'connected' : 'not connected'}`))
76
+
77
+ const wan: NetworkInterface = new NetworkInterface('eth0')
78
+ wan.on('change', ({added, removed}) => {
79
+ for (const address of added) console.log(`new address ${address.cidr}`)
80
+ for (const address of removed) console.log(`address gone ${address.cidr}`)
81
+ })
82
+ wan.on('down', () => console.log('the cable of eth0 is unplugged'))
83
+ wan.on('up', () => console.log('the cable of eth0 is plugged in'))
84
+ const address: InterfaceAddress = await wan.waitForAddress({family: 'IPv4', timeout: 60000})
85
+ console.log(`reachable at ${address.address}`)
86
+ ```
87
+
88
+ - `listInterfaces()` 也列出没有地址的网口,并带上它们的状态;`os.networkInterfaces()` 只列出有地址的网口。
89
+ - IPv6 地址等它可用后才算:加上后大约一秒,重复地址检测结束时。
90
+ - `close()` 停止跟踪这个网口(它的监听器也会关闭)。
91
+
92
+ ## 4. 网口上的 UDP
93
+
94
+ `createUDPSocket()` 返回这个网口的 UDP 套接字,方法和事件和 `dgram` 的套接字一样。下面是一个只回答管理网络里发现广播的设备:
95
+
96
+ ```typescript
97
+ import {type InterfaceUDPSocket, NetworkInterface} from '@lakutata/netif'
98
+
99
+ const management: NetworkInterface = new NetworkInterface('eth1')
100
+ const socket: InterfaceUDPSocket = await management.createUDPSocket({port: 30303, family: 'IPv4'})
101
+ socket.setBroadcast(true)
102
+ socket.on('message', (message, remote) => {
103
+ const answer: string = `device at ${management.addresses[0]?.address}`
104
+ if (message.toString() === 'who is there') socket.send(answer, remote.port, remote.address)
105
+ })
106
+ socket.on('error', error => console.error(error))
107
+ ```
108
+
109
+ 在 Linux 上,这个套接字只接收从这个网口进来的数据,包括这个网口上的广播和用 `addMembership()` 加入的组播组,别的网口来的一概收不到。在 Windows 和 macOS 上,网口的每个地址各绑一个套接字;macOS 上这样的套接字收不到广播和组播。
110
+
111
+ ## 5. 在应用中使用
112
+
113
+ lakutata 里自己监听端口的入口都有 `interface` 选项:`expressHTTPEntrypoint()`、`fastifyHTTPEntrypoint()`、`httpServiceEntrypoint()`、`socketIOServiceEntrypoint()`、`jsonRPCServiceEntrypoint()`、`@lakutata/grpc` 的 `grpcServiceEntrypoint()` 和 `@lakutata/mcp` 的 `httpMCPEntrypoint()`。设置后,控制器的动作就在这个网口上提供,不管它是什么地址:
114
+
115
+ ```typescript
116
+ import {Application} from 'lakutata'
117
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
118
+ import {GET} from 'lakutata/decorator/ctrl'
119
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
120
+
121
+ class ManagementController extends Controller {
122
+ @GET('/status')
123
+ public async status(): Promise<{ ok: boolean }> {
124
+ return {ok: true}
125
+ }
126
+ }
127
+
128
+ Application.run(() => ({
129
+ id: 'device.app',
130
+ name: 'Device',
131
+ components: {
132
+ entrypoint: buildEntrypoints({
133
+ controllers: [ManagementController],
134
+ http: expressHTTPEntrypoint({port: 8080, interface: 'eth1'})
135
+ })
136
+ },
137
+ bootstrap: ['entrypoint']
138
+ }))
139
+ ```
140
+
141
+ - `family`、`strict` 和 `onError` 与 `interface` 配合使用。没有设置 `onError` 时,重新监听的错误会写到应用的日志里。适配器的 `listening` 回调会拿到监听器。这些选项的说明见 [HTTP](HTTP.md) 第 2 节。
142
+ - 网口还不存在时应用也能启动,网口出现后就能访问它的动作。
143
+ - 入口通过 `IInterfaceListening` 按网口监听。这是 lakutata 定义的接口(来自 `@lakutata/core`),这个包实现了它,入口默认加载这个包。另一种实现可以用入口的 `interfaceListening` 选项给出;这个包的 `InterfaceListener` 就是一个 `IInterfaceListener`。
144
+
145
+ 不属于入口的服务器由一个组件持有:在 `startup()` 里开始监听,在 `destroy()` 里停止。网口和端口作为组件的选项。
146
+
147
+ ```typescript
148
+ import {createServer} from 'node:http'
149
+ import express from 'express'
150
+ import {Application, Component, DTO, type ILogger} from 'lakutata'
151
+ import {Configurable, Inject} from 'lakutata/decorator/di'
152
+ import {type InterfaceListener, listenOn} from '@lakutata/netif'
153
+
154
+ class ManagementPages extends Component {
155
+ @Inject('log')
156
+ protected readonly log: ILogger
157
+
158
+ @Configurable(DTO.String().required())
159
+ protected readonly interfaceName: string
160
+
161
+ @Configurable(DTO.Number().port().default(8080))
162
+ protected readonly port: number
163
+
164
+ protected listener: InterfaceListener
165
+
166
+ protected async startup(): Promise<void> {
167
+ const app = express()
168
+ app.get('/', (request, response) => response.send('management'))
169
+ this.listener = await listenOn(this.interfaceName, createServer(app), {port: this.port})
170
+ this.listener.on('error', (error: Error): void => this.log.error(error.message))
171
+ this.listener.on('listening', (): void => this.log.info('listening on %o', this.listener.addresses))
172
+ this.log.info('enforced: %s', this.listener.enforced)
173
+ }
174
+
175
+ protected async destroy(): Promise<void> {
176
+ await this.listener?.close()
177
+ }
178
+ }
179
+
180
+ Application.run(() => ({
181
+ id: 'device.app',
182
+ name: 'Device',
183
+ components: {
184
+ management: {class: ManagementPages, interfaceName: 'eth1', port: 8080}
185
+ },
186
+ bootstrap: ['management']
187
+ }))
188
+ ```
189
+
190
+ ## 6. 系统、运行环境和打包
191
+
192
+ - **Linux** 把套接字绑定在网口上:系统会挡住别的网口,新加的地址立刻就能访问,UDP 能收到这个网口的广播和组播。Linux 5.7 以前,请给进程 `CAP_NET_RAW` 能力(`setcap cap_net_raw+ep`,或在 systemd 单元里写 `AmbientCapabilities=CAP_NET_RAW`)。
193
+ - **Windows** 监听网口的地址;它的强主机模型会挡住别的网口,除非有网口被设成了弱主机模型(`netsh interface ipv4 set interface "<名字>" weakhostreceive=enabled`,有些负载均衡软件会这样做)或者开了转发。`reason` 会说出是哪个网口。
194
+ - **macOS** 监听网口的地址,除非系统设置了,否则没有保证。macOS 27 对进程隐藏 MAC 地址(给出的都是 `02:00:00:00:00:00`):在那里请按名字指定网口。
195
+ - **Bun**:它的 `https` 服务器收到交过来的连接时不做 TLS,所以在 Bun 上 `listen()` 会拒绝它(用 `https` 选项创建的 Fastify 实例也一样);在 Bun 上请改用 `http2.createSecureServer({key, cert, allowHTTP1: true})` 或 `tls.createServer()`。Bun 的服务器不会关闭交给它的空闲长连接,入口会自己关闭它们。交给服务器的连接不能升级为 WebSocket(在 Bun 1.4 上观察到),所以在 Bun 上,入口按网口监听时不支持 WebSocket。
196
+ - **Electron**:在主进程里使用。
197
+ - `lakutata package` 生成的打包文件、可执行文件和安装包会带上编译好的原生插件(`build/Release/netif.node`):请在每个目标平台上先安装这个包,再在那里打包(见[打包](Packaging.md)和[原生插件](NativeAddons.md))。
198
+
199
+ ## 7. 异常
200
+
201
+ | 异常 | 什么时候 |
202
+ |---|---|
203
+ | `NetIfNotFoundException` | 指定的序号没有网口,指定的 MAC 有几个网口 |
204
+ | `NetIfNotEnforcedException` | 用了 `strict`,但系统挡不住别的网口;或者运行中保证丢了(`error` 事件) |
205
+ | `NetIfNotSupportedException` | 系统或运行环境做不到的操作:在 Linux 以外调用 `bindSocket()`,在 macOS 上给绑定在地址上的套接字加组播,在 Bun 上监听 `https` 服务器 |
206
+ | `NetIfTimeoutException` | `waitForAddress()` 超过了 `timeout` |
207
+ | `NetIfException` | 以上所有异常的基类;系统拒绝时,`errno` 是系统的错误码(`'EADDRINUSE'`、`'EACCES'`…) |
@@ -35,7 +35,7 @@
35
35
  | `lakutata/security`、`lakutata/provider/passwordHash` | 哈希、加解密、签名、`PasswordHash` 提供者 |
36
36
  | `lakutata/utils` | 辅助函数 |
37
37
 
38
- 不在本包中的:`@lakutata/cli`(`lakutata` 命令:创建项目、执行迁移、打包应用,见 [Command](Command.md))、`@lakutata/grpc`(gRPC 客户端、服务端和服务传输)、`@lakutata/nats`(NATS 组件和服务传输)、`@lakutata/dbus`(D-Bus)、`@lakutata/mcp`(Model Context Protocol)、`@lakutata/webrtc`(WebRTC:信令、TURN 服务器、媒体服务器、对等连接)和 `@lakutata/ffi`(调用动态库中的 C 函数)。
38
+ 不在本包中的:`@lakutata/cli`(`lakutata` 命令:创建项目、执行迁移、打包应用,见 [Command](Command.md))、`@lakutata/grpc`(gRPC 客户端、服务端和服务传输)、`@lakutata/nats`(NATS 组件和服务传输)、`@lakutata/dbus`(D-Bus)、`@lakutata/mcp`(Model Context Protocol)、`@lakutata/webrtc`(WebRTC:信令、TURN 服务器、媒体服务器、对等连接)、`@lakutata/ffi`(调用动态库中的 C 函数)、`@lakutata/math`(符号计算和数值计算,直接作为函数使用,不需要 lakutata 应用)和 `@lakutata/netif`(服务器和 UDP 套接字按网口监听,不管网口的地址怎么变;本机的网口和它们的变化)。
39
39
 
40
40
  ## 指南
41
41
 
@@ -80,6 +80,8 @@
80
80
  | [MCP](MCP.md) | 模型上下文协议(`@lakutata/mcp`)的工具、资源和提示词,基于 stdio 和 HTTP |
81
81
  | [WebRTC](WebRTC.md) | 点对点连接和媒体服务器(`@lakutata/webrtc`):信令、TURN 服务器、浏览器和应用、通过数据通道调用应用的动作、WHIP 和 WHEP |
82
82
  | [FFI](FFI.md) | 调用动态库中的 C 函数(`@lakutata/ffi`):从头文件复制的声明、结构体、回调、内存、打包 |
83
+ | [Math](Math.md) | 数学计算(`@lakutata/math`):公式和精确结果、方程、任意精度的十进制数、矩阵、统计、数值解法、单位 |
84
+ | [Netif](Netif.md) | 网口(`@lakutata/netif`):服务器按网口监听,不管网口的地址怎么变;挡住别的网口的连接;网口和它们的变化;单个网口上的 UDP |
83
85
  | [Command](Command.md) | `lakutata` 命令:创建项目、迁移、翻译、环境变量 |
84
86
  | [Workspaces](Workspaces.md) | 由多个包组成的项目:运行各个包的脚本、统一版本号、添加包 |
85
87
  | [NativeAddons](NativeAddons.md) | 用 C++ 或 Rust 编写的原生插件:创建、链接库、发布 |
@@ -181,7 +181,7 @@ export async function call<T>(message: Record<string, unknown>): Promise<T> {
181
181
  | `host` | 所有网络接口 | 监听的地址 |
182
182
  | `path` | `'/'` | 请求的路径;事件在它下面的 `<path>/events` |
183
183
  | `limit` | `10485760`(10 MiB) | 请求体的最大字节数 |
184
- | `listening` | | `(server) => …`,服务器开始监听后调用 |
184
+ | `listening` | | `(server, listeners) => …`,服务器开始监听后调用(`listeners`:按网口监听时才有,见下文) |
185
185
 
186
186
  - 请求是发往 `path` 的 `POST`,请求体是 JSON 格式的消息。无论动作的结果如何,都以 200 和封套响应。请求体不是 JSON 时以 400 响应,超过 `limit` 时以 413 响应,其他方法或路径以 404 响应;这些响应也带有封套。
187
187
  - CORS 响应头允许任何来源,允许的方法是 `GET`、`POST` 和 `OPTIONS`,并允许任何请求头,例如 `Authorization`。
@@ -255,7 +255,7 @@ Application
255
255
  | `options` | | Socket.IO 服务器的选项:`cors`、`path`、`maxHttpBufferSize`…… |
256
256
  | `server` | | 共用的 HTTP 服务器,或者返回它的函数(见下文) |
257
257
  | `io` | | 一个已有的 Socket.IO 服务器,按原样使用 |
258
- | `listening` | | `(io, server) => …`,适配器就绪后调用 |
258
+ | `listening` | | `(io, server, listeners) => …`,适配器就绪后调用 |
259
259
 
260
260
  ```typescript
261
261
  import {io, type Socket} from 'socket.io-client'
@@ -307,7 +307,7 @@ export const entrypoint = buildEntrypoints({
307
307
  | `input`、`output` | `process.stdin`、`process.stdout` | stdio 使用的流 |
308
308
  | `end` | `'application'` | stdio 的输入结束时:`'application'` 在正在处理的请求都得到响应后退出应用,`'none'` 什么都不做,或者一个函数 |
309
309
  | `heartbeat` | `30000` | WebSocket 的 ping 间隔,单位毫秒;`0` 表示不发送 |
310
- | `listening` | | `(server) => …`,HTTP 或 WebSocket 的服务器开始监听后调用 |
310
+ | `listening` | | `(server, listeners) => …`,HTTP 或 WebSocket 的服务器开始监听后调用 |
311
311
 
312
312
  请求的方法名和参数构成消息:`{"method": "order.get", "params": {"id": 1}}` 就是消息 `{method: 'order.get', id: 1}`,所以动作要在模式中声明方法名:
313
313
 
@@ -378,6 +378,19 @@ $ echo '{"jsonrpc": "2.0", "id": 1, "method": "sum", "params": {"values": [1, 2,
378
378
 
379
379
  输入结束、请求都得到响应之后,应用就会退出(`end`)。
380
380
 
381
+ ### 按网口监听
382
+
383
+ `httpServiceEntrypoint()`、使用自己服务器的 `socketIOServiceEntrypoint()`,以及走 HTTP 或 WebSocket 的 `jsonRPCServiceEntrypoint()`,都可以按网口监听,而不是按地址监听,不管网口现在是什么地址。这需要另外安装 `@lakutata/netif` 包。它们接受和 HTTP 适配器一样的 `interface`、`family`、`strict`、`onError` 和 `interfaceListening` 选项(见 [HTTP](HTTP.md) 第 2 节),`listening` 回调的最后一个参数是网口的监听器:
384
+
385
+ ```typescript
386
+ import {httpServiceEntrypoint} from 'lakutata/com/entrypoint'
387
+
388
+ //服务只在 eth1 上提供,不管它是什么地址
389
+ export const service = httpServiceEntrypoint({port: 3000, interface: 'eth1'})
390
+ ```
391
+
392
+ 在 Bun 上,交给服务器的连接不能升级为 WebSocket(在 Bun 1.4 上观察到)。所以在 Bun 上,走 WebSocket 的 JSON-RPC 会启动失败;Socket.IO 也会启动失败,除非只用轮询(`options: {transports: ['polling']}`,客户端也要这样设置)。
393
+
381
394
  ### Electron
382
395
 
383
396
  `electronIPCServiceEntrypoint()` 在 Electron 应用的主进程中,向它的窗口提供动作。