mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
fix(environment): release batch replies as they are encoded; judge exact numbers by value
answerBatch kept every settled reply in its pending array until the batch ended, so a batch alternating data/add and data/get held each loaded state alive; each reply now leaves the queue as it is encoded. The exact-number policy judged a number by its recorded spelling, so a state loaded with 1e20 came back from data/get as 100000000000000000000, which data/add refused, breaking the promise that data/get output loads back unchanged. A number is exact only when both its recorded spelling and the one serialization writes are safe, so integers from 2^53 below 1e21 are refused in any spelling; replay keeps such bodies as recorded text. A domain test checks that every accepted number is accepted again once written back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5956e00dd6
commit
5a61853a69
6 files changed
+50
-4
No files matched your search
@@ -478,7 +478,9 @@ calls find entities under the checked line's recorded path. Each document
|
||||
names a `document` view once, at its initial document's `path`. The `version`,
|
||||
and the `state_evidence_id` and `types` that `data/get` reports, must match
|
||||
the served state, so a `data/get` result loads back unchanged. A number without
|
||||
an exact native value or a `__proto__` member is refused rather than altered.
|
||||
an exact native value, or whose serialized spelling would not be read as exact
|
||||
(an integer from 2^53 below 1e21, such as `1e20`, in any spelling), or a
|
||||
`__proto__` member is refused rather than altered.
|
||||
An invalid document is `-32602` naming the first problem by JSON pointer, as in
|
||||
`/rea_state/entities/3/collection` (also in `error.data.path`); a problem
|
||||
inside caller data (`fields`, `value`, `types`) is located at that member, so
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("replay JSON value fidelity", () => {
|
||||
'{"id":9007199254740993}',
|
||||
'{"id":9007199254740993.0}',
|
||||
'{"id":9.007199254740993e15}',
|
||||
'{"id":1e20}',
|
||||
'[{"id":18446744073709551615}]',
|
||||
'{"value":0.100000000000000005}',
|
||||
'{"value":1e-1000}',
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { isExactJsonNumber } from "./exactJsonNumber.js";
|
||||
|
||||
const spellings = [
|
||||
"0",
|
||||
"42",
|
||||
"-7",
|
||||
"1.5",
|
||||
"0.1",
|
||||
"1e3",
|
||||
"1e-7",
|
||||
"9007199254740991",
|
||||
"9007199254740992",
|
||||
"9.007199254740992e15",
|
||||
"1e20",
|
||||
"100000000000000000000",
|
||||
"1e21",
|
||||
"1.7976931348623157e308",
|
||||
];
|
||||
|
||||
it("judges a number alike in every spelling, so an accepted number is accepted again once written back", () => {
|
||||
for (const spelling of spellings) {
|
||||
const value = Number(spelling);
|
||||
if (isExactJsonNumber(value, spelling))
|
||||
expect(isExactJsonNumber(value, String(value)), spelling).toBe(true);
|
||||
}
|
||||
expect(isExactJsonNumber(1e20, "1e20")).toBe(false);
|
||||
expect(isExactJsonNumber(1e20, "100000000000000000000")).toBe(false);
|
||||
expect(isExactJsonNumber(1e21, "1e21")).toBe(true);
|
||||
expect(isExactJsonNumber(9007199254740991, "9007199254740991")).toBe(true);
|
||||
expect(isExactJsonNumber(-0, "-0")).toBe(false);
|
||||
});
|
||||
@@ -2,12 +2,19 @@ import { isSafeNumber } from "lossless-json";
|
||||
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
|
||||
import { safeParseJson } from "./safeJson.js";
|
||||
|
||||
/** Whether native JSON conversion and serialization preserve the recorded number. */
|
||||
/**
|
||||
* Whether native JSON conversion and serialization preserve the recorded number. Both the recorded
|
||||
* spelling and the one serialization writes must be safe, so `1e20` and `100000000000000000000`
|
||||
* are judged alike and anything accepted is accepted again once written back.
|
||||
*/
|
||||
export const isExactJsonNumber = (
|
||||
value: number,
|
||||
source: string | undefined,
|
||||
): boolean =>
|
||||
source !== undefined && isSafeNumber(source) && !Object.is(value, -0);
|
||||
source !== undefined &&
|
||||
isSafeNumber(source) &&
|
||||
isSafeNumber(String(value)) &&
|
||||
!Object.is(value, -0);
|
||||
|
||||
/** Parse JSON text whose every number survives native conversion exactly; undefined otherwise. */
|
||||
export const parseExactJson = (text: string): JsonValue | undefined => {
|
||||
|
||||
@@ -248,7 +248,8 @@ const answerBatch = async (
|
||||
for (const reply of pending) void reply.catch(() => undefined);
|
||||
const encode = replyEncoder();
|
||||
const texts: string[] = [];
|
||||
for (const next of pending) {
|
||||
// Taking each settled reply off the queue releases it, and any state it holds, once encoded.
|
||||
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
||||
const reply = await next;
|
||||
if (reply !== undefined) texts.push(encode(reply));
|
||||
}
|
||||
|
||||
@@ -202,6 +202,8 @@ describe("state data plane", () => {
|
||||
});
|
||||
for (const altered of [
|
||||
'"login":9007199254740993',
|
||||
// Exact in this spelling, but data/get would write it as digits it could not load again.
|
||||
'"login":1e20',
|
||||
'"__proto__":{"polluted":true},"login":"renamed"',
|
||||
]) {
|
||||
const reply = await plane.post(
|
||||
|
||||
Reference in new issue
Block a user