effect 4.1.1 → 4.1.2

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 (142) hide show
  1. package/ai-docs/package.json +2 -2
  2. package/dist/ByteSize.d.ts.map +1 -1
  3. package/dist/ByteSize.js +27 -14
  4. package/dist/ByteSize.js.map +1 -1
  5. package/dist/Iterable.d.ts.map +1 -1
  6. package/dist/Iterable.js +27 -24
  7. package/dist/Iterable.js.map +1 -1
  8. package/dist/JsonSchema.d.ts.map +1 -1
  9. package/dist/JsonSchema.js +27 -6
  10. package/dist/JsonSchema.js.map +1 -1
  11. package/dist/MutableRef.d.ts.map +1 -1
  12. package/dist/MutableRef.js +0 -7
  13. package/dist/MutableRef.js.map +1 -1
  14. package/dist/Ref.d.ts.map +1 -1
  15. package/dist/Ref.js +0 -8
  16. package/dist/Ref.js.map +1 -1
  17. package/dist/Schedule.d.ts.map +1 -1
  18. package/dist/Schedule.js +8 -3
  19. package/dist/Schedule.js.map +1 -1
  20. package/dist/Stream.d.ts.map +1 -1
  21. package/dist/Stream.js +6 -5
  22. package/dist/Stream.js.map +1 -1
  23. package/dist/ai/McpServer.d.ts.map +1 -1
  24. package/dist/ai/McpServer.js +61 -33
  25. package/dist/ai/McpServer.js.map +1 -1
  26. package/dist/ai/internal/mcpProtocol/v2026_07_28.js +4 -3
  27. package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
  28. package/dist/ai/internal/structured-output.js +11 -2
  29. package/dist/ai/internal/structured-output.js.map +1 -1
  30. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  31. package/dist/cluster/ClusterWorkflowEngine.js +18 -9
  32. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  33. package/dist/cluster/Entity.d.ts +26 -0
  34. package/dist/cluster/Entity.d.ts.map +1 -1
  35. package/dist/cluster/Entity.js.map +1 -1
  36. package/dist/cluster/MessageStorage.d.ts +6 -6
  37. package/dist/cluster/MessageStorage.d.ts.map +1 -1
  38. package/dist/cluster/MessageStorage.js +1 -3
  39. package/dist/cluster/MessageStorage.js.map +1 -1
  40. package/dist/cluster/Runners.d.ts.map +1 -1
  41. package/dist/cluster/Runners.js +13 -2
  42. package/dist/cluster/Runners.js.map +1 -1
  43. package/dist/cluster/Sharding.d.ts +13 -0
  44. package/dist/cluster/Sharding.d.ts.map +1 -1
  45. package/dist/cluster/Sharding.js.map +1 -1
  46. package/dist/cluster/internal/clusterAbandon.d.ts.map +1 -1
  47. package/dist/cluster/internal/clusterAbandon.js +6 -4
  48. package/dist/cluster/internal/clusterAbandon.js.map +1 -1
  49. package/dist/cluster/internal/entityManager.js +48 -4
  50. package/dist/cluster/internal/entityManager.js.map +1 -1
  51. package/dist/cluster/internal/transactionNotifications.d.ts +5 -0
  52. package/dist/cluster/internal/transactionNotifications.d.ts.map +1 -0
  53. package/dist/cluster/internal/transactionNotifications.js +5 -0
  54. package/dist/cluster/internal/transactionNotifications.js.map +1 -0
  55. package/dist/encoding/Json5.d.ts +44 -0
  56. package/dist/encoding/Json5.d.ts.map +1 -0
  57. package/dist/encoding/Json5.js +56 -0
  58. package/dist/encoding/Json5.js.map +1 -0
  59. package/dist/encoding/Jsonc.d.ts +47 -0
  60. package/dist/encoding/Jsonc.d.ts.map +1 -0
  61. package/dist/encoding/Jsonc.js +120 -0
  62. package/dist/encoding/Jsonc.js.map +1 -0
  63. package/dist/encoding/SchemaBinary.d.ts +94 -0
  64. package/dist/encoding/SchemaBinary.d.ts.map +1 -1
  65. package/dist/encoding/SchemaBinary.js +219 -6
  66. package/dist/encoding/SchemaBinary.js.map +1 -1
  67. package/dist/encoding/index.d.ts +10 -0
  68. package/dist/encoding/index.d.ts.map +1 -1
  69. package/dist/encoding/index.js +10 -0
  70. package/dist/encoding/index.js.map +1 -1
  71. package/dist/eventlog/EventLogServer.d.ts.map +1 -1
  72. package/dist/eventlog/EventLogServer.js +28 -12
  73. package/dist/eventlog/EventLogServer.js.map +1 -1
  74. package/dist/http/HttpClient.d.ts +15 -9
  75. package/dist/http/HttpClient.d.ts.map +1 -1
  76. package/dist/http/HttpClient.js +18 -11
  77. package/dist/http/HttpClient.js.map +1 -1
  78. package/dist/internal/dateTime.js +17 -3
  79. package/dist/internal/dateTime.js.map +1 -1
  80. package/dist/internal/jsonParser.d.ts +5 -0
  81. package/dist/internal/jsonParser.d.ts.map +1 -0
  82. package/dist/internal/jsonParser.js +280 -0
  83. package/dist/internal/jsonParser.js.map +1 -0
  84. package/dist/internal/persistence.js +8 -1
  85. package/dist/internal/persistence.js.map +1 -1
  86. package/dist/internal/sql.js +16 -0
  87. package/dist/internal/sql.js.map +1 -1
  88. package/dist/internal/version.js +1 -1
  89. package/dist/observability/OtlpExporter.d.ts +4 -0
  90. package/dist/observability/OtlpExporter.d.ts.map +1 -1
  91. package/dist/observability/OtlpExporter.js +40 -12
  92. package/dist/observability/OtlpExporter.js.map +1 -1
  93. package/dist/persistence/RateLimiter.d.ts +7 -0
  94. package/dist/persistence/RateLimiter.d.ts.map +1 -1
  95. package/dist/persistence/RateLimiter.js +13 -4
  96. package/dist/persistence/RateLimiter.js.map +1 -1
  97. package/dist/rpc/RpcServer.d.ts.map +1 -1
  98. package/dist/rpc/RpcServer.js +10 -1
  99. package/dist/rpc/RpcServer.js.map +1 -1
  100. package/dist/sql/SqlClient.d.ts +8 -0
  101. package/dist/sql/SqlClient.d.ts.map +1 -1
  102. package/dist/sql/SqlClient.js +36 -12
  103. package/dist/sql/SqlClient.js.map +1 -1
  104. package/dist/sql/Statement.d.ts +5 -0
  105. package/dist/sql/Statement.d.ts.map +1 -1
  106. package/dist/sql/Statement.js +8 -6
  107. package/dist/sql/Statement.js.map +1 -1
  108. package/package.json +4 -4
  109. package/src/ByteSize.ts +29 -14
  110. package/src/Iterable.ts +20 -14
  111. package/src/JsonSchema.ts +45 -7
  112. package/src/MutableRef.ts +1 -8
  113. package/src/Ref.ts +0 -8
  114. package/src/Schedule.ts +8 -6
  115. package/src/Stream.ts +6 -5
  116. package/src/ai/McpServer.ts +66 -43
  117. package/src/ai/internal/mcpProtocol/v2026_07_28.ts +4 -5
  118. package/src/ai/internal/structured-output.ts +10 -4
  119. package/src/cluster/ClusterWorkflowEngine.ts +29 -28
  120. package/src/cluster/Entity.ts +52 -0
  121. package/src/cluster/MessageStorage.ts +3 -6
  122. package/src/cluster/Runners.ts +16 -2
  123. package/src/cluster/Sharding.ts +13 -0
  124. package/src/cluster/internal/clusterAbandon.ts +7 -7
  125. package/src/cluster/internal/entityManager.ts +65 -5
  126. package/src/cluster/internal/transactionNotifications.ts +7 -0
  127. package/src/encoding/Json5.ts +59 -0
  128. package/src/encoding/Jsonc.ts +126 -0
  129. package/src/encoding/SchemaBinary.ts +228 -7
  130. package/src/encoding/index.ts +12 -0
  131. package/src/eventlog/EventLogServer.ts +36 -12
  132. package/src/http/HttpClient.ts +34 -22
  133. package/src/internal/dateTime.ts +26 -3
  134. package/src/internal/jsonParser.ts +307 -0
  135. package/src/internal/persistence.ts +9 -1
  136. package/src/internal/sql.ts +25 -0
  137. package/src/internal/version.ts +1 -1
  138. package/src/observability/OtlpExporter.ts +42 -18
  139. package/src/persistence/RateLimiter.ts +13 -4
  140. package/src/rpc/RpcServer.ts +12 -1
  141. package/src/sql/SqlClient.ts +48 -14
  142. package/src/sql/Statement.ts +8 -13
