Skip to content

Commit e71b680

Browse files
committed
implement create-invitation
1 parent a550cca commit e71b680

File tree

10 files changed

+253
-66
lines changed

10 files changed

+253
-66
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { SpaceEvent } from 'graph-framework';
2+
3+
export function DebugSpaceEvents({ events }: { events: SpaceEvent[] }) {
4+
return (
5+
<ul className="text-xs">
6+
{events.map((event) => {
7+
return (
8+
<li key={event.transaction.id} className="border border-gray-300">
9+
<pre>{JSON.stringify(event, null, 2)}</pre>
10+
</li>
11+
);
12+
})}
13+
</ul>
14+
);
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { SpaceState } from 'graph-framework';
2+
3+
export function DebugSpaceState(props: { state: SpaceState | undefined }) {
4+
return (
5+
<div className="text-xs">
6+
<pre>{JSON.stringify(props, null, 2)}</pre>
7+
</div>
8+
);
9+
}

apps/events/src/routes/playground.tsx

Lines changed: 116 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
1+
import { DebugSpaceEvents } from '@/components/debug-space-events';
2+
import { DebugSpaceState } from '@/components/debug-space-state';
13
import { Button } from '@/components/ui/button';
24
import { assertExhaustive } from '@/lib/assertExhaustive';
35
import { createFileRoute } from '@tanstack/react-router';
4-
import { Effect } from 'effect';
6+
import { Effect, Exit } from 'effect';
57
import * as Schema from 'effect/Schema';
6-
import type { EventMessage, RequestListSpaces, RequestSubscribeToSpace } from 'graph-framework';
7-
import { ResponseMessage, createSpace } from 'graph-framework';
8+
import type { EventMessage, RequestListSpaces, RequestSubscribeToSpace, SpaceEvent, SpaceState } from 'graph-framework';
9+
import { ResponseMessage, applyEvent, createInvitation, createSpace } from 'graph-framework';
810
import { useEffect, useState } from 'react';
911

12+
const availableAccounts = [
13+
{
14+
accountId: '0262701b2eb1b6b37ad03e24445dfcad1b91309199e43017b657ce2604417c12f5',
15+
signaturePrivateKey: '88bb6f20de8dc1787c722dc847f4cf3d00285b8955445f23c483d1237fe85366',
16+
},
17+
{
18+
accountId: '03bf5d2a1badf15387b08a007d1a9a13a9bfd6e1c56f681e251514d9ba10b57462',
19+
signaturePrivateKey: '1eee32d3bc202dcb5d17c3b1454fb541d2290cb941860735408f1bfe39e7bc15',
20+
},
21+
{
22+
accountId: '0351460706cf386282d9b6ebee2ccdcb9ba61194fd024345e53037f3036242e6a2',
23+
signaturePrivateKey: '434518a2c9a665a7c20da086232c818b6c1592e2edfeecab29a40cf5925ca8fe',
24+
},
25+
];
26+
27+
type SpaceStorageEntry = {
28+
id: string;
29+
events: SpaceEvent[];
30+
state: SpaceState | undefined;
31+
};
32+
1033
const decodeResponseMessage = Schema.decodeUnknownEither(ResponseMessage);
1134

1235
export const Route = createFileRoute('/playground')({
@@ -15,26 +38,63 @@ export const Route = createFileRoute('/playground')({
1538

1639
const App = ({ accountId, signaturePrivateKey }: { accountId: string; signaturePrivateKey: string }) => {
1740
const [websocketConnection, setWebsocketConnection] = useState<WebSocket>();
18-
const [spaces, setSpaces] = useState<{ id: string }[]>([]);
41+
const [spaces, setSpaces] = useState<SpaceStorageEntry[]>([]);
1942

2043
useEffect(() => {
2144
// temporary until we have a way to create accounts and authenticate them
2245
const websocketConnection = new WebSocket(`ws://localhost:3030/?accountId=${accountId}`);
2346
setWebsocketConnection(websocketConnection);
2447

25-
const onMessage = (event: MessageEvent) => {
48+
const onMessage = async (event: MessageEvent) => {
2649
console.log('message received', event.data);
2750
const data = JSON.parse(event.data);
2851
const message = decodeResponseMessage(data);
2952
if (message._tag === 'Right') {
3053
const response = message.right;
3154
switch (response.type) {
3255
case 'list-spaces': {
33-
setSpaces(response.spaces.map((space) => ({ id: space.id })));
56+
setSpaces((existingSpaces) => {
57+
return response.spaces.map((space) => {
58+
const existingSpace = existingSpaces.find((s) => s.id === space.id);
59+
return { id: space.id, events: existingSpace?.events ?? [], state: existingSpace?.state };
60+
});
61+
});
62+
// fetch all spaces (for debugging purposes)
63+
for (const space of response.spaces) {
64+
const message: RequestSubscribeToSpace = { type: 'subscribe-space', id: space.id };
65+
websocketConnection?.send(JSON.stringify(message));
66+
}
3467
break;
3568
}
3669
case 'space': {
37-
console.log('space', response);
70+
let state: SpaceState | undefined = undefined;
71+
72+
// TODO fix typing
73+
for (const event of response.events) {
74+
if (state === undefined) {
75+
const applyEventResult = await Effect.runPromiseExit(applyEvent({ event }));
76+
if (Exit.isSuccess(applyEventResult)) {
77+
state = applyEventResult.value;
78+
}
79+
} else {
80+
const applyEventResult = await Effect.runPromiseExit(applyEvent({ event, state }));
81+
if (Exit.isSuccess(applyEventResult)) {
82+
state = applyEventResult.value;
83+
}
84+
}
85+
}
86+
87+
const newState = state as SpaceState;
88+
89+
setSpaces((spaces) =>
90+
spaces.map((space) => {
91+
if (space.id === response.id) {
92+
// TODO fix readonly type issue
93+
return { ...space, events: response.events as SpaceEvent[], state: newState };
94+
}
95+
return space;
96+
}),
97+
);
3898
break;
3999
}
40100
case 'event': {
@@ -86,7 +146,7 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
86146
},
87147
}),
88148
);
89-
const message: EventMessage = { type: 'event', event: spaceEvent };
149+
const message: EventMessage = { type: 'event', event: spaceEvent, spaceId: spaceEvent.transaction.id };
90150
websocketConnection?.send(JSON.stringify(message));
91151
}}
92152
>
@@ -107,7 +167,7 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
107167
{spaces.map((space) => {
108168
return (
109169
<li key={space.id}>
110-
<h3>{space.id}</h3>
170+
<h3>Space id: {space.id}</h3>
111171
<Button
112172
onClick={() => {
113173
const message: RequestSubscribeToSpace = { type: 'subscribe-space', id: space.id };
@@ -116,6 +176,47 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
116176
>
117177
Get data and subscribe to Space
118178
</Button>
179+
<br />
180+
{availableAccounts.map((invitee) => {
181+
return (
182+
<Button
183+
key={invitee.accountId}
184+
onClick={async () => {
185+
if (!space.state) {
186+
console.error('No state found for space');
187+
return;
188+
}
189+
const spaceEvent = await Effect.runPromiseExit(
190+
createInvitation({
191+
author: {
192+
signaturePublicKey: accountId,
193+
encryptionPublicKey: 'TODO',
194+
signaturePrivateKey,
195+
},
196+
previousEventHash: space.state.lastEventHash,
197+
invitee: {
198+
signaturePublicKey: invitee.accountId,
199+
encryptionPublicKey: 'TODO',
200+
},
201+
}),
202+
);
203+
if (Exit.isFailure(spaceEvent)) {
204+
console.error('Failed to create invitation', spaceEvent);
205+
return;
206+
}
207+
const message: EventMessage = { type: 'event', event: spaceEvent.value, spaceId: space.id };
208+
websocketConnection?.send(JSON.stringify(message));
209+
}}
210+
>
211+
Invite {invitee.accountId.substring(0, 4)}
212+
</Button>
213+
);
214+
})}
215+
<h3>State</h3>
216+
<DebugSpaceState state={space.state} />
217+
<h3>Events</h3>
218+
<DebugSpaceEvents events={space.events} />
219+
<hr />
119220
</li>
120221
);
121222
})}
@@ -132,33 +233,24 @@ export const ChooseAccount = () => {
132233
<h1>Choose account</h1>
133234
<Button
134235
onClick={() => {
135-
setAccount({
136-
accountId: '0262701b2eb1b6b37ad03e24445dfcad1b91309199e43017b657ce2604417c12f5',
137-
signaturePrivateKey: '88bb6f20de8dc1787c722dc847f4cf3d00285b8955445f23c483d1237fe85366',
138-
});
236+
setAccount(availableAccounts[0]);
139237
}}
140238
>
141-
`abc`
239+
{availableAccounts[0].accountId.substring(0, 4)}
142240
</Button>
143241
<Button
144242
onClick={() => {
145-
setAccount({
146-
accountId: '03bf5d2a1badf15387b08a007d1a9a13a9bfd6e1c56f681e251514d9ba10b57462',
147-
signaturePrivateKey: '1eee32d3bc202dcb5d17c3b1454fb541d2290cb941860735408f1bfe39e7bc15',
148-
});
243+
setAccount(availableAccounts[1]);
149244
}}
150245
>
151-
`cde`
246+
{availableAccounts[1].accountId.substring(0, 4)}
152247
</Button>
153248
<Button
154249
onClick={() => {
155-
setAccount({
156-
accountId: '0351460706cf386282d9b6ebee2ccdcb9ba61194fd024345e53037f3036242e6a2',
157-
signaturePrivateKey: '434518a2c9a665a7c20da086232c818b6c1592e2edfeecab29a40cf5925ca8fe',
158-
});
250+
setAccount(availableAccounts[2]);
159251
}}
160252
>
161-
`def`
253+
{availableAccounts[2].accountId.substring(0, 4)}
162254
</Button>
163255
Account: {account?.accountId ? account.accountId : 'none'}
164256
<hr />

apps/server/src/handlers/applySpaceEvent.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ type Params = {
1010
};
1111

1212
export async function applySpaceEvent({ accountId, spaceId, event }: Params) {
13-
return await prisma.$transaction(async (transaction) => {
14-
if (event.transaction.type === 'create-space') {
15-
throw new Error('applySpaceEvent does not support create-space events.');
16-
}
13+
if (event.transaction.type === 'create-space') {
14+
throw new Error('applySpaceEvent does not support create-space events.');
15+
}
1716

17+
return await prisma.$transaction(async (transaction) => {
1818
// verify that the account is a member of the space
1919
// TODO verify that the account is a admin of the space
2020
await transaction.space.findUniqueOrThrow({
@@ -26,8 +26,9 @@ export async function applySpaceEvent({ accountId, spaceId, event }: Params) {
2626
orderBy: { counter: 'desc' },
2727
});
2828

29-
const result = await Effect.runPromiseExit(applyEvent({ event }));
29+
const result = await Effect.runPromiseExit(applyEvent({ event, state: JSON.parse(lastEvent.state) }));
3030
if (Exit.isFailure(result)) {
31+
console.log('Failed to apply event', result);
3132
throw new Error('Invalid event');
3233
}
3334

apps/server/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import cors from 'cors';
22
import 'dotenv/config';
3-
import { parse } from 'node:url';
43
import { Effect, Exit, Schema } from 'effect';
54
import express from 'express';
65
import type { ResponseListSpaces, ResponseSpace } from 'graph-framework-messages';
76
import { RequestMessage } from 'graph-framework-messages';
87
import { type CreateSpaceEvent, applyEvent } from 'graph-framework-space-events';
8+
import { parse } from 'node:url';
99
import type WebSocket from 'ws';
1010
import { WebSocketServer } from 'ws';
11+
import { applySpaceEvent } from './handlers/applySpaceEvent.js';
1112
import { createSpace } from './handlers/createSpace.js';
1213
import { getSpace } from './handlers/getSpace.js';
1314
import { listSpaces } from './handlers/listSpaces.js';
@@ -88,6 +89,14 @@ webSocketServer.on('connection', async (webSocket: WebSocket, request: Request)
8889
break;
8990
}
9091
case 'create-invitation': {
92+
await applySpaceEvent({ accountId, spaceId: data.spaceId, event: data.event });
93+
const spaceWithEvents = await getSpace({ accountId, spaceId: data.spaceId });
94+
const outgoingMessage: ResponseSpace = {
95+
type: 'space',
96+
id: data.spaceId,
97+
events: spaceWithEvents.events.map((wrapper) => JSON.parse(wrapper.event)),
98+
};
99+
webSocket.send(JSON.stringify(outgoingMessage));
91100
break;
92101
}
93102
}

packages/graph-framework-messages/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { SpaceEvent } from 'graph-framework-space-events';
33

44
export const EventMessage = Schema.Struct({
55
type: Schema.Literal('event'),
6+
spaceId: Schema.String,
67
event: SpaceEvent,
78
});
89

packages/graph-framework-space-events/src/apply-event.test.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@ import { createInvitation } from './create-invitation.js';
77
import { createSpace } from './create-space.js';
88
import { InvalidEventError, VerifySignatureError } from './types.js';
99

10-
it('should fail in case of an invalid signature', async () => {
11-
const author = {
12-
signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
13-
signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
14-
encryptionPublicKey: 'encryption',
15-
};
10+
const author = {
11+
signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
12+
signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
13+
encryptionPublicKey: 'encryption',
14+
};
15+
16+
const invitee = {
17+
signaturePublicKey: '03bf5d2a1badf15387b08a007d1a9a13a9bfd6e1c56f681e251514d9ba10b57462',
18+
signaturePrivateKey: '1eee32d3bc202dcb5d17c3b1454fb541d2290cb941860735408f1bfe39e7bc15',
19+
encryptionPublicKey: 'encryption',
20+
};
1621

22+
it('should fail in case of an invalid signature', async () => {
1723
const result = await Effect.runPromiseExit(
1824
Effect.gen(function* () {
1925
const spaceEvent = yield* createSpace({ author });
@@ -37,18 +43,12 @@ it('should fail in case of an invalid signature', async () => {
3743
});
3844

3945
it('should fail in case state is not provided for an event other than createSpace', async () => {
40-
const author = {
41-
signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
42-
signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
43-
encryptionPublicKey: 'encryption',
44-
};
45-
4646
const result = await Effect.runPromiseExit(
4747
Effect.gen(function* () {
4848
const spaceEvent = yield* createSpace({ author });
4949
const state = yield* applyEvent({ event: spaceEvent });
5050

51-
const spaceEvent2 = yield* createInvitation({ author, id: state.id, previousEventHash: state.lastEventHash });
51+
const spaceEvent2 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee });
5252
return yield* applyEvent({ event: spaceEvent2 });
5353
}),
5454
);
@@ -63,12 +63,6 @@ it('should fail in case state is not provided for an event other than createSpac
6363
});
6464

6565
it('should fail in case of an event is applied that is not based on the previous event', async () => {
66-
const author = {
67-
signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
68-
signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
69-
encryptionPublicKey: 'encryption',
70-
};
71-
7266
const result = await Effect.runPromiseExit(
7367
Effect.gen(function* () {
7468
const spaceEvent = yield* createSpace({ author });
@@ -77,7 +71,7 @@ it('should fail in case of an event is applied that is not based on the previous
7771
const spaceEvent2 = yield* createSpace({ author });
7872
const state2 = yield* applyEvent({ state, event: spaceEvent2 });
7973

80-
const spaceEvent3 = yield* createInvitation({ author, id: state.id, previousEventHash: state.lastEventHash });
74+
const spaceEvent3 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee });
8175
return yield* applyEvent({ state: state2, event: spaceEvent3 });
8276
}),
8377
);

packages/graph-framework-space-events/src/apply-event.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ export const applyEvent = ({
7070
members = {};
7171
invitations = {};
7272
} else if (event.transaction.type === 'create-invitation') {
73+
if (members[event.transaction.signaturePublicKey] !== undefined) {
74+
return Effect.fail(new InvalidEventError());
75+
}
76+
for (const invitation of Object.values(invitations)) {
77+
if (invitation.signaturePublicKey === event.transaction.signaturePublicKey) {
78+
return Effect.fail(new InvalidEventError());
79+
}
80+
}
81+
7382
invitations[event.transaction.id] = {
7483
signaturePublicKey: event.transaction.signaturePublicKey,
7584
encryptionPublicKey: event.transaction.encryptionPublicKey,

0 commit comments

Comments
 (0)