Skip to content

Commit

Permalink
Fix onUpdate when PK is of type Identity (#25)
Browse files Browse the repository at this point in the history
When the primary key is of type Identity we were still doing ===
comparison by using the Map data structure. This commit introduces a
different data structure called OperationsMap which can also use isEqual
to compare keys if isEqual function is available
  • Loading branch information
drogus authored Oct 6, 2023
1 parent c3e342e commit a99c6eb
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 3 deletions.
45 changes: 45 additions & 0 deletions src/operations_map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export default class OperationsMap<K, V> {
private items: { key: K; value: V }[] = [];

private isEqual(a: K, b: K): boolean {
if (a && typeof a === "object" && "isEqual" in a) {
return (a as any).isEqual(b);
}
return a === b;
}

set(key: K, value: V): void {
const existingIndex = this.items.findIndex(({ key: k }) =>
this.isEqual(k, key)
);
if (existingIndex > -1) {
this.items[existingIndex].value = value;
} else {
this.items.push({ key, value });
}
}

get(key: K): V | undefined {
const item = this.items.find(({ key: k }) => this.isEqual(k, key));
return item ? item.value : undefined;
}

delete(key: K): boolean {
const existingIndex = this.items.findIndex(({ key: k }) =>
this.isEqual(k, key)
);
if (existingIndex > -1) {
this.items.splice(existingIndex, 1);
return true;
}
return false;
}

has(key: K): boolean {
return this.items.some(({ key: k }) => this.isEqual(k, key));
}

values(): Array<V> {
return this.items.map((i) => i.value);
}
}
3 changes: 2 additions & 1 deletion src/spacetimedb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
TableRowOperation_OperationType,
} from "./client_api";
import BinaryReader from "./binary_reader";
import OperationsMap from "./operations_map";