@@ -0,0 +1,307 @@
1
+ const json5Space = /(?:[\t\n\v\f\r \u00A0\u2028\u2029\uFEFF\p{Zs}]|\/\/[^\n\r\u2028\u2029]*|\/\*[^]*?\*\/)*/uy
2
+ const idStart = /[$_\p{ID_Start}]/u
3
+ const idPart = /[$\u200C\u200D\p{ID_Continue}]/u
4
+ const idName = /[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*/uy
5
+ const hexDigits = /[\da-fA-F]*/y
6
+ const isDigit = (code: number) => code >= 0x30 && code <= 0x39
7
+ const isAsciiIdPart = (code: number) =>
8
+ (code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || isDigit(code) || code === 0x5f || code === 0x24
9
+
10
+ const jsoncSpace = /(?:[ \t\r\n]|\/\/[^\r\n]*|\/\*[^]*?\*\/)*/uy
11
+ const jsonNumber = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y
12
+
13
+ // The escapes JSON and JSON5 share, indexed by the character after the backslash.
14
+ // A null prototype keeps absent entries from resolving to inherited properties.
15
+ const escapes: Record<number, string | undefined> = Object.create(null)
16
+ for (const [escape, value] of ["\"\"", "\\\\", "//", "b\b", "f\f", "n\n", "r\r", "t\t"]) {
17
+ escapes[escape.charCodeAt(0)] = value
18
+ }
19
+
20
+ // Runs of string content that need no further inspection.
21
+ const json5Double = /[^"\\\n\r]*/y
22
+ const json5Single = /[^'\\\n\r]*/y
23
+ // JSON forbids unescaped U+0000 through U+001F.
24
+ // oxlint-disable-next-line no-control-regex
25
+ const jsonDouble = /[^"\\\x00-\x1f]*/y
26
+
27
+ type Format = "JSON5" | "JSONC"
28
+
29
+ export const syntaxError = (text: string, at: number, format: Format): SyntaxError => {
30
+ const json5 = format === "JSON5"
31
+ const lines = text.slice(0, at).split(json5 ? /\r\n|[\n\r\u2028\u2029]/ : /\r\n|[\n\r]/)
32
+ return new SyntaxError(
33
+ `Invalid ${json5 ? "JSON5" : "JSONC"} at line ${lines.length}, column ${lines[lines.length - 1].length + 1}`
34
+ )
35
+ }
36
+
37
+ type Frame =
38
+ | { readonly object: true; readonly value: Record<string, unknown>; key: string; first: boolean }
39
+ | { readonly object: false; readonly value: Array<unknown>; first: boolean }
40
+
41
+ const set = (object: Record<string, unknown>, key: string, value: unknown): void => {
42
+ // Members are own data properties, as with JSON.parse. Assignment creates one unless
43
+ // the key is inherited (`__proto__`, `constructor`, added accessors, ...), where it
44
+ // could run a setter or throw, so inherited keys are defined directly.
45
+ if (key in Object.prototype) {
46
+ Object.defineProperty(object, key, { value, writable: true, enumerable: true, configurable: true })
47
+ } else object[key] = value
48
+ }
49
+
50
+ // JSONC only uses the strict mode to locate errors after JSON.parse fails.
51
+ export const parse = (text: string, format: Format): unknown => {
52
+ const json5 = format === "JSON5"
53
+ const space = json5 ? json5Space : jsoncSpace
54
+ // JSON strings forbid raw characters below U+0020; JSON5 strings only forbid line breaks.
55
+ const plainFloor = json5 ? 0 : 0x20
56
+ let i = 0
57
+
58
+ const fail = (at: number): never => {
59
+ throw syntaxError(text, at, format)
60
+ }
61
+
62
+ /** Skips whitespace and comments, rejecting a `/` that starts neither. */
63
+ const skip = () => {
64
+ // ASCII whitespace dominates formatted documents; the pattern handles the rest.
65
+ let code = text.charCodeAt(i)
66
+ while (code === 0x20 || code === 0x0a || code === 0x0d || code === 0x09) code = text.charCodeAt(++i)
67
+ if (code > 0x20 && code !== 0x2f && code < 0xa0) return
68
+ space.lastIndex = i
69
+ space.test(text)
70
+ i = space.lastIndex
71
+ if (text.charCodeAt(i) === 0x2f) {
72
+ fail(text.charAt(i + 1) === "*" ? text.length : i + 1)
73
+ }
74
+ }
75
+
76
+ /** Advances past a sticky pattern at `i`, returning the matched length. */
77
+ const run = (pattern: RegExp) => {
78
+ pattern.lastIndex = i
79
+ const length = pattern.test(text) ? pattern.lastIndex - i : 0
80
+ i += length
81
+ return length
82
+ }
83
+
84
+ const word = <A>(literal: string, result: A): A => {
85
+ if (!text.startsWith(literal, i)) {
86
+ let at = i
87
+ while (text.charAt(at) === literal[at - i]) at++
88
+ fail(at)
89
+ }
90
+ i += literal.length
91
+ return result
92
+ }
93
+
94
+ const hex = (count: number) => {
95
+ let value = 0
96
+ for (const end = i + count; i < end; i++) {
97
+ const code = text.charCodeAt(i)
98
+ const lower = code | 0x20
99
+ if (isDigit(code)) value = value * 16 + code - 0x30
100
+ else if (lower >= 0x61 && lower <= 0x66) value = value * 16 + lower - 0x57
101
+ else fail(i)
102
+ }
103
+ return String.fromCharCode(value)
104
+ }
105
+
106
+ const number = () => {
107
+ if (!json5) {
108
+ const start = i
109
+ if (run(jsonNumber) === 0) fail(i)
110
+ return Number(text.slice(start, i))
111
+ }
112
+ let code = text.charCodeAt(i)
113
+ let sign = 1
114
+ if (code === 0x2b || code === 0x2d) {
115
+ if (code === 0x2d) sign = -1
116
+ code = text.charCodeAt(++i)
117
+ }
118
+ const start = i
119
+ if (code === 0x49) return word("Infinity", sign * Infinity)
120
+ if (code === 0x4e) return word("NaN", NaN)
121
+ if (code === 0x30 && (text.charCodeAt(i + 1) | 0x20) === 0x78) {
122
+ i += 2
123
+ if (run(hexDigits) === 0) fail(i)
124
+ return sign * Number(text.slice(start, i))
125
+ }
126
+ // Integers below 2^53 are accumulated directly; anything else goes through Number.
127
+ let integer = 0
128
+ if (code === 0x30) code = text.charCodeAt(++i)
129
+ else if (isDigit(code)) {
130
+ do {
131
+ integer = integer * 10 + code - 0x30
132
+ code = text.charCodeAt(++i)
133
+ } while (isDigit(code))
134
+ } else if (code !== 0x2e) fail(i)
135
+ if (code !== 0x2e && code !== 0x65 && code !== 0x45 && i - start < 16) return sign * integer
136
+ if (code === 0x2e) {
137
+ const fraction = ++i
138
+ while (isDigit(text.charCodeAt(i))) i++
139
+ // A leading decimal point needs digits after it; a trailing one does not.
140
+ if (i === fraction && fraction === start + 1) fail(i)
141
+ code = text.charCodeAt(i)
142
+ }
143
+ if (code === 0x65 || code === 0x45) {
144
+ code = text.charCodeAt(++i)
145
+ if (code === 0x2b || code === 0x2d) i++
146
+ const exponent = i
147
+ while (isDigit(text.charCodeAt(i))) i++
148
+ if (i === exponent) fail(i)
149
+ }
150
+ return sign * Number(text.slice(start, i))
151
+ }
152
+
153
+ const string = (quote: number) => {
154
+ const plain = quote === 0x27 ? json5Single : json5 ? json5Double : jsonDouble
155
+ let result = ""
156
+ for (i++;;) {
157
+ // Plain content runs until the quote, an escape, a line break, a control character
158
+ // JSON forbids, or the end of input, where `code` is NaN and fails the last test.
159
+ const start = i
160
+ let code = text.charCodeAt(i)
161
+ // Short runs are cheaper to scan by hand; long runs are handed to the pattern.
162
+ const handoff = i + 24
163
+ while (code !== quote && code !== 0x5c && code !== 0x0a && code !== 0x0d && code >= plainFloor) {
164
+ if (++i === handoff) {
165
+ run(plain)
166
+ code = text.charCodeAt(i)
167
+ break
168
+ }
169
+ code = text.charCodeAt(i)
170
+ }
171
+ result += text.slice(start, i)
172
+ i++
173
+ if (code === quote) return result
174
+ // Anything else that stops a plain run is an escape or a forbidden character.
175
+ if (code !== 0x5c) fail(i - 1)
176
+ const escape = text.charCodeAt(i++)
177
+ const simple = escapes[escape]
178
+ if (simple !== undefined) result += simple
179
+ else if (escape === 0x75) result += hex(4)
180
+ else if (!json5 || i > text.length || (escape >= 0x31 && escape <= 0x39)) fail(i - 1)
181
+ else if (escape === 0x76) result += "\v"
182
+ else if (escape === 0x78) result += hex(2)
183
+ else if (escape === 0x30) {
184
+ if (isDigit(text.charCodeAt(i))) fail(i)
185
+ result += "\0"
186
+ } else if (escape === 0x0d) {
187
+ // A CR LF line continuation contributes nothing, like a single line terminator.
188
+ if (text.charCodeAt(i) === 0x0a) i++
189
+ } else if (escape !== 0x0a && escape !== 0x2028 && escape !== 0x2029) result += text[i - 1]
190
+ }
191
+ }
192
+
193
+ const identifier = () => {
194
+ const start = i
195
+ let code = text.charCodeAt(i)
196
+ if (isAsciiIdPart(code) && !isDigit(code)) {
197
+ do code = text.charCodeAt(++i)
198
+ while (isAsciiIdPart(code))
199
+ // Past an ASCII name, only an escape or a non-ASCII character can continue it.
200
+ if (code !== 0x5c && !(code >= 0x80)) return text.slice(start, i)
201
+ i = start
202
+ }
203
+ run(idName)
204
+ let name = text.slice(start, i)
205
+ if (name !== "" && text.charAt(i) !== "\\") return name
206
+ // Escapes are validated one code point at a time.
207
+ for (;;) {
208
+ const at = i
209
+ const valid = name === "" ? idStart : idPart
210
+ let c = String.fromCodePoint(text.codePointAt(i) ?? 0)
211
+ if (c === "\\") {
212
+ if (text.charAt(i + 1) !== "u") fail(i + 1)
213
+ i += 2
214
+ c = hex(4)
215
+ if (!valid.test(c)) fail(at)
216
+ } else if (i < text.length && valid.test(c)) i += c.length
217
+ else if (name === "") fail(i)
218
+ else return name
219
+ name += c
220
+ }
221
+ }
222
+
223
+ const stack: Array<Frame> = []
224
+
225
+ /**
226
+ * Reads a value at `i`, which must already be past any whitespace. Opening a
227
+ * collection returns `undefined`; the collection is completed later.
228
+ */
229
+ const value = (): unknown => {
230
+ const code = text.charCodeAt(i)
231
+ switch (code) {
232
+ case 0x7b:
233
+ stack.push({ object: true, value: {}, key: "", first: true })
234
+ i++
235
+ return undefined
236
+ case 0x5b:
237
+ stack.push({ object: false, value: [], first: true })
238
+ i++
239
+ return undefined
240
+ case 0x22:
241
+ return string(0x22)
242
+ case 0x27:
243
+ return json5 ? string(0x27) : fail(i)
244
+ case 0x6e:
245
+ return word("null", null)
246
+ case 0x74:
247
+ return word("true", true)
248
+ case 0x66:
249
+ return word("false", false)
250
+ case 0x2b:
251
+ case 0x2d:
252
+ case 0x2e:
253
+ case 0x49:
254
+ case 0x4e:
255
+ return number()
256
+ }
257
+ return isDigit(code) ? number() : fail(i)
258
+ }
259
+
260
+ /** After a member: consumes a comma unless the collection ends. */
261
+ const separated = (close: number) => {
262
+ skip()
263
+ const code = text.charCodeAt(i)
264
+ if (code === close) return
265
+ if (code !== 0x2c) fail(i)
266
+ i++
267
+ skip()
268
+ if (!json5 && text.charCodeAt(i) === close) fail(i)
269
+ }
270
+
271
+ skip()
272
+ let result = value()
273
+ // An explicit stack keeps deep nesting off the call stack.
274
+ while (stack.length > 0) {
275
+ const frame = stack[stack.length - 1]
276
+ const close = frame.object ? 0x7d : 0x5d
277
+ if (frame.first) skip()
278
+ else separated(close)
279
+ if (text.charCodeAt(i) === close) {
280
+ stack.pop()
281
+ i++
282
+ const parent = stack[stack.length - 1]
283
+ if (parent === undefined) result = frame.value
284
+ else if (parent.object) set(parent.value, parent.key, frame.value)
285
+ else parent.value.push(frame.value)
286
+ continue
287
+ }
288
+ frame.first = false
289
+ if (!frame.object) {
290
+ const item = value()
291
+ if (item !== undefined) frame.value.push(item)
292
+ } else {
293
+ const quote = text.charCodeAt(i)
294
+ if (!json5 && quote !== 0x22) fail(i)
295
+ const key = quote === 0x22 || quote === 0x27 ? string(quote) : identifier()
296
+ skip()
297
+ if (text.charCodeAt(i++) !== 0x3a) fail(i - 1)
298
+ skip()
299
+ const item = value()
300
+ if (item === undefined) frame.key = key
301
+ else set(frame.value, key, item)
302
+ }
303
+ }
304
+ skip()
305
+ if (i < text.length) fail(i)
306
+ return result
307
+ }
@@ -1,13 +1,21 @@
1
1
  /** @internal */
2
2
  export const sqlCleanupBatchSize = 1000
3
3
 
4
+ /**
5
+ * Percent-encodes `%` and `:` so the result contains no `:` and distinct inputs
6
+ * stay distinct.
7
+ *
8
+ * @internal
9
+ */
10
+ export const escapeKeySegment = (segment: string): string => segment.replace(/%/g, "%25").replace(/:/g, "%3A")
11
+
4
12
  /**
5
13
  * Key prefix for a persistence store. The store id is escaped so the namespace
6
14
  * always ends at its first `:`, and distinct store / key pairs never collide.
7
15
  *
8
16
  * @internal
9
17
  */
10
- export const storeNamespace = (storeId: string): string => `${storeId.replace(/%/g, "%25").replace(/:/g, "%3A")}:`
18
+ export const storeNamespace = (storeId: string): string => `${escapeKeySegment(storeId)}:`
11
19
 
12
20
  /** @internal */
13
21
  export const escapeRedisGlob = (pattern: string): string => pattern.replace(/[\\*?[\]]/g, "\\$&")
@@ -1,4 +1,8 @@
1
+ import { Clock } from "../Clock.ts"
1
2
  import * as Context from "../Context.ts"
3
+ import type * as Fiber from "../Fiber.ts"
4
+ import type * as Tracer from "../Tracer.ts"
5
+ import { TracerTimingEnabled } from "./references.ts"
2
6
 
3
7
  /**
4
8
  * The transaction service of the innermost transaction started with
@@ -12,3 +16,24 @@ export const CurrentTransactionService = Context.Reference<Context.Key<any, obje
12
16
  "effect/sql/CurrentTransactionService",
13
17
  { defaultValue: () => undefined }
14
18
  )
19
+
20
+ /**
21
+ * The clock to measure connection wait time with, or `undefined` without span
22
+ * timing, when the span has no start time to measure from.
23
+ *
24
+ * @internal
25
+ */
26
+ export const waitClock = (fiber: Fiber.Fiber<unknown, unknown>): Clock | undefined =>
27
+ fiber.getRef(TracerTimingEnabled) ? fiber.getRef(Clock) : undefined
28
+
29
+ /**
30
+ * Records the time from the span's start to the connection hand-off as
31
+ * `db.client.connection.wait_time_ms`.
32
+ *
33
+ * @internal
34
+ */
35
+ export const recordConnectionWait = (span: Tracer.Span, clock: Clock): void =>
36
+ span.attribute(
37
+ "db.client.connection.wait_time_ms",
38
+ Number(clock.currentTimeNanosUnsafe() - span.status.startTime) / 1e6
39
+ )
@@ -1,2 +1,2 @@
1
1
  /** @internal */
2
- export const version = "4.1.1"
2
+ export const version = "4.1.2"
@@ -21,6 +21,7 @@ import type { HttpBody } from "../http/HttpBody.ts"
21
21
  import * as HttpClient from "../http/HttpClient.ts"
22
22
  import * as HttpClientError from "../http/HttpClientError.ts"
23
23
  import * as HttpClientRequest from "../http/HttpClientRequest.ts"
24
+ import * as Latch from "../Latch.ts"
24
25
  import * as Layer from "../Layer.ts"
25
26
  import * as Result from "../Result.ts"
26
27
  import * as Schedule from "../Schedule.ts"
@@ -154,6 +155,10 @@ export const layerFlusher: Layer.Layer<Flusher> = Layer.sync(Flusher, () => {
154
155
  * after unhandled failures, and flushes during scope finalization up to
155
156
  * `shutdownTimeout`.
156
157
  *
158
+ * The export interval starts with the first push and stops once an export
159
+ * empties the buffer, so an idle exporter keeps no timer pending. With
160
+ * `exportEmpty` and batching disabled, it exports on every interval instead.
161
+ *
157
162
  * Optional gzip compression uses `HttpClientRequest.compressBody` before the
158
163
  * retry loop, so retries reuse the compressed bytes. Bodies smaller than 1KB
159
164
  * remain uncompressed. Compression requires runtime `CompressionStream` support;
@@ -186,7 +191,6 @@ export const make: (
186
191
  const clock = Context.get(services, Clock)
187
192
  const scope = Context.get(services, Scope.Scope)
188
193
  const exportInterval = Duration.max(Duration.fromInputUnsafe(options.exportInterval), Duration.zero)
189
- let disabledUntil: number | undefined = undefined
190
194
 
191
195
  const client = Context.get(services, HttpClient.HttpClient).pipe(
192
196
  HttpClient.tap((response) => Effect.ignore(response.arrayBuffer)),
@@ -206,14 +210,18 @@ export const make: (
206
210
  )
207
211
 
208
212
  const request = HttpClientRequest.post(options.url, { headers })
213
+ const exportsEmpty = options.maxBatchSize === "disabled" && options.exportEmpty === true
209
214
  let buffer: Array<any> = []
215
+ let disabledUntil: number | undefined = undefined
216
+ const isDisabled = () => {
217
+ if (disabledUntil === undefined) return false
218
+ if (clock.currentTimeMillisUnsafe() < disabledUntil) return true
219
+ disabledUntil = undefined
220
+ return false
221
+ }
222
+
210
223
  const runExport = Effect.suspend(() => {
211
- if (disabledUntil !== undefined && clock.currentTimeMillisUnsafe() < disabledUntil) {
212
- return Effect.void
213
- } else if (disabledUntil !== undefined) {
214
- disabledUntil = undefined
215
- }
216
- if (buffer.length === 0 && (options.maxBatchSize !== "disabled" || options.exportEmpty !== true)) {
224
+ if (isDisabled() || (buffer.length === 0 && !exportsEmpty)) {
217
225
  return Effect.void
218
226
  }
219
227
  const items = buffer
@@ -245,9 +253,6 @@ export const make: (
245
253
  const exportFibers = yield* FiberSet.make<void, never>()
246
254
  const runExportFork = yield* FiberSet.runtime(exportFibers)<never>()
247
255
 
248
- const flusher = yield* Flusher
249
- yield* flusher.register(runExport)
250
-
251
256
  yield* Scope.addFinalizer(
252
257
  scope,
253
258
  Effect.suspend(() => {
@@ -261,19 +266,38 @@ export const make: (
261
266
  )
262
267
  )
263
268
 
264
- yield* Effect.sleep(exportInterval).pipe(
265
- Effect.andThen(FiberSet.run(exportFibers, runExport)),
266
- Effect.flatMap(Fiber.await),
267
- Effect.forever,
268
- Effect.forkIn(scope)
269
- )
269
+ // `armed` is open while there is something to export. It changes in the
270
+ // same synchronous step as the buffer check, so a push is never stranded.
271
+ const armed = yield* Latch.make(exportsEmpty)
272
+ const wake = yield* Latch.make()
273
+ const disarmIfEmpty = Effect.sync(() => {
274
+ if (exportsEmpty || buffer.length > 0 || !armed.closeUnsafe()) return
275
+ wake.openUnsafe()
276
+ })
277
+ // Forked after the shutdown finalizer, so the loop stops before the shutdown flush.
278
+ yield* Effect.gen(function*() {
279
+ while (true) {
280
+ wake.closeUnsafe()
281
+ yield* armed.await
282
+ yield* Effect.race(Effect.sleep(exportInterval), wake.await)
283
+ yield* Fiber.await(yield* FiberSet.run(exportFibers, runExport))
284
+ yield* disarmIfEmpty
285
+ }
286
+ }).pipe(Effect.forkIn(scope))
287
+ const exportNow = Effect.andThen(runExport, disarmIfEmpty)
288
+
289
+ const flusher = yield* Flusher
290
+ yield* flusher.register(exportNow)
270
291
 
271
292
  return {
272
293
  push(data) {
273
- if (disabledUntil !== undefined) return
294
+ // With no idle timer, the next push ends the disabled window.
295
+ if (isDisabled()) return
274
296
  buffer.push(data)
275
297
  if (options.maxBatchSize !== "disabled" && buffer.length >= options.maxBatchSize) {
276
- runExportFork(runExport)
298
+ runExportFork(exportNow)
299
+ } else {
300
+ armed.openUnsafe()
277
301
  }
278
302
  }
279
303
  }
@@ -16,6 +16,7 @@ import * as Context from "../Context.ts"
16
16
  import * as Duration from "../Duration.ts"
17
17
  import * as Effect from "../Effect.ts"
18
18
  import { flow, identity } from "../Function.ts"
19
+ import { escapeKeySegment } from "../internal/persistence.ts"
19
20
  import * as Layer from "../Layer.ts"
20
21
  import * as Schedule from "../Schedule.ts"
21
22
  import * as Schema from "../Schema.ts"
@@ -996,6 +997,13 @@ export const layerStoreMemory: Layer.Layer<
996
997
  * Creates a Redis-backed `RateLimiterStore` using Lua scripts and the
997
998
  * configured key prefix.
998
999
  *
1000
+ * **Details**
1001
+ *
1002
+ * State for a rate-limit key is stored under `<prefix><key>`, with
1003
+ * `<prefix><key>:refill` holding token-bucket refill times and
1004
+ * `<prefix><key>:adaptive` holding adaptive state. `%` and `:` in the key are
1005
+ * percent-encoded so different keys never share Redis state.
1006
+ *
999
1007
  * @stability unstable
1000
1008
  * @category constructors
1001
1009
  * @since 4.0.0
@@ -1007,6 +1015,7 @@ export const makeStoreRedis = Effect.fnUntraced(function*(
1007
1015
  ) {
1008
1016
  const prefix = options?.prefix ?? "ratelimiter:"
1009
1017
  const redis = yield* Redis.Redis
1018
+ const redisKey = (key: string) => `${prefix}${escapeKeySegment(key)}`
1010
1019
 
1011
1020
  const fixedWindow = redis.eval(fixedWindowScript)
1012
1021
  const tokenBucket = redis.eval(tokenBucketScript)
@@ -1015,7 +1024,7 @@ export const makeStoreRedis = Effect.fnUntraced(function*(
1015
1024
 
1016
1025
  return RateLimiterStore.of({
1017
1026
  fixedWindow(options) {
1018
- const key = `${prefix}${options.key}`
1027
+ const key = redisKey(options.key)
1019
1028
  const refillMillis = Duration.toMillis(options.refillRate)
1020
1029
  return Effect.mapError(
1021
1030
  Effect.map(
@@ -1032,7 +1041,7 @@ export const makeStoreRedis = Effect.fnUntraced(function*(
1032
1041
  )
1033
1042
  },
1034
1043
  tokenBucket(options) {
1035
- const key = `${prefix}${options.key}`
1044
+ const key = redisKey(options.key)
1036
1045
  const lastRefillKey = `${key}:refill`
1037
1046
  const refillMillis = Duration.toMillis(options.refillRate)
1038
1047
  return Effect.clockWith((clock) =>
@@ -1060,7 +1069,7 @@ export const makeStoreRedis = Effect.fnUntraced(function*(
1060
1069
  )
1061
1070
  },
1062
1071
  adaptiveConsume(options) {
1063
- const key = `${prefix}${options.key}:adaptive`
1072
+ const key = `${redisKey(options.key)}:adaptive`
1064
1073
  return Effect.map(
1065
1074
  Effect.mapError(
1066
1075
  adaptiveConsume(
@@ -1087,7 +1096,7 @@ export const makeStoreRedis = Effect.fnUntraced(function*(
1087
1096
  adaptiveFeedback(options) {
1088
1097
  if (options.status !== 429 || options.retryAfter === undefined) return Effect.void
1089
1098
  const retryAfterMillis = clampAdaptiveDurationMillis(Duration.toMillis(options.retryAfter))
1090
- const key = `${prefix}${options.key}:adaptive`
1099
+ const key = `${redisKey(options.key)}:adaptive`
1091
1100
  return Effect.asVoid(
1092
1101
  Effect.mapError(
1093
1102
  adaptiveFeedback(
@@ -902,6 +902,17 @@ export const make: <Rpcs extends Rpc.Any>(
902
902
  request: RequestEncoded,
903
903
  payload: unknown
904
904
  ) => {
905
+ // the headers come from the wire unvalidated, and malformed input makes
906
+ // `Headers.fromInput` throw
907
+ let headers: Headers.Headers
908
+ try {
909
+ headers = Headers.fromInput(request.headers)
910
+ } catch {
911
+ const reason = "Invalid request headers"
912
+ return request.isNotification === true
913
+ ? dropNotification(reason)
914
+ : sendRequestDefect(client, requestId, schemas.encodeExit, reason)
915
+ }
905
916
  // nothing is encoded for a notification, and only a reply would remove its schemas
906
917
  if (request.isNotification !== true) {
907
918
  client.schemas.set(
@@ -919,7 +930,7 @@ export const make: <Rpcs extends Rpc.Any>(
919
930
  const decoded = request as Types.Mutable<RequestEncoded>
920
931
  decoded.id = requestId
921
932
  decoded.payload = payload
922
- decoded.headers = Headers.fromInput(request.headers) as any
933
+ decoded.headers = headers as any
923
934
  return server.write(client.id, decoded as any)
924
935
  }
925
936