Skip to content

Commit

Permalink
Compression Upgrade
Browse files Browse the repository at this point in the history
Did more investigating into multiple ways of using the Compression Streams API, and I realized that this is a really nice setup to have, possibly for a standalone module outside of NBTify. This provides a more cross-platform zlib API which would be similar to using `node:zlib`, but available both in the browser, and Node! It's build off of the Compression Streams API too, so it's only an API wrapper, and possibly a polyfill where necessary.

I'm going to try settings things up first here, then move them to a dedicated project which both handles the compression polyfills, and provides these more functional-based APIs, which both provide more performance than my previous streams-specific `Blob` handling that I had before, and are simpler to use directly with `ArrayBuffer` and the various `TypedArray` objects, rather than having to go back and forth between `Blob` containers, which does appear to be a little less performant with the simple test that I set up.

Oh my, that last sentence was horrible, I'm sorry. I guess I'm more of a dev than I am a writer.

Offroaders123/Dovetail#1
whatwg/compression#8
https://wicg.github.io/compression/#example-deflate-compress
https://jakearchibald.com/2017/async-iterators-and-generators/#making-streams-iterate
  • Loading branch information
Offroaders123 committed May 17, 2023
1 parent 5bcf05a commit c3b5e95
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 56 deletions.
94 changes: 70 additions & 24 deletions src/compression.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,71 @@
export type CompressionFormat = "gzip" | "deflate" | "deflate-raw";

export interface CompressionOptions {
format: CompressionFormat;
}

/**
* Compresses a Uint8Array using a specific compression format.
*/
export async function compress(data: Uint8Array | ArrayBufferLike, { format }: CompressionOptions): Promise<Uint8Array> {
const { body } = new Response(data instanceof Uint8Array ? data : new Uint8Array(data));
const readable = body!.pipeThrough(new CompressionStream(format));
const buffer = await new Response(readable).arrayBuffer();
return new Uint8Array(buffer);
}