export {
ProductValue,
Expand Down Expand Up @@ -169,7 +170,7 @@ class Table {
if (this.entityClass.primaryKey !== undefined) {
const pkName = this.entityClass.primaryKey;
const inserts: any[] = [];
const deleteMap = new Map();
const deleteMap = new OperationsMap<any, DBOp>();
for (const dbOp of dbOps) {
if (dbOp.type === "insert") {
inserts.push(dbOp);
Expand Down
125 changes: 123 additions & 2 deletions tests/spacetimedb_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SpacetimeDBClient, ReducerEvent } from "../src/spacetimedb";
import { Identity } from "../src/identity";
import WebsocketTestAdapter from "../src/websocket_test_adapter";
import Player from "./types/player";
import User from "./types/user";
import Point from "./types/point";
import CreatePlayerReducer from "./types/create_player_reducer";

Expand Down Expand Up @@ -283,7 +284,7 @@ describe("SpacetimeDBClient", () => {
{
op: "delete",
row_pk: "abcdef",
row: ["player-2", "Jamie", [0, 0]],
row: ["player-2", "Jaime", [0, 0]],
},
{
op: "insert",
Expand All @@ -299,7 +300,7 @@ describe("SpacetimeDBClient", () => {
wsAdapter.sendToClient({ data: transactionUpdate });

expect(updates).toHaveLength(2);
expect(updates[1]["oldPlayer"].name).toBe("Jamie");
expect(updates[1]["oldPlayer"].name).toBe("Jaime");
expect(updates[1]["newPlayer"].name).toBe("Kingslayer");
});

Expand Down Expand Up @@ -364,4 +365,124 @@ describe("SpacetimeDBClient", () => {

expect(callbackLog).toEqual(["Player", "CreatePlayerReducer"]);
});

test("it calls onUpdate callback when a record is added with a subscription update and then with a transaction update when the PK is of type Identity", async () => {
const client = new SpacetimeDBClient(
"ws://127.0.0.1:1234",
"db",
undefined,
"json"
);
const wsAdapter = new WebsocketTestAdapter();
client._setCreateWSFn((_url: string, _protocol: string) => {
return wsAdapter;
});

let called = false;
client.onConnect(() => {
called = true;
});

await client.connect();
wsAdapter.acceptConnection();

const tokenMessage = {
data: {
IdentityToken: {
identity: "an-identity",
token: "a-token",
},
},
};
wsAdapter.sendToClient(tokenMessage);

const updates: { oldUser: User; newUser: User }[] = [];
User.onUpdate((oldUser: User, newUser: User) => {
updates.push({
oldUser,
newUser,
});
});

const subscriptionMessage = {
SubscriptionUpdate: {
table_updates: [
{
table_id: 35,
table_name: "User",
table_row_operations: [
{
op: "delete",
row_pk: "abcd123",
row: [
"41db74c20cdda916dd2637e5a11b9f31eb1672249aa7172f7e22b4043a6a9008",
"drogus",
],
},
{
op: "insert",
row_pk: "def456",
row: [
"41db74c20cdda916dd2637e5a11b9f31eb1672249aa7172f7e22b4043a6a9008",
"mr.drogus",
],
},
],
},
],
},
};
wsAdapter.sendToClient({ data: subscriptionMessage });

expect(updates).toHaveLength(1);
expect(updates[0]["oldUser"].username).toBe("drogus");
expect(updates[0]["newUser"].username).toBe("mr.drogus");

const transactionUpdate = {
TransactionUpdate: {
event: {
timestamp: 1681391805281203,
status: "committed",
caller_identity: "identity-0",
function_call: {
reducer: "create_user",
args: '["A User",[0.2, 0.3]]',
},
energy_quanta_used: 33841000,
message: "",
},
subscription_update: {
table_updates: [
{
table_id: 35,
table_name: "User",
table_row_operations: [
{
op: "delete",
row_pk: "abcdef",
row: [
"11db74c20cdda916dd2637e5a11b9f31eb1672249aa7172f7e22b4043a6a9008",
"jaime",
],
},
{
op: "insert",
row_pk: "123456",
row: [
"11db74c20cdda916dd2637e5a11b9f31eb1672249aa7172f7e22b4043a6a9008",
"kingslayer",
],
},
],
},
],
},
},
};
wsAdapter.sendToClient({ data: transactionUpdate });

expect(updates).toHaveLength(2);
expect(updates[1]["oldUser"].username).toBe("jaime");
expect(updates[1]["newUser"].username).toBe("kingslayer");
});
});
145 changes: 145 additions & 0 deletions tests/types/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD.

// @ts-ignore
import {
__SPACETIMEDB__,
AlgebraicType,
ProductType,
BuiltinType,
ProductTypeElement,
SumType,
SumTypeVariant,
IDatabaseTable,
AlgebraicValue,
ReducerEvent,
Identity,
} from "../../src/index";

export class User extends IDatabaseTable {
public static tableName = "User";
public identity: Identity;
public username: string;

public static primaryKey: string | undefined = "identity";

constructor(identity: Identity, username: string) {
super();
this.identity = identity;
this.username = username;
}

public static serialize(value: User): object {
return [Array.from(value.identity.toUint8Array()), value.username];
}

public static getAlgebraicType(): AlgebraicType {
return AlgebraicType.createProductType([
new ProductTypeElement(
"identity",
AlgebraicType.createProductType([
new ProductTypeElement(
"__identity_bytes",
AlgebraicType.createArrayType(
AlgebraicType.createPrimitiveType(BuiltinType.Type.U8)
)
),
])
),
new ProductTypeElement(
"username",
AlgebraicType.createPrimitiveType(BuiltinType.Type.String)
),
]);
}

public static fromValue(value: AlgebraicValue): User {
let productValue = value.asProductValue();
let __identity = new Identity(
productValue.elements[0].asProductValue().elements[0].asBytes()
);
let __username = productValue.elements[1].asString();
return new this(__identity, __username);
}

public static count(): number {
return __SPACETIMEDB__.clientDB.getTable("User").count();
}

public static all(): User[] {
return __SPACETIMEDB__.clientDB
.getTable("User")
.getInstances() as unknown as User[];
}

public static filterByIdentity(value: Identity): User | null {
for (let instance of __SPACETIMEDB__.clientDB
.getTable("User")
.getInstances()) {
if (instance.identity.isEqual(value)) {
return instance;
}
}
return null;
}

public static filterByUsername(value: string): User[] {
let result: User[] = [];
for (let instance of __SPACETIMEDB__.clientDB
.getTable("User")
.getInstances()) {
if (instance.username === value) {
result.push(instance);
}
}
return result;
}

public static onInsert(
callback: (value: User, reducerEvent: ReducerEvent | undefined) => void
) {
__SPACETIMEDB__.clientDB.getTable("User").onInsert(callback);
}

public static onUpdate(
callback: (
oldValue: User,
newValue: User,
reducerEvent: ReducerEvent | undefined
) => void
) {
__SPACETIMEDB__.clientDB.getTable("User").onUpdate(callback);
}

public static onDelete(
callback: (value: User, reducerEvent: ReducerEvent | undefined) => void
) {
__SPACETIMEDB__.clientDB.getTable("User").onDelete(callback);
}

public static removeOnInsert(
callback: (value: User, reducerEvent: ReducerEvent | undefined) => void
) {
__SPACETIMEDB__.clientDB.getTable("User").removeOnInsert(callback);
}

public static removeOnUpdate(
callback: (
oldValue: User,
newValue: User,
reducerEvent: ReducerEvent | undefined
) => void
) {
__SPACETIMEDB__.clientDB.getTable("User").removeOnUpdate(callback);
}

public static removeOnDelete(
callback: (value: User, reducerEvent: ReducerEvent | undefined) => void
) {
__SPACETIMEDB__.clientDB.getTable("User").removeOnDelete(callback);
}
}

export default User;

__SPACETIMEDB__.registerComponent("User", User);

0 comments on commit a99c6eb

Please sign in to comment.