Skip to content

Add effect/encoding/FlatBuffers - #10643

Open
fubhy wants to merge 7 commits into
mainfrom
feat/encoding-flatbuffers
Open

fubhy wants to merge 7 commits into
mainfrom
feat/encoding-flatbuffers

Conversation

@fubhy

@fubhy fubhy commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

Stack

Arrow chain (each PR is based on the one before it):

  1. This PR effect/encoding/FlatBuffers (base: main)
  2. Add effect/arrow #10649 effect/arrow (base: feat/encoding-flatbuffers)
  3. Add effect/dataframe #10641 effect/dataframe (base: feat/arrow-support)

Independent foundation, based on main and not part of the chain: #10652 effect/encoding/Thrift, for the planned Parquet reader. A later Parquet to ArrowTable layer joins Parquet and Arrow.

Arrow Flight, Protobuf / gRPC / ConnectRPC and Thrift's binary protocol / RPC envelopes are out of scope.

Why

Arrow IPC (the draft effect/arrow in #10641) stores its schema and record batch metadata as FlatBuffers. That PR carries a private FlatBuffers reader and writer under arrow/internal. Anything else that reads or writes FlatBuffers would need its own copy: Arrow Flight, Parquet-adjacent formats, TFLite and other model files, game/IoT protocols.

Requirement

  • Read a FlatBuffer without a schema compiler, the way generated code does: field N of this table is a string, a table, a vector of structs…
  • Never read outside the buffer, and never throw anything but a typed error on malformed or hostile input.
  • Write buffers that other FlatBuffers implementations read, ideally identical to what the official builders write, so the output can be checked against them.

Alternatives

  • Depend on the flatbuffers npm package: it adds a runtime dependency to effect; its reads are unchecked (they return garbage or throw RangeError); and it has no Result/error type.
  • Keep it internal to Arrow: every other consumer would duplicate it.
  • Schema-driven API (.fbs → Effect Schema, typed tables): possible later, on top of this module. Codegen needs this layer either way.

Summary

effect/encoding/FlatBuffers
  root(bytes, { identifier?, sizePrefixed?, maxDepth? }) -> Table   throws FlatBuffersError
  decode(bytes, read, options)                -> Result<A, FlatBuffersError>   rethrows Usage
  Table   has / bool…float64(slot, default) / string / table / struct / vector / bytes
  Vector  length, element accessors by index (scalars, string, table, struct), checked against elementSize
  Struct  fixed-offset scalar accessors, confined to the struct's size
  makeBuilder({ initialSize?, forceDefaults? }) -> Builder   initialSize: integer in 1..2^31, else Usage
    createString / createSharedString / createBytes / createOffsetVector
    startVector … endVector, startTable / addField* / endTable, finish(root, options)
const builder = FlatBuffers.makeBuilder()
const name = builder.createString("Orc")
builder.startTable(2)
builder.addFieldOffset(0, name)
builder.addFieldInt16(1, 300, 100)
const bytes = builder.finish(builder.endTable(), { identifier: "MONS" })

FlatBuffers.decode(bytes, (root) => ({ name: root.string(0), hp: root.int16(1, 100) }), { identifier: "MONS" })
// => Result.succeed({ name: "Orc", hp: 300 })
  • Builder follows the official algorithm: prep alignment, vtable dedup, defaults skipped unless forceDefaults, identifier and size prefix in finish.
  • FlatBuffersError kinds: OutOfBounds, InvalidVtable, InvalidUtf8, Identifier, Limit (tables nested past maxDepth, default 64), Usage (caller bugs, thrown even from decode).
  • Table, Vector and Struct are interfaces; root and decode are the only way in.
  • Experimental, @since 4.1.5. Arrow (Add effect/arrow #10649) reads and writes its IPC metadata with this module.

Evidence

The oracle is the official Python builder (flatbuffers 25.12.19). The test file has two buffers it wrote, inlined as hex, next to the TypeScript builder calls that make the same bytes: a monster table (identifier, shared strings, deduplicated vtables, a padded struct vector, an inline struct, a skipped default) and a size-prefixed buffer (identifier, forceDefaults, growth from initialSize: 1).

FlatBuffers.test.ts (11 tests, all pass)
  builds the bytes the official builder builds       both buffers byte-identical
  reads the official builder's bytes
  only fails with FlatBuffersError on truncated or corrupted buffers   every prefix and every flipped byte
  absent-field defaults, decode failure kinds, rethrown errors, builder misuse
  maxDepth limit, invalid slots and reads that don't match the vector or struct shape
  UTF-8 strings and identifiers with a leading BOM, null references under forceDefaults
  • An earlier 311-case generated corpus (11 by feature, 300 seeded random) also matched byte for byte. It was replaced by the two inline cases to drop the 190 KB fixture.
  • Before: the first oracle run caught an addOffset bug in this module (the offset was computed after moving the head, so it was off by 4).
    After: identical.
  • Type check (effect-tsgo tsrs), oxlint, dprint, the JSDoc check, the module doctest and ai-docgen pass.

Merge Danger

Door: two-way

A new experimental module with no existing callers. The only shared file touched is the encoding barrel.

Blast Radius: none

Closes EFF-3806

Schema-less FlatBuffers reading and writing. Table, Vector and Struct
read fields from the bytes on demand. Every read is bounds-checked, and
decode returns a Result with a FlatBuffersError on malformed input.
Builder follows the official builder algorithm (alignment, vtable
dedup, force defaults, file identifier and size prefix).

The fixtures hold 311 builder-call sequences together with the bytes
the official Python flatbuffers 25.12.19 builder writes for them. The
tests check that our bytes are identical, read every value back, and
fuzz corrupted buffers.
@changeset-bot

changeset-bot Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6630442

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 35 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/vitest Patch
@effect/ai-amazon-bedrock Patch
@effect/ai-anthropic Patch
@effect/ai-cloudflare Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/ai-typesafe Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-cloudflare Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/graphql-generator Patch
@effect/openapi-generator Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@effect-janitor effect-janitor Bot added the 4.0 label Nov 14, 2026
@fubhy

fubhy commented Nov 14, 2026

Copy link
Copy Markdown
Member Author

@effect-bot

@effect-bot

Copy link
Copy Markdown
Contributor

Tracked this takeover request in EFF-3806, which already covers this PR and the review/refinement checklist. I’ve added your request there so the work stays on the existing issue and PR.

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
arbitrary-combinators.ts 40.09 KB 40.09 KB 0.00 KB (0.00%)
basic.ts 7.31 KB 7.31 KB 0.00 KB (0.00%)
batching.ts 11.21 KB 11.21 KB 0.00 KB (0.00%)
brand.ts 7.00 KB 7.00 KB 0.00 KB (0.00%)
cache.ts 11.73 KB 11.73 KB 0.00 KB (0.00%)
config.ts 23.09 KB 23.09 KB 0.00 KB (0.00%)
differ.ts 22.20 KB 22.20 KB 0.00 KB (0.00%)
http-client.ts 27.45 KB 27.45 KB 0.00 KB (0.00%)
http-router.ts 37.55 KB 37.55 KB 0.00 KB (0.00%)
logger.ts 12.19 KB 12.19 KB 0.00 KB (0.00%)
metric.ts 9.65 KB 9.65 KB 0.00 KB (0.00%)
optic.ts 7.21 KB 7.21 KB 0.00 KB (0.00%)
pubsub.ts 16.21 KB 16.21 KB 0.00 KB (0.00%)
queue.ts 13.09 KB 13.09 KB 0.00 KB (0.00%)
schedule.ts 11.99 KB 11.99 KB 0.00 KB (0.00%)
schema-bigdecimal.ts 14.29 KB 14.29 KB 0.00 KB (0.00%)
schema-binary.ts 42.45 KB 42.45 KB 0.00 KB (0.00%)
schema-class.ts 21.96 KB 21.96 KB 0.00 KB (0.00%)
schema-codegen.ts 30.70 KB 30.70 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 30.56 KB 30.56 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 28.26 KB 28.26 KB 0.00 KB (0.00%)
schema-string-transformation.ts 15.54 KB 15.54 KB 0.00 KB (0.00%)
schema-string.ts 12.50 KB 12.50 KB 0.00 KB (0.00%)
schema-template-literal.ts 16.43 KB 16.43 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 39.63 KB 39.63 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 21.11 KB 21.11 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 21.28 KB 21.28 KB 0.00 KB (0.00%)
schema-toFormatter.ts 21.37 KB 21.37 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 26.42 KB 26.42 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 21.45 KB 21.45 KB 0.00 KB (0.00%)
schema.ts 21.05 KB 21.05 KB 0.00 KB (0.00%)
stm.ts 14.40 KB 14.40 KB 0.00 KB (0.00%)
stream.ts 10.56 KB 10.56 KB 0.00 KB (0.00%)

Vectors and structs carry their size and reject reads outside it, tables fail past maxDepth with a Limit error, decode rethrows Usage errors, and the builder rejects tables a vtable can't address, a second finish and non-power-of-two alignments. Table, Vector and Struct become interfaces, make becomes makeBuilder and the shared Options interface is inlined.
…uments intact

Strings decode through the shared BOM-preserving UTF-8 decoder and identifier() keeps a leading U+FEFF. addFieldOffset skips a null target even with forceDefaults, as the C++ builder does. The builder rejects non-integer slots, vector lengths, element sizes and alignments, and readers reject negative or fractional slots while slots past the vtable stay absent.
Derive the builder's nested state from the open table or vector, patch the vtable offset in one place, and let fail() narrow so the non-null assertions go away. Tests use the Hex module and the shared throws helper. No behaviour change.
This was referenced Nov 15, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants