@nxtedition/lib 26.4.4 → 26.4.6
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/app.js +21 -3
- package/http.js +12 -0
- package/package.json +1 -1
- package/rxjs/auditMap.js +27 -1
- package/rxjs/auditMap.test.js +64 -0
- package/rxjs/retry.js +112 -0
- package/s3.js +9 -0
- package/serializers.js +0 -2
package/app.js
CHANGED
|
@@ -129,8 +129,26 @@ export function makeApp(appConfig, onTerminate) {
|
|
|
129
129
|
const serviceModule = appConfig.module ?? 'main'
|
|
130
130
|
const serviceWorkerId = appConfig.workerId ?? threadId
|
|
131
131
|
const serviceInstanceId =
|
|
132
|
-
|
|
133
|
-
appConfig.
|
|
132
|
+
appConfig.instanceId ??
|
|
133
|
+
appConfig.containerId ??
|
|
134
|
+
(() => {
|
|
135
|
+
if (!isProduction) {
|
|
136
|
+
// We are not running in docker, so we use pid as instance id.
|
|
137
|
+
return `pid${process.pid}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Normally we have the container ID as hostname, but that can be changed
|
|
142
|
+
// so we read it from mountinfo instead. This is not 100% reliable as it
|
|
143
|
+
// depends on the container runtime.
|
|
144
|
+
return fs
|
|
145
|
+
.readFileSync('/proc/self/mountinfo', { encoding: 'utf8' })
|
|
146
|
+
.match(/[/]([0-9a-f]{64})[/]hostname/)[1]
|
|
147
|
+
.slice(0, 12)
|
|
148
|
+
} catch {}
|
|
149
|
+
|
|
150
|
+
return os.hostname()
|
|
151
|
+
})()
|
|
134
152
|
|
|
135
153
|
if (!serviceName) {
|
|
136
154
|
throw new Error('Service name is required')
|
|
@@ -141,7 +159,7 @@ export function makeApp(appConfig, onTerminate) {
|
|
|
141
159
|
(serviceName &&
|
|
142
160
|
`${serviceName}/${
|
|
143
161
|
serviceVersion || '*'
|
|
144
|
-
} (module:${serviceModule}; instance:${serviceInstanceId}
|
|
162
|
+
} (module:${serviceModule}; instance:${serviceInstanceId}; worker:${serviceWorkerId}) Node/${process.version}`) ??
|
|
145
163
|
null)
|
|
146
164
|
|
|
147
165
|
if (isMainThread && serviceName) {
|
package/http.js
CHANGED
|
@@ -423,6 +423,7 @@ export class ServerResponse extends http.ServerResponse {
|
|
|
423
423
|
|
|
424
424
|
/**
|
|
425
425
|
* @returns {{
|
|
426
|
+
* created: number,
|
|
426
427
|
* connect: number,
|
|
427
428
|
* headers: number,
|
|
428
429
|
* data: number,
|
|
@@ -431,6 +432,7 @@ export class ServerResponse extends http.ServerResponse {
|
|
|
431
432
|
*/
|
|
432
433
|
get timing() {
|
|
433
434
|
return {
|
|
435
|
+
created: performance.timeOrigin + this.#created,
|
|
434
436
|
connect: this.#connect,
|
|
435
437
|
headers: this.#headers,
|
|
436
438
|
data: this.#data,
|
|
@@ -656,8 +658,18 @@ export class Http2ServerResponse extends http2.Http2ServerResponse {
|
|
|
656
658
|
#data = -1
|
|
657
659
|
#end = -1
|
|
658
660
|
|
|
661
|
+
/**
|
|
662
|
+
* @returns {{
|
|
663
|
+
* created: number,
|
|
664
|
+
* connect: number,
|
|
665
|
+
* headers: number,
|
|
666
|
+
* data: number,
|
|
667
|
+
* end: number
|
|
668
|
+
* }}
|
|
669
|
+
*/
|
|
659
670
|
get timing() {
|
|
660
671
|
return {
|
|
672
|
+
created: performance.timeOrigin + this.#created,
|
|
661
673
|
connect: this.#connect,
|
|
662
674
|
headers: this.#headers,
|
|
663
675
|
data: this.#data,
|
package/package.json
CHANGED
package/rxjs/auditMap.js
CHANGED
|
@@ -5,6 +5,7 @@ function auditMapImpl(project) {
|
|
|
5
5
|
let pendingValue = null
|
|
6
6
|
let hasPendingValue = false
|
|
7
7
|
let isComplete = false
|
|
8
|
+
let abortController = null
|
|
8
9
|
|
|
9
10
|
let innerSubscription = null
|
|
10
11
|
let outerSubscription = null
|
|
@@ -15,6 +16,7 @@ function auditMapImpl(project) {
|
|
|
15
16
|
|
|
16
17
|
function _innerComplete() {
|
|
17
18
|
innerSubscription = null
|
|
19
|
+
abortController = null
|
|
18
20
|
|
|
19
21
|
if (hasPendingValue) {
|
|
20
22
|
const value = pendingValue
|
|
@@ -30,9 +32,30 @@ function auditMapImpl(project) {
|
|
|
30
32
|
o.next(val)
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
function _drain() {
|
|
36
|
+
if (!hasPendingValue) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const value = pendingValue
|
|
40
|
+
pendingValue = null
|
|
41
|
+
hasPendingValue = false
|
|
42
|
+
return { value }
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
function _tryNext(value) {
|
|
34
46
|
try {
|
|
35
|
-
const result = project(value
|
|
47
|
+
const result = project(value, {
|
|
48
|
+
get signal() {
|
|
49
|
+
if (!abortController) {
|
|
50
|
+
abortController = new AbortController()
|
|
51
|
+
if (hasPendingValue) {
|
|
52
|
+
abortController.abort()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return abortController.signal
|
|
56
|
+
},
|
|
57
|
+
_drain,
|
|
58
|
+
})
|
|
36
59
|
const observable = typeof result.then === 'function' ? from(result) : result
|
|
37
60
|
innerSubscription = observable.subscribe({
|
|
38
61
|
next: _innerNext,
|
|
@@ -51,6 +74,7 @@ function auditMapImpl(project) {
|
|
|
51
74
|
if (innerSubscription) {
|
|
52
75
|
pendingValue = value
|
|
53
76
|
hasPendingValue = true
|
|
77
|
+
abortController?.abort()
|
|
54
78
|
} else {
|
|
55
79
|
_tryNext(value)
|
|
56
80
|
}
|
|
@@ -60,6 +84,8 @@ function auditMapImpl(project) {
|
|
|
60
84
|
isComplete = true
|
|
61
85
|
if (!innerSubscription) {
|
|
62
86
|
o.complete()
|
|
87
|
+
} else {
|
|
88
|
+
abortController?.abort()
|
|
63
89
|
}
|
|
64
90
|
}
|
|
65
91
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import auditMap from './auditMap.js'
|
|
4
|
+
import * as rxjs from 'rxjs'
|
|
5
|
+
import tp from 'node:timers/promises'
|
|
6
|
+
|
|
7
|
+
test('auditMap sync', (t) => {
|
|
8
|
+
t.plan(1, { wait: true })
|
|
9
|
+
rxjs
|
|
10
|
+
.of(1, 2, 3)
|
|
11
|
+
.pipe(
|
|
12
|
+
auditMap((val) => rxjs.of(val * 2)),
|
|
13
|
+
rxjs.toArray(),
|
|
14
|
+
)
|
|
15
|
+
.subscribe((val) => {
|
|
16
|
+
t.assert.deepStrictEqual(val, [2, 4, 6])
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('auditMap async', (t) => {
|
|
21
|
+
t.plan(1, { wait: true })
|
|
22
|
+
rxjs
|
|
23
|
+
.of(1, 2, 3)
|
|
24
|
+
.pipe(
|
|
25
|
+
auditMap(async (val) => val * 2),
|
|
26
|
+
rxjs.toArray(),
|
|
27
|
+
)
|
|
28
|
+
.subscribe((val) => {
|
|
29
|
+
t.assert.deepStrictEqual(val, [2, 6])
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('auditMap drain', (t) => {
|
|
34
|
+
t.plan(1, { wait: true })
|
|
35
|
+
rxjs
|
|
36
|
+
.of(1, 2, 3)
|
|
37
|
+
.pipe(
|
|
38
|
+
auditMap(async (val, { _drain }) => {
|
|
39
|
+
await tp.setTimeout(1)
|
|
40
|
+
return _drain()?.value ?? val
|
|
41
|
+
}),
|
|
42
|
+
rxjs.toArray(),
|
|
43
|
+
)
|
|
44
|
+
.subscribe((val) => {
|
|
45
|
+
t.assert.deepStrictEqual(val, [3])
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('auditMap signal', (t) => {
|
|
50
|
+
t.plan(1, { wait: true })
|
|
51
|
+
rxjs
|
|
52
|
+
.of(1, 2, 3)
|
|
53
|
+
.pipe(
|
|
54
|
+
auditMap(async (val, { signal }) => {
|
|
55
|
+
await tp.setTimeout(1)
|
|
56
|
+
assert.strictEqual(signal.aborted, val === 1)
|
|
57
|
+
return val
|
|
58
|
+
}),
|
|
59
|
+
rxjs.toArray(),
|
|
60
|
+
)
|
|
61
|
+
.subscribe((val) => {
|
|
62
|
+
t.assert.deepStrictEqual(val, [1, 3])
|
|
63
|
+
})
|
|
64
|
+
})
|
package/rxjs/retry.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { operate } from 'rxjs/internal/util/lift'
|
|
2
|
+
import { createOperatorSubscriber } from 'rxjs/internal/operators/OperatorSubscriber'
|
|
3
|
+
import { identity, timer } from 'rxjs'
|
|
4
|
+
import { innerFrom } from 'rxjs/internal/observable/innerFrom'
|
|
5
|
+
|
|
6
|
+
// This is from https://github.com/ReactiveX/rxjs/blob/7.x/src/internal/operators/retry.ts
|
|
7
|
+
// But with some custom code added to emit a value on each retry attempt.
|
|
8
|
+
// The default delay function is also changed to use exponential backoff, capped at 1 minute.
|
|
9
|
+
//
|
|
10
|
+
// It uses internals from rxjs, but otherwise the code would have to be modified more heavily.
|
|
11
|
+
|
|
12
|
+
const DEFAULT_DELAY = (err, retryCount) => {
|
|
13
|
+
return timer(Math.min(2 ** (retryCount - 1) * 1000, 60_000))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function retry(configOrCount) {
|
|
17
|
+
let config
|
|
18
|
+
if (configOrCount && typeof configOrCount === 'object') {
|
|
19
|
+
config = configOrCount
|
|
20
|
+
} else {
|
|
21
|
+
config = {
|
|
22
|
+
count: configOrCount,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const { count = Infinity, delay = DEFAULT_DELAY, resetOnSuccess = false, emitOnRetry } = config
|
|
26
|
+
|
|
27
|
+
return count <= 0
|
|
28
|
+
? identity
|
|
29
|
+
: operate((source, subscriber) => {
|
|
30
|
+
let soFar = 0
|
|
31
|
+
let innerSub
|
|
32
|
+
const subscribeForRetry = () => {
|
|
33
|
+
let syncUnsub = false
|
|
34
|
+
innerSub = source.subscribe(
|
|
35
|
+
createOperatorSubscriber(
|
|
36
|
+
subscriber,
|
|
37
|
+
(value) => {
|
|
38
|
+
// If we're resetting on success
|
|
39
|
+
if (resetOnSuccess) {
|
|
40
|
+
soFar = 0
|
|
41
|
+
}
|
|
42
|
+
subscriber.next(value)
|
|
43
|
+
},
|
|
44
|
+
// Completions are passed through to consumer.
|
|
45
|
+
undefined,
|
|
46
|
+
(err) => {
|
|
47
|
+
if (soFar++ < count) {
|
|
48
|
+
// We are still under our retry count
|
|
49
|
+
const resub = () => {
|
|
50
|
+
if (innerSub) {
|
|
51
|
+
innerSub.unsubscribe()
|
|
52
|
+
innerSub = null
|
|
53
|
+
subscribeForRetry()
|
|
54
|
+
} else {
|
|
55
|
+
syncUnsub = true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ------- START CUSTOM CODE -------
|
|
60
|
+
if (emitOnRetry) {
|
|
61
|
+
try {
|
|
62
|
+
subscriber.next(emitOnRetry(err, soFar))
|
|
63
|
+
} catch (e) {
|
|
64
|
+
subscriber.error(e)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ------- END CUSTOM CODE -------
|
|
69
|
+
|
|
70
|
+
if (delay != null) {
|
|
71
|
+
// The user specified a retry delay.
|
|
72
|
+
// They gave us a number, use a timer, otherwise, it's a function,
|
|
73
|
+
// and we're going to call it to get a notifier.
|
|
74
|
+
const notifier =
|
|
75
|
+
typeof delay === 'number' ? timer(delay) : innerFrom(delay(err, soFar))
|
|
76
|
+
const notifierSubscriber = createOperatorSubscriber(
|
|
77
|
+
subscriber,
|
|
78
|
+
() => {
|
|
79
|
+
// After we get the first notification, we
|
|
80
|
+
// unsubscribe from the notifier, because we don't want anymore
|
|
81
|
+
// and we resubscribe to the source.
|
|
82
|
+
notifierSubscriber.unsubscribe()
|
|
83
|
+
resub()
|
|
84
|
+
},
|
|
85
|
+
() => {
|
|
86
|
+
// The notifier completed without emitting.
|
|
87
|
+
// The author is telling us they want to complete.
|
|
88
|
+
subscriber.complete()
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
notifier.subscribe(notifierSubscriber)
|
|
92
|
+
} else {
|
|
93
|
+
// There was no notifier given. Just resub immediately.
|
|
94
|
+
resub()
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// We're past our maximum number of retries.
|
|
98
|
+
// Just send along the error.
|
|
99
|
+
subscriber.error(err)
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
if (syncUnsub) {
|
|
105
|
+
innerSub.unsubscribe()
|
|
106
|
+
innerSub = null
|
|
107
|
+
subscribeForRetry()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
subscribeForRetry()
|
|
111
|
+
})
|
|
112
|
+
}
|
package/s3.js
CHANGED
|
@@ -327,7 +327,16 @@ function getTransformedHeaders(headers) {
|
|
|
327
327
|
*/
|
|
328
328
|
const transformedHeaders = {}
|
|
329
329
|
|
|
330
|
+
const isRangedResponse = headers['content-range'] != null
|
|
331
|
+
|
|
330
332
|
for (const name of Object.keys(headers)) {
|
|
333
|
+
// Google Cloud S3 returns checksums for the full object
|
|
334
|
+
// even with range requests, which causes checksum mismatches.
|
|
335
|
+
// I assume the checksum headers shouldn't be sent with ranged responses.
|
|
336
|
+
if (isRangedResponse && /^x-amz-checksum-/i.test(name)) {
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
331
340
|
const headerValues = headers[name]
|
|
332
341
|
transformedHeaders[name] = Array.isArray(headerValues)
|
|
333
342
|
? headerValues.join(',')
|
package/serializers.js
CHANGED
|
@@ -140,7 +140,6 @@ export default {
|
|
|
140
140
|
ureq && {
|
|
141
141
|
url: getUrl(ureq),
|
|
142
142
|
id: ureq.id || getHeader(ureq, 'request-id'),
|
|
143
|
-
userAgent: ureq.userAgent ?? getHeader(ureq, 'user-agent'),
|
|
144
143
|
timing: ureq.timing,
|
|
145
144
|
method: ureq.method,
|
|
146
145
|
body: typeof ureq.body === 'string' ? ureq.body : undefined,
|
|
@@ -156,7 +155,6 @@ export default {
|
|
|
156
155
|
ures: (ures) =>
|
|
157
156
|
ures && {
|
|
158
157
|
id: ures.id || getHeader(ures, 'request-id') || getHeader(ures.req, 'request-id'),
|
|
159
|
-
userAgent: ures.userAgent ?? getHeader(ures, 'user-agent'),
|
|
160
158
|
timing: ures.timing,
|
|
161
159
|
statusCode: ures.statusCode ?? ures.status,
|
|
162
160
|
bytesRead: ures.bytesRead,
|