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.
- package/ai-docs/package.json +2 -2
- package/dist/ByteSize.d.ts.map +1 -1
- package/dist/ByteSize.js +27 -14
- package/dist/ByteSize.js.map +1 -1
- package/dist/Iterable.d.ts.map +1 -1
- package/dist/Iterable.js +27 -24
- package/dist/Iterable.js.map +1 -1
- package/dist/JsonSchema.d.ts.map +1 -1
- package/dist/JsonSchema.js +27 -6
- package/dist/JsonSchema.js.map +1 -1
- package/dist/MutableRef.d.ts.map +1 -1
- package/dist/MutableRef.js +0 -7
- package/dist/MutableRef.js.map +1 -1
- package/dist/Ref.d.ts.map +1 -1
- package/dist/Ref.js +0 -8
- package/dist/Ref.js.map +1 -1
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +8 -3
- package/dist/Schedule.js.map +1 -1
- package/dist/Stream.d.ts.map +1 -1
- package/dist/Stream.js +6 -5
- package/dist/Stream.js.map +1 -1
- package/dist/ai/McpServer.d.ts.map +1 -1
- package/dist/ai/McpServer.js +61 -33
- package/dist/ai/McpServer.js.map +1 -1
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js +4 -3
- package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
- package/dist/ai/internal/structured-output.js +11 -2
- package/dist/ai/internal/structured-output.js.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
- package/dist/cluster/ClusterWorkflowEngine.js +18 -9
- package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
- package/dist/cluster/Entity.d.ts +26 -0
- package/dist/cluster/Entity.d.ts.map +1 -1
- package/dist/cluster/Entity.js.map +1 -1
- package/dist/cluster/MessageStorage.d.ts +6 -6
- package/dist/cluster/MessageStorage.d.ts.map +1 -1
- package/dist/cluster/MessageStorage.js +1 -3
- package/dist/cluster/MessageStorage.js.map +1 -1
- package/dist/cluster/Runners.d.ts.map +1 -1
- package/dist/cluster/Runners.js +13 -2
- package/dist/cluster/Runners.js.map +1 -1
- package/dist/cluster/Sharding.d.ts +13 -0
- package/dist/cluster/Sharding.d.ts.map +1 -1
- package/dist/cluster/Sharding.js.map +1 -1
- package/dist/cluster/internal/clusterAbandon.d.ts.map +1 -1
- package/dist/cluster/internal/clusterAbandon.js +6 -4
- package/dist/cluster/internal/clusterAbandon.js.map +1 -1
- package/dist/cluster/internal/entityManager.js +48 -4
- package/dist/cluster/internal/entityManager.js.map +1 -1
- package/dist/cluster/internal/transactionNotifications.d.ts +5 -0
- package/dist/cluster/internal/transactionNotifications.d.ts.map +1 -0
- package/dist/cluster/internal/transactionNotifications.js +5 -0
- package/dist/cluster/internal/transactionNotifications.js.map +1 -0
- package/dist/encoding/Json5.d.ts +44 -0
- package/dist/encoding/Json5.d.ts.map +1 -0
- package/dist/encoding/Json5.js +56 -0
- package/dist/encoding/Json5.js.map +1 -0
- package/dist/encoding/Jsonc.d.ts +47 -0
- package/dist/encoding/Jsonc.d.ts.map +1 -0
- package/dist/encoding/Jsonc.js +120 -0
- package/dist/encoding/Jsonc.js.map +1 -0
- package/dist/encoding/SchemaBinary.d.ts +94 -0
- package/dist/encoding/SchemaBinary.d.ts.map +1 -1
- package/dist/encoding/SchemaBinary.js +219 -6
- package/dist/encoding/SchemaBinary.js.map +1 -1
- package/dist/encoding/index.d.ts +10 -0
- package/dist/encoding/index.d.ts.map +1 -1
- package/dist/encoding/index.js +10 -0
- package/dist/encoding/index.js.map +1 -1
- package/dist/eventlog/EventLogServer.d.ts.map +1 -1
- package/dist/eventlog/EventLogServer.js +28 -12
- package/dist/eventlog/EventLogServer.js.map +1 -1
- package/dist/http/HttpClient.d.ts +15 -9
- package/dist/http/HttpClient.d.ts.map +1 -1
- package/dist/http/HttpClient.js +18 -11
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/internal/dateTime.js +17 -3
- package/dist/internal/dateTime.js.map +1 -1
- package/dist/internal/jsonParser.d.ts +5 -0
- package/dist/internal/jsonParser.d.ts.map +1 -0
- package/dist/internal/jsonParser.js +280 -0
- package/dist/internal/jsonParser.js.map +1 -0
- package/dist/internal/persistence.js +8 -1
- package/dist/internal/persistence.js.map +1 -1
- package/dist/internal/sql.js +16 -0
- package/dist/internal/sql.js.map +1 -1
- package/dist/internal/version.js +1 -1
- package/dist/observability/OtlpExporter.d.ts +4 -0
- package/dist/observability/OtlpExporter.d.ts.map +1 -1
- package/dist/observability/OtlpExporter.js +40 -12
- package/dist/observability/OtlpExporter.js.map +1 -1
- package/dist/persistence/RateLimiter.d.ts +7 -0
- package/dist/persistence/RateLimiter.d.ts.map +1 -1
- package/dist/persistence/RateLimiter.js +13 -4
- package/dist/persistence/RateLimiter.js.map +1 -1
- package/dist/rpc/RpcServer.d.ts.map +1 -1
- package/dist/rpc/RpcServer.js +10 -1
- package/dist/rpc/RpcServer.js.map +1 -1
- package/dist/sql/SqlClient.d.ts +8 -0
- package/dist/sql/SqlClient.d.ts.map +1 -1
- package/dist/sql/SqlClient.js +36 -12
- package/dist/sql/SqlClient.js.map +1 -1
- package/dist/sql/Statement.d.ts +5 -0
- package/dist/sql/Statement.d.ts.map +1 -1
- package/dist/sql/Statement.js +8 -6
- package/dist/sql/Statement.js.map +1 -1
- package/package.json +4 -4
- package/src/ByteSize.ts +29 -14
- package/src/Iterable.ts +20 -14
- package/src/JsonSchema.ts +45 -7
- package/src/MutableRef.ts +1 -8
- package/src/Ref.ts +0 -8
- package/src/Schedule.ts +8 -6
- package/src/Stream.ts +6 -5
- package/src/ai/McpServer.ts +66 -43
- package/src/ai/internal/mcpProtocol/v2026_07_28.ts +4 -5
- package/src/ai/internal/structured-output.ts +10 -4
- package/src/cluster/ClusterWorkflowEngine.ts +29 -28
- package/src/cluster/Entity.ts +52 -0
- package/src/cluster/MessageStorage.ts +3 -6
- package/src/cluster/Runners.ts +16 -2
- package/src/cluster/Sharding.ts +13 -0
- package/src/cluster/internal/clusterAbandon.ts +7 -7
- package/src/cluster/internal/entityManager.ts +65 -5
- package/src/cluster/internal/transactionNotifications.ts +7 -0
- package/src/encoding/Json5.ts +59 -0
- package/src/encoding/Jsonc.ts +126 -0
- package/src/encoding/SchemaBinary.ts +228 -7
- package/src/encoding/index.ts +12 -0
- package/src/eventlog/EventLogServer.ts +36 -12
- package/src/http/HttpClient.ts +34 -22
- package/src/internal/dateTime.ts +26 -3
- package/src/internal/jsonParser.ts +307 -0
- package/src/internal/persistence.ts +9 -1
- package/src/internal/sql.ts +25 -0
- package/src/internal/version.ts +1 -1
- package/src/observability/OtlpExporter.ts +42 -18
- package/src/persistence/RateLimiter.ts +13 -4
- package/src/rpc/RpcServer.ts +12 -1
- package/src/sql/SqlClient.ts +48 -14
- 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
|
|
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, "\\$&")
|
package/src/internal/sql.ts
CHANGED
|
@@ -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
|
+
)
|
package/src/internal/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** @internal */
|
|
2
|
-
export const version = "4.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 (
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 = `${
|
|
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 = `${
|
|
1099
|
+
const key = `${redisKey(options.key)}:adaptive`
|
|
1091
1100
|
return Effect.asVoid(
|
|
1092
1101
|
Effect.mapError(
|
|
1093
1102
|
adaptiveFeedback(
|
package/src/rpc/RpcServer.ts
CHANGED
|
@@ -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 =
|
|
933
|
+
decoded.headers = headers as any
|
|
923
934
|
return server.write(client.id, decoded as any)
|
|
924
935
|
}
|
|
925
936
|
|