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
package/src/cluster/Sharding.ts
CHANGED
|
@@ -143,7 +143,20 @@ export class Sharding extends Context.Service<Sharding, {
|
|
|
143
143
|
readonly concurrency?: number | "unbounded" | undefined
|
|
144
144
|
readonly mailboxCapacity?: number | "unbounded" | undefined
|
|
145
145
|
readonly mailboxMetrics?: boolean | undefined
|
|
146
|
+
/**
|
|
147
|
+
* When `true`, a handler defect fails only its request instead of
|
|
148
|
+
* restarting the entity and replaying its in-flight requests.
|
|
149
|
+
*/
|
|
146
150
|
readonly disableFatalDefects?: boolean | undefined
|
|
151
|
+
/**
|
|
152
|
+
* Schedule for restarting an entity after a defect and for retrying failed
|
|
153
|
+
* handler construction.
|
|
154
|
+
*
|
|
155
|
+
* Backoff is tracked per entity and resets once a request completes.
|
|
156
|
+
* When the policy ends, the default (500 ms growing by 1.5x, capped at 10
|
|
157
|
+
* seconds) continues, so restarts never stop; use `disableFatalDefects` to
|
|
158
|
+
* avoid them.
|
|
159
|
+
*/
|
|
147
160
|
readonly defectRetryPolicy?: Schedule.Schedule<any, unknown> | undefined
|
|
148
161
|
readonly spanAttributes?: Record<string, string> | undefined
|
|
149
162
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type * as Cause from "../../Cause.ts"
|
|
2
2
|
import * as Effect from "../../Effect.ts"
|
|
3
|
-
import * as Fiber from "../../Fiber.ts"
|
|
4
3
|
import { Abandon } from "../ClusterSchema.ts"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
)
|
|
5
|
+
// Annotate before opening the interruptible region. An interrupt already
|
|
6
|
+
// pending on an uninterruptible fiber fires as soon as the region opens, and
|
|
7
|
+
// the annotation is merged into that pending cause instead of being lost.
|
|
8
|
+
export const interrupt: Effect.Effect<never> = Effect.withFiber((fiber) => {
|
|
9
|
+
fiber.interruptUnsafe(fiber.id, Abandon.annotation)
|
|
10
|
+
return Effect.interruptible(Effect.never)
|
|
11
|
+
})
|
|
12
12
|
|
|
13
13
|
export const isCause = (cause: Cause.Cause<unknown>): boolean =>
|
|
14
14
|
cause.reasons.some((reason) => reason._tag === "Interrupt" && reason.annotations.has(Abandon.key))
|
|
@@ -116,6 +116,10 @@ export type EntityState = {
|
|
|
116
116
|
/** Set when the request should not outlive its caller. */
|
|
117
117
|
callerScope?: Scope.Scope | undefined
|
|
118
118
|
transaction?: RequestTransaction | undefined
|
|
119
|
+
/** Set when an interrupt arrives while no handlers are ready. */
|
|
120
|
+
interrupted?: boolean | undefined
|
|
121
|
+
/** The handler generation the remembered interrupt was last sent to. */
|
|
122
|
+
interruptSentTo?: EntityState["write"] | undefined
|
|
119
123
|
}>
|
|
120
124
|
lastActiveCheck: number
|
|
121
125
|
write: RpcServer.RpcServer<any>["write"]
|
|
@@ -172,7 +176,7 @@ export const make = Effect.fnUntraced(function*<
|
|
|
172
176
|
const defectRetryPolicy = options.defectRetryPolicy
|
|
173
177
|
? Schedule.concat(options.defectRetryPolicy, defaultRetryPolicy)
|
|
174
178
|
: defaultRetryPolicy
|
|
175
|
-
const
|
|
179
|
+
const newRestartStep = Schedule.toStepWithSleep(defectRetryPolicy)
|
|
176
180
|
const entityRpcs = new Map(entity.protocol.requests)
|
|
177
181
|
|
|
178
182
|
// The series reads the mailbox size on snapshot, so delivery does no extra work.
|
|
@@ -294,13 +298,38 @@ export const make = Effect.fnUntraced(function*<
|
|
|
294
298
|
const retired = Latch.makeUnsafe()
|
|
295
299
|
const isActive = () => !retired.isOpen()
|
|
296
300
|
|
|
301
|
+
// Per-activation restart backoff, reset when a request completes.
|
|
302
|
+
let restartStep: Effect.Success<typeof newRestartStep> | undefined
|
|
303
|
+
const restartDelay = Effect.gen(function*() {
|
|
304
|
+
restartStep ??= yield* newRestartStep
|
|
305
|
+
yield* restartStep(void 0)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// An interrupted request is not run again. The server answers the
|
|
309
|
+
// interrupt as it would for a request without a running handler. Each
|
|
310
|
+
// generation receives it at most once, and the flag stays so a later
|
|
311
|
+
// generation still applies it if this one fails first.
|
|
312
|
+
const writeInterrupt = (
|
|
313
|
+
request: {
|
|
314
|
+
readonly message: Message.IncomingRequestLocal<any>
|
|
315
|
+
interruptSentTo?: EntityState["write"] | undefined
|
|
316
|
+
},
|
|
317
|
+
write: EntityState["write"]
|
|
318
|
+
): Effect.Effect<void> => {
|
|
319
|
+
if (request.interruptSentTo === write) return Effect.void
|
|
320
|
+
request.interruptSentTo = write
|
|
321
|
+
return write(0, interruptEnvelope(request))
|
|
322
|
+
}
|
|
323
|
+
|
|
297
324
|
// Replay previously dispatched requests before admitting new work.
|
|
298
325
|
const replay = Effect.fnUntraced(function*(write: EntityState["write"]) {
|
|
299
326
|
if (!isActive()) return
|
|
300
327
|
for (const request of activeRequests.values()) {
|
|
301
328
|
if (!request.delivered) continue
|
|
302
329
|
request.sentExit = false
|
|
303
|
-
yield*
|
|
330
|
+
yield* request.interrupted
|
|
331
|
+
? writeInterrupt(request, write)
|
|
332
|
+
: write(0, requestEnvelope(request), requestWriteOptions(request))
|
|
304
333
|
}
|
|
305
334
|
})
|
|
306
335
|
|
|
@@ -405,6 +434,7 @@ export const make = Effect.fnUntraced(function*<
|
|
|
405
434
|
)
|
|
406
435
|
})
|
|
407
436
|
const complete = Effect.sync(() => {
|
|
437
|
+
restartStep = undefined
|
|
408
438
|
if (storageEnabled) {
|
|
409
439
|
processedRequestIds.add(request.message.envelope.requestId)
|
|
410
440
|
}
|
|
@@ -527,10 +557,22 @@ export const make = Effect.fnUntraced(function*<
|
|
|
527
557
|
address
|
|
528
558
|
)
|
|
529
559
|
|
|
560
|
+
// Forward interrupts remembered after their request was replayed, which
|
|
561
|
+
// the replacement handlers have not seen yet.
|
|
562
|
+
const forwardInterrupts = Effect.suspend(() => {
|
|
563
|
+
const write = writeRef.getUnsafe()
|
|
564
|
+
if (write === undefined) return Effect.void
|
|
565
|
+
const interrupts: Array<Effect.Effect<void>> = []
|
|
566
|
+
for (const request of activeRequests.values()) {
|
|
567
|
+
if (request.interrupted) interrupts.push(writeInterrupt(request, write))
|
|
568
|
+
}
|
|
569
|
+
return Effect.all(interrupts, { discard: true })
|
|
570
|
+
})
|
|
571
|
+
|
|
530
572
|
function restart(cause: Cause.Cause<unknown>, rebuild: Effect.Effect<void>): Effect.Effect<void> {
|
|
531
573
|
return Effect.logError("Defect in entity, restarting", cause).pipe(
|
|
532
|
-
Effect.andThen(Effect.ignore(
|
|
533
|
-
Effect.flatMap(() => isActive() ? rebuild : endLatch.open),
|
|
574
|
+
Effect.andThen(Effect.ignore(restartDelay)),
|
|
575
|
+
Effect.flatMap(() => isActive() ? Effect.andThen(rebuild, forwardInterrupts) : endLatch.open),
|
|
534
576
|
Effect.annotateLogs({
|
|
535
577
|
module: "EntityManager",
|
|
536
578
|
address,
|
|
@@ -552,6 +594,16 @@ export const make = Effect.fnUntraced(function*<
|
|
|
552
594
|
if (message._tag !== "Request") {
|
|
553
595
|
// Interrupts, acks and EOF reach the handlers even during shutdown.
|
|
554
596
|
const write = writeRef.getUnsafe()
|
|
597
|
+
if (write === undefined && message._tag === "Interrupt") {
|
|
598
|
+
// Waiting could stall the sender indefinitely if every rebuild
|
|
599
|
+
// fails, so remember the interrupt for the next generation.
|
|
600
|
+
// Uninterruptible requests ignore it.
|
|
601
|
+
const request = activeRequests.get(Snowflake.Snowflake(message.requestId))
|
|
602
|
+
if (request && !isUninterruptibleForServer(request.message.annotations)) {
|
|
603
|
+
request.interrupted = true
|
|
604
|
+
}
|
|
605
|
+
return Effect.void
|
|
606
|
+
}
|
|
555
607
|
return write
|
|
556
608
|
? write(clientId, message, writeOptions)
|
|
557
609
|
: Effect.flatMap(writeRef.await, (write) => write(clientId, message, writeOptions))
|
|
@@ -567,7 +619,7 @@ export const make = Effect.fnUntraced(function*<
|
|
|
567
619
|
const request = activeRequests.get(Snowflake.Snowflake(message.id))
|
|
568
620
|
if (!request) return Effect.void
|
|
569
621
|
request.delivered = true
|
|
570
|
-
return write(clientId, message, writeOptions)
|
|
622
|
+
return request.interrupted ? writeInterrupt(request, write) : write(clientId, message, writeOptions)
|
|
571
623
|
})
|
|
572
624
|
},
|
|
573
625
|
activeRequests,
|
|
@@ -1016,6 +1068,14 @@ const requestEnvelope = (entry: {
|
|
|
1016
1068
|
} as any) as any
|
|
1017
1069
|
})
|
|
1018
1070
|
|
|
1071
|
+
const interruptEnvelope = (entry: {
|
|
1072
|
+
readonly message: Message.IncomingRequestLocal<any>
|
|
1073
|
+
}): Parameters<EntityState["write"]>[1] => ({
|
|
1074
|
+
_tag: "Interrupt",
|
|
1075
|
+
requestId: entry.message.envelope.requestId as any,
|
|
1076
|
+
interruptors: []
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1019
1079
|
const retryRespond = <A, E, R>(times: number, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
1020
1080
|
times === 0 ?
|
|
1021
1081
|
effect :
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as Context from "../../Context.ts"
|
|
2
|
+
import type * as Effect from "../../Effect.ts"
|
|
3
|
+
import type { MalformedMessage, PersistenceError } from "../ClusterError.ts"
|
|
4
|
+
|
|
5
|
+
export const TransactionNotifications = Context.Reference<
|
|
6
|
+
Array<Effect.Effect<void, PersistenceError | MalformedMessage>> | undefined
|
|
7
|
+
>("effect/cluster/MessageStorage/TransactionNotifications", { defaultValue: () => undefined })
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses JSON5 configuration files without external dependencies.
|
|
3
|
+
*
|
|
4
|
+
* The compatibility target is [JSON5 1.0.0](https://spec.json5.org/) and the
|
|
5
|
+
* value-parsing behavior of `json5` 2.2.3, without its optional reviver.
|
|
6
|
+
* Identifier names use the runtime's Unicode property tables.
|
|
7
|
+
*
|
|
8
|
+
* @stability unstable
|
|
9
|
+
* @since 4.1.2
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as internal from "../internal/jsonParser.ts"
|
|
13
|
+
import * as Result from "../Result.ts"
|
|
14
|
+
import { EncodingError } from "./EncodingError.ts"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parses a JSON5 document into a JavaScript value.
|
|
18
|
+
*
|
|
19
|
+
* **Details**
|
|
20
|
+
*
|
|
21
|
+
* Returns `Result.succeed` with the parsed value, or `Result.fail` with an
|
|
22
|
+
* `EncodingError` when the input is not valid JSON5. Accepts comments,
|
|
23
|
+
* trailing commas, unquoted identifier keys, single-quoted strings,
|
|
24
|
+
* hexadecimal numbers, `Infinity`, and `NaN`. Duplicate keys use the last
|
|
25
|
+
* value; `__proto__` is preserved as an own data property.
|
|
26
|
+
*
|
|
27
|
+
* **Gotchas**
|
|
28
|
+
*
|
|
29
|
+
* Error messages give a line and column but do not include the input text.
|
|
30
|
+
* Unlike `JSON.parse`, this function has no reviver.
|
|
31
|
+
*
|
|
32
|
+
* **Example** (Parsing a JSON5 document)
|
|
33
|
+
*
|
|
34
|
+
* ```ts import.meta.vitest
|
|
35
|
+
* import { Result } from "effect"
|
|
36
|
+
* import { Json5 } from "effect/encoding"
|
|
37
|
+
*
|
|
38
|
+
* Json5.parse("{ port: 8080, }") // => Result.succeed({ port: 8080 })
|
|
39
|
+
* Result.isFailure(Json5.parse("{ port: }")) // => true
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @stability unstable
|
|
43
|
+
* @category decoding
|
|
44
|
+
* @since 4.1.2
|
|
45
|
+
*/
|
|
46
|
+
export const parse = (input: string): Result.Result<unknown, EncodingError> => {
|
|
47
|
+
try {
|
|
48
|
+
return Result.succeed(internal.parse(input, "JSON5"))
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return Result.fail(
|
|
51
|
+
new EncodingError({
|
|
52
|
+
module: "Json5",
|
|
53
|
+
kind: "Decode",
|
|
54
|
+
input,
|
|
55
|
+
message: e instanceof Error ? e.message : "Invalid input"
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses JSON with JavaScript-style comments without external dependencies.
|
|
3
|
+
*
|
|
4
|
+
* The syntax target is the [JSONC draft specification](https://jsonc.org/)
|
|
5
|
+
* with `jsonc-parser` 3.3.1's default profile: JSON plus line and block comments,
|
|
6
|
+
* with trailing commas disabled. One leading BOM is accepted, matching the
|
|
7
|
+
* TOML and YAML parsers. Decoded values follow `JSON.parse` semantics; invalid
|
|
8
|
+
* documents fail instead of returning a partial result.
|
|
9
|
+
*
|
|
10
|
+
* @stability unstable
|
|
11
|
+
* @since 4.1.2
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as internal from "../internal/jsonParser.ts"
|
|
15
|
+
import * as Result from "../Result.ts"
|
|
16
|
+
import { EncodingError } from "./EncodingError.ts"
|
|
17
|
+
|
|
18
|
+
const fail = (input: string): never => {
|
|
19
|
+
// Native errors differ across engines and may quote the input, so the strict parser
|
|
20
|
+
// locates the error. It accepts exactly what JSON.parse accepts once comments are
|
|
21
|
+
// removed; the final throw only guards against an engine that disagrees.
|
|
22
|
+
internal.parse(input, "JSONC")
|
|
23
|
+
throw internal.syntaxError(input, input.length, "JSONC")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lineEnd = /[\r\n]/g
|
|
27
|
+
|
|
28
|
+
const parseUnsafe = (input: string): unknown => {
|
|
29
|
+
// Ignore one leading BOM without shifting original-input error positions.
|
|
30
|
+
if (input.charCodeAt(0) === 0xfeff) input = " " + input.slice(1)
|
|
31
|
+
let slash = input.indexOf("/")
|
|
32
|
+
let json = input
|
|
33
|
+
if (slash !== -1) {
|
|
34
|
+
let stripped = ""
|
|
35
|
+
let start = 0
|
|
36
|
+
let quote = input.indexOf("\"")
|
|
37
|
+
while (slash !== -1) {
|
|
38
|
+
if (quote !== -1 && quote < slash) {
|
|
39
|
+
// Skip the string; a quote after an odd run of backslashes is escaped.
|
|
40
|
+
let end = quote
|
|
41
|
+
for (;;) {
|
|
42
|
+
end = input.indexOf("\"", end + 1)
|
|
43
|
+
if (end === -1) break
|
|
44
|
+
let backslash = end - 1
|
|
45
|
+
while (input.charCodeAt(backslash) === 92) backslash--
|
|
46
|
+
if ((end - backslash) % 2 === 1) break
|
|
47
|
+
}
|
|
48
|
+
if (end === -1) break
|
|
49
|
+
quote = input.indexOf("\"", end + 1)
|
|
50
|
+
if (slash < end) slash = input.indexOf("/", end + 1)
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
const next = input.charCodeAt(slash + 1)
|
|
54
|
+
let index: number
|
|
55
|
+
if (next === 47) {
|
|
56
|
+
lineEnd.lastIndex = slash + 2
|
|
57
|
+
index = lineEnd.test(input) ? lineEnd.lastIndex - 1 : input.length
|
|
58
|
+
} else if (next === 42) {
|
|
59
|
+
const end = input.indexOf("*/", slash + 2)
|
|
60
|
+
if (end === -1) fail(input)
|
|
61
|
+
index = end + 2
|
|
62
|
+
} else {
|
|
63
|
+
// Not a comment; JSON.parse rejects it below.
|
|
64
|
+
slash = input.indexOf("/", slash + 1)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
// A space prevents comments inside invalid tokens from joining those tokens.
|
|
68
|
+
stripped += input.slice(start, slash) + " "
|
|
69
|
+
start = index
|
|
70
|
+
slash = input.indexOf("/", index)
|
|
71
|
+
if (quote !== -1 && quote < index) quote = input.indexOf("\"", index)
|
|
72
|
+
}
|
|
73
|
+
json = stripped + input.slice(start)
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(json)
|
|
77
|
+
} catch {
|
|
78
|
+
return fail(input)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parses a JSONC document into a JavaScript value.
|
|
84
|
+
*
|
|
85
|
+
* **Details**
|
|
86
|
+
*
|
|
87
|
+
* Returns `Result.succeed` with the parsed value, or `Result.fail` with an
|
|
88
|
+
* `EncodingError` when the input is not valid JSONC. Accepts one leading BOM
|
|
89
|
+
* and line and block comments wherever JSON permits whitespace. All other
|
|
90
|
+
* syntax follows `JSON.parse`, including duplicate keys and `__proto__`
|
|
91
|
+
* handling.
|
|
92
|
+
*
|
|
93
|
+
* **Gotchas**
|
|
94
|
+
*
|
|
95
|
+
* Error messages give a 1-based line and column, counted in UTF-16 code units,
|
|
96
|
+
* but do not include the input text. Trailing commas and JSON5 extensions are
|
|
97
|
+
* not accepted. Unlike `JSON.parse`, this function has no reviver.
|
|
98
|
+
*
|
|
99
|
+
* **Example** (Parsing a JSONC document)
|
|
100
|
+
*
|
|
101
|
+
* ```ts import.meta.vitest
|
|
102
|
+
* import { Result } from "effect"
|
|
103
|
+
* import { Jsonc } from "effect/encoding"
|
|
104
|
+
*
|
|
105
|
+
* Jsonc.parse("{ \"port\": 8080 } // comment") // => Result.succeed({ port: 8080 })
|
|
106
|
+
* Result.isFailure(Jsonc.parse("{ \"port\": 8080, }")) // => true
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @stability unstable
|
|
110
|
+
* @category decoding
|
|
111
|
+
* @since 4.1.2
|
|
112
|
+
*/
|
|
113
|
+
export const parse = (input: string): Result.Result<unknown, EncodingError> => {
|
|
114
|
+
try {
|
|
115
|
+
return Result.succeed(parseUnsafe(input))
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return Result.fail(
|
|
118
|
+
new EncodingError({
|
|
119
|
+
module: "Jsonc",
|
|
120
|
+
kind: "Decode",
|
|
121
|
+
input,
|
|
122
|
+
message: e instanceof Error ? e.message : "Invalid input"
|
|
123
|
+
})
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|