-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.js
More file actions
219 lines (198 loc) · 8.7 KB
/
index.js
File metadata and controls
219 lines (198 loc) · 8.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import {
writeStruct, writeStructInPlace,
readStruct, onLoadedStructures, prepareStructures, saveState,
SOURCE_SYMBOL,
} from './struct.js';
/**
* Creates a class that extends `BaseClass` (msgpackr's Packr or cbor-x's Encoder)
* and adds random-access struct encoding/decoding.
*
* Two encoding paths are supported:
*
* - **Fast path** (BaseClass advertises `SUPPORTS_STRUCT_HOOKS`): structon
* sets per-instance hook methods (`_writeStruct`, `_readStruct`, …) which
* the BaseClass's encode/decode pipeline dispatches to. The struct is
* written directly into the BaseClass's shared target buffer with no
* intermediate allocations.
*
* - **Standalone path** (any other base): structon wraps `encode`/`decode`
* and uses its own pre-allocated work buffers, returning a fresh
* `Uint8Array` per encode call.
*
* The on-the-wire binary format is identical in both paths.
*
* @param {class} BaseClass - msgpackr Packr / Encoder or cbor-x Encoder
* @returns {class} Structon subclass
*/
export function createStructon(BaseClass) {
const _baseDecode = BaseClass.prototype.decode;
const _baseUnpack = BaseClass.prototype.unpack || null;
// A BaseClass advertises hook support via a static `SUPPORTS_STRUCT_HOOKS`
// flag (set by msgpackr ≥ 2.0.1, cbor-x post-update, etc.). Walking the
// prototype chain lets a Packr subclass be passed in unchanged.
const fastPath = lookupStatic(BaseClass, 'SUPPORTS_STRUCT_HOOKS') === true;
class Structon extends BaseClass {
constructor(options = {}) {
super(options);
// Honor maxOwnStructures for the typed-struct path: bounds the per-encoder typed-structure
// dictionary (+ transition trie). Once reached, novel shapes fall back to plain encoding
// instead of growing the dictionary without limit. Default: uncapped (no behavior change).
if (options?.maxOwnStructures != null) this.maxOwnStructures = options.maxOwnStructures;
// Initialise typed structures state on this instance
if (!this.typedStructs) this.typedStructs = [];
if (fastPath) {
// Set per-instance hook methods. The BaseClass's encode/decode
// pipeline picks these up automatically.
this._writeStruct = writeStructInPlace;
this._readStruct = readStruct;
this._onLoadedStructures = onLoadedStructures;
this._onSaveState = saveState;
this._prepareStructures = prepareStructures;
return;
}
// Standalone path: wrap encode (set as own property by the base ctor).
if (Object.prototype.hasOwnProperty.call(this, 'encode')) {
const _super = this.encode.bind(this);
const self = this;
this.encode = function structonEncode(value, encodeOptions) {
return self._structonEncode(value, encodeOptions, _super);
};
}
}
_structonEncode(value, encodeOptions, superEncode) {
if (value && typeof value === 'object' && value.constructor === Object) {
const prevLen = this.typedStructs.length;
let structuresUpdated = false;
this._onStructureAdded = () => { structuresUpdated = true; };
try {
const encoded = writeStruct(value, v => this.encode(v), this);
if (encoded !== null) {
if (structuresUpdated || this.typedStructs.length !== prevLen) {
this._saveTypedStructures();
}
return encoded;
}
// Capped miss: fall back to plain base encoding. The base may persist its own
// named structures via saveStructures, overwriting our combined {named, typed}
// payload and stranding previously written struct data. Re-save afterward so the
// typed structures survive (this.structures now also holds any base record added).
const result = superEncode(value, encodeOptions);
if (this.typedStructs && this.typedStructs.length > 0) this._saveTypedStructures();
return result;
} finally {
this._onStructureAdded = null;
}
}
return superEncode(value, encodeOptions);
}
decode(source, options) {
// Fast path: let the BaseClass's checkedRead dispatch via _readStruct.
if (fastPath) return _baseDecode.call(this, source, options);
// Standalone path: intercept top-level struct bytes ourselves.
let src = toUint8Array(source);
const start = options?.start || 0;
const srcEnd = options?.end ?? src.length;
if (srcEnd > start && src[start] >= 0x20 && src[start] < 0x40) {
const recordId = peekRecordId(src);
// _ensureTypedStructures may call getStructures which reads from the DB into
// the same reusable buffer — copy src before that can overwrite it
src = Uint8Array.prototype.slice.call(src, 0, srcEnd);
this._ensureTypedStructures(recordId, srcEnd - start);
if (recordId !== -1 && this.typedStructs && this.typedStructs[recordId]) {
return readStruct.call(this, src, start, srcEnd);
}
}
return _baseDecode.call(this, source, options);
}
/**
* Decode bytes from src[start..end) — used by readStruct's OBJECT_DATA
* getters when the base class doesn't support unpack(src, {start, end})
* (e.g. cbor-x without hook support).
*/
_decodeSliceDirect(src, start, end) {
if (end > start && src[start] >= 0x20 && src[start] < 0x40) {
const slice = src.subarray ? src.subarray(start, end) : src.slice(start, end);
const recordId = peekRecordId(slice);
if (recordId !== -1 && this.typedStructs && this.typedStructs[recordId]) {
return readStruct.call(this, slice, 0, slice.length);
}
}
if (_baseUnpack) return _baseUnpack.call(this, src, { start, end });
const slice2 = src.subarray ? src.subarray(start, end) : src.slice(start, end);
return _baseDecode.call(this, slice2);
}
_ensureTypedStructures(recordId, byteLength) {
// Load once if we have never loaded. Additionally, reload when a specific structure id was
// requested but is absent from our (possibly stale) cache: another encoder may have minted
// and persisted that structure after our last load. Without this, a once-populated cache
// never refreshes, and decode silently falls through to the base decoder for any structure
// added later — a record that exists comes back undecodable (HarperFast/harper#1163). The
// base decoder already reloads classic shared structures on a miss; this brings typed
// structures to parity.
if (this.typedStructs && this.typedStructs.transitions) {
if (recordId === undefined || recordId === -1 || this.typedStructs[recordId]) return;
// A single byte in the struct-header range (0x20-0x3f) is a base-encoded positive fixint
// (32-63), not a struct — a real struct record is always longer than its id header. Only
// reload for a multi-byte payload, so we never reload (and then mis-read) a pass-through
// integer whose value collides with an as-yet-unloaded structure id.
if (!(byteLength > 1)) return;
}
this._loadStructures();
}
_loadStructures() {
let sharedData;
if (typeof this.getStructures === 'function') sharedData = this.getStructures();
else if (typeof this.getShared === 'function') sharedData = this.getShared();
if (sharedData) onLoadedStructures.call(this, sharedData);
}
_saveTypedStructures() {
if (typeof this.saveStructures === 'function') {
const structures = prepareStructures(this.structures || [], this);
this.saveStructures(structures);
} else if (typeof this.saveShared === 'function') {
this.saveShared({
structures: this.structures || [],
typedStructs: this.typedStructs,
});
}
}
_mergeStructures(loadedStructures) {
// On the fast path the BaseClass already calls _onLoadedStructures
// itself; only the standalone path needs to invoke it manually.
if (!fastPath && loadedStructures) onLoadedStructures.call(this, loadedStructures);
if (super._mergeStructures) return super._mergeStructures(loadedStructures);
}
clearSharedData() {
if (super.clearSharedData) super.clearSharedData();
this.typedStructs = [];
}
}
return Structon;
}
function lookupStatic(cls, name) {
let c = cls;
while (c) {
if (Object.prototype.hasOwnProperty.call(c, name)) return c[name];
c = Object.getPrototypeOf(c);
}
return undefined;
}
function toUint8Array(source) {
if (source instanceof Uint8Array) return source;
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(source)) return source;
if (source instanceof ArrayBuffer) return new Uint8Array(source);
return new Uint8Array(source);
}
function peekRecordId(src) {
if (src.length < 1) return -1;
let id = src[0] - 0x20;
if (id < 24) return id;
switch (id) {
case 24: return src.length > 1 ? src[1] : -1;
case 25: return src.length > 2 ? src[1] + (src[2] << 8) : -1;
case 26: return src.length > 3 ? src[1] + (src[2] << 8) + (src[3] << 16) : -1;
case 27: return src.length > 4 ? src[1] + (src[2] << 8) + (src[3] << 16) + (src[4] << 24) : -1;
default: return -1;
}
}
export { SOURCE_SYMBOL };