Skip to content

Commit 1764040

Browse files
authored
Fix onUpdate when PK is of type Identity (#25)
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
1 parent fd996a6 commit 1764040

File tree

4 files changed

+315
-3
lines changed

4 files changed

+315
-3
lines changed

src/operations_map.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export default class OperationsMap<K, V> {
2+
private items: { key: K; value: V }[] = [];
3+
4+
private isEqual(a: K, b: K): boolean {
5+
if (a && typeof a === "object" && "isEqual" in a) {
6+
return (a as any).isEqual(b);
7+
}
8+
return a === b;
9+
}
10+
11+
set(key: K, value: V): void {
12+
const existingIndex = this.items.findIndex(({ key: k }) =>
13+
this.isEqual(k, key)
14+
);
15+
if (existingIndex > -1) {
16+
this.items[existingIndex].value = value;
17+
} else {
18+
this.items.push({ key, value });
19+
}
20+
}
21+
22+
get(key: K): V | undefined {
23+
const item = this.items.find(({ key: k }) => this.isEqual(k, key));
24+
return item ? item.value : undefined;
25+
}
26+
27+
delete(key: K): boolean {
28+
const existingIndex = this.items.findIndex(({ key: k }) =>
29+
this.isEqual(k, key)
30+
);
31+
if (existingIndex > -1) {
32+
this.items.splice(existingIndex, 1);
33+
return true;
34+
}
35+
return false;
36+
}
37+
38+
has(key: K): boolean {
39+
return this.items.some(({ key: k }) => this.isEqual(k, key));
40+
}
41+
42+
values(): Array<V> {
43+
return this.items.map((i) => i.value);
44+
}
45+
}

src/spacetimedb.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
TableRowOperation_OperationType,
3232
} from "./client_api";
3333
import BinaryReader from "./binary_reader";
34+
import OperationsMap from "./operations_map";
3435

