Skip to content

Commit

Permalink
Implement random access structs
Browse files Browse the repository at this point in the history
  • Loading branch information
kriszyp committed Jul 17, 2022
1 parent 6a5979c commit 83b076d
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 106 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ tests/samples
# Visual Studio Code directory
.vscode
.vs
.idea

build
dist/test.js
1 change: 1 addition & 0 deletions node-index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { Packr, Encoder, addExtension, pack, encode, NEVER, ALWAYS, DECIMAL_ROUND, DECIMAL_FIT } from './pack.js'
export { Unpackr, Decoder, C1, unpack, unpackMultiple, decode, FLOAT32_OPTIONS, clearSource, roundFloat32, isNativeAccelerationEnabled } from './unpack.js'
import './struct.js'
export { PackrStream, UnpackrStream, PackrStream as EncoderStream, UnpackrStream as DecoderStream } from './stream.js'
export { decodeIter, encodeIter } from './iterators.js'
export const useRecords = false
Expand Down
23 changes: 21 additions & 2 deletions pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ let targetView
let position = 0
let safeEnd
let bundledStrings = null
let writeSlots
const MAX_BUNDLE_SIZE = 0xf000
const hasNonLatin = /[\u0080-\uFFFF]/
const RECORD_SYMBOL = Symbol('record-id')
export const RECORD_SYMBOL = Symbol('record-id')
export class Packr extends Unpackr {
constructor(options) {
super(options)
Expand Down Expand Up @@ -122,7 +123,10 @@ export class Packr extends Unpackr {
if (hasSharedUpdate)
hasSharedUpdate = false
try {
pack(value)
if (encodeOptions & RANDOM_ACCESS_STRUCT)
writeStruct(value);
else
pack(value)
if (bundledStrings) {
writeBundles(start, pack)
}
Expand Down Expand Up @@ -687,6 +691,17 @@ export class Packr extends Unpackr {
target[insertionOffset + start] = keysTarget[0]
}
}
const writeStruct = (object, safePrototype) => {
let queuedReferences = writeSlots(object, target, position, structures, makeRoom)
if (!queuedReferences)
return writeObject(object, true);
position = queuedReferences.position;
for (let i = 0, l = queuedReferences.length; i < l;) {
let value = queuedReferences[i++];
target.uint32[queuedReferences[i++]] = 0x60000000 | position;
pack(value);
}
}
}
useBuffer(buffer) {
// this means we are finished using our own buffer and we can write over it safely
Expand Down Expand Up @@ -921,6 +936,9 @@ export function addExtension(extension) {
}
unpackAddExtension(extension)
}
export function setWriteSlots(func) {
writeSlots = func;
}

let defaultPackr = new Packr({ useRecords: false })
export const pack = defaultPackr.pack
Expand All @@ -931,3 +949,4 @@ import { FLOAT32_OPTIONS } from './unpack.js'
export const { NEVER, ALWAYS, DECIMAL_ROUND, DECIMAL_FIT } = FLOAT32_OPTIONS
export const REUSE_BUFFER_MODE = 512
export const RESET_BUFFER_MODE = 1024
export const RANDOM_ACCESS_STRUCT = 2048
263 changes: 169 additions & 94 deletions struct.js
Original file line number Diff line number Diff line change
@@ -1,104 +1,179 @@
/*
types:
double
float
uint16
uint8
ref16
ref32
str16 - starting with 0x8 (first bit set) means latin1
str32 - starting with 0xf, means latin1
// first four bits
// 0000 - unsigned int
// 0010 - float32
// 0011 - float32
// 0100 - float32
// 0101 - float32
// 0110 - plain reference
// 0111 - latin string reference
// 1000 - structure reference
// 1001 - random access structure reference
// 1010 - float32
// 1011 - float32
// 1100 - float32
// 1101 - float32
// 1110 - constants and 3-byte strings
// 1111 - negative int

all types:
(0xff)*, 0xc0 = null
(0xff)*, msgpackr encoding -> decode with msgpackr
*/
// first three bits
// 000 - unsigned int
// 001 - float32
// 010 - float32
// 011 - latin string reference
// 100 - reference
// 101 - float32
// 110 - float32
// 111 - constants and 3-byte strings

const TYPE_SIZE = {
double: 8,
float: 4,
uint16: 2,
uint8: 1,
ref16: 2,
ref32: 4,
str16: 2,
str32: 4,
ref0: 0,
str0: 0,
}
function readObject() {

var prototype = {}
// sort with biggest types first for optimal packing and alignment
positioned = structure.slice(0).sort((a, b) => {
let asSize = TYPE_SIZE[a.type];
let bSize = TYPE_SIZE[b.type];
return asSize > bSize ? 1 : aSize < bSize ? -1 : 0;
})
let l = structure.length;
let offset = 0;
let lastReferencingOffset;
for (let i = 0; i < l; i++) {
let property = positioned[i];
property.offset = offset;
property.start = lastReferencingOffset;
lastReferencingOffset = offset;
offset += TYPE_SIZE[property.type];
}
for (let i = 0, l = structure.length; i < l; i++) {
let property = structure[i]
let get, set
let offset = property.offset
switch (property.type) {
case 'double':
get = function() {
return this.buffer.dataView.getFloat64(this.position + offset)
};
import { setWriteSlots, RECORD_SYMBOL } from './pack.js'
import { setReadStruct, unpack } from './unpack.js';
const hasNonLatin = /[\u0080-\uFFFF]/
setWriteSlots(writeSlots);
function writeSlots(object, target, position, structures, makeRoom) {
let transition = structures.transitions || false
let newTransitions = 0
let keyCount = 0;
let start = position;
position = (position >> 2) + 1;
let queuedReferences = [];
let uint32 = target.uint32 || (target.uint32 = new Uint32Array(target.buffer));
let encoded;
let stringData = '';
let safeEnd = (target.length - 10) >> 2;
for (let key in object) {
let nextTransition = transition[key]
if (!nextTransition) {
return; // bail
//nextTransition = transition[key] = Object.create(null)
//newTransitions++
}
if (position > safeEnd) {
target = makeRoom(position << 2)
position -= start
start = 0
safeEnd = target.length - 10
}
transition = nextTransition
let value = object[key];
switch (typeof value) {
case 'number':
if (value >>> 0 === value && value < 0x10000000)
encoded = value;
break;
case 'uint16':
get = function() {
let value = this.buffer.dataView.getUint16(this.position + offset)
if (value >= 0xffc0) {
return unpack(this.buffer, this.position + 2, this.position + 1);
}
return value;
};
case 'string':
if (hasNonLatin.test(value)) {
queuedReferences.push(value, position++);
continue;
}
if (value.length < 4) { // we can inline really small strings
encoded = 0xf8000000 + (value.length << 24) + (value.charCodeAt(0) << 16) + (value.charCodeAt(1) << 8) + (value.charCodeAt(2) || 0)
} else if (value.length < 256 && stringData.length < 61440) {
// bundle these strings
encoded = 0x60000000 | (value.length << 16) | stringData.length;
stringData += value;
} else { // else queue it
queuedReferences.push(value, position++);
continue;
}
break;
case 'ref16':
let start = property.start;
get = function() {

let refEnd = this.buffer.dataView.getUint16(this.position + offset) + this.position + offset;

if (value >= 0xffc0) {
return unpack(this.buffer, this.position + 2, this.position + 1);
}
return value;
case 'object':
if (value) {
queuedReferences.push(value, position++);
continue;
} else { // null
encoded = 0xe0000000;
}

break;
case 'boolean':
encoded = value ? 0xe3000000 : 0xe2000000;
break;
case 'undefined':
encoded = 0xe1000000;
break;
}
Object.defineProperty(prototype, property.name, { get, set, enumerable: true })

uint32[position++] = encoded;
}
structure.read = function() {
let object = {
position,
source,
__proto__: prototype
}
position +=
let recordId = transition[RECORD_SYMBOL]
if (!(recordId < 256)) {
// for now just punt and go back to writeObject
return;
// newRecord(transition, transition.__keys__ || Object.keys(object), newTransitions, true)
}

if (readObject.count++ > inlineObjectReadThreshold) {
let readObject = structure.read = (new Function('r', 'return function(){return {' + structure.map(key => validName.test(key) ? key + ':r()' : ('[' + JSON.stringify(key) + ']:r()')).join(',') + '}}'))(read)
if (structure.highByte === 0)
structure.read = createSecondByteReader(firstId, structure.read)
return readObject() // second byte is already read, if there is one so immediately read object
position = position << 2;
safeEnd = safeEnd << 2;
let stringLength = stringData.length;
if (stringData) {
if (position + stringLength > safeEnd) {
target = makeRoom(position + stringLength);
}
position += target.latin1Write(stringData, position, 0xffffffff);
}
target[start++] = 0xc1;
target[start++] = recordId;
target[start++] = stringLength >> 8;
target[start++] = stringLength & 0xff;
queuedReferences.position = position;
return queuedReferences;
}
var sourceSymbol = Symbol('source')
function readStruct(src, position, srcEnd, structure) {
var stringLength = (src[position++] << 8) | src[position++];
var construct = structure.construct;
var srcString;
if (!construct) {
construct = structure.construct = function() {
}
var prototype = construct.prototype;
for (let i = 0, l = structure.length; i < l; i++) {
let key = structure[i];
Object.defineProperty(prototype, key, {
get() {
let source = this[sourceSymbol];
let src = source.src;
let uint32 = src.uint32 || (src.uint32 = new Uint32Array(src.buffer, src.byteOffset, src.byteLength));
let value = uint32[source.position + i];
let start;
switch (value >>> 29) {
case 0:
return value;
case 1: case 2: case 5: case 6:
return float32[source.position + i];
case 3:
if (!srcString) {
start = (source.position + l) << 2;
srcString = src.toString('utf-8', start, start + stringLength);
}
start = value & 0xffff;
return srcString.slice(start, start + ((value >> 16) & 0x7ff));
case 4:
start = 0x1fffffff & value;
let end = srcEnd;
for (let next = i + 1; next < l; next++) {
let nextValue = uint32[source.position + next];
if ((nextValue & 0xf0000000) == 0x30000000) {
end = 0x1fffffff & nextValue;
}
}
return unpack(src.slice(start, end));
case 7:
switch((value >> 24) & 0xf) {
case 0: return null;
case 1: return undefined;
case 2: return false;
case 3: return true;
}
}
},
enumerable: true,
});
}
}
let object = {}
for (let i = 0, l = structure.length; i < l; i++) {
let key = structure[i]
object[key] = read()
var instance = new construct();
instance[sourceSymbol] = {
src,
uint32: src.uint32,
position: position >> 2,
}
return object
}
return instance;
}
setReadStruct(readStruct)
Loading

0 comments on commit 83b076d

Please sign in to comment.