Files
SweepStore/dart/sweepstore_old.dart

1541 lines
43 KiB
Dart

/*
/$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$$$ /$$ /$$ /$$$$$ /$$$$$$ /$$ /$$ /$$$$$$$$ /$$$$$$$$
|_ $$_/| $$$ /$$$| $$__ $$| $$_____/| $$$ | $$ |__ $$|_ $$_/ | $$$ | $$| $$_____/|__ $$__/
| $$ | $$$$ /$$$$| $$ \ $$| $$ | $$$$| $$ | $$ | $$ | $$$$| $$| $$ | $$
| $$ | $$ $$/$$ $$| $$$$$$$ | $$$$$ | $$ $$ $$ | $$ | $$ | $$ $$ $$| $$$$$ | $$
| $$ | $$ $$$| $$| $$__ $$| $$__/ | $$ $$$$ /$$ | $$ | $$ | $$ $$$$| $$__/ | $$
| $$ | $$\ $ | $$| $$ \ $$| $$ | $$\ $$$| $$ | $$ | $$ | $$\ $$$| $$ | $$
/$$$$$$| $$ \/ | $$| $$$$$$$/| $$$$$$$$| $$ \ $$| $$$$$$/ /$$$$$$ /$$| $$ \ $$| $$$$$$$$ | $$
|______/|__/ |__/|_______/ |________/|__/ \__/ \______/ |______/|__/|__/ \__/|________/ |__/
© 2025-26 by Benjamin Watt of IMBENJI.NET LIMITED - All rights reserved.
Use of this source code is governed by the Business Source License 1.1 that can be found in the LICENSE file.
This file is part of the SweepStore (formerly Binary Table) package for Dart.
*/
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
enum BT_Type {
POINTER (8),
ADDRESS_TABLE (-1),
INTEGER (4),
FLOAT (4),
STRING (-1),
INTEGER_ARRAY (-1, arrayType: true),
FLOAT_ARRAY (-1, arrayType: true),;
final int size;
final bool arrayType;
const BT_Type(this.size, {
this.arrayType = false,
});
static BT_Type fromId(int id) {
if (id < 0 || id >= BT_Type.values.length) {
throw ArgumentError('Invalid BT_Type id: $id');
}
return BT_Type.values[id];
}
static BT_Type fromDynamic(dynamic value) {
if (value is int) {
return BT_Type.INTEGER;
} else if (value is double) {
return BT_Type.FLOAT;
} else if (value is String) {
return BT_Type.STRING;
} else if (value is List<int>) {
return BT_Type.INTEGER_ARRAY;
} else if (value is List<double>) {
return BT_Type.FLOAT_ARRAY;
} else {
throw ArgumentError('Unsupported type: ${value.runtimeType}');
}
}
}
List<int> encodeValue(dynamic value) {
BT_Type valueType = BT_Type.fromDynamic(value);
List<int> valueBuffer = [valueType.index];
if (valueType == BT_Type.INTEGER) {
valueBuffer.addAll((ByteData(4)..setInt32(0, value as int, Endian.little)).buffer.asUint8List());
} else if (valueType == BT_Type.FLOAT) {
valueBuffer.addAll((ByteData(4)..setFloat32(0, value as double, Endian.little)).buffer.asUint8List());
} else if (valueType == BT_Type.STRING) {
// String length
valueBuffer.addAll((ByteData(4)
..setInt32(0, (value as String).length, Endian.little)).buffer
.asUint8List());
// String bytes
valueBuffer.addAll(value.codeUnits);
} else if (valueType == BT_Type.INTEGER_ARRAY) {
List<int> list = value as List<int>;
// Length
valueBuffer.addAll((ByteData(4)
..setInt32(0, list.length, Endian.little)).buffer.asUint8List());
// Entries
for (var item in list) {
valueBuffer.addAll(encodeValue(item));
}
} else if (valueType == BT_Type.FLOAT_ARRAY) {
List<double> list = value as List<double>;
// Length
valueBuffer.addAll((ByteData(4)
..setInt32(0, list.length, Endian.little)).buffer.asUint8List());
// Entries
for (var item in list) {
valueBuffer.addAll(encodeValue(item));
}
} else {
throw ArgumentError('Unsupported type: ${value.runtimeType}');
}
return valueBuffer;
}
class BT_Pointer {
final int address;
const BT_Pointer(this.address);
bool get isNull => address == -1;
operator ==(Object other) {
if (other is! BT_Pointer) return false;
return address == other.address;
}
@override
String toString() => '0x${address.toRadixString(16)} ($address)';
}
const BT_Pointer BT_Null = BT_Pointer(-1);
class BT_Reference {
BinaryTable _table;
BT_Pointer _pointer;
BT_Reference(this._table, this._pointer);
dynamic decodeValue() {
if (_pointer.isNull) {
return null;
}
_table._file.setPositionSync(_pointer.address);
int typeId = _table._file.readByteSync();
BT_Type type = BT_Type.fromId(typeId);
if (type == BT_Type.INTEGER) {
return _table._file.readIntSync(4);
} else if (type == BT_Type.FLOAT) {
return _table._file.readFloat32Sync();
} else if (type == BT_Type.STRING) {
int length = _table._file.readIntSync(4);
List<int> bytes = _table._file.readSync(length);
return String.fromCharCodes(bytes);
} else if (type == BT_Type.ADDRESS_TABLE) {
// Address table decoding not implemented
throw UnimplementedError('Address table decoding not implemented');
} else if (type == BT_Type.POINTER) {
return _table._file.readPointerSync();
} else if (type == BT_Type.INTEGER_ARRAY) {
return BT_UniformArray(_table, _pointer);
} else if (type == BT_Type.FLOAT_ARRAY) {
return BT_UniformArray(_table, _pointer);
} else {
throw Exception('Unsupported type: $type');
}
}
/// Determine the size in storage in bytes with the least amount of reads possible
int get size {
if (_pointer.isNull) {
return 0;
}
_table._file.setPositionSync(_pointer.address);
int typeId = _table._file.readByteSync();
BT_Type type = BT_Type.fromId(typeId);
if (type == BT_Type.INTEGER) {
return 1 + 4; // Type byte + Int32
} else if (type == BT_Type.FLOAT) {
return 1 + 4; // Type byte + Float32
} else if (type == BT_Type.STRING) {
int length = _table._file.readIntSync(4);
return 1 + 4 + length; // Type byte + Length Int32 + String bytes
} else if (type == BT_Type.ADDRESS_TABLE) {
// Address table size is variable, need to read the count
int count = _table._file.readIntSync(4);
return 1 + 4 + count *
(8 + BT_Type.POINTER.size); // Type byte + Count Int32 + Entries
} else {
throw Exception('Unsupported type: $type');
}
}
BT_Type? get type {
if (_pointer.isNull) {
return null;
}
_table._file.setPositionSync(_pointer.address);
int typeId = _table._file.readByteSync();
return BT_Type.fromId(typeId);
}
@override
String toString() => _pointer.toString();
}
class BT_FreeListEntry {
BT_Pointer pointer;
int size;
BT_FreeListEntry(this.pointer, this.size);
}
/// Wrapper for interacting with uniform arrays in the binary table. A uniform array is an array where all elements are of the same type, allowing for more efficient storage and retrieval using random access.
class BT_UniformArray extends BT_Reference {
BT_UniformArray(super._table, super._pointer);
/// Get the length of the array - Don't mix up with size.
int get length {
if (_pointer.isNull) {
return 0;
}
_table._file.setPositionSync(_pointer.address);
int typeId = _table._file.readByteSync();
BT_Type type = BT_Type.fromId(typeId);
if (!type.arrayType) {
throw Exception('Not an array');
}
return _table._file.readIntSync(4);
}
dynamic operator [](int index) {
if (_pointer.isNull) {
throw Exception('Null pointer');
}
int len = length;
if (index < 0 || index >= len) {
throw RangeError.index(index, this, 'index', null, len);
}
// Determine the type of the array by reading the first items type
_table._file.setPositionSync(_pointer.address + 1 + 4);
int typeId = _table._file.readByteSync();
BT_Type type = BT_Type.fromId(typeId);
int itemOffset = index * (1 + type.size); // Type byte + data size
BT_Reference itemRef = BT_Reference(_table, BT_Pointer((_pointer.address + 1 + 4) + itemOffset));
return itemRef.decodeValue();
}
void operator []=(int index, dynamic value) {
if (_pointer.isNull) {
throw Exception('Null pointer');
}
int len = length;
if (index < 0 || index >= len) {
throw RangeError.index(index, this, 'index', null, len);
}
// Determine the type of the array by reading the first items type
_table._file.setPositionSync(_pointer.address + 1 + 4);
int typeId = _table._file.readByteSync();
BT_Type type = BT_Type.fromId(typeId);
if (type.size == -1) {
throw Exception("Types with variable size are not supported in uniform arrays. Use a non-uniform array instead.");
}
// Ensure the new value is of the correct type
BT_Type newValueType = BT_Type.fromDynamic(value);
if (newValueType != type) {
throw Exception('Type mismatch. Expected $type but got $newValueType');
}
// Calculate the offset of the item to update
int itemOffset = index * (1 + type.size); // Type byte + data size
BT_Pointer itemPointer = BT_Pointer((_pointer.address + 1 + 4) + itemOffset);
// Encode the new value
List<int> valueBuffer = encodeValue(value);
// Place the new value in the file
_table._file.setPositionSync(itemPointer.address);
_table._file.writeFromSync(valueBuffer);
}
void addAll(Iterable<dynamic> values) {
_table._antiFreeListScope(() {
// Determine the type of the array by reading the first items type
BT_Type type = elementType ?? BT_Type.fromDynamic(values.first);
// Validate all new values are of the correct type
for (int i = 0; i < values.length; i++) {
BT_Type newValueType = BT_Type.fromDynamic(values.elementAt(i));
if (newValueType != type) {
throw Exception('Type mismatch at index $i. Expected $type but got $newValueType');
} else if (newValueType.size == -1) {
throw Exception("Types with variable size are not supported in uniform arrays. Use a non-uniform array instead.");
}
}
// Read the full array by loading the full buffer
List<int> fullBuffer = [];
int bufferSize = 1 + 4 + length * (1 + type.size);
_table._file.setPositionSync(_pointer.address);
fullBuffer = _table._file.readSync(bufferSize).toList();
// Encode the new values and add them to the buffer
for (var value in values) {
fullBuffer.addAll(encodeValue(value));
}
// Update the length in the buffer
int newLength = length + values.length;
List<int> lengthBytes = (ByteData(4)..setInt32(0, newLength, Endian.little)).buffer.asUint8List();
fullBuffer.replaceRange(1, 5, lengthBytes);
// Free the old array
_table._free(_pointer, size);
// Allocate new space for the updated array
BT_Pointer newPointer = _table._alloc(fullBuffer.length);
// Replace any references to the old pointer with the new one
Map<int, BT_Pointer> addressTable = _table._addressTable;
addressTable.updateAll((key, value) {
if (value == _pointer) {
return newPointer;
}
return value;
});
_table._addressTable = addressTable;
_pointer = newPointer;
// Write the updated buffer to the new location
_table._file.setPositionSync(newPointer.address);
_table._file.writeFromSync(fullBuffer);
});
}
void add(dynamic value) {
addAll([value]);
}
List<dynamic> fetchSublist([int start = 0, int end = -1]) {
// Read the full array by loading the full buffer of only whats needed
BT_Type? type = elementType;
if (type == null) {
return [];
}
if (type.size == -1) {
throw Exception("Types with variable size are not supported in uniform arrays. Use a non-uniform array instead.");
}
end = end == -1 ? length : end;
int bufferStart = 1 + 4 + start * (1 + type.size);
int bufferEnd = 1 + 4 + end * (1 + type.size);
int bufferSize = bufferEnd - bufferStart;
_table._file.setPositionSync(_pointer.address + bufferStart);
List<int> buffer = _table._file.readSync(bufferSize).toList();
List<dynamic> values = [];
for (int i = 0; i < (end - start); i++) {
int offset = i * (1 + type.size);
BT_Reference itemRef = BT_Reference(_table, BT_Pointer((_pointer.address + bufferStart) + offset));
values.add(itemRef.decodeValue());
}
return values;
}
@override
int get size {
if (length == 0) {
return 1 + 4; // Type byte + Length Int32
}
_table._file.setPositionSync(_pointer.address);
int typeId = _table._file.readByteSync();
BT_Type type = BT_Type.fromId(typeId);
if (type.arrayType) {
_table._file.setPositionSync(_pointer.address);
int bufferSize = 1 + 4 + length * (1 + elementType!.size);
return bufferSize;
}
return super.size;
}
BT_Type? get elementType {
if (length == 0) {
return null;
} else {
_table._file.setPositionSync(_pointer.address + 1 + 4);
int typeId = _table._file.readByteSync();
BT_Type type = BT_Type.fromId(typeId);
return type;
}
}
@override
String toString([bool readValues = false]) {
if (readValues) {
return 'Uniform Array of length $length';
}
List<dynamic> preview = [];
int len = length;
for (int i = 0; i < length; i++) {
preview.add(this[i]);
}
return 'Uniform Array: $preview';
}
}
extension FreeList on List<BT_FreeListEntry> {
void removePointer(BT_Pointer pointer) {
removeWhere((entry) => entry.pointer == pointer);
}
List<int> bt_encode() {
List<int> buffer = [];
/*
New encoding should reflect a "read from the end" approach.
So it should look like:
- Entries (variable)
Entry:
- Pointer (8 bytes)
- Size (4 bytes)
This means that when reading, we can always assume that the last 4 bytes in the file are the entry count for the free list.
The entries however, can be read from front to back. Once we know the count, we know where to start reading.
*/
for (var entry in this) {
// Pointer
buffer.addAll((ByteData(BT_Type.POINTER.size)..setInt64(0, entry.pointer.address, Endian.little)).buffer.asUint8List());
// Size
buffer.addAll((ByteData(4)..setInt32(0, entry.size, Endian.little)).buffer.asUint8List());
}
return buffer;
}
}
extension fnv1a on String {
int get bt_hash {
List<int> bytes = utf8.encode(this);
int hash = 0xcbf29ce484222325; // FNV offset basis
for (int byte in bytes) {
hash ^= byte;
hash *= 0x100000001b3; // FNV prime
}
return hash;
}
}
// Convert double to float16
extension on double {
List<int> _toFloat16Bytes() {
ByteData float32Data = ByteData(4);
float32Data.setFloat32(0, this, Endian.little);
int f = float32Data.getUint32(0);
int sign = (f >> 31) & 0x1;
int exponent = (f >> 23) & 0xff;
int mantissa = f & 0x7fffff;
if (exponent == 0xff) { // Inf or NaN
if (mantissa != 0) {
return [(sign << 7) | 0x7e, 0x01]; // NaN
} else {
return [(sign << 7) | 0x7c, 0x00]; // Inf
}
}
if (exponent == 0) { // Zero or denormalized
return [sign << 7, 0x00]; // Zero
}
// Adjust exponent for float16 (bias: 127 for float32, 15 for float16)
exponent = exponent - 127 + 15;
if (exponent >= 31) { // Overflow to infinity
return [(sign << 7) | 0x7c, 0x00]; // Inf
}
if (exponent <= 0) { // Underflow to zero
return [sign << 7, 0x00]; // Zero
}
// Extract the top 10 bits of mantissa for float16
mantissa >>= 13;
int float16 = (sign << 15) | (exponent << 10) | mantissa;
return [float16 & 0xff, (float16 >> 8) & 0xff]; // Little endian
}
}
enum BT_TicketState {
IDLE,
WAITING,
APPROVED,
EXECUTING,
COMPLETED,
}
enum BT_TicketOpcode {
NONE,
READ,
MODIFY,
WRITE,
}
class BT_Ticket {
final int heartbeat;
final BT_TicketState state;
final BT_TicketOpcode opcode;
final int keyHash;
// Defined by the slave
final int writeSize;
// Defined by the master
final BT_Pointer writePointer;
BT_Ticket({
required this.heartbeat,
required this.state,
required this.opcode,
required this.keyHash,
this.writeSize = 0,
this.writePointer = BT_Null,
});
BT_Ticket.fromIntList(List<int> data)
: heartbeat = ByteData.sublistView(Uint8List.fromList(data.sublist(0, 4))).getInt32(0, Endian.little),
state = BT_TicketState.values[data[4]],
opcode = BT_TicketOpcode.values[data[5]],
keyHash = ByteData.sublistView(Uint8List.fromList(data.sublist(6, 14))).getInt64(0, Endian.little),
writePointer = BT_Pointer(ByteData.sublistView(Uint8List.fromList(data.sublist(14, 22))).getInt64(0, Endian.little)),
writeSize = ByteData.sublistView(Uint8List.fromList(data.sublist(22, 26))).getInt32(0, Endian.little);
List<int> toIntList() {
List<int> data = [];
// Heartbeat (4 bytes)
data.addAll((ByteData(4)..setInt32(0, heartbeat, Endian.little)).buffer.asUint8List());
// State (1 byte)
data.add(state.index);
// Opcode (1 byte)
data.add(opcode.index);
// Key Hash (8 bytes)
data.addAll((ByteData(8)..setInt64(0, keyHash, Endian.little)).buffer.asUint8List());
// Write Pointer (8 bytes)
data.addAll((ByteData(8)..setInt64(0, writePointer.address, Endian.little)).buffer.asUint8List());
// Write Size (4 bytes)
data.addAll((ByteData(4)..setInt32(0, writeSize, Endian.little)).buffer.asUint8List());
return data;
}
String toString() {
return 'BT_Ticket(heartbeat: $heartbeat, state: $state, opcode: $opcode, keyHash: $keyHash, writePointer: $writePointer, writeSize: $writeSize)';
}
}
class BinaryTable {
late final int sessionId;
RandomAccessFile _file;
BinaryTable(String path) : _file = File(path).openSync(mode: FileMode.append) {
var nextSessionId = Random.secure();
int high = nextSessionId.nextInt(1 << 32);
int low = nextSessionId.nextInt(1 << 32);
sessionId = (high << 32) | low;
// Check if the file is initialised
_file.setPositionSync(0);
List<int> magicNumber = _file.readSync(4);
bool isInitialised = magicNumber.length == 4 &&
magicNumber[0] == 'S'.codeUnitAt(0) &&
magicNumber[1] == 'W'.codeUnitAt(0) &&
magicNumber[2] == 'P'.codeUnitAt(0) &&
magicNumber[3] == 'S'.codeUnitAt(0);
if (isInitialised && !isMasterAlive) {
_initialiseMaster();
}
}
void initialise({
int concurrentReaders = 4,
})
{
_file.setPositionSync(0);
// Ensure the file hasnt already been initialised
List<int> magicNumber = _file.readSync(4);
bool isInitialised = magicNumber.length == 4 &&
magicNumber[0] == 'S'.codeUnitAt(0) &&
magicNumber[1] == 'W'.codeUnitAt(0) &&
magicNumber[2] == 'P'.codeUnitAt(0) &&
magicNumber[3] == 'S'.codeUnitAt(0);
if (isInitialised) {
throw Exception('Binary Table file is already initialised.');
}
// Magic number "SWPS" (0/4 bytes)
_file.writeFromSync("SWPS".codeUnits);
// Version (1.0 float16) (4/2 bytes)
_file.writeFromSync(1.0._toFloat16Bytes());
// Address table pointer (null) (6/8 bytes)
_file.writePointerSync(BT_Null);
// Free list count (0) (14/4 bytes)
_file.writeIntSync(0, 4); // Free list entry count
/*
The values below are for concurrency.
*/
// Master Identifier (18/8 bytes)
_file.writeIntSync(sessionId & 0xFFFFFFFF, 8);
// Master Heartbeat (26/4 bytes)
int now = DateTime.now().millisecondsSinceEpoch;
_file.writeIntSync(now, 4);
// Number of concurrent readers (30/4 bytes) // Cannot be changed after initialisation
_file.writeIntSync(concurrentReaders, 4);
// Allow reads (34/1 bytes)
_file.writeByteSync(1);
// Everything else is operator slots starting at 35 bytes.
for (int i = 0; i < concurrentReaders; i++) {
// Slave Heartbeat (4 bytes)
_file.writeIntSync(0, 4);
// Ticket state (1 byte)
_file.writeByteSync(0);
// Ticket Opcode (1 byte)
_file.writeByteSync(0);
// Key Hash (8 bytes)
_file.writeIntSync(0, 8);
// Write Pointer (8 bytes)
_file.writeIntSync(-1, 8);
// Write Size (4 bytes)
_file.writeIntSync(0, 4);
}
// Run the master initialisation
_initialiseMaster();
}
/*
Address Table
*/
Map<int, BT_Pointer> get _addressTable {
_file.setPositionSync(6);
BT_Reference tableRef = BT_Reference(this, _file.readPointerSync());
if (tableRef._pointer.isNull) {
return {};
}
_file.setPositionSync(tableRef._pointer.address + 1);
int fileSize = _file.lengthSync();
int tableCount = _file.readIntSync(4);
int tableSize = tableCount * (8 + BT_Type.POINTER.size);
List<int> buffer = _file.readSync(tableSize).toList();
Map<int, BT_Pointer> addressTable = {};
for (int i = 0; i < tableCount; i++) {
int offset = i * (8 + BT_Type.POINTER.size);
// Key Hash
List<int> keyHashBytes = buffer.sublist(offset, offset + 8);
int keyHash = ByteData.sublistView(Uint8List.fromList(keyHashBytes)).getInt64(0, Endian.little);
// Value Pointer
List<int> valuePointerBytes = buffer.sublist(offset + 8, offset + 8 + BT_Type.POINTER.size);
int valuePointerAddress = ByteData.sublistView(Uint8List.fromList(valuePointerBytes)).getInt64(0, Endian.little);
BT_Pointer valuePointer = BT_Pointer(valuePointerAddress);
// Add to address table
addressTable[keyHash] = valuePointer;
}
return addressTable;
}
set _addressTable(Map<int, BT_Pointer> table) {
List<int> buffer = [
BT_Type.ADDRESS_TABLE.index
];
buffer.addAll((ByteData(4)..setInt32(0, table.length, Endian.little)).buffer.asUint8List());
table.forEach((key, value) {
buffer.addAll((ByteData(8)..setInt64(0, key, Endian.little)).buffer.asUint8List());
buffer.addAll((ByteData(BT_Type.POINTER.size)..setInt64(0, value.address, Endian.little)).buffer.asUint8List());
});
// Write new address table at end of file
BT_Pointer tableAddress = _alloc(buffer.length);
_file.setPositionSync(tableAddress.address);
_file.writeFromSync(buffer);
// Read old table pointer before updating
_file.setPositionSync(6);
BT_Reference oldTableRef = BT_Reference(this, _file.readPointerSync());
// Update header to point to new table
_file.setPositionSync(6);
_file.writePointerSync(tableAddress);
// Now free the old table if it exists and is not the same as the new one
if (!oldTableRef._pointer.isNull && oldTableRef._pointer != tableAddress) {
_free(oldTableRef._pointer, oldTableRef.size);
}
}
/*
Free List
*/
bool freeListLifted = false;
List<BT_FreeListEntry>? _freeListCache;
List<BT_FreeListEntry> get _freeList {
if (freeListLifted) {
return _freeListCache ?? [];
}
_file.setPositionSync(14);
int entryCount = _file.readIntSync(4);
if (entryCount == 0) {
return [];
}
int entrySize = BT_Type.POINTER.size + 4; // Pointer + Size
int freeListSize = entryCount * entrySize;
_file.setPositionSync(_file.lengthSync() - freeListSize);
List<int> buffer = _file.readSync(freeListSize);
List<BT_FreeListEntry> freeList = [];
for (int i = 0; i < entryCount; i++) {
int offset = i * entrySize;
// Pointer
List<int> pointerBytes = buffer.sublist(offset, offset + BT_Type.POINTER.size);
int pointerAddress = ByteData.sublistView(Uint8List.fromList(pointerBytes)).getInt64(0, Endian.little);
BT_Pointer pointer = BT_Pointer(pointerAddress);
// Size
List<int> sizeBytes = buffer.sublist(offset + BT_Type.POINTER.size, offset + entrySize);
int size = ByteData.sublistView(Uint8List.fromList(sizeBytes)).getInt32(0, Endian.little);
freeList.add(BT_FreeListEntry(pointer, size));
}
return freeList;
}
set _freeList(List<BT_FreeListEntry> list) {
if (freeListLifted) {
_freeListCache = list;
return;
}
// Read OLD count from header
_file.setPositionSync(14);
int oldEntryCount = _file.readIntSync(4);
// Calculate old free list size (entries only, not count)
int oldListSize = oldEntryCount * (BT_Type.POINTER.size + 4);
// Remove old free list entries from EOF
if (oldEntryCount > 0) {
int currentLength = _file.lengthSync();
_file.truncateSync(currentLength - oldListSize);
}
// Write NEW count to header
_file.setPositionSync(14);
_file.writeIntSync(list.length, 4);
// Write NEW entries to EOF (if any)
if (list.isNotEmpty) {
List<int> buffer = list.bt_encode();
_file.setPositionSync(_file.lengthSync());
_file.writeFromSync(buffer);
}
}
/// Caches the free list in memory, and removed it from the file.
void _liftFreeList() {
if (freeListLifted) {
throw StateError('Free list is already lifted');
}
// Cache the free list
_freeListCache = _freeList;
// Read count from header
_file.setPositionSync(14);
int oldEntryCount = _file.readIntSync(4);
if (oldEntryCount > 0) {
int oldEntrySize = BT_Type.POINTER.size + 4;
int oldFreeListSize = oldEntryCount * oldEntrySize; // Just entries, no count
// Remove free list entries from EOF
_file.truncateSync(_file.lengthSync() - oldFreeListSize);
}
// Clear count in header
_file.setPositionSync(14);
_file.writeIntSync(0, 4);
freeListLifted = true;
}
/// Appends the cached free list back to the file, and clears the cache.
void dropFreeList() {
if (!freeListLifted) {
throw StateError('Free list is not lifted');
}
freeListLifted = false;
_freeList = _freeListCache!; // This now writes count to header and entries to EOF
_freeListCache = null;
}
void _antiFreeListScope(void Function() fn) {
_liftFreeList();
try {
fn();
} finally {
dropFreeList();
}
}
void _free(BT_Pointer pointer, int size) {
if (!freeListLifted) {
throw StateError('Free list must be lifted before freeing memory');
}
if (pointer.isNull || size <= 0) {
throw ArgumentError('Cannot free null pointer or zero size');
}
// Fetch current free list
List<BT_FreeListEntry> freeList = _freeList;
// Add new free entry
freeList.add(BT_FreeListEntry(pointer, size));
// Merge contiguous free entries
List<BT_FreeListEntry> mergeContiguousFreeBlocks(List<BT_FreeListEntry> freeList) {
if (freeList.isEmpty) return [];
// Create a copy and sort by address to check for contiguous blocks
List<BT_FreeListEntry> sorted = List.from(freeList);
sorted.sort((a, b) => a.pointer.address.compareTo(b.pointer.address));
List<BT_FreeListEntry> merged = [];
for (var entry in sorted) {
if (merged.isEmpty) {
// First entry, just add it
merged.add(BT_FreeListEntry(entry.pointer, entry.size));
} else {
var last = merged.last;
// Check if current entry is contiguous with the last merged entry
if (last.pointer.address + last.size == entry.pointer.address) {
// Merge: extend the size of the last entry
last.size += entry.size;
} else {
// Not contiguous, add as separate entry
merged.add(BT_FreeListEntry(entry.pointer, entry.size));
}
}
}
return merged;
}
freeList = mergeContiguousFreeBlocks(freeList);
// Update free list
_freeList = freeList;
}
BT_Pointer _alloc(int size) {
if (!freeListLifted) {
throw StateError('Free list must be lifted before allocation');
}
// Fetch current free list
List<BT_FreeListEntry> freeList = _freeList;
// If its empty, allocate at end of file
if (freeList.isEmpty) {
// "[ALLOC] CODE 1: No free blocks available, allocating at end of file".yellow.log();
return BT_Pointer(_file.lengthSync());
}
// Find a free block that fits the size (exact fit or larger)
BT_FreeListEntry? bestFit;
for (var entry in freeList) {
if (entry.size >= size) {
bestFit = entry;
break;
}
}
if (bestFit == null) {
// "[ALLOC] CODE 2: No suitable free block found, allocating at end of file".yellow.log();
return BT_Pointer(_file.lengthSync());
}
bool exactFit = bestFit.size == size;
if (exactFit) {
// "[ALLOC] CODE 3: Found exact fit in free block at ${bestFit.pointer} of size ${bestFit.size} bytes".green.log();
// Remove from free list
freeList.removePointer(bestFit.pointer);
_freeList = freeList;
return bestFit.pointer;
} else {
// "[ALLOC] CODE 4: Found larger free block at ${bestFit.pointer} of size ${bestFit.size} bytes, allocating ${size} bytes. Splitting block, remaining size ${bestFit.size - size} bytes".green.log();
// Allocate from start of block, reduce size and move pointer
BT_Pointer allocatedPointer = bestFit.pointer;
bestFit = BT_FreeListEntry(BT_Pointer(bestFit.pointer.address + size), bestFit.size - size);
// Update free list
freeList.removePointer(allocatedPointer);
freeList.add(bestFit);
_freeList = freeList;
return allocatedPointer;
}
}
/*
Concurrency
*/
void _initialiseMaster() {
Isolate.spawn((String filePath) {
RandomAccessFile file = File(filePath).openSync(mode: FileMode.append);
// Print with yellow [MASTER] prefix - use ansi
void _mstPrint(String message) {
print('\x1B[33m[MASTER]\x1B[0m $message');
}
late final int concurrentReaders;
file.setPositionSync(30);
concurrentReaders = file.readIntSync(4);
while (true) {
int now = DateTime.now().millisecondsSinceEpoch32();
file.setPositionSync(26);
file.writeIntSync(now, 4);
_mstPrint('Master heartbeat updated at $now');
file.setPositionSync(35);
for (int i = 0; i < concurrentReaders; i++) {
// Read ticket
List<int> ticketBuffer = file.readSync(26);
BT_Ticket ticket = BT_Ticket.fromIntList(ticketBuffer);
// Check if ticket is alive and waiting
int ticketHeartbeat = ticket.heartbeat;
bool isTicketAlive = (now - ticket.heartbeat) <= 5000;
BT_TicketState state = ticket.state;
if (isTicketAlive && state == BT_TicketState.WAITING) {
_mstPrint("Ticket ${i} is alive and waiting...");
// If reading is still allowed, we need to disallow it
file.setPositionSync(34);
if (file.readByteSync() == 1) {
_mstPrint("Disallowing reads for ticket processing...");
file.setPositionSync(34);
file.writeByteSync(0);
// _liftFreeList(); // Broken/Incorrect Logic. the isolate cant access the main instance's free list
}
// We need to give it a write pointer and approve it
// BT_Pointer allocation = _alloc(ticket.writeSize); // Broken/Incorrect Logic. the isolate cant access the main instance's free list
BT_Ticket approvedTicket = BT_Ticket(
heartbeat: ticket.heartbeat,
state: BT_TicketState.APPROVED,
opcode: ticket.opcode,
keyHash: ticket.keyHash,
writeSize: ticket.writeSize,
// writePointer: allocation, // Broken/Incorrect Logic. the isolate cant access the main instance's free list
);
// Write approved ticket back to file
file.setPositionSync(35 + i * 26);
file.writeFromSync(approvedTicket.toIntList());
}
}
// If reads are disallowed, we need to allow them again, and drop the free list
file.setPositionSync(34);
if (file.readByteSync() == 0) {
_mstPrint("Allowing reads after ticket processing...");
file.setPositionSync(34);
file.writeByteSync(1);
dropFreeList();
}
sleep(Duration(seconds: 1));
}
}, _file.path);
}
bool get isMasterAlive {
_file.setPositionSync(18);
int masterId = _file.readIntSync(8);
_file.setPositionSync(26);
int masterHeartbeat = _file.readIntSync(4);
int now = DateTime.now().millisecondsSinceEpoch32();
return (now - masterHeartbeat) <= 5000;
}
int get concurrentReaders {
_file.setPositionSync(30);
return _file.readIntSync(4);
}
bool get allowReads {
_file.setPositionSync(34);
int flag = _file.readByteSync();
return flag == 1;
}
// Add ticket
void _ticket({
required BT_TicketOpcode operation,
required int keyHash,
required int writeSize,
required void Function(BT_Ticket ticket)? onApproved,
}) {
// Reduce the chance of a race condition/deadlock by adding a small random delay
sleep(Duration(milliseconds: Random().nextInt(10)));
int? ticketIndex;
// Were gonna iterate through all the tickets and find an empty slot
while (ticketIndex == null) {
_file.setPositionSync(35);
for (int i = 0; i < concurrentReaders; i++) {
List<int> ticketBuffer = _file.readSync(26); // Size of a ticket entry
BT_Ticket ticket = BT_Ticket.fromIntList(ticketBuffer);
// Check if the heartbeat is stale (older than 5 seconds)
int now = DateTime.now().millisecondsSinceEpoch64();
if (now - ticket.heartbeat > 5000) {
// We found a stale ticket, we can take this slot
ticketIndex = i;
break;
}
}
sleep(Duration(milliseconds: 500));
}
if (ticketIndex == null) {
throw Exception('Failed to acquire ticket');
}
BT_Ticket newTicket = BT_Ticket(
heartbeat: DateTime.now().millisecondsSinceEpoch32(),
state: BT_TicketState.WAITING,
opcode: operation,
keyHash: keyHash,
writeSize: writeSize,
);
// Write the new ticket to the file
_file.setPositionSync(35 + ticketIndex * 38);
_file.writeFromSync(newTicket.toIntList());
// Wait for approval
while (true) {
print('Waiting for ticket approval...');
_file.setPositionSync(35 + ticketIndex * 38);
List<int> ticketBuffer = _file.readSync(38);
BT_Ticket ticket = BT_Ticket.fromIntList(ticketBuffer);
if (ticket.state == BT_TicketState.APPROVED) {
// Ticket approved
onApproved?.call(ticket);
break;
}
if (!isMasterAlive) {
print('Master is not alive, cannot proceed with ticket');
}
sleep(Duration(milliseconds: 1000));
// Update heartbeat
_file.setPositionSync(35 + ticketIndex * 38);
int now = DateTime.now().millisecondsSinceEpoch32();
_file.writeIntSync(now, 4);
}
}
/*
Key-Value Operations
*/
operator []=(String key, dynamic value) {
Map<int, BT_Pointer> addressTable = _addressTable;
// Check if key already exists
int keyHash = key.bt_hash;
if (addressTable.containsKey(keyHash)) {
throw Exception('Key already exists'); // The pair should be deleted first
}
List<int> valueBuffer = encodeValue(value);
_ticket(
operation: BT_TicketOpcode.WRITE, // Modification operations will come later,
keyHash: keyHash,
writeSize: valueBuffer.length,
onApproved: (BT_Ticket ticket) {
// Write value to file
_file.setPositionSync(ticket.writePointer.address);
_file.writeFromSync(valueBuffer);
}
);
// v1.0.0 implementation
// _antiFreeListScope(() {
// Map<int, BT_Pointer> addressTable = _addressTable;
//
// int keyHash = key.bt_hash;
//
// // 1.1 Note: I cant remember why i did this check here.
// if (addressTable.containsKey(keyHash)) {
// throw Exception('Key already exists');
// }
//
//
//
// List<int> valueBuffer = encodeValue(value);
//
// // Write value to file
// BT_Pointer valueAddress = _alloc(valueBuffer.length);
//
// _file.setPositionSync(valueAddress.address);
// _file.writeFromSync(valueBuffer);
//
// // Update address table
// addressTable[keyHash] = valueAddress;
// _addressTable = addressTable;
// });
}
operator [](String key) {
while (!allowReads) {
// Wait until reads are allowed
sleep(Duration(milliseconds: 1));
}
Map<int, BT_Pointer> addressTable = _addressTable;
int keyHash = key.bt_hash;
if (!addressTable.containsKey(keyHash)) {
throw Exception('Key does not exist');
}
BT_Pointer valuePointer = addressTable[keyHash]!;
BT_Reference valueRef = BT_Reference(this, valuePointer);
return valueRef.decodeValue();
}
void delete(String key) {
_antiFreeListScope(() {
Map<int, BT_Pointer> addressTable = _addressTable;
int keyHash = key.bt_hash;
if (!addressTable.containsKey(keyHash)) {
throw Exception('Key does not exist');
}
BT_Pointer valuePointer = addressTable[keyHash]!;
BT_Reference valueRef = BT_Reference(this, valuePointer);
// Free the value
_free(valuePointer, valueRef.size);
// Remove from address table
addressTable.remove(keyHash);
_addressTable = addressTable;
});
}
void truncate() {
_antiFreeListScope(() {
// Relocate the address table if possible
_addressTable = _addressTable;
// Check if the last free block is at the end of the file
List<BT_FreeListEntry> freeList = _freeList;
freeList.sort((a, b) => a.pointer.address.compareTo(b.pointer.address));
if (freeList.isEmpty) {
return;
}
BT_FreeListEntry lastEntry = freeList.last;
int fileEnd = _file.lengthSync();
int expectedEnd = lastEntry.pointer.address + lastEntry.size;
if (expectedEnd != fileEnd) {
return;
}
// Remove the last entry from the free list
freeList.removeLast();
_freeList = freeList;
// Truncate the file
int newLength = lastEntry.pointer.address;
_file.truncateSync(newLength);
});
}
}
void main() {
File file = File('example.bin');
if (file.existsSync()) {
file.deleteSync();
}
file.createSync();
BinaryTable table = BinaryTable(file.path);
table.initialise();
print("File dump:");
print(binaryDump(file.readAsBytesSync()));
print("File size: ${file.lengthSync()} bytes");
print(" ");
table["int_array"] = [6, 3, 9, 2, 5];
table["float_array"] = [1.5, 2.5, 3.5];
table["empty"] = <int>[];
table["int_array"][0] = 1;
table["float_array"][1] = 4.5;
print("int_array pointer: ${table["int_array"]._pointer}");
print("float_array pointer: ${table["float_array"]._pointer}");
table["int_array"].add(10);
table["float_array"].add(5.5);
table["int_array"].addAll([420, 69, 1337, 1738]);
table["float_array"].addAll([6.5, 7.5, 8.5]);
// Test the full read method
var intArrayFull = table["int_array"].fetchSublist(0, 3);
var floatArrayFull = table["float_array"].fetchSublist(0, 2);
print("Full read int_array (0-3): $intArrayFull");
print("Full read float_array (0-2): $floatArrayFull");
var readback1 = table["int_array"];
var readback2 = table["float_array"];
var readback3 = table["empty"];
print("Readback1: $readback1");
print("Readback2: $readback2");
print("Readback3: $readback3");
print(" ");
print("File dump:");
print(binaryDump(file.readAsBytesSync()));
print("File size: ${file.lengthSync()} bytes");
}
extension on RandomAccessFile {
// Read/Write Int Dynamic - Can specify size in bytes, does not have to align to 1, 2, 4, or 8 bytes. Default is 4 bytes (Int32)
int readIntSync([int size = 4, Endian endianness = Endian.little]) {
if (size < 1 || size > 8) {
throw ArgumentError('Size must be between 1 and 8 bytes');
}
List<int> bytes = readSync(size);
// Build integer from bytes with proper endianness
int result = 0;
if (endianness == Endian.little) {
for (int i = size - 1; i >= 0; i--) {
result = (result << 8) | bytes[i];
}
} else {
for (int i = 0; i < size; i++) {
result = (result << 8) | bytes[i];
}
}
// Sign extend if MSB is set
int signBit = 1 << (size * 8 - 1);
if (result & signBit != 0) {
result -= 1 << (size * 8);
}
return result;
}
void writeIntSync(int value, [int size = 4, Endian endianness = Endian.little]) {
if (size < 1 || size > 8) {
throw ArgumentError('Size must be between 1 and 8 bytes');
}
List<int> bytes = List.filled(size, 0);
// Extract bytes with proper endianness
if (endianness == Endian.little) {
for (int i = 0; i < size; i++) {
bytes[i] = (value >> (i * 8)) & 0xFF;
}
} else {
for (int i = 0; i < size; i++) {
bytes[size - 1 - i] = (value >> (i * 8)) & 0xFF;
}
}
writeFromSync(bytes);
}
// Read/Write Pointers
BT_Pointer readPointerSync() {
int offset = readIntSync(BT_Type.POINTER.size);
return BT_Pointer(offset);
}
void writePointerSync(BT_Pointer pointer) {
writeIntSync(pointer.address, BT_Type.POINTER.size);
}
// Read/Write Float32
double readFloat32Sync([Endian endianness = Endian.little]) {
List<int> bytes = readSync(4);
return ByteData.sublistView(Uint8List.fromList(bytes)).getFloat32(0, endianness);
}
void writeFloat32Sync(double value, [Endian endianness = Endian.little]) {
ByteData byteData = ByteData(4);
byteData.setFloat32(0, value, endianness);
writeFromSync(byteData.buffer.asUint8List());
}
// Read/Write Float64 (Double)
double readFloat64Sync([Endian endianness = Endian.little]) {
List<int> bytes = readSync(8);
return ByteData.sublistView(Uint8List.fromList(bytes)).getFloat64(0, endianness);
}
void writeFloat64Sync(double value, [Endian endianness = Endian.little]) {
ByteData byteData = ByteData(8);
byteData.setFloat64(0, value, endianness);
writeFromSync(byteData.buffer.asUint8List());
}
}
String binaryDump(Uint8List data) {
StringBuffer buffer = StringBuffer();
for (int i = 0; i < data.length; i += 16) {
// Address
buffer.write('0x${i.toRadixString(16).padLeft(4, '0').toUpperCase()} (${i.toString().padLeft(4)}) | ');
// Hex bytes
for (int j = 0; j < 16; j++) {
if (i + j < data.length) {
buffer.write('${data[i + j].toRadixString(16).padLeft(2, '0').toUpperCase()} ');
} else {
buffer.write(' ');
}
}
buffer.write(' | ');
// Integer representation
for (int j = 0; j < 16; j++) {
if (i + j < data.length) {
buffer.write('${data[i + j].toString().padLeft(3)} ');
} else {
buffer.write(' ');
}
}
buffer.write(' | ');
// ASCII representation
for (int j = 0; j < 16; j++) {
if (i + j < data.length) {
int byte = data[i + j];
if (byte >= 32 && byte <= 126) {
buffer.write(String.fromCharCode(byte));
} else {
buffer.write('.');
}
}
}
buffer.write(' | ');
if (i + 16 < data.length) buffer.writeln();
}
return buffer.toString();
}
// DateTime to 32-bit Unix timestamp (seconds since epoch)
extension on DateTime {
int millisecondsSinceEpoch32() {
return (millisecondsSinceEpoch ~/ 1000) & 0xFFFFFFFF;
}
int millisecondsSinceEpoch64() {
return millisecondsSinceEpoch;
}
}