/**
* Decompresses a Uint8Array using a specific decompression format.
*/
export async function decompress(data: Uint8Array | ArrayBufferLike, { format }: CompressionOptions): Promise<Uint8Array> {
const { body } = new Response(data instanceof Uint8Array ? data : new Uint8Array(data));
const readable = body!.pipeThrough(new DecompressionStream(format));
const buffer = await new Response(readable).arrayBuffer();
return new Uint8Array(buffer);
export async function gzip(data: BufferSource): Promise<Uint8Array> {
return compress(data,"gzip");
}

export async function gunzip(data: BufferSource): Promise<Uint8Array> {
return decompress(data,"gzip");
}

export async function deflate(data: BufferSource): Promise<Uint8Array> {
return compress(data,"deflate");
}

export async function inflate(data: BufferSource): Promise<Uint8Array> {
return decompress(data,"deflate");
}

export async function deflateRaw(data: BufferSource): Promise<Uint8Array> {
return compress(data,"deflate-raw");
}

export async function inflateRaw(data: BufferSource): Promise<Uint8Array> {
return decompress(data,"deflate-raw");
}

async function compress(data: BufferSource, format: CompressionFormat): Promise<Uint8Array> {
const compressionStream = new CompressionStream(format);
return pipeThroughCompressionStream(data,compressionStream);
}

async function decompress(data: BufferSource, format: CompressionFormat): Promise<Uint8Array> {
const decompressionStream = new DecompressionStream(format);
return pipeThroughCompressionStream(data,decompressionStream);
}

async function pipeThroughCompressionStream(data: BufferSource, compressionStream: CompressionStream | DecompressionStream): Promise<Uint8Array> {
const writer = compressionStream.writable.getWriter();

writer.write(data);
writer.close();

const chunks: Uint8Array[] = [];
let byteLength = 0;

for await (const chunk of readableStreamToAsyncGenerator(compressionStream.readable)){
chunks.push(chunk);
byteLength += chunk.byteLength;
}

const result = new Uint8Array(byteLength);
let byteOffset = 0;

for (const chunk of chunks){
result.set(chunk,byteOffset);
byteOffset += chunk.byteLength;
}

return result;
}

async function* readableStreamToAsyncGenerator(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array,void,void> {
const reader = stream.getReader();
try {
while (true){
const { done, value } = await reader.read();
if (done) return;
yield value;
}
} finally {
reader.releaseLock();
}
}
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ export * from "./stringify.js";
export * from "./definition.js";
export * from "./data.js";
export * from "./tag.js";
export * from "./primitive.js";
export * from "./compression.js";
export * from "./primitive.js";
6 changes: 3 additions & 3 deletions src/read.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Name, Endian, Compression, BedrockLevel, NBTData } from "./data.js";
import { Int8, Int16, Int32, Float32 } from "./primitive.js";
import { TAG } from "./tag.js";
import { decompress } from "./compression.js";
import { gunzip, inflate } from "./compression.js";

import type { Tag, ListTag, CompoundTag } from "./tag.js";

Expand Down Expand Up @@ -79,11 +79,11 @@ export async function read<T extends object = any>(data: Uint8Array | ArrayBuffe
}

if (compression === "gzip"){
data = await decompress(data,{ format: "gzip" });
data = await gunzip(data);
}

if (compression === "deflate"){
data = await decompress(data,{ format: "deflate" });
data = await inflate(data);
}

if (bedrockLevel === undefined){
Expand Down
6 changes: 3 additions & 3 deletions src/write.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Name, Endian, Compression, BedrockLevel, NBTData } from "./data.js";
import { TAG, getTagType } from "./tag.js";
import { Int32 } from "./primitive.js";
import { compress } from "./compression.js";
import { gzip, deflate } from "./compression.js";

import type { RootTag, Tag, ByteTag, BooleanTag, ShortTag, IntTag, LongTag, FloatTag, DoubleTag, ByteArrayTag, StringTag, ListTag, CompoundTag, IntArrayTag, LongArrayTag } from "./tag.js";

Expand Down Expand Up @@ -57,11 +57,11 @@ export async function write(data: RootTag | NBTData, { name, endian, compression
}

if (compression === "gzip"){
result = await compress(result,{ format: "gzip" });
result = await gzip(result);
}

if (compression === "deflate"){
result = await compress(result,{ format: "deflate" });
result = await deflate(result);
}

return result;
Expand Down
27 changes: 3 additions & 24 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,9 @@ console.log(data,"\n");
const result = await NBT.read<LCEPlayer>(data,{ strict: false });
console.log(result,"\n");

console.log(result.data.SelectedItem.id,"\n");

const recompile = await NBT.write(result).then(Buffer.from);
console.log(recompile,"\n");

console.log(Buffer.compare(data,recompile));

interface TempTest {
noice: NBT.BooleanTag;
}

const { data: tempTest } = new NBT.NBTData<TempTest>({
noice: true
});

tempTest.noice
// @ts-expect-error
tempTest.notAProperty

const demo = new NBT.NBTData({ nice: true, smartTypes: 10 });

demo.data.smartTypes;

interface MyData { Version: boolean; }
declare const heya: NBT.NBTData<MyData>;
const noice = new NBT.NBTData(heya);

const noice2 = await NBT.read(new Uint8Array(),heya);
const noice3 = new NBT.NBTReader().read(new Uint8Array(),heya);
console.log(Buffer.compare(data,recompile));
24 changes: 24 additions & 0 deletions test/types-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as NBT from "../src/index.js";

interface TempTest {
noice: NBT.BooleanTag;
}

const { data: tempTest } = new NBT.NBTData<TempTest>({
noice: true
});

tempTest.noice
// @ts-expect-error
tempTest.notAProperty

const demo = new NBT.NBTData({ nice: true, smartTypes: 10 });

demo.data.smartTypes;

interface MyData { Version: boolean; }
declare const heya: NBT.NBTData<MyData>;
const noice = new NBT.NBTData(heya);

const noice2 = await NBT.read(new Uint8Array(),heya);
const noice3 = new NBT.NBTReader().read(new Uint8Array(),heya);

0 comments on commit c3b5e95

Please sign in to comment.