anchordb-sync-server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/cjs/adapters/express.d.ts +33 -0
- package/dist/cjs/adapters/express.d.ts.map +1 -0
- package/dist/cjs/adapters/express.js +40 -0
- package/dist/cjs/adapters/express.js.map +1 -0
- package/dist/cjs/adapters/nestjs.d.ts +34 -0
- package/dist/cjs/adapters/nestjs.d.ts.map +1 -0
- package/dist/cjs/adapters/nestjs.js +98 -0
- package/dist/cjs/adapters/nestjs.js.map +1 -0
- package/dist/cjs/adapters/nextjs.d.ts +20 -0
- package/dist/cjs/adapters/nextjs.d.ts.map +1 -0
- package/dist/cjs/adapters/nextjs.js +43 -0
- package/dist/cjs/adapters/nextjs.js.map +1 -0
- package/dist/cjs/handlers.d.ts +17 -0
- package/dist/cjs/handlers.d.ts.map +1 -0
- package/dist/cjs/handlers.js +168 -0
- package/dist/cjs/handlers.js.map +1 -0
- package/dist/cjs/index.d.ts +16 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +27 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/store/memory.d.ts +25 -0
- package/dist/cjs/store/memory.d.ts.map +1 -0
- package/dist/cjs/store/memory.js +65 -0
- package/dist/cjs/store/memory.js.map +1 -0
- package/dist/cjs/store/mongoose.d.ts +90 -0
- package/dist/cjs/store/mongoose.d.ts.map +1 -0
- package/dist/cjs/store/mongoose.js +129 -0
- package/dist/cjs/store/mongoose.js.map +1 -0
- package/dist/cjs/store/types.d.ts +62 -0
- package/dist/cjs/store/types.d.ts.map +1 -0
- package/dist/cjs/store/types.js +3 -0
- package/dist/cjs/store/types.js.map +1 -0
- package/dist/esm/adapters/express.d.ts +33 -0
- package/dist/esm/adapters/express.d.ts.map +1 -0
- package/dist/esm/adapters/express.js +37 -0
- package/dist/esm/adapters/express.js.map +1 -0
- package/dist/esm/adapters/nestjs.d.ts +34 -0
- package/dist/esm/adapters/nestjs.d.ts.map +1 -0
- package/dist/esm/adapters/nestjs.js +93 -0
- package/dist/esm/adapters/nestjs.js.map +1 -0
- package/dist/esm/adapters/nextjs.d.ts +20 -0
- package/dist/esm/adapters/nextjs.d.ts.map +1 -0
- package/dist/esm/adapters/nextjs.js +40 -0
- package/dist/esm/adapters/nextjs.js.map +1 -0
- package/dist/esm/handlers.d.ts +17 -0
- package/dist/esm/handlers.d.ts.map +1 -0
- package/dist/esm/handlers.js +162 -0
- package/dist/esm/handlers.js.map +1 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/store/memory.d.ts +25 -0
- package/dist/esm/store/memory.d.ts.map +1 -0
- package/dist/esm/store/memory.js +61 -0
- package/dist/esm/store/memory.js.map +1 -0
- package/dist/esm/store/mongoose.d.ts +90 -0
- package/dist/esm/store/mongoose.d.ts.map +1 -0
- package/dist/esm/store/mongoose.js +124 -0
- package/dist/esm/store/mongoose.js.map +1 -0
- package/dist/esm/store/types.d.ts +62 -0
- package/dist/esm/store/types.d.ts.map +1 -0
- package/dist/esm/store/types.js +2 -0
- package/dist/esm/store/types.js.map +1 -0
- package/package.json +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AnchorDB contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# anchordb-sync-server
|
|
2
|
+
|
|
3
|
+
The server half of the [AnchorDB](https://www.npmjs.com/package/anchordb) sync protocol, with
|
|
4
|
+
Express, NestJS and Next.js adapters.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install anchordb-sync-server
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Express
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import express from "express";
|
|
14
|
+
import { createAnchorRouter, MemoryStore } from "anchordb-sync-server";
|
|
15
|
+
|
|
16
|
+
const app = express();
|
|
17
|
+
app.use(express.json());
|
|
18
|
+
app.use("/anchor/v1", createAnchorRouter(express.Router, { store: new MemoryStore() }));
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Next.js (App Router)
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// app/anchor/v1/[...anchor]/route.ts
|
|
25
|
+
import { createAnchorRouteHandlers } from "anchordb-sync-server/nextjs";
|
|
26
|
+
import { MemoryStore } from "anchordb-sync-server";
|
|
27
|
+
|
|
28
|
+
export const { POST, GET } = createAnchorRouteHandlers({ store: new MemoryStore() });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## NestJS
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { AnchorSyncService, createAnchorSyncService } from "anchordb-sync-server/nestjs";
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`NESTJS_CONTROLLER_EXAMPLE` exports a ready-to-paste controller and module. NestJS itself is not a
|
|
38
|
+
dependency of this package — the controller lives in your app, so it keeps your routing and your
|
|
39
|
+
own Nest version.
|
|
40
|
+
|
|
41
|
+
## Stores
|
|
42
|
+
|
|
43
|
+
| Store | Use |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `MemoryStore` | default; development, tests, demos. Runs with no database |
|
|
46
|
+
| `MongooseStore` | MongoDB, from `anchordb-sync-server/mongoose` |
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import mongoose from "mongoose";
|
|
50
|
+
import { MongooseStore } from "anchordb-sync-server/mongoose";
|
|
51
|
+
|
|
52
|
+
const store = new MongooseStore({ mongoose });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Mongoose is a peer dependency and is **passed in** rather than imported, so the package installs
|
|
56
|
+
without it and your app keeps a single mongoose instance.
|
|
57
|
+
|
|
58
|
+
## What the server is responsible for
|
|
59
|
+
|
|
60
|
+
1. **Version counters** — per-document integers; a stale `baseVersion` is a conflict.
|
|
61
|
+
2. **Authoritative timestamps** — every response carries `serverTime`, which clients use to correct
|
|
62
|
+
their own clock skew.
|
|
63
|
+
3. **A global change sequence** — pull cursors are positions in it, so a partial sync resumes.
|
|
64
|
+
4. **Idempotency** — `(clientId, mutationId)` verdicts are memoised, so a retry after a timeout
|
|
65
|
+
replays the original result instead of applying the write twice.
|
|
66
|
+
|
|
67
|
+
The server never picks a conflict winner. It returns its document and lets the client's configured
|
|
68
|
+
strategy decide, because only the client knows which strategy the developer chose.
|
|
69
|
+
|
|
70
|
+
## Status
|
|
71
|
+
|
|
72
|
+
**0.1.0.** All three adapters share one protocol implementation and are held to the same 49-test
|
|
73
|
+
conformance suite. `MongooseStore` is typechecked and written against the documented Mongoose API
|
|
74
|
+
but is **not exercised against a live MongoDB** in this package's tests — set `MONGODB_URI` and run
|
|
75
|
+
your own integration check before relying on it.
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SyncServerOptions } from "../store/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Express adapter — a thin translation layer, nothing more.
|
|
4
|
+
*
|
|
5
|
+
* All protocol semantics live in `handlers.ts`, so this file only maps HTTP to those calls and
|
|
6
|
+
* back. Typed structurally rather than against `express` types so the package installs without
|
|
7
|
+
* express present.
|
|
8
|
+
*/
|
|
9
|
+
interface Req {
|
|
10
|
+
body: unknown;
|
|
11
|
+
headers: Record<string, string | undefined>;
|
|
12
|
+
}
|
|
13
|
+
interface Res {
|
|
14
|
+
status(code: number): Res;
|
|
15
|
+
json(body: unknown): unknown;
|
|
16
|
+
}
|
|
17
|
+
type Next = (err?: unknown) => void;
|
|
18
|
+
type Handler = (req: Req, res: Res, next: Next) => void | Promise<void>;
|
|
19
|
+
interface RouterLike {
|
|
20
|
+
post(path: string, handler: Handler): unknown;
|
|
21
|
+
get(path: string, handler: Handler): unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface ExpressRouterFactory {
|
|
24
|
+
(): RouterLike;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `app.use("/anchor/v1", createAnchorRouter(express.Router, { store }))`
|
|
28
|
+
*
|
|
29
|
+
* The Router factory is injected rather than imported so this module never requires express itself.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createAnchorRouter(routerFactory: ExpressRouterFactory, options: SyncServerOptions): RouterLike;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=express.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../../src/adapters/express.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D;;;;;;GAMG;AAEH,UAAU,GAAG;IACX,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC7C;AACD,UAAU,GAAG;IACX,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC;IAC1B,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;CAC9B;AACD,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AACpC,KAAK,OAAO,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAExE,UAAU,UAAU;IAClB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;IAC9C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;CAC9C;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,UAAU,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,oBAAoB,EAAE,OAAO,EAAE,iBAAiB,GAAG,UAAU,CA+B9G"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAnchorRouter = createAnchorRouter;
|
|
4
|
+
const handlers_ts_1 = require("../handlers.js");
|
|
5
|
+
/**
|
|
6
|
+
* `app.use("/anchor/v1", createAnchorRouter(express.Router, { store }))`
|
|
7
|
+
*
|
|
8
|
+
* The Router factory is injected rather than imported so this module never requires express itself.
|
|
9
|
+
*/
|
|
10
|
+
function createAnchorRouter(routerFactory, options) {
|
|
11
|
+
const router = routerFactory();
|
|
12
|
+
const guard = (handler) => {
|
|
13
|
+
return async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
if (options.authenticate) {
|
|
16
|
+
const identity = await options.authenticate(req.headers);
|
|
17
|
+
if (!identity) {
|
|
18
|
+
res.status(401).json({ error: "unauthorized" });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
res.status(200).json(await handler(req));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err instanceof handlers_ts_1.ProtocolError) {
|
|
26
|
+
res.status(err.status).json({ error: err.message });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Never leak an internal error message to a client; log it server-side instead.
|
|
30
|
+
console.error("[anchor/sync-server]", err);
|
|
31
|
+
res.status(500).json({ error: "internal error" });
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
router.post("/sync/push", guard((req) => (0, handlers_ts_1.handlePush)(req.body, options)));
|
|
36
|
+
router.post("/sync/pull", guard((req) => (0, handlers_ts_1.handlePull)(req.body, options)));
|
|
37
|
+
router.get("/sync/health", guard(async () => ({ ok: true, store: options.store.name, protocolVersion: 1 })));
|
|
38
|
+
return router;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../../src/adapters/express.ts"],"names":[],"mappings":";;AAoCA,gDA+BC;AAnED,gDAAuE;AA+BvE;;;;GAIG;AACH,SAAgB,kBAAkB,CAAC,aAAmC,EAAE,OAA0B;IAChG,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAE/B,MAAM,KAAK,GAAG,CAAC,OAAuC,EAAW,EAAE;QACjE,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;YACxB,IAAI,CAAC;gBACH,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;oBACzB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;oBACzD,IAAI,CAAC,QAAQ,EAAE,CAAC;wBACd,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;wBAChD,OAAO;oBACT,CAAC;gBACH,CAAC;gBACD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YAC3C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,2BAAa,EAAE,CAAC;oBACjC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;oBACpD,OAAO;gBACT,CAAC;gBACD,gFAAgF;gBAChF,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;gBAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;YACpD,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAA,wBAAU,EAAC,GAAG,CAAC,IAAa,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAClF,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAA,wBAAU,EAAC,GAAG,CAAC,IAAa,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAClF,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAE7G,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SyncServerOptions } from "../store/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* NestJS adapter.
|
|
4
|
+
*
|
|
5
|
+
* NestJS is not imported here — decorators and DI tokens are supplied by the consuming app, so this
|
|
6
|
+
* package installs cleanly for people using Express or Next.js. What we export is:
|
|
7
|
+
*
|
|
8
|
+
* - `AnchorSyncService`, a plain class the app registers as a provider, and
|
|
9
|
+
* - a documented controller you paste into your project (see `NESTJS_CONTROLLER_EXAMPLE`).
|
|
10
|
+
*
|
|
11
|
+
* The alternative — importing `@nestjs/common` to decorate a controller in the library — would make
|
|
12
|
+
* NestJS a hard dependency of everyone, and would bake in route paths the app cannot change.
|
|
13
|
+
*/
|
|
14
|
+
export declare class AnchorSyncService {
|
|
15
|
+
private readonly options;
|
|
16
|
+
constructor(options: SyncServerOptions);
|
|
17
|
+
push(body: unknown, headers?: Record<string, string | undefined>): Promise<unknown>;
|
|
18
|
+
pull(body: unknown, headers?: Record<string, string | undefined>): Promise<unknown>;
|
|
19
|
+
health(): {
|
|
20
|
+
ok: true;
|
|
21
|
+
store: string;
|
|
22
|
+
protocolVersion: number;
|
|
23
|
+
};
|
|
24
|
+
private assertAuthorised;
|
|
25
|
+
}
|
|
26
|
+
/** Factory for `useFactory` in a NestJS module. */
|
|
27
|
+
export declare function createAnchorSyncService(options: SyncServerOptions): AnchorSyncService;
|
|
28
|
+
export declare const ANCHOR_SYNC_SERVICE = "ANCHOR_SYNC_SERVICE";
|
|
29
|
+
/**
|
|
30
|
+
* Copy this controller and module into your NestJS app. It is exported as a string rather than as
|
|
31
|
+
* live code precisely so that `@nestjs/common` stays out of this package's dependency graph.
|
|
32
|
+
*/
|
|
33
|
+
export declare const NESTJS_CONTROLLER_EXAMPLE: string;
|
|
34
|
+
//# sourceMappingURL=nestjs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nestjs.d.ts","sourceRoot":"","sources":["../../../src/adapters/nestjs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;gBAEhC,OAAO,EAAE,iBAAiB;IAIhC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKvF,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK7F,MAAM,IAAI;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE;YAIhD,gBAAgB;CAK/B;AAED,mDAAmD;AACnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAErF;AAED,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AAEzD;;;GAGG;AACH,eAAO,MAAM,yBAAyB,QA8C9B,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NESTJS_CONTROLLER_EXAMPLE = exports.ANCHOR_SYNC_SERVICE = exports.AnchorSyncService = void 0;
|
|
4
|
+
exports.createAnchorSyncService = createAnchorSyncService;
|
|
5
|
+
const handlers_ts_1 = require("../handlers.js");
|
|
6
|
+
/**
|
|
7
|
+
* NestJS adapter.
|
|
8
|
+
*
|
|
9
|
+
* NestJS is not imported here — decorators and DI tokens are supplied by the consuming app, so this
|
|
10
|
+
* package installs cleanly for people using Express or Next.js. What we export is:
|
|
11
|
+
*
|
|
12
|
+
* - `AnchorSyncService`, a plain class the app registers as a provider, and
|
|
13
|
+
* - a documented controller you paste into your project (see `NESTJS_CONTROLLER_EXAMPLE`).
|
|
14
|
+
*
|
|
15
|
+
* The alternative — importing `@nestjs/common` to decorate a controller in the library — would make
|
|
16
|
+
* NestJS a hard dependency of everyone, and would bake in route paths the app cannot change.
|
|
17
|
+
*/
|
|
18
|
+
class AnchorSyncService {
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
async push(body, headers = {}) {
|
|
23
|
+
await this.assertAuthorised(headers);
|
|
24
|
+
return (0, handlers_ts_1.handlePush)(body, this.options);
|
|
25
|
+
}
|
|
26
|
+
async pull(body, headers = {}) {
|
|
27
|
+
await this.assertAuthorised(headers);
|
|
28
|
+
return (0, handlers_ts_1.handlePull)(body, this.options);
|
|
29
|
+
}
|
|
30
|
+
health() {
|
|
31
|
+
return { ok: true, store: this.options.store.name, protocolVersion: 1 };
|
|
32
|
+
}
|
|
33
|
+
async assertAuthorised(headers) {
|
|
34
|
+
if (!this.options.authenticate)
|
|
35
|
+
return;
|
|
36
|
+
const identity = await this.options.authenticate(headers);
|
|
37
|
+
if (!identity)
|
|
38
|
+
throw new handlers_ts_1.ProtocolError(401, "unauthorized");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.AnchorSyncService = AnchorSyncService;
|
|
42
|
+
/** Factory for `useFactory` in a NestJS module. */
|
|
43
|
+
function createAnchorSyncService(options) {
|
|
44
|
+
return new AnchorSyncService(options);
|
|
45
|
+
}
|
|
46
|
+
exports.ANCHOR_SYNC_SERVICE = "ANCHOR_SYNC_SERVICE";
|
|
47
|
+
/**
|
|
48
|
+
* Copy this controller and module into your NestJS app. It is exported as a string rather than as
|
|
49
|
+
* live code precisely so that `@nestjs/common` stays out of this package's dependency graph.
|
|
50
|
+
*/
|
|
51
|
+
exports.NESTJS_CONTROLLER_EXAMPLE = `
|
|
52
|
+
import { Body, Controller, Get, Headers, Module, Post } from "@nestjs/common";
|
|
53
|
+
import {
|
|
54
|
+
AnchorSyncService,
|
|
55
|
+
ANCHOR_SYNC_SERVICE,
|
|
56
|
+
createAnchorSyncService,
|
|
57
|
+
} from "anchordb-sync-server/nestjs";
|
|
58
|
+
import { MemoryStore } from "anchordb-sync-server";
|
|
59
|
+
// import { MongooseStore } from "anchordb-sync-server/mongoose";
|
|
60
|
+
|
|
61
|
+
@Controller("anchor/v1/sync")
|
|
62
|
+
export class AnchorSyncController {
|
|
63
|
+
constructor(private readonly sync: AnchorSyncService) {}
|
|
64
|
+
|
|
65
|
+
@Post("push")
|
|
66
|
+
push(@Body() body: unknown, @Headers() headers: Record<string, string>) {
|
|
67
|
+
return this.sync.push(body, headers);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Post("pull")
|
|
71
|
+
pull(@Body() body: unknown, @Headers() headers: Record<string, string>) {
|
|
72
|
+
return this.sync.pull(body, headers);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Get("health")
|
|
76
|
+
health() {
|
|
77
|
+
return this.sync.health();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Module({
|
|
82
|
+
controllers: [AnchorSyncController],
|
|
83
|
+
providers: [
|
|
84
|
+
{
|
|
85
|
+
provide: AnchorSyncService,
|
|
86
|
+
useFactory: () =>
|
|
87
|
+
createAnchorSyncService({
|
|
88
|
+
store: new MemoryStore(),
|
|
89
|
+
// store: new MongooseStore({ mongoose }),
|
|
90
|
+
uniqueIndexes: [{ collection: "users", field: "email" }],
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
exports: [AnchorSyncService],
|
|
95
|
+
})
|
|
96
|
+
export class AnchorSyncModule {}
|
|
97
|
+
`.trim();
|
|
98
|
+
//# sourceMappingURL=nestjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nestjs.js","sourceRoot":"","sources":["../../../src/adapters/nestjs.ts"],"names":[],"mappings":";;;AA4CA,0DAEC;AA9CD,gDAAuE;AAGvE;;;;;;;;;;;GAWG;AACH,MAAa,iBAAiB;IAG5B,YAAY,OAA0B;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAa,EAAE,UAA8C,EAAE;QACxE,MAAM,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACrC,OAAO,IAAA,wBAAU,EAAC,IAAa,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAa,EAAE,UAA8C,EAAE;QACxE,MAAM,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACrC,OAAO,IAAA,wBAAU,EAAC,IAAa,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC;IAC1E,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,OAA2C;QACxE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY;YAAE,OAAO;QACvC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC1D,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,2BAAa,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAC9D,CAAC;CACF;AA1BD,8CA0BC;AAED,mDAAmD;AACnD,SAAgB,uBAAuB,CAAC,OAA0B;IAChE,OAAO,IAAI,iBAAiB,CAAC,OAAO,CAAC,CAAC;AACxC,CAAC;AAEY,QAAA,mBAAmB,GAAG,qBAAqB,CAAC;AAEzD;;;GAGG;AACU,QAAA,yBAAyB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CxC,CAAC,IAAI,EAAE,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SyncServerOptions } from "../store/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Next.js App Router adapter.
|
|
4
|
+
*
|
|
5
|
+
* Usage in `app/anchor/v1/[...anchor]/route.ts`:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { createAnchorRouteHandlers } from "anchordb-sync-server/nextjs";
|
|
9
|
+
* const handlers = createAnchorRouteHandlers({ store });
|
|
10
|
+
* export const { POST, GET } = handlers;
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Built on the Web `Request`/`Response` types Next uses, so it needs no Next import at all.
|
|
14
|
+
*/
|
|
15
|
+
export interface NextRouteHandlers {
|
|
16
|
+
POST(request: Request): Promise<Response>;
|
|
17
|
+
GET(request: Request): Promise<Response>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createAnchorRouteHandlers(options: SyncServerOptions): NextRouteHandlers;
|
|
20
|
+
//# sourceMappingURL=nextjs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../../../src/adapters/nextjs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1C,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC1C;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAoCvF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAnchorRouteHandlers = createAnchorRouteHandlers;
|
|
4
|
+
const handlers_ts_1 = require("../handlers.js");
|
|
5
|
+
function createAnchorRouteHandlers(options) {
|
|
6
|
+
const json = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
7
|
+
const authorise = async (request) => {
|
|
8
|
+
if (!options.authenticate)
|
|
9
|
+
return true;
|
|
10
|
+
const headers = {};
|
|
11
|
+
request.headers.forEach((value, key) => {
|
|
12
|
+
headers[key] = value;
|
|
13
|
+
});
|
|
14
|
+
return Boolean(await options.authenticate(headers));
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
async POST(request) {
|
|
18
|
+
try {
|
|
19
|
+
if (!(await authorise(request)))
|
|
20
|
+
return json({ error: "unauthorized" }, 401);
|
|
21
|
+
const url = new URL(request.url);
|
|
22
|
+
const body = await request.json();
|
|
23
|
+
if (url.pathname.endsWith("/push"))
|
|
24
|
+
return json(await (0, handlers_ts_1.handlePush)(body, options));
|
|
25
|
+
if (url.pathname.endsWith("/pull"))
|
|
26
|
+
return json(await (0, handlers_ts_1.handlePull)(body, options));
|
|
27
|
+
return json({ error: `Unknown sync route ${url.pathname}` }, 404);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err instanceof handlers_ts_1.ProtocolError)
|
|
31
|
+
return json({ error: err.message }, err.status);
|
|
32
|
+
console.error("[anchor/sync-server]", err);
|
|
33
|
+
return json({ error: "internal error" }, 500);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
async GET(request) {
|
|
37
|
+
if (!(await authorise(request)))
|
|
38
|
+
return json({ error: "unauthorized" }, 401);
|
|
39
|
+
return json({ ok: true, store: options.store.name, protocolVersion: 1 });
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=nextjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nextjs.js","sourceRoot":"","sources":["../../../src/adapters/nextjs.ts"],"names":[],"mappings":";;AAqBA,8DAoCC;AAzDD,gDAAuE;AAqBvE,SAAgB,yBAAyB,CAAC,OAA0B;IAClE,MAAM,IAAI,GAAG,CAAC,IAAa,EAAE,MAAM,GAAG,GAAG,EAAY,EAAE,CACrD,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAC;IAElG,MAAM,SAAS,GAAG,KAAK,EAAE,OAAgB,EAAoB,EAAE;QAC7D,IAAI,CAAC,OAAO,CAAC,YAAY;YAAE,OAAO,IAAI,CAAC;QACvC,MAAM,OAAO,GAAuC,EAAE,CAAC;QACvD,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YACrC,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACvB,CAAC,CAAC,CAAC;QACH,OAAO,OAAO,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,OAAgB;YACzB,IAAI,CAAC;gBACH,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,OAAO,CAAC,CAAC;oBAAE,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,CAAC;gBAE7E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBACjC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;gBAElC,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC;oBAAE,OAAO,IAAI,CAAC,MAAM,IAAA,wBAAU,EAAC,IAAa,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC1F,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC;oBAAE,OAAO,IAAI,CAAC,MAAM,IAAA,wBAAU,EAAC,IAAa,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC1F,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,sBAAsB,GAAG,CAAC,QAAQ,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;YACpE,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,2BAAa;oBAAE,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;gBAClF,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;gBAC3C,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,GAAG,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;QAED,KAAK,CAAC,GAAG,CAAC,OAAgB;YACxB,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,OAAO,CAAC,CAAC;gBAAE,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,CAAC;YAC7E,OAAO,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type PullRequest, type PullResponse, type PushRequest, type PushResponse } from "anchordb";
|
|
2
|
+
import type { StoredServerDocument, SyncServerOptions } from "./store/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The server half of the sync protocol, as pure functions over a `ServerStore`.
|
|
5
|
+
*
|
|
6
|
+
* Express, NestJS and Next.js all delegate here, so there is exactly one implementation of the
|
|
7
|
+
* rules and the framework adapters are genuinely thin. That matters for the brief's promise that
|
|
8
|
+
* what the tests verify is what your backend runs.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ProtocolError extends Error {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
constructor(status: number, message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare function handlePush(request: PushRequest, options: SyncServerOptions): Promise<PushResponse>;
|
|
15
|
+
export declare function handlePull(request: PullRequest, options: SyncServerOptions): Promise<PullResponse>;
|
|
16
|
+
export type { StoredServerDocument };
|
|
17
|
+
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,YAAY,EAElB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEhF;;;;;;GAMG;AAEH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBACZ,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK5C;AAqBD,wBAAsB,UAAU,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAexG;AAwGD,wBAAsB,UAAU,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmCxG;AAED,YAAY,EAAE,oBAAoB,EAAE,CAAC"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProtocolError = void 0;
|
|
4
|
+
exports.handlePush = handlePush;
|
|
5
|
+
exports.handlePull = handlePull;
|
|
6
|
+
const anchordb_1 = require("anchordb");
|
|
7
|
+
/**
|
|
8
|
+
* The server half of the sync protocol, as pure functions over a `ServerStore`.
|
|
9
|
+
*
|
|
10
|
+
* Express, NestJS and Next.js all delegate here, so there is exactly one implementation of the
|
|
11
|
+
* rules and the framework adapters are genuinely thin. That matters for the brief's promise that
|
|
12
|
+
* what the tests verify is what your backend runs.
|
|
13
|
+
*/
|
|
14
|
+
class ProtocolError extends Error {
|
|
15
|
+
constructor(status, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.name = "ProtocolError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.ProtocolError = ProtocolError;
|
|
22
|
+
let hlcCounter = 0;
|
|
23
|
+
function serverHlc(now) {
|
|
24
|
+
hlcCounter = (hlcCounter + 1) % 100_000;
|
|
25
|
+
return `${String(now).padStart(15, "0")}-${String(hlcCounter).padStart(5, "0")}-server`;
|
|
26
|
+
}
|
|
27
|
+
function validatePush(request) {
|
|
28
|
+
if (!request || typeof request !== "object")
|
|
29
|
+
throw new ProtocolError(400, "Request body must be a JSON object");
|
|
30
|
+
if (request.protocolVersion !== anchordb_1.PROTOCOL_VERSION) {
|
|
31
|
+
throw new ProtocolError(400, `Unsupported protocolVersion ${String(request.protocolVersion)}; this server speaks ${anchordb_1.PROTOCOL_VERSION}`);
|
|
32
|
+
}
|
|
33
|
+
if (!request.clientId)
|
|
34
|
+
throw new ProtocolError(400, "clientId is required");
|
|
35
|
+
if (!Array.isArray(request.changes))
|
|
36
|
+
throw new ProtocolError(400, "changes must be an array");
|
|
37
|
+
}
|
|
38
|
+
async function handlePush(request, options) {
|
|
39
|
+
validatePush(request);
|
|
40
|
+
const now = options.now ?? (() => Date.now());
|
|
41
|
+
const results = [];
|
|
42
|
+
for (const change of request.changes) {
|
|
43
|
+
results.push(await applyChange(change, request.clientId, options, now()));
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
protocolVersion: anchordb_1.PROTOCOL_VERSION,
|
|
47
|
+
serverTime: new Date(now()).toISOString(),
|
|
48
|
+
serverHlc: serverHlc(now()),
|
|
49
|
+
results,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function applyChange(change, clientId, options, now) {
|
|
53
|
+
const { store } = options;
|
|
54
|
+
// Idempotency first: a replayed mutation must never be evaluated twice.
|
|
55
|
+
const memoised = await store.getIdempotent(clientId, change.mutationId);
|
|
56
|
+
if (memoised)
|
|
57
|
+
return memoised;
|
|
58
|
+
const result = await decide(change, options, now);
|
|
59
|
+
await store.setIdempotent(clientId, change.mutationId, result);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
async function decide(change, options, now) {
|
|
63
|
+
const { store } = options;
|
|
64
|
+
if (options.allowedCollections && !options.allowedCollections.includes(change.collection)) {
|
|
65
|
+
return {
|
|
66
|
+
mutationId: change.mutationId,
|
|
67
|
+
status: "rejected",
|
|
68
|
+
reason: `Collection "${change.collection}" is not accepted by this server`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const existing = await store.get(change.collection, change.documentId);
|
|
72
|
+
const currentVersion = existing?.version ?? 0;
|
|
73
|
+
// Optimistic concurrency. The client edited a version the server has since moved past, so we
|
|
74
|
+
// return the server's document and let the CLIENT's configured strategy decide — the server does
|
|
75
|
+
// not pick a winner, because only the client knows which strategy the developer chose.
|
|
76
|
+
if (currentVersion !== change.baseVersion) {
|
|
77
|
+
return {
|
|
78
|
+
mutationId: change.mutationId,
|
|
79
|
+
status: "conflict",
|
|
80
|
+
version: currentVersion,
|
|
81
|
+
document: existing && !existing.deleted ? existing.document : null,
|
|
82
|
+
hlc: existing?.hlc ?? "",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const timestamp = new Date(now).toISOString();
|
|
86
|
+
if (change.operation === "delete") {
|
|
87
|
+
if (existing) {
|
|
88
|
+
await store.put({
|
|
89
|
+
documentId: change.documentId,
|
|
90
|
+
collection: change.collection,
|
|
91
|
+
document: existing.document,
|
|
92
|
+
version: currentVersion + 1,
|
|
93
|
+
deleted: true,
|
|
94
|
+
updatedAt: timestamp,
|
|
95
|
+
hlc: serverHlc(now),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return { mutationId: change.mutationId, status: "accepted", version: currentVersion + 1, document: null };
|
|
99
|
+
}
|
|
100
|
+
const payload = (change.payload ?? {});
|
|
101
|
+
for (const index of options.uniqueIndexes ?? []) {
|
|
102
|
+
if (index.collection !== change.collection)
|
|
103
|
+
continue;
|
|
104
|
+
const value = payload[index.field];
|
|
105
|
+
if (value === undefined || value === null)
|
|
106
|
+
continue;
|
|
107
|
+
const clash = await store.findByUniqueField(index.collection, index.field, value, change.documentId);
|
|
108
|
+
if (clash) {
|
|
109
|
+
// Two devices created a colliding document while offline; neither could see it locally.
|
|
110
|
+
// The client treats this as non-retryable and parks the mutation.
|
|
111
|
+
return {
|
|
112
|
+
mutationId: change.mutationId,
|
|
113
|
+
status: "rejected",
|
|
114
|
+
code: anchordb_1.DUPLICATE_KEY_CODE,
|
|
115
|
+
reason: `E11000 duplicate key error collection: ${change.collection} index: ${index.field}_1 ` +
|
|
116
|
+
`dup key: { ${index.field}: "${String(value)}" }`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const hlc = serverHlc(now);
|
|
121
|
+
const stored = await store.put({
|
|
122
|
+
documentId: change.documentId,
|
|
123
|
+
collection: change.collection,
|
|
124
|
+
document: { ...payload, _id: change.documentId, updatedAt: timestamp },
|
|
125
|
+
version: currentVersion + 1,
|
|
126
|
+
deleted: false,
|
|
127
|
+
updatedAt: timestamp,
|
|
128
|
+
hlc,
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
mutationId: change.mutationId,
|
|
132
|
+
status: "accepted",
|
|
133
|
+
version: stored.version,
|
|
134
|
+
document: stored.document,
|
|
135
|
+
hlc,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function handlePull(request, options) {
|
|
139
|
+
if (request.protocolVersion !== anchordb_1.PROTOCOL_VERSION) {
|
|
140
|
+
throw new ProtocolError(400, `Unsupported protocolVersion ${String(request.protocolVersion)}; this server speaks ${anchordb_1.PROTOCOL_VERSION}`);
|
|
141
|
+
}
|
|
142
|
+
const now = options.now ?? (() => Date.now());
|
|
143
|
+
const after = request.cursor ? Number(request.cursor) : 0;
|
|
144
|
+
if (Number.isNaN(after))
|
|
145
|
+
throw new ProtocolError(400, `Malformed cursor "${String(request.cursor)}"`);
|
|
146
|
+
const limit = Math.min(Math.max(request.limit ?? 100, 1), 1000);
|
|
147
|
+
const page = await options.store.listChanges(after, limit, request.collections);
|
|
148
|
+
const changes = page.map((d) => ({
|
|
149
|
+
collection: d.collection,
|
|
150
|
+
documentId: d.documentId,
|
|
151
|
+
document: d.deleted ? null : d.document,
|
|
152
|
+
version: d.version,
|
|
153
|
+
deleted: d.deleted,
|
|
154
|
+
updatedAt: d.updatedAt,
|
|
155
|
+
hlc: d.hlc,
|
|
156
|
+
}));
|
|
157
|
+
const last = page[page.length - 1];
|
|
158
|
+
const nextCursor = last ? String(last.seq) : (request.cursor ?? null);
|
|
159
|
+
return {
|
|
160
|
+
protocolVersion: anchordb_1.PROTOCOL_VERSION,
|
|
161
|
+
serverTime: new Date(now()).toISOString(),
|
|
162
|
+
serverHlc: serverHlc(now()),
|
|
163
|
+
changes,
|
|
164
|
+
nextCursor,
|
|
165
|
+
hasMore: last ? await options.store.hasMoreAfter(last.seq, request.collections) : false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../src/handlers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* anchordb-sync-server — the server half of the Anchor sync protocol.
|
|
3
|
+
*
|
|
4
|
+
* The protocol lives in `handlers.ts` as pure functions over a `ServerStore`. Express, NestJS and
|
|
5
|
+
* Next.js adapters are thin translations of those, and all three are held to one conformance suite.
|
|
6
|
+
*/
|
|
7
|
+
export { handlePush, handlePull, ProtocolError } from "./handlers.js";
|
|
8
|
+
export { MemoryStore } from "./store/memory.js";
|
|
9
|
+
export { MongooseStore } from "./store/mongoose.js";
|
|
10
|
+
export type { MongooseStoreOptions } from "./store/mongoose.js";
|
|
11
|
+
export type { ServerStore, StoredServerDocument, SyncServerOptions, UniqueIndexSpec, } from "./store/types.js";
|
|
12
|
+
export { createAnchorRouter } from "./adapters/express.js";
|
|
13
|
+
export { createAnchorRouteHandlers } from "./adapters/nextjs.js";
|
|
14
|
+
export type { NextRouteHandlers } from "./adapters/nextjs.js";
|
|
15
|
+
export { AnchorSyncService, createAnchorSyncService, ANCHOR_SYNC_SERVICE, NESTJS_CONTROLLER_EXAMPLE, } from "./adapters/nestjs.js";
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,YAAY,EACV,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjE,YAAY,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC"}
|