Skip to content

Commit 00bdce0

Browse files
define sync api
1 parent 0a2e686 commit 00bdce0

5 files changed

Lines changed: 214 additions & 38 deletions

File tree

README.md

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,3 @@
1-
# How to implement a backend with Effect
2-
`@effect/platform` provides a type safe API for building backend apps. Any runtime, any database, and with all the features you expect from a TypeScript backend.
1+
# Async Sync Engine
2+
[Source](https://x.com/i/grok?conversation=1893597036278100311)
33

4-
> [**Check out the full article**](https://www.typeonce.dev/article/how-to-implement-a-backend-with-effect) to learn how to get started 🚀
5-
6-
7-
## Getting started
8-
This repository includes the following:
9-
- Shared `effect` API definition ([`packages/api`](./packages/api/))
10-
- Backend implementation with `effect` ([`apps/server`](/apps/server/))
11-
- Frontend implementation with [TanStack Router](https://tanstack.com/router/latest) ([`apps/client`](/apps/client/))
12-
- Docker compose for local Postgres + [PgAdmin](https://www.pgadmin.org/) environment ([`docker-compose.yaml`](./docker-compose.yaml))
13-
14-
First, open [Docker Desktop](https://www.docker.com/products/docker-desktop/) and execute the below command to start the database and PgAdmin:
15-
16-
> Make sure to create a `.env` file inside *both* the root directory *and* `apps/server`, containing the parameters listed inside `.env.example`.
17-
18-
```sh
19-
docker compose up
20-
```
21-
22-
This will start the database and PgAdmin. You can access `http://localhost:5050/` to login into the **local PgAdmin dashboard**.
23-
24-
> Use the credentials from `.env`: `PGADMIN_MAIL`+`PGADMIN_PW`.
25-
26-
You can then execute both server and client in the monorepo. Open a second terminal and run the below commands:
27-
28-
```sh
29-
pnpm install
30-
pnpm run dev
31-
```
32-
33-
This will start `client` on `http://localhost:3001/`, and `server` on `http://localhost:3000/`.
34-
35-
Done ✨
36-
37-
Server, client, and database all now all connected. Explore more of the `effect` API by playing around with the code in the repository 🕹️
38-
39-
> [**Check out the full article**](https://www.typeonce.dev/article/how-to-implement-a-backend-with-effect) for the details of how the code works 🚀

packages/sync/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "@local/sync",
3+
"type": "module",
4+
"scripts": {
5+
"typecheck": "tsc"
6+
},
7+
"exports": {
8+
".": "./src/main.ts"
9+
},
10+
"devDependencies": {},
11+
"dependencies": {
12+
"@effect/platform": "^0.77.2",
13+
"effect": "^3.13.2"
14+
}
15+
}

packages/sync/src/main.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import {
2+
HttpApi,
3+
HttpApiEndpoint,
4+
HttpApiGroup,
5+
HttpApiSchema,
6+
} from "@effect/platform";
7+
import { Schema } from "effect";
8+
9+
export const ClientId = Schema.UUID;
10+
export const WorkspaceId = Schema.UUID;
11+
export const Scope = Schema.Literal("read", "read_write");
12+
13+
export class ClientTable extends Schema.Class<ClientTable>("ClientTable")({
14+
clientId: ClientId,
15+
createdAt: Schema.DateFromString,
16+
}) {}
17+
18+
export class WorkspaceTable extends Schema.Class<WorkspaceTable>(
19+
"WorkspaceTable"
20+
)({
21+
workspaceId: WorkspaceId,
22+
ownerClientId: ClientId,
23+
createdAt: Schema.DateFromString,
24+
clientId: ClientId,
25+
snapshot: Schema.Uint8Array,
26+
}) {}
27+
28+
export class TokenTable extends Schema.Class<TokenTable>("TokenTable")({
29+
tokenId: Schema.Number,
30+
tokenValue: Schema.String,
31+
clientId: ClientId,
32+
workspaceId: WorkspaceId,
33+
isMaster: Schema.Boolean,
34+
scope: Scope,
35+
issuedAt: Schema.DateFromString,
36+
expiresAt: Schema.NullOr(Schema.DateFromString),
37+
revokedAt: Schema.NullOr(Schema.DateFromString),
38+
}) {}
39+
40+
export class SyncAuthGroup extends HttpApiGroup.make("syncAuth")
41+
.add(
42+
/**
43+
Allows a client to create a new workshop and upload its initial data to the server. The server marks the client as the owner and issues a master token for full control.
44+
*/
45+
HttpApiEndpoint.post("generateToken")`/workspaces`
46+
.setPayload(
47+
Schema.Struct({
48+
clientId: ClientId,
49+
workspaceId: WorkspaceId,
50+
snapshot: WorkspaceTable.fields.snapshot,
51+
})
52+
)
53+
.addError(Schema.String)
54+
.addSuccess(Schema.Struct({ token: Schema.String }))
55+
)
56+
.add(
57+
/**
58+
Allows the owner (via master token) to generate an access token for another client, specifying permissions and expiration. The owner shares this token with the client securely.
59+
*/
60+
HttpApiEndpoint.post(
61+
"issueToken"
62+
)`/workspaces/${HttpApiSchema.param("workspaceId", Schema.UUID)}/token`
63+
.setPayload(
64+
Schema.Struct({
65+
clientId: ClientId,
66+
scope: Scope,
67+
expiresIn: Schema.Duration,
68+
})
69+
)
70+
.addError(Schema.String)
71+
.setHeaders(
72+
Schema.Struct({
73+
Authorization: Schema.String,
74+
})
75+
)
76+
.addSuccess(
77+
Schema.Struct({
78+
token: Schema.String,
79+
scope: Scope,
80+
expiresAt: Schema.DateFromString,
81+
})
82+
)
83+
)
84+
.add(
85+
/**
86+
Lets the owner revoke access for a specific client by invalidating their access token. Requires the master token and targets the `clientId` tied to the token.
87+
*/
88+
HttpApiEndpoint.del(
89+
"revokeToken"
90+
)`/workspaces/${HttpApiSchema.param("workspaceId", Schema.UUID)}/token/${HttpApiSchema.param("clientId", Schema.UUID)}`
91+
.addError(Schema.String)
92+
.setHeaders(
93+
Schema.Struct({
94+
Authorization: Schema.String,
95+
})
96+
)
97+
.addSuccess(Schema.Boolean)
98+
)
99+
.add(
100+
/**
101+
Provides the owner with a list of all active tokens (master and access) for a workshop, showing their status. Useful for managing access.
102+
*/
103+
HttpApiEndpoint.get(
104+
"listTokens"
105+
)`/workspaces/${HttpApiSchema.param("workspaceId", Schema.UUID)}/tokens`
106+
.addError(Schema.String)
107+
.setHeaders(
108+
Schema.Struct({
109+
Authorization: Schema.String,
110+
})
111+
)
112+
.addSuccess(
113+
Schema.Array(
114+
TokenTable.pipe(
115+
Schema.pick(
116+
"clientId",
117+
"tokenValue",
118+
"scope",
119+
"isMaster",
120+
"issuedAt",
121+
"expiresAt",
122+
"revokedAt"
123+
)
124+
)
125+
)
126+
)
127+
) {}
128+
129+
export class SyncDataGroup extends HttpApiGroup.make("syncData")
130+
.add(
131+
/**
132+
Updates the workshop data on the server with changes from a client. Requires a valid token with `read_write` scope.
133+
*/
134+
HttpApiEndpoint.put(
135+
"push"
136+
)`/workspaces/${HttpApiSchema.param("workspaceId", Schema.UUID)}/sync`
137+
.setPayload(WorkspaceTable.pipe(Schema.pick("clientId", "snapshot")))
138+
.addError(Schema.String)
139+
.setHeaders(
140+
Schema.Struct({
141+
Authorization: Schema.String,
142+
})
143+
)
144+
.addSuccess(
145+
WorkspaceTable.pipe(Schema.pick("workspaceId", "createdAt", "snapshot"))
146+
)
147+
)
148+
.add(
149+
/**
150+
Retrieves the current workshop data for a client (owner or authorized user). Requires a valid token (master or access) with at least `read` scope. Used for initial download or sync verification.
151+
*/
152+
HttpApiEndpoint.get(
153+
"pull"
154+
)`/workspaces/${HttpApiSchema.param("workspaceId", Schema.UUID)}`
155+
.addError(Schema.String)
156+
.setHeaders(
157+
Schema.Struct({
158+
Authorization: Schema.String,
159+
})
160+
)
161+
.addSuccess(
162+
WorkspaceTable.pipe(Schema.pick("workspaceId", "createdAt", "snapshot"))
163+
)
164+
) {}
165+
166+
export class SyncApi extends HttpApi.make("SyncApi")
167+
.add(SyncAuthGroup)
168+
.add(SyncDataGroup) {}

packages/sync/tsconfig.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"esModuleInterop": true,
4+
"skipLibCheck": true,
5+
"target": "es2022",
6+
"allowJs": true,
7+
"resolveJsonModule": true,
8+
"moduleDetection": "force",
9+
"isolatedModules": true,
10+
"verbatimModuleSyntax": true,
11+
"strict": true,
12+
"noUncheckedIndexedAccess": true,
13+
"noImplicitOverride": true,
14+
"module": "preserve",
15+
"noEmit": true,
16+
"lib": ["es2022"]
17+
},
18+
"include": ["**/*.ts"],
19+
"exclude": ["node_modules"]
20+
}

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)