-
-
Notifications
You must be signed in to change notification settings - Fork 1
Implement FlexBuffers codec for binary JSON serialization #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
4
commits into
master
Choose a base branch
from
copilot/fix-36
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
5364d93
Initial plan
Copilot e32c585
Add initial FlexBuffers implementation with basic structure
Copilot 016ea1d
Complete FlexBuffers basic implementation - scalars, strings, empty c…
Copilot 8db9f29
Address review feedback: const enums, remove StreamingBinaryJsonEncod…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,300 @@ | ||
import {Reader} from '@jsonjoy.com/util/lib/buffers/Reader'; | ||
import type {BinaryJsonDecoder, PackValue} from '../types'; | ||
import { | ||
FlexBufferType, | ||
BitWidth, | ||
unpackType, | ||
unpackBitWidth, | ||
bitWidthToByteSize, | ||
} from './constants'; | ||
|
||
export class FlexBuffersDecoder implements BinaryJsonDecoder { | ||
public reader = new Reader(); | ||
|
||
public read(uint8: Uint8Array): PackValue { | ||
this.reader.reset(uint8); | ||
return this.readRoot(); | ||
} | ||
|
||
public decode(uint8: Uint8Array): unknown { | ||
this.reader.reset(uint8); | ||
return this.readRoot(); | ||
} | ||
|
||
public readAny(): PackValue { | ||
return this.readRoot(); | ||
} | ||
|
||
private readRoot(): PackValue { | ||
const reader = this.reader; | ||
const uint8 = reader.uint8; | ||
const length = uint8.length; | ||
|
||
if (length < 3) { | ||
throw new Error('FlexBuffer too short'); | ||
} | ||
|
||
// Read from the end - the last byte is the width in bytes of the root (not BitWidth enum) | ||
const rootByteWidth = uint8[length - 1]; // This is actual byte size (1, 2, 4, 8) | ||
const rootTypeByte = uint8[length - 2]; | ||
const rootType = unpackType(rootTypeByte); | ||
const rootTypeBitWidth = unpackBitWidth(rootTypeByte); | ||
|
||
// Convert byte width to BitWidth enum | ||
const rootBitWidth = this.byteSizeToBitWidth(rootByteWidth); | ||
|
||
// For scalar values, the root value occupies bytes before the type and bit width | ||
const rootPos = length - 2 - rootByteWidth; | ||
|
||
if (rootPos < 0) { | ||
throw new Error('Invalid FlexBuffer format'); | ||
} | ||
|
||
// Read root value using the root bit width for scalars | ||
// For inline types, the bit width in the type byte is unused | ||
if (this.isInlineType(rootType)) { | ||
return this.readValueAt(rootType, rootBitWidth, rootPos); | ||
} else { | ||
// For offset types, use the type bit width | ||
return this.readValueAt(rootType, rootTypeBitWidth, rootPos); | ||
} | ||
} | ||
|
||
private byteSizeToBitWidth(byteSize: number): BitWidth { | ||
switch (byteSize) { | ||
case 1: return BitWidth.W8; | ||
case 2: return BitWidth.W16; | ||
case 4: return BitWidth.W32; | ||
case 8: return BitWidth.W64; | ||
default: throw new Error(`Invalid byte size: ${byteSize}`); | ||
} | ||
} | ||
|
||
private isInlineType(type: FlexBufferType): boolean { | ||
switch (type) { | ||
case FlexBufferType.NULL: | ||
case FlexBufferType.BOOL: | ||
case FlexBufferType.INT: | ||
case FlexBufferType.UINT: | ||
case FlexBufferType.FLOAT: | ||
return true; | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
private readValueAt(type: FlexBufferType, bitWidth: BitWidth, pos: number): PackValue { | ||
const reader = this.reader; | ||
const originalPos = reader.x; | ||
reader.x = pos; | ||
|
||
const result = this.readValue(type, bitWidth); | ||
reader.x = originalPos; | ||
return result; | ||
} | ||
|
||
private readValue(type: FlexBufferType, bitWidth: BitWidth): PackValue { | ||
switch (type) { | ||
case FlexBufferType.NULL: | ||
return null; | ||
|
||
case FlexBufferType.BOOL: | ||
return this.readUInt(bitWidth) !== 0; | ||
|
||
case FlexBufferType.INT: | ||
return this.readInt(bitWidth); | ||
|
||
case FlexBufferType.UINT: | ||
return this.readUInt(bitWidth); | ||
|
||
case FlexBufferType.FLOAT: | ||
return this.readFloat(bitWidth); | ||
|
||
case FlexBufferType.STRING: | ||
return this.readString(); | ||
|
||
case FlexBufferType.BLOB: | ||
return this.readBlob(); | ||
|
||
case FlexBufferType.VECTOR: | ||
return this.readVector(); | ||
|
||
case FlexBufferType.MAP: | ||
return this.readMap(); | ||
|
||
default: | ||
throw new Error(`Unsupported FlexBuffer type: ${type}`); | ||
} | ||
} | ||
|
||
private readInt(bitWidth: BitWidth): number | bigint { | ||
const reader = this.reader; | ||
const view = reader.view; | ||
const pos = reader.x; | ||
|
||
switch (bitWidth) { | ||
case BitWidth.W8: | ||
reader.x += 1; | ||
return view.getInt8(pos); | ||
case BitWidth.W16: | ||
reader.x += 2; | ||
return view.getInt16(pos, true); | ||
case BitWidth.W32: | ||
reader.x += 4; | ||
return view.getInt32(pos, true); | ||
case BitWidth.W64: | ||
reader.x += 8; | ||
const bigint = view.getBigInt64(pos, true); | ||
// Return regular number if it fits | ||
if (bigint >= Number.MIN_SAFE_INTEGER && bigint <= Number.MAX_SAFE_INTEGER) { | ||
return Number(bigint); | ||
} | ||
return bigint; | ||
default: | ||
throw new Error(`Invalid int bit width: ${bitWidth}`); | ||
} | ||
} | ||
|
||
private readUInt(bitWidth: BitWidth): number { | ||
const reader = this.reader; | ||
const view = reader.view; | ||
const pos = reader.x; | ||
|
||
switch (bitWidth) { | ||
case BitWidth.W8: | ||
reader.x += 1; | ||
return view.getUint8(pos); | ||
case BitWidth.W16: | ||
reader.x += 2; | ||
return view.getUint16(pos, true); | ||
case BitWidth.W32: | ||
reader.x += 4; | ||
return view.getUint32(pos, true); | ||
case BitWidth.W64: | ||
reader.x += 8; | ||
const bigint = view.getBigUint64(pos, true); | ||
// Return regular number if it fits | ||
if (bigint <= Number.MAX_SAFE_INTEGER) { | ||
return Number(bigint); | ||
} | ||
throw new Error('UInt64 too large for JavaScript number'); | ||
default: | ||
throw new Error(`Invalid uint bit width: ${bitWidth}`); | ||
} | ||
} | ||
|
||
private readFloat(bitWidth: BitWidth): number { | ||
const reader = this.reader; | ||
const view = reader.view; | ||
const pos = reader.x; | ||
|
||
switch (bitWidth) { | ||
case BitWidth.W32: | ||
reader.x += 4; | ||
return view.getFloat32(pos, true); | ||
case BitWidth.W64: | ||
reader.x += 8; | ||
return view.getFloat64(pos, true); | ||
default: | ||
throw new Error(`Invalid float bit width: ${bitWidth}`); | ||
} | ||
} | ||
|
||
private readString(): string { | ||
const reader = this.reader; | ||
|
||
// Read size (uint8) | ||
const size = reader.u8(); | ||
|
||
// Move back to read the string data (stored before size) | ||
reader.x -= size + 2; // -1 for size, -1 for null terminator | ||
|
||
// Read string data | ||
const stringData = reader.buf(size); | ||
|
||
// Skip null terminator | ||
reader.x++; | ||
|
||
// Skip size (we already read it) | ||
reader.x++; | ||
|
||
return new TextDecoder().decode(stringData); | ||
} | ||
|
||
private readBlob(): Uint8Array { | ||
const reader = this.reader; | ||
|
||
// Read size (uint8) | ||
const size = reader.u8(); | ||
|
||
// Move back to read the blob data (stored before size) | ||
reader.x -= size + 1; // -1 for size | ||
|
||
// Read blob data | ||
const blobData = reader.buf(size); | ||
|
||
// Skip size (we already read it) | ||
reader.x++; | ||
|
||
return blobData; | ||
} | ||
|
||
private readVector(): PackValue[] { | ||
const reader = this.reader; | ||
const uint8 = reader.uint8; | ||
|
||
// Read type bytes from the end (after size) | ||
const currentPos = reader.x; | ||
const size = uint8[currentPos]; | ||
|
||
const result: PackValue[] = []; | ||
|
||
if (size === 0) { | ||
reader.x++; // Skip size | ||
return result; | ||
} | ||
|
||
// Type bytes are after the size | ||
const typesPos = currentPos + 1; | ||
|
||
// Element data is before the size | ||
let elementPos = currentPos - size; | ||
|
||
for (let i = 0; i < size; i++) { | ||
const typeInfo = uint8[typesPos + i]; | ||
const elementType = unpackType(typeInfo); | ||
const elementBitWidth = unpackBitWidth(typeInfo); | ||
|
||
const element = this.readValueAt(elementType, elementBitWidth, elementPos); | ||
result.push(element); | ||
|
||
// Move to next element position (this is simplified - should calculate based on element size) | ||
elementPos += 1; // This is wrong but a simplification for now | ||
} | ||
|
||
// Move past size and type bytes | ||
reader.x = typesPos + size; | ||
|
||
return result; | ||
} | ||
|
||
private readMap(): Record<string, PackValue> { | ||
const reader = this.reader; | ||
const uint8 = reader.uint8; | ||
|
||
// Read type bytes from the end (after key offset info and size) | ||
const currentPos = reader.x; | ||
|
||
// Skip backwards to read map structure | ||
// This is a simplified implementation | ||
const size = uint8[currentPos]; | ||
|
||
const result: Record<string, PackValue> = {}; | ||
|
||
// For now, return empty object for simplicity | ||
// A full implementation would need to properly parse the key vector | ||
reader.x++; // Skip size | ||
|
||
return result; | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do no use
TextDecoder
, use existing string decoding implementations from this library.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced TextDecoder with
reader.utf8(size)
to use the library's existing string decoding implementation. Commit 8db9f29.