@geoprotocol/grc-20 0.3.1 → 0.4.1

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.
@@ -1,5 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { EditBuilder, createEdit, createEntity, createRelation, updateEntity, deleteEntity, restoreEntity, updateRelation, deleteRelation, restoreRelation, createValueRef, encodeEdit, decodeEdit, encodeEditCompressed, decodeEditCompressed, encodeEditAuto, decodeEditAuto, isCompressed, preloadCompression, isCompressionReady, parseId, formatId, formatIdHex, randomId, derivedUuid, derivedUuidFromString, uniqueRelationId, relationEntityId, properties, types, relationTypes, languages, idsEqual, Reader, } from "../index.js";
2
+ import { EditBuilder, createEdit, createEntity, createRelation, updateEntity, deleteEntity, restoreEntity, updateRelation, deleteRelation, restoreRelation, createValueRef, encodeEdit, decodeEdit, encodeEditCompressed, decodeEditCompressed, encodeEditAuto, decodeEditAuto, isCompressed, preloadCompression, isCompressionReady, parseId, formatId, randomId, derivedUuid, derivedUuidFromString, uniqueRelationId, relationEntityId, properties, types, relationTypes, languages, idsEqual, Reader, Writer, } from "../index.js";
3
+ import { decodeValuePayload, encodeValuePayload } from "../codec/value.js";
4
+ import { DataType } from "../types/value.js";
3
5
  function readObjectIds(encoded) {
4
6
  const reader = new Reader(encoded);
5
7
  reader.readBytes(4);
@@ -20,21 +22,40 @@ function readObjectIds(encoded) {
20
22
  reader.readIdVec(); // context IDs
21
23
  return objects;
22
24
  }
25
+ function positiveBigIntToMinimalTwosComplement(value) {
26
+ let hex = value.toString(16);
27
+ if (hex.length % 2 !== 0) {
28
+ hex = `0${hex}`;
29
+ }
30
+ const raw = new Uint8Array(hex.length / 2);
31
+ for (let i = 0; i < raw.length; i++) {
32
+ raw[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
33
+ }
34
+ if (raw.length === 0) {
35
+ return new Uint8Array([0]);
36
+ }
37
+ if ((raw[0] & 0x80) === 0) {
38
+ return raw;
39
+ }
40
+ const bytes = new Uint8Array(raw.length + 1);
41
+ bytes.set(raw, 1);
42
+ return bytes;
43
+ }
23
44
  describe("ID utilities", () => {
24
- it("formatId produces Base58 string", () => {
45
+ it("formatId produces 32 hex chars", () => {
25
46
  const id = randomId();
26
- const b58 = formatId(id);
27
- expect(b58.length).toBeLessThanOrEqual(22);
28
- expect(/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/.test(b58)).toBe(true);
47
+ const hex = formatId(id);
48
+ expect(hex.length).toBe(32);
49
+ expect(/^[0-9a-f]{32}$/.test(hex)).toBe(true);
29
50
  });
30
- it("parseId roundtrips with formatId (Base58)", () => {
51
+ it("parseId roundtrips with formatId", () => {
31
52
  const id = randomId();
32
- const b58 = formatId(id);
33
- const parsed = parseId(b58);
53
+ const hex = formatId(id);
54
+ const parsed = parseId(hex);
34
55
  expect(parsed).toBeDefined();
35
56
  expect(idsEqual(id, parsed)).toBe(true);
36
57
  });
37
- it("parseId accepts hex for backwards compatibility", () => {
58
+ it("parseId accepts hyphens", () => {
38
59
  const withHyphens = "550e8400-e29b-41d4-a716-446655440000";
39
60
  const withoutHyphens = "550e8400e29b41d4a716446655440000";
40
61
  const id1 = parseId(withHyphens);
@@ -76,24 +97,24 @@ describe("ID utilities", () => {
76
97
  describe("Genesis IDs", () => {
77
98
  it("properties.NAME matches spec", () => {
78
99
  const id = properties.NAME;
79
- expect(formatIdHex(id)).toBe("a126ca530c8e48d5b88882c734c38935");
100
+ expect(formatId(id)).toBe("a126ca530c8e48d5b88882c734c38935");
80
101
  expect(idsEqual(id, properties.name())).toBe(true);
81
102
  });
82
103
  it("properties.DESCRIPTION matches spec", () => {
83
104
  const id = properties.DESCRIPTION;
84
- expect(formatIdHex(id)).toBe("9b1f76ff9711404c861e59dc3fa7d037");
105
+ expect(formatId(id)).toBe("9b1f76ff9711404c861e59dc3fa7d037");
85
106
  });
86
107
  it("properties.COVER matches spec", () => {
87
108
  const id = properties.COVER;
88
- expect(formatIdHex(id)).toBe("34f535072e6b42c5a84443981a77cfa2");
109
+ expect(formatId(id)).toBe("34f535072e6b42c5a84443981a77cfa2");
89
110
  });
90
111
  it("types.IMAGE matches spec", () => {
91
112
  const id = types.IMAGE;
92
- expect(formatIdHex(id)).toBe("f3f790c4c74e4d23a0a91e8ef84e30d9");
113
+ expect(formatId(id)).toBe("f3f790c4c74e4d23a0a91e8ef84e30d9");
93
114
  });
94
115
  it("relationTypes.TYPES matches spec", () => {
95
116
  const id = relationTypes.TYPES;
96
- expect(formatIdHex(id)).toBe("8f151ba4de204e3c9cb499ddf96f48f1");
117
+ expect(formatId(id)).toBe("8f151ba4de204e3c9cb499ddf96f48f1");
97
118
  expect(idsEqual(id, relationTypes.types())).toBe(true);
98
119
  });
99
120
  it("languages.english is deterministic", () => {
@@ -1007,6 +1028,341 @@ it("throws when time or datetime is missing a timezone offset", () => {
1007
1028
  };
1008
1029
  expect(() => encodeEdit(edit)).toThrow("Invalid RFC 3339 time");
1009
1030
  });
1031
+ describe("Decimal normalization", () => {
1032
+ it("normalizes i64 mantissa with trailing zeros", () => {
1033
+ const editId = randomId();
1034
+ const entityId = randomId();
1035
+ const propId = randomId();
1036
+ // mantissa 100, exponent -2 represents 1.00 — should normalize to mantissa 1, exponent 0
1037
+ const edit = {
1038
+ id: editId,
1039
+ name: "Decimal Test",
1040
+ authors: [],
1041
+ createdAt: 0n,
1042
+ ops: [
1043
+ {
1044
+ type: "createEntity",
1045
+ id: entityId,
1046
+ values: [
1047
+ { property: propId, value: { type: "decimal", exponent: -2, mantissa: { type: "i64", value: 100n } } },
1048
+ ],
1049
+ },
1050
+ ],
1051
+ };
1052
+ const encoded = encodeEdit(edit);
1053
+ const decoded = decodeEdit(encoded);
1054
+ const op = decoded.ops[0];
1055
+ expect(op.type).toBe("createEntity");
1056
+ if (op.type === "createEntity") {
1057
+ const val = op.values[0].value;
1058
+ expect(val.type).toBe("decimal");
1059
+ if (val.type === "decimal") {
1060
+ // Should be normalized: 100 * 10^-2 = 1 * 10^0
1061
+ expect(val.mantissa).toEqual({ type: "i64", value: 1n });
1062
+ expect(val.exponent).toBe(0);
1063
+ }
1064
+ }
1065
+ });
1066
+ it("normalizes i64 mantissa 1230 with exponent -2", () => {
1067
+ const editId = randomId();
1068
+ const entityId = randomId();
1069
+ const propId = randomId();
1070
+ const edit = {
1071
+ id: editId,
1072
+ name: "Decimal Test",
1073
+ authors: [],
1074
+ createdAt: 0n,
1075
+ ops: [
1076
+ {
1077
+ type: "createEntity",
1078
+ id: entityId,
1079
+ values: [
1080
+ { property: propId, value: { type: "decimal", exponent: -2, mantissa: { type: "i64", value: 1230n } } },
1081
+ ],
1082
+ },
1083
+ ],
1084
+ };
1085
+ const encoded = encodeEdit(edit);
1086
+ const decoded = decodeEdit(encoded);
1087
+ const op = decoded.ops[0];
1088
+ if (op.type === "createEntity") {
1089
+ const val = op.values[0].value;
1090
+ if (val.type === "decimal") {
1091
+ // 1230 * 10^-2 = 123 * 10^-1
1092
+ expect(val.mantissa).toEqual({ type: "i64", value: 123n });
1093
+ expect(val.exponent).toBe(-1);
1094
+ }
1095
+ }
1096
+ });
1097
+ it("normalizes zero mantissa with non-zero exponent", () => {
1098
+ const editId = randomId();
1099
+ const entityId = randomId();
1100
+ const propId = randomId();
1101
+ const edit = {
1102
+ id: editId,
1103
+ name: "Decimal Test",
1104
+ authors: [],
1105
+ createdAt: 0n,
1106
+ ops: [
1107
+ {
1108
+ type: "createEntity",
1109
+ id: entityId,
1110
+ values: [
1111
+ { property: propId, value: { type: "decimal", exponent: 5, mantissa: { type: "i64", value: 0n } } },
1112
+ ],
1113
+ },
1114
+ ],
1115
+ };
1116
+ const encoded = encodeEdit(edit);
1117
+ const decoded = decodeEdit(encoded);
1118
+ const op = decoded.ops[0];
1119
+ if (op.type === "createEntity") {
1120
+ const val = op.values[0].value;
1121
+ if (val.type === "decimal") {
1122
+ expect(val.mantissa).toEqual({ type: "i64", value: 0n });
1123
+ expect(val.exponent).toBe(0);
1124
+ }
1125
+ }
1126
+ });
1127
+ it("does not modify already-normalized decimals", () => {
1128
+ const editId = randomId();
1129
+ const entityId = randomId();
1130
+ const propId = randomId();
1131
+ const edit = {
1132
+ id: editId,
1133
+ name: "Decimal Test",
1134
+ authors: [],
1135
+ createdAt: 0n,
1136
+ ops: [
1137
+ {
1138
+ type: "createEntity",
1139
+ id: entityId,
1140
+ values: [
1141
+ { property: propId, value: { type: "decimal", exponent: -2, mantissa: { type: "i64", value: 12345n } } },
1142
+ ],
1143
+ },
1144
+ ],
1145
+ };
1146
+ const encoded = encodeEdit(edit);
1147
+ const decoded = decodeEdit(encoded);
1148
+ const op = decoded.ops[0];
1149
+ if (op.type === "createEntity") {
1150
+ const val = op.values[0].value;
1151
+ if (val.type === "decimal") {
1152
+ expect(val.mantissa).toEqual({ type: "i64", value: 12345n });
1153
+ expect(val.exponent).toBe(-2);
1154
+ }
1155
+ }
1156
+ });
1157
+ it("normalizes negative mantissa with trailing zeros", () => {
1158
+ const editId = randomId();
1159
+ const entityId = randomId();
1160
+ const propId = randomId();
1161
+ const edit = {
1162
+ id: editId,
1163
+ name: "Decimal Test",
1164
+ authors: [],
1165
+ createdAt: 0n,
1166
+ ops: [
1167
+ {
1168
+ type: "createEntity",
1169
+ id: entityId,
1170
+ values: [
1171
+ { property: propId, value: { type: "decimal", exponent: 0, mantissa: { type: "i64", value: -500n } } },
1172
+ ],
1173
+ },
1174
+ ],
1175
+ };
1176
+ const encoded = encodeEdit(edit);
1177
+ const decoded = decodeEdit(encoded);
1178
+ const op = decoded.ops[0];
1179
+ if (op.type === "createEntity") {
1180
+ const val = op.values[0].value;
1181
+ if (val.type === "decimal") {
1182
+ // -500 * 10^0 = -5 * 10^2
1183
+ expect(val.mantissa).toEqual({ type: "i64", value: -5n });
1184
+ expect(val.exponent).toBe(2);
1185
+ }
1186
+ }
1187
+ });
1188
+ it("normalizes big mantissa with trailing zeros", () => {
1189
+ const editId = randomId();
1190
+ const entityId = randomId();
1191
+ const propId = randomId();
1192
+ // Big-endian two's complement for 500: 0x01F4
1193
+ const bigBytes = new Uint8Array([0x01, 0xF4]);
1194
+ const edit = {
1195
+ id: editId,
1196
+ name: "Decimal Test",
1197
+ authors: [],
1198
+ createdAt: 0n,
1199
+ ops: [
1200
+ {
1201
+ type: "createEntity",
1202
+ id: entityId,
1203
+ values: [
1204
+ { property: propId, value: { type: "decimal", exponent: 0, mantissa: { type: "big", bytes: bigBytes } } },
1205
+ ],
1206
+ },
1207
+ ],
1208
+ };
1209
+ const encoded = encodeEdit(edit);
1210
+ const decoded = decodeEdit(encoded);
1211
+ const op = decoded.ops[0];
1212
+ if (op.type === "createEntity") {
1213
+ const val = op.values[0].value;
1214
+ if (val.type === "decimal") {
1215
+ // 500 * 10^0 = 5 * 10^2, and 5 fits in i64
1216
+ expect(val.mantissa).toEqual({ type: "i64", value: 5n });
1217
+ expect(val.exponent).toBe(2);
1218
+ }
1219
+ }
1220
+ });
1221
+ it("normalizes raw decoded decimals so TypeScript matches Rust", () => {
1222
+ const writer = new Writer();
1223
+ writer.writeSignedVarint(-2n);
1224
+ writer.writeByte(0x00);
1225
+ writer.writeSignedVarint(100n);
1226
+ const decoded = decodeValuePayload(new Reader(writer.finish()), DataType.Decimal);
1227
+ expect(decoded).toEqual({
1228
+ type: "decimal",
1229
+ exponent: 0,
1230
+ mantissa: { type: "i64", value: 1n },
1231
+ });
1232
+ });
1233
+ it("normalizes raw decoded large big mantissas without truncation", () => {
1234
+ const large = new Uint8Array([0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1235
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
1236
+ const writer = new Writer();
1237
+ writer.writeSignedVarint(0n);
1238
+ writer.writeByte(0x01);
1239
+ writer.writeLengthPrefixedBytes(large);
1240
+ const decoded = decodeValuePayload(new Reader(writer.finish()), DataType.Decimal);
1241
+ expect(decoded).toEqual({
1242
+ type: "decimal",
1243
+ exponent: 1,
1244
+ mantissa: {
1245
+ type: "big",
1246
+ bytes: new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1247
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
1248
+ },
1249
+ });
1250
+ });
1251
+ it("keeps negative big power-of-two mantissas minimally encoded", () => {
1252
+ const writer = new Writer();
1253
+ encodeValuePayload(writer, {
1254
+ type: "decimal",
1255
+ exponent: 0,
1256
+ mantissa: {
1257
+ type: "big",
1258
+ bytes: new Uint8Array([0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
1259
+ },
1260
+ });
1261
+ const reader = new Reader(writer.finish());
1262
+ expect(reader.readSignedVarint()).toBe(0n);
1263
+ expect(reader.readByte()).toBe(0x01);
1264
+ const encodedBytes = reader.readLengthPrefixedBytes();
1265
+ expect(encodedBytes).toEqual(new Uint8Array([0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
1266
+ });
1267
+ it("re-encodes non-minimal big mantissas to canonical bytes", () => {
1268
+ const writer = new Writer();
1269
+ encodeValuePayload(writer, {
1270
+ type: "decimal",
1271
+ exponent: 0,
1272
+ mantissa: {
1273
+ type: "big",
1274
+ bytes: new Uint8Array([0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01]),
1275
+ },
1276
+ });
1277
+ const reader = new Reader(writer.finish());
1278
+ expect(reader.readSignedVarint()).toBe(0n);
1279
+ expect(reader.readByte()).toBe(0x01);
1280
+ expect(reader.readLengthPrefixedBytes()).toEqual(new Uint8Array([0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01]));
1281
+ });
1282
+ it("rejects non-minimal decimal mantissa bytes on decode", () => {
1283
+ const writer = new Writer();
1284
+ writer.writeSignedVarint(0n);
1285
+ writer.writeByte(0x01);
1286
+ writer.writeLengthPrefixedBytes(new Uint8Array([0x00, 0x7F]));
1287
+ expect(() => decodeValuePayload(new Reader(writer.finish()), DataType.Decimal))
1288
+ .toThrow("decimal mantissa bytes are not minimal");
1289
+ });
1290
+ it("rejects empty big mantissa bytes on decode", () => {
1291
+ const writer = new Writer();
1292
+ writer.writeSignedVarint(0n);
1293
+ writer.writeByte(0x01);
1294
+ writer.writeLengthPrefixedBytes(new Uint8Array([]));
1295
+ expect(() => decodeValuePayload(new Reader(writer.finish()), DataType.Decimal))
1296
+ .toThrow("decimal mantissa bytes are not minimal");
1297
+ });
1298
+ it("rejects empty big mantissa bytes on encode", () => {
1299
+ const writer = new Writer();
1300
+ expect(() => encodeValuePayload(writer, {
1301
+ type: "decimal",
1302
+ exponent: 0,
1303
+ mantissa: { type: "big", bytes: new Uint8Array([]) },
1304
+ })).toThrow("DECIMAL mantissa bytes must not be empty");
1305
+ });
1306
+ it("rejects decimal big mantissas over the maximum on encode", () => {
1307
+ const writer = new Writer();
1308
+ const oversized = new Uint8Array(64 * 1024 * 1024 + 1);
1309
+ oversized[0] = 1;
1310
+ expect(() => encodeValuePayload(writer, {
1311
+ type: "decimal",
1312
+ exponent: 0,
1313
+ mantissa: { type: "big", bytes: oversized },
1314
+ })).toThrow("decimal mantissa length 67108865 exceeds maximum 67108864");
1315
+ });
1316
+ it("rejects excessive decimal normalization work on decode", () => {
1317
+ const writer = new Writer();
1318
+ const bytes = positiveBigIntToMinimalTwosComplement(10n ** 4097n);
1319
+ writer.writeSignedVarint(0n);
1320
+ writer.writeByte(0x01);
1321
+ writer.writeLengthPrefixedBytes(bytes);
1322
+ expect(() => decodeValuePayload(new Reader(writer.finish()), DataType.Decimal))
1323
+ .toThrow("DECIMAL normalization exceeds maximum steps");
1324
+ });
1325
+ it("rejects excessive decimal normalization work on encode", () => {
1326
+ const writer = new Writer();
1327
+ expect(() => encodeValuePayload(writer, {
1328
+ type: "decimal",
1329
+ exponent: 0,
1330
+ mantissa: { type: "big", bytes: positiveBigIntToMinimalTwosComplement(10n ** 4097n) },
1331
+ })).toThrow("DECIMAL normalization exceeds maximum steps");
1332
+ });
1333
+ it("rejects exponents outside int32 range on decode", () => {
1334
+ const writer = new Writer();
1335
+ writer.writeSignedVarint(2147483648n);
1336
+ writer.writeByte(0x00);
1337
+ writer.writeSignedVarint(1n);
1338
+ expect(() => decodeValuePayload(new Reader(writer.finish()), DataType.Decimal))
1339
+ .toThrow("DECIMAL exponent outside int32 range");
1340
+ });
1341
+ it("rejects exponent overflow during decode-time normalization", () => {
1342
+ const writer = new Writer();
1343
+ writer.writeSignedVarint(2147483647n);
1344
+ writer.writeByte(0x00);
1345
+ writer.writeSignedVarint(10n);
1346
+ expect(() => decodeValuePayload(new Reader(writer.finish()), DataType.Decimal))
1347
+ .toThrow("DECIMAL exponent outside int32 range");
1348
+ });
1349
+ it("rejects exponent overflow during encode-time normalization", () => {
1350
+ const writer = new Writer();
1351
+ expect(() => encodeValuePayload(writer, {
1352
+ type: "decimal",
1353
+ exponent: 2147483647,
1354
+ mantissa: { type: "i64", value: 10n },
1355
+ })).toThrow("DECIMAL exponent outside int32 range");
1356
+ });
1357
+ it("rejects decimal big mantissa lengths over the maximum", () => {
1358
+ const writer = new Writer();
1359
+ writer.writeSignedVarint(0n);
1360
+ writer.writeByte(0x01);
1361
+ writer.writeVarintNumber(64 * 1024 * 1024 + 1);
1362
+ expect(() => decodeValuePayload(new Reader(writer.finish()), DataType.Decimal))
1363
+ .toThrow("decimal mantissa length 67108865 exceeds maximum 67108864");
1364
+ });
1365
+ });
1010
1366
  describe("Compression", () => {
1011
1367
  it("isCompressed detects GRC2Z magic", () => {
1012
1368
  const compressed = new Uint8Array([0x47, 0x52, 0x43, 0x32, 0x5a, 0x00]); // "GRC2Z" + data