Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/four-bottles-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"memotable": minor
---

Reduce bundle size
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"release": "pnpm changeset version && pnpm install && git add . && git commit -m 'chore: version bump' && git push",
"publish:ci": "pnpm changeset publish",
"prepare": "husky install",
"size": "pnpm build && pnpm size-limit && node measure-size.ts memo-table Table && pnpm size:mangle",
"size": "pnpm build && pnpm size-limit && node measure-size.ts memotable Table && pnpm size:mangle",
"size:mangle": "npx esbuild src/index.ts --bundle --minify --mangle-props='^_' --outfile=dist/mangled.js && brotli -c dist/mangled.js | wc -c",
"benchmark": "vitest run Table.benchmark.test.ts --testTimeout=120000 --no-coverage",
"ci": "pnpm format && pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm size"
Expand Down Expand Up @@ -114,7 +114,7 @@
"name": "Table",
"path": "dist/index.js",
"import": "{ Table }",
"limit": "1500 B"
"limit": "1000 B"
}
],
"packageManager": "pnpm@10.26.2",
Expand Down
65 changes: 13 additions & 52 deletions src/Table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,16 +280,16 @@ describe("Table", () => {
table.set("1", { tags: ["A"] });

table.index((_) => "");
expect(table.partitions().length).toEqual(0);
expect(Array.from(table.partitions()).length).toEqual(0);

table.index((_) => null);
expect(table.partitions().length).toEqual(0);
expect(Array.from(table.partitions()).length).toEqual(0);

table.index((_) => []);
expect(table.partitions().length).toEqual(0);
expect(Array.from(table.partitions()).length).toEqual(0);

table.index((_) => ["VALID", "", null]);
expect(table.partitions().length).toEqual(1);
expect(Array.from(table.partitions()).length).toEqual(1);
expect(table.partition("VALID").size).toEqual(1);
});

Expand All @@ -304,7 +304,13 @@ describe("Table", () => {
// Eagerly access a partition to create it
table.partition("E").sort(() => 0);

expect(table.partitions().map(([key]) => key)).toEqual(["A", "B", "C", "D", "E"]); // Should include empty "E" partition
expect(Array.from(table.partitions()).map(([key]) => key)).toEqual([
"A",
"B",
"C",
"D",
"E",
]); // Should include empty "E" partition
});