3536
export {
3637
ProductValue,
@@ -169,7 +170,7 @@ class Table {
169170
if (this.entityClass.primaryKey !== undefined) {
170171
const pkName = this.entityClass.primaryKey;
171172
const inserts: any[] = [];
172-
const deleteMap = new Map();
173+
const deleteMap = new OperationsMap<any, DBOp>();
173174
for (const dbOp of dbOps) {
174175
if (dbOp.type === "insert") {
175176
inserts.push(dbOp);

tests/spacetimedb_client.test.ts

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { SpacetimeDBClient, ReducerEvent } from "../src/spacetimedb";
22
import { Identity } from "../src/identity";
33
import WebsocketTestAdapter from "../src/websocket_test_adapter";
44
import Player from "./types/player";
5+
import User from "./types/user";
56
import Point from "./types/point";
67
import CreatePlayerReducer from "./types/create_player_reducer";
78

@@ -283,7 +284,7 @@ describe("SpacetimeDBClient", () => {
283284
{
284285
op: "delete",
285286
row_pk: "abcdef",
286-
row: ["player-2", "Jamie", [0, 0]],
287+
row: ["player-2", "Jaime", [0, 0]],
287288
},
288289
{
289290
op: "insert",
@@ -299,7 +300,7 @@ describe("SpacetimeDBClient", () => {
299300
wsAdapter.sendToClient({ data: transactionUpdate });
300301

301302
expect(updates).toHaveLength(2);
302-
expect(updates[1]["oldPlayer"].name).toBe("Jamie");
303+
expect(updates[1]["oldPlayer"].name).toBe("Jaime");
303304
expect(updates[1]["newPlayer"].name).toBe("Kingslayer");
304305
});
305306

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

365366
expect(callbackLog).toEqual(["Player", "CreatePlayerReducer"]);
366367
});
368+
369+
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 () => {
370+
const client = new SpacetimeDBClient(
371+
"ws://127.0.0.1:1234",
372+
"db",
373+
undefined,
374+
"json"
375+
);
376+
const wsAdapter = new WebsocketTestAdapter();
377+
client._setCreateWSFn((_url: string, _protocol: string) => {
378+
return wsAdapter;
379+
});
380+
381+
let called = false;
382+
client.onConnect(() => {
383+
called = true;
384+
});
385+
386+
await client.connect();
387+
wsAdapter.acceptConnection();
388+
389+
const tokenMessage = {
390+
data: {
391+
IdentityToken: {
392+
identity: "an-identity",
393+
token: "a-token",
394+
},
395+
},
396+
};
397+
wsAdapter.sendToClient(tokenMessage);
398+
399+
const updates: { oldUser: User; newUser: User }[] = [];
400+
User.onUpdate((oldUser: User, newUser: User) => {
401+
updates.push({
402+
oldUser,
403+
newUser,
404+
});
405+
});
406+
407+
const subscriptionMessage = {
408+
SubscriptionUpdate: {
409+
table_updates: [
410+
{
411+
table_id: 35,
412+
table_name: "User",
413+
table_row_operations: [
414+
{
415+
op: "delete",
416+
row_pk: "abcd123",
417+
row: [
418+
"41db74c20cdda916dd2637e5a11b9f31eb1672249aa7172f7e22b4043a6a9008",
419+
"drogus",
420+
],
421+
},
422+
{
423+
op: "insert",
424+
row_pk: "def456",
425+
row: [
426+
"41db74c20cdda916dd2637e5a11b9f31eb1672249aa7172f7e22b4043a6a9008",
427+
"mr.drogus",
428+
],
429+
},
430+
],
431+
},
432+
],
433+
},
434+
};
435+
wsAdapter.sendToClient({ data: subscriptionMessage });
436+
437+
expect(updates).toHaveLength(1);
438+
expect(updates[0]["oldUser"].username).toBe("drogus");
439+
expect(updates[0]["newUser"].username).toBe("mr.drogus");
440+
441+
const transactionUpdate = {
442+
TransactionUpdate: {
443+
event: {
444+
timestamp: 1681391805281203,
445+
status: "committed",
446+
caller_identity: "identity-0",
447+
function_call: {
448+
reducer: "create_user",
449+
args: '["A User",[0.2, 0.3]]',
450+
},
451+
energy_quanta_used: 33841000,
452+
message: "",
453+
},
454+
subscription_update: {
455+
table_updates: [
456+
{
457+
table_id: 35,
458+
table_name: "User",
459+
table_row_operations: [
460+
{
461+
op: "delete",
462+
row_pk: "abcdef",
463+
row: [
464+
"11db74c20cdda916dd2637e5a11b9f31eb1672249aa7172f7e22b4043a6a9008",
465+
"jaime",
466+
],
467+
},
468+
{
469+
op: "insert",
470+
row_pk: "123456",
471+
row: [
472+
"11db74c20cdda916dd2637e5a11b9f31eb1672249aa7172f7e22b4043a6a9008",
473+
"kingslayer",
474+
],
475+
},
476+
],
477+
},
478+
],
479+
},
480+
},
481+
};
482+
wsAdapter.sendToClient({ data: transactionUpdate });
483+
484+
expect(updates).toHaveLength(2);
485+
expect(updates[1]["oldUser"].username).toBe("jaime");
486+
expect(updates[1]["newUser"].username).toBe("kingslayer");
487+
});
367488
});

tests/types/user.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
2+
// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD.
3+
4+
// @ts-ignore
5+
import {
6+
__SPACETIMEDB__,
7+
AlgebraicType,
8+
ProductType,
9+
BuiltinType,
10+
ProductTypeElement,
11+
SumType,
12+
SumTypeVariant,
13+
IDatabaseTable,
14+
AlgebraicValue,
15+
ReducerEvent,
16+
Identity,
17+
} from "../../src/index";
18+
19+
export class User extends IDatabaseTable {
20+
public static tableName = "User";
21+
public identity: Identity;
22+
public username: string;
23+
24+
public static primaryKey: string | undefined = "identity";
25+
26+
constructor(identity: Identity, username: string) {
27+
super();
28+
this.identity = identity;
29+
this.username = username;
30+
}
31+
32+
public static serialize(value: User): object {
33+
return [Array.from(value.identity.toUint8Array()), value.username];
34+
}
35+
36+
public static getAlgebraicType(): AlgebraicType {
37+
return AlgebraicType.createProductType([
38+
new ProductTypeElement(
39+
"identity",
40+
AlgebraicType.createProductType([
41+
new ProductTypeElement(
42+
"__identity_bytes",
43+
AlgebraicType.createArrayType(
44+
AlgebraicType.createPrimitiveType(BuiltinType.Type.U8)
45+
)
46+
),
47+
])
48+
),
49+
new ProductTypeElement(
50+
"username",
51+
AlgebraicType.createPrimitiveType(BuiltinType.Type.String)
52+
),
53+
]);
54+
}
55+
56+
public static fromValue(value: AlgebraicValue): User {
57+
let productValue = value.asProductValue();
58+
let __identity = new Identity(
59+
productValue.elements[0].asProductValue().elements[0].asBytes()
60+
);
61+
let __username = productValue.elements[1].asString();
62+
return new this(__identity, __username);
63+
}
64+
65+
public static count(): number {
66+
return __SPACETIMEDB__.clientDB.getTable("User").count();
67+
}
68+
69+
public static all(): User[] {
70+
return __SPACETIMEDB__.clientDB
71+
.getTable("User")
72+
.getInstances() as unknown as User[];
73+
}
74+
75+
public static filterByIdentity(value: Identity): User | null {
76+
for (let instance of __SPACETIMEDB__.clientDB
77+
.getTable("User")
78+
.getInstances()) {
79+
if (instance.identity.isEqual(value)) {
80+
return instance;
81+
}
82+
}
83+
return null;
84+
}
85+
86+
public static filterByUsername(value: string): User[] {
87+
let result: User[] = [];
88+
for (let instance of __SPACETIMEDB__.clientDB
89+
.getTable("User")
90+
.getInstances()) {
91+
if (instance.username === value) {
92+
result.push(instance);
93+
}
94+
}
95+
return result;
96+
}
97+
98+
public static onInsert(
99+
callback: (value: User, reducerEvent: ReducerEvent | undefined) => void
100+
) {
101+
__SPACETIMEDB__.clientDB.getTable("User").onInsert(callback);
102+
}
103+
104+
public static onUpdate(
105+
callback: (
106+
oldValue: User,
107+
newValue: User,
108+
reducerEvent: ReducerEvent | undefined
109+
) => void
110+
) {
111+
__SPACETIMEDB__.clientDB.getTable("User").onUpdate(callback);
112+
}
113+
114+
public static onDelete(
115+
callback: (value: User, reducerEvent: ReducerEvent | undefined) => void
116+
) {
117+
__SPACETIMEDB__.clientDB.getTable("User").onDelete(callback);
118+
}
119+
120+
public static removeOnInsert(
121+
callback: (value: User, reducerEvent: ReducerEvent | undefined) => void
122+
) {
123+
__SPACETIMEDB__.clientDB.getTable("User").removeOnInsert(callback);
124+
}
125+
126+
public static removeOnUpdate(
127+
callback: (
128+
oldValue: User,
129+
newValue: User,
130+
reducerEvent: ReducerEvent | undefined
131+
) => void
132+
) {
133+
__SPACETIMEDB__.clientDB.getTable("User").removeOnUpdate(callback);
134+
}
135+
136+
public static removeOnDelete(
137+
callback: (value: User, reducerEvent: ReducerEvent | undefined) => void
138+
) {
139+
__SPACETIMEDB__.clientDB.getTable("User").removeOnDelete(callback);
140+
}
141+
}
142+
143+
export default User;
144+
145+
__SPACETIMEDB__.registerComponent("User", User);

0 commit comments

Comments
 (0)