test("should update partitions correctly when values are added, updated or removed", () => {
Expand Down Expand Up @@ -495,7 +501,7 @@ describe("Table", () => {
const table = createTable<string, ITaggedValue>();
table.set("1", { tags: ["A"] });
table.index();
expect(table.partitions().length).toBe(0);
expect(Array.from(table.partitions()).length).toBe(0);
});

test("nested indexing", () => {
Expand Down Expand Up @@ -532,7 +538,7 @@ describe("Table", () => {
table.partition(`partition${i}`);
}

expect(table.partitions().length).toBe(1000);
expect(Array.from(table.partitions()).length).toBe(1000);

// But table is still empty
expect(table.size).toBe(0);
Expand Down Expand Up @@ -1053,51 +1059,6 @@ describe("Table", () => {
expect(subscriber).not.toHaveBeenCalled();
});

test("should allow modifications via external reference in a batch (because it's not necessary and can make things complicated)", () => {
const table = createTable<string, ITask>();
table.set("1", { title: "Initial Task" });

expect(() =>
table.batch(() => {
table.set("2", { title: "New task" });
}),
).toThrow();

expect(() =>
table.batch(() => {
table.delete("1");
}),
).toThrow();

expect(() =>
table.batch(() => {
table.touch("1");
}),
).toThrow();
});

test("should not allow sort,index and memo via external reference in a batch (because these can't be reverted if batch fails)", () => {
const table = createTable<string, ITask>();

expect(() =>
table.batch(() => {
table.sort(() => 0);
}),
).toThrow();

expect(() =>
table.batch(() => {
table.index(() => "");
}),
).toThrow();

expect(() =>
table.batch(() => {
table.memo();
}),
).toThrow();
});

test("subscriptions for batch operations", () => {
const table = createTable<string, ITask>();

Expand Down
99 changes: 29 additions & 70 deletions src/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,9 @@ export class Table<K, V> implements ITable<K, V> {
}

public *entries(): MapIterator<[K, V]> {
const keys = this.keys();

do {
const next = keys.next();
if (next.done) return;

yield [next.value, this._map.get(next.value)!];
} while (true);
for (const key of this.keys()) {
yield [key, this._map.get(key)!];
}
}

public [Symbol.iterator](): MapIterator<[K, V]> {
Expand All @@ -80,8 +75,6 @@ export class Table<K, V> implements ITable<K, V> {
private _shouldMemoize: boolean = false;

public memo(flag?: boolean): void {
this._throwIfBatchOperationInProgress();

this._shouldMemoize = flag ?? true;

// Step 1: Propagate memoization to all partitions
Expand All @@ -90,7 +83,7 @@ export class Table<K, V> implements ITable<K, V> {
}

// Step 2: Refresh memoization for the current table
this._refreshMemoization();
this._refreshMemoizedData();
}

// #endregion
Expand Down Expand Up @@ -128,45 +121,34 @@ export class Table<K, V> implements ITable<K, V> {

// #region BATCHING

// Flag to indicate if a batch operation is in progress
private _isBatchOperationInProgress: boolean = false;

public batch(fn: (t: IBatch<K, V>) => void): void {
// Step 1: Run the batch of operations and mark start and end to disable change propagation on every change
this._isBatchOperationInProgress = true;

// Tracks keys (and the new values) that have been updated in this batch
const _updates = new Map<K, V>();

// Tracks keys that have been deleted in this batch
const _deletes = new Set<K>();

try {
fn({
set: (key: K, value: V) => {
_updates.set(key, value);
fn({
set: (key: K, value: V) => {
_updates.set(key, value);
_deletes.delete(key); // In case it was marked for deletion earlier
},
delete: (key: K) => {
_updates.delete(key); // In case it was marked for update earlier

// Only mark for deletion if the key exists in the target map
if (this._map.has(key)) {
_deletes.add(key);
}
},
touch: (key: K): void => {
const currentValue = this._map.get(key);
if (currentValue !== undefined) {
_updates.set(key, currentValue);
_deletes.delete(key); // In case it was marked for deletion earlier
},
delete: (key: K) => {
_updates.delete(key); // In case it was marked for update earlier

// Only mark for deletion if the key exists in the target map
if (this._map.has(key)) {
_deletes.add(key);
}
},
touch: (key: K): void => {
const currentValue = this._map.get(key);
if (currentValue !== undefined) {
_updates.set(key, currentValue);
_deletes.delete(key); // In case it was marked for deletion earlier
}
},
});
} catch (e) {
this._isBatchOperationInProgress = false;
throw e;
}
}
},
});

// Step 2: Apply all changes to the internal map and reset batch
for (const [key, value] of _updates) {
Expand All @@ -177,8 +159,6 @@ export class Table<K, V> implements ITable<K, V> {
this._map.delete(key);
}

this._isBatchOperationInProgress = false;

// Step 3: Propagate all changes as a batch
const keys = [..._updates.keys(), ..._deletes];
if (keys.length > 0) {
Expand Down Expand Up @@ -207,11 +187,7 @@ export class Table<K, V> implements ITable<K, V> {
private _sortedValues: V[] | null = null;
private _comparator: IComparator<V> | null = null;

public sort(comparator: IComparator<V> | null): void;
public sort(): void;
public sort(comparator?: IComparator<V> | null) {
this._throwIfBatchOperationInProgress();

// If comparator is not provided, re-apply the existing comparator
if (comparator === undefined) {
comparator = this._comparator;
Expand All @@ -225,7 +201,7 @@ export class Table<K, V> implements ITable<K, V> {
}

// Step 2: Refresh memoization based on the new comparator
this._refreshMemoization();
this._refreshMemoizedData();

// Step 3: Notify subscribers because we fallback to internal map enforced order
this._notifyListeners([]);
Expand All @@ -247,18 +223,10 @@ export class Table<K, V> implements ITable<K, V> {
/** All partitions created by this index */
private _partitions: Map<string, ITable<K, V>> = new Map();

public index(
definition: IIndexDefinition<V>,
partitionInitializer?: (partition: IReadonlyTable<K, V>, name: string) => void,
): void;
public index(definition: null): void;
public index(): void;
public index(
definition?: IIndexDefinition<V> | null,
partitionInitializer?: (partition: IReadonlyTable<K, V>, name: string) => void,
): void {
this._throwIfBatchOperationInProgress();

// Step 1: Handle clearing the index
if (definition === null) {
this._indexAccessor = null;
Expand Down Expand Up @@ -299,8 +267,8 @@ export class Table<K, V> implements ITable<K, V> {
return this._getPartition(name);
}

public partitions(): [string, IReadonlyTable<K, V>][] {
return Array.from(this._partitions.entries());
public partitions(): MapIterator<[string, IReadonlyTable<K, V>]> {
return this._partitions.entries();
}

// #endregion
Expand Down Expand Up @@ -333,13 +301,11 @@ export class Table<K, V> implements ITable<K, V> {
* @param updatedKeys Array of keys that have been updated
*/
private _propagateChanges(updatedKeys: Iterable<K>): void {
this._throwIfBatchOperationInProgress();

// Step 1: Update indexes if any
this._applyIndexUpdate(updatedKeys);

// Step 2: Update view
this._refreshMemoization();
this._refreshMemoizedData();

// Step 3: Notify subscribers about the changes
this._notifyListeners(updatedKeys);
Expand Down Expand Up @@ -424,7 +390,7 @@ export class Table<K, V> implements ITable<K, V> {
/**
* Refresh the memoized data based on the current comparator and memoization flag.
*/
private _refreshMemoization(): void {
private _refreshMemoizedData(): void {
this._sortedKeys = this._sortedValues = null;

// Table should be memoized when a comparator is set (otherwise memoization is not helpful)
Expand All @@ -438,13 +404,6 @@ export class Table<K, V> implements ITable<K, V> {
}
}

/** Throw operation not allowed error if a batch operation is pending */
private _throwIfBatchOperationInProgress() {
if (this._isBatchOperationInProgress) {
throw new Error("NotAllowed");
}
}

// #endregion
}

Expand Down
2 changes: 1 addition & 1 deletion src/contracts/IIndexableTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,5 @@ export interface IIndexableTable<K, V> {
/**
* Get all (potentially empty) partitions in this table.
*/
partitions(): [string, IReadonlyTable<K, V>][];
partitions(): MapIterator<[string, IReadonlyTable<K, V>]>;
}