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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/cjs/adapters/express.d.ts +33 -0
  4. package/dist/cjs/adapters/express.d.ts.map +1 -0
  5. package/dist/cjs/adapters/express.js +40 -0
  6. package/dist/cjs/adapters/express.js.map +1 -0
  7. package/dist/cjs/adapters/nestjs.d.ts +34 -0
  8. package/dist/cjs/adapters/nestjs.d.ts.map +1 -0
  9. package/dist/cjs/adapters/nestjs.js +98 -0
  10. package/dist/cjs/adapters/nestjs.js.map +1 -0
  11. package/dist/cjs/adapters/nextjs.d.ts +20 -0
  12. package/dist/cjs/adapters/nextjs.d.ts.map +1 -0
  13. package/dist/cjs/adapters/nextjs.js +43 -0
  14. package/dist/cjs/adapters/nextjs.js.map +1 -0
  15. package/dist/cjs/handlers.d.ts +17 -0
  16. package/dist/cjs/handlers.d.ts.map +1 -0
  17. package/dist/cjs/handlers.js +168 -0
  18. package/dist/cjs/handlers.js.map +1 -0
  19. package/dist/cjs/index.d.ts +16 -0
  20. package/dist/cjs/index.d.ts.map +1 -0
  21. package/dist/cjs/index.js +27 -0
  22. package/dist/cjs/index.js.map +1 -0
  23. package/dist/cjs/package.json +3 -0
  24. package/dist/cjs/store/memory.d.ts +25 -0
  25. package/dist/cjs/store/memory.d.ts.map +1 -0
  26. package/dist/cjs/store/memory.js +65 -0
  27. package/dist/cjs/store/memory.js.map +1 -0
  28. package/dist/cjs/store/mongoose.d.ts +90 -0
  29. package/dist/cjs/store/mongoose.d.ts.map +1 -0
  30. package/dist/cjs/store/mongoose.js +129 -0
  31. package/dist/cjs/store/mongoose.js.map +1 -0
  32. package/dist/cjs/store/types.d.ts +62 -0
  33. package/dist/cjs/store/types.d.ts.map +1 -0
  34. package/dist/cjs/store/types.js +3 -0
  35. package/dist/cjs/store/types.js.map +1 -0
  36. package/dist/esm/adapters/express.d.ts +33 -0
  37. package/dist/esm/adapters/express.d.ts.map +1 -0
  38. package/dist/esm/adapters/express.js +37 -0
  39. package/dist/esm/adapters/express.js.map +1 -0
  40. package/dist/esm/adapters/nestjs.d.ts +34 -0
  41. package/dist/esm/adapters/nestjs.d.ts.map +1 -0
  42. package/dist/esm/adapters/nestjs.js +93 -0
  43. package/dist/esm/adapters/nestjs.js.map +1 -0
  44. package/dist/esm/adapters/nextjs.d.ts +20 -0
  45. package/dist/esm/adapters/nextjs.d.ts.map +1 -0
  46. package/dist/esm/adapters/nextjs.js +40 -0
  47. package/dist/esm/adapters/nextjs.js.map +1 -0
  48. package/dist/esm/handlers.d.ts +17 -0
  49. package/dist/esm/handlers.d.ts.map +1 -0
  50. package/dist/esm/handlers.js +162 -0
  51. package/dist/esm/handlers.js.map +1 -0
  52. package/dist/esm/index.d.ts +16 -0
  53. package/dist/esm/index.d.ts.map +1 -0
  54. package/dist/esm/index.js +13 -0
  55. package/dist/esm/index.js.map +1 -0
  56. package/dist/esm/package.json +3 -0
  57. package/dist/esm/store/memory.d.ts +25 -0
  58. package/dist/esm/store/memory.d.ts.map +1 -0
  59. package/dist/esm/store/memory.js +61 -0
  60. package/dist/esm/store/memory.js.map +1 -0
  61. package/dist/esm/store/mongoose.d.ts +90 -0
  62. package/dist/esm/store/mongoose.d.ts.map +1 -0
  63. package/dist/esm/store/mongoose.js +124 -0
  64. package/dist/esm/store/mongoose.js.map +1 -0
  65. package/dist/esm/store/types.d.ts +62 -0
  66. package/dist/esm/store/types.d.ts.map +1 -0
  67. package/dist/esm/store/types.js +2 -0
  68. package/dist/esm/store/types.js.map +1 -0
  69. package/package.json +109 -0
@@ -0,0 +1,93 @@
1
+ import { handlePull, handlePush, ProtocolError } from "../handlers.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 class AnchorSyncService {
15
+ constructor(options) {
16
+ this.options = options;
17
+ }
18
+ async push(body, headers = {}) {
19
+ await this.assertAuthorised(headers);
20
+ return handlePush(body, this.options);
21
+ }
22
+ async pull(body, headers = {}) {
23
+ await this.assertAuthorised(headers);
24
+ return handlePull(body, this.options);
25
+ }
26
+ health() {
27
+ return { ok: true, store: this.options.store.name, protocolVersion: 1 };
28
+ }
29
+ async assertAuthorised(headers) {
30
+ if (!this.options.authenticate)
31
+ return;
32
+ const identity = await this.options.authenticate(headers);
33
+ if (!identity)
34
+ throw new ProtocolError(401, "unauthorized");
35
+ }
36
+ }
37
+ /** Factory for `useFactory` in a NestJS module. */
38
+ export function createAnchorSyncService(options) {
39
+ return new AnchorSyncService(options);
40
+ }
41
+ export const ANCHOR_SYNC_SERVICE = "ANCHOR_SYNC_SERVICE";
42
+ /**
43
+ * Copy this controller and module into your NestJS app. It is exported as a string rather than as
44
+ * live code precisely so that `@nestjs/common` stays out of this package's dependency graph.
45
+ */
46
+ export const NESTJS_CONTROLLER_EXAMPLE = `
47
+ import { Body, Controller, Get, Headers, Module, Post } from "@nestjs/common";
48
+ import {
49
+ AnchorSyncService,
50
+ ANCHOR_SYNC_SERVICE,
51
+ createAnchorSyncService,
52
+ } from "anchordb-sync-server/nestjs";
53
+ import { MemoryStore } from "anchordb-sync-server";
54
+ // import { MongooseStore } from "anchordb-sync-server/mongoose";
55
+
56
+ @Controller("anchor/v1/sync")
57
+ export class AnchorSyncController {
58
+ constructor(private readonly sync: AnchorSyncService) {}
59
+
60
+ @Post("push")
61
+ push(@Body() body: unknown, @Headers() headers: Record<string, string>) {
62
+ return this.sync.push(body, headers);
63
+ }
64
+
65
+ @Post("pull")
66
+ pull(@Body() body: unknown, @Headers() headers: Record<string, string>) {
67
+ return this.sync.pull(body, headers);
68
+ }
69
+
70
+ @Get("health")
71
+ health() {
72
+ return this.sync.health();
73
+ }
74
+ }
75
+
76
+ @Module({
77
+ controllers: [AnchorSyncController],
78
+ providers: [
79
+ {
80
+ provide: AnchorSyncService,
81
+ useFactory: () =>
82
+ createAnchorSyncService({
83
+ store: new MemoryStore(),
84
+ // store: new MongooseStore({ mongoose }),
85
+ uniqueIndexes: [{ collection: "users", field: "email" }],
86
+ }),
87
+ },
88
+ ],
89
+ exports: [AnchorSyncService],
90
+ })
91
+ export class AnchorSyncModule {}
92
+ `.trim();
93
+ //# sourceMappingURL=nestjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nestjs.js","sourceRoot":"","sources":["../../../src/adapters/nestjs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGvE;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,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,UAAU,CAAC,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,UAAU,CAAC,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,aAAa,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAC9D,CAAC;CACF;AAED,mDAAmD;AACnD,MAAM,UAAU,uBAAuB,CAAC,OAA0B;IAChE,OAAO,IAAI,iBAAiB,CAAC,OAAO,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AAEzD;;;GAGG;AACH,MAAM,CAAC,MAAM,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,40 @@
1
+ import { handlePull, handlePush, ProtocolError } from "../handlers.js";
2
+ export function createAnchorRouteHandlers(options) {
3
+ const json = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
4
+ const authorise = async (request) => {
5
+ if (!options.authenticate)
6
+ return true;
7
+ const headers = {};
8
+ request.headers.forEach((value, key) => {
9
+ headers[key] = value;
10
+ });
11
+ return Boolean(await options.authenticate(headers));
12
+ };
13
+ return {
14
+ async POST(request) {
15
+ try {
16
+ if (!(await authorise(request)))
17
+ return json({ error: "unauthorized" }, 401);
18
+ const url = new URL(request.url);
19
+ const body = await request.json();
20
+ if (url.pathname.endsWith("/push"))
21
+ return json(await handlePush(body, options));
22
+ if (url.pathname.endsWith("/pull"))
23
+ return json(await handlePull(body, options));
24
+ return json({ error: `Unknown sync route ${url.pathname}` }, 404);
25
+ }
26
+ catch (err) {
27
+ if (err instanceof ProtocolError)
28
+ return json({ error: err.message }, err.status);
29
+ console.error("[anchor/sync-server]", err);
30
+ return json({ error: "internal error" }, 500);
31
+ }
32
+ },
33
+ async GET(request) {
34
+ if (!(await authorise(request)))
35
+ return json({ error: "unauthorized" }, 401);
36
+ return json({ ok: true, store: options.store.name, protocolVersion: 1 });
37
+ },
38
+ };
39
+ }
40
+ //# sourceMappingURL=nextjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nextjs.js","sourceRoot":"","sources":["../../../src/adapters/nextjs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAqBvE,MAAM,UAAU,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,UAAU,CAAC,IAAa,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC1F,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC;oBAAE,OAAO,IAAI,CAAC,MAAM,UAAU,CAAC,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,aAAa;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,162 @@
1
+ import { DUPLICATE_KEY_CODE, PROTOCOL_VERSION, } from "anchordb";
2
+ /**
3
+ * The server half of the sync protocol, as pure functions over a `ServerStore`.
4
+ *
5
+ * Express, NestJS and Next.js all delegate here, so there is exactly one implementation of the
6
+ * rules and the framework adapters are genuinely thin. That matters for the brief's promise that
7
+ * what the tests verify is what your backend runs.
8
+ */
9
+ export class ProtocolError extends Error {
10
+ constructor(status, message) {
11
+ super(message);
12
+ this.status = status;
13
+ this.name = "ProtocolError";
14
+ }
15
+ }
16
+ let hlcCounter = 0;
17
+ function serverHlc(now) {
18
+ hlcCounter = (hlcCounter + 1) % 100_000;
19
+ return `${String(now).padStart(15, "0")}-${String(hlcCounter).padStart(5, "0")}-server`;
20
+ }
21
+ function validatePush(request) {
22
+ if (!request || typeof request !== "object")
23
+ throw new ProtocolError(400, "Request body must be a JSON object");
24
+ if (request.protocolVersion !== PROTOCOL_VERSION) {
25
+ throw new ProtocolError(400, `Unsupported protocolVersion ${String(request.protocolVersion)}; this server speaks ${PROTOCOL_VERSION}`);
26
+ }
27
+ if (!request.clientId)
28
+ throw new ProtocolError(400, "clientId is required");
29
+ if (!Array.isArray(request.changes))
30
+ throw new ProtocolError(400, "changes must be an array");
31
+ }
32
+ export async function handlePush(request, options) {
33
+ validatePush(request);
34
+ const now = options.now ?? (() => Date.now());
35
+ const results = [];
36
+ for (const change of request.changes) {
37
+ results.push(await applyChange(change, request.clientId, options, now()));
38
+ }
39
+ return {
40
+ protocolVersion: PROTOCOL_VERSION,
41
+ serverTime: new Date(now()).toISOString(),
42
+ serverHlc: serverHlc(now()),
43
+ results,
44
+ };
45
+ }
46
+ async function applyChange(change, clientId, options, now) {
47
+ const { store } = options;
48
+ // Idempotency first: a replayed mutation must never be evaluated twice.
49
+ const memoised = await store.getIdempotent(clientId, change.mutationId);
50
+ if (memoised)
51
+ return memoised;
52
+ const result = await decide(change, options, now);
53
+ await store.setIdempotent(clientId, change.mutationId, result);
54
+ return result;
55
+ }
56
+ async function decide(change, options, now) {
57
+ const { store } = options;
58
+ if (options.allowedCollections && !options.allowedCollections.includes(change.collection)) {
59
+ return {
60
+ mutationId: change.mutationId,
61
+ status: "rejected",
62
+ reason: `Collection "${change.collection}" is not accepted by this server`,
63
+ };
64
+ }
65
+ const existing = await store.get(change.collection, change.documentId);
66
+ const currentVersion = existing?.version ?? 0;
67
+ // Optimistic concurrency. The client edited a version the server has since moved past, so we
68
+ // return the server's document and let the CLIENT's configured strategy decide — the server does
69
+ // not pick a winner, because only the client knows which strategy the developer chose.
70
+ if (currentVersion !== change.baseVersion) {
71
+ return {
72
+ mutationId: change.mutationId,
73
+ status: "conflict",
74
+ version: currentVersion,
75
+ document: existing && !existing.deleted ? existing.document : null,
76
+ hlc: existing?.hlc ?? "",
77
+ };
78
+ }
79
+ const timestamp = new Date(now).toISOString();
80
+ if (change.operation === "delete") {
81
+ if (existing) {
82
+ await store.put({
83
+ documentId: change.documentId,
84
+ collection: change.collection,
85
+ document: existing.document,
86
+ version: currentVersion + 1,
87
+ deleted: true,
88
+ updatedAt: timestamp,
89
+ hlc: serverHlc(now),
90
+ });
91
+ }
92
+ return { mutationId: change.mutationId, status: "accepted", version: currentVersion + 1, document: null };
93
+ }
94
+ const payload = (change.payload ?? {});
95
+ for (const index of options.uniqueIndexes ?? []) {
96
+ if (index.collection !== change.collection)
97
+ continue;
98
+ const value = payload[index.field];
99
+ if (value === undefined || value === null)
100
+ continue;
101
+ const clash = await store.findByUniqueField(index.collection, index.field, value, change.documentId);
102
+ if (clash) {
103
+ // Two devices created a colliding document while offline; neither could see it locally.
104
+ // The client treats this as non-retryable and parks the mutation.
105
+ return {
106
+ mutationId: change.mutationId,
107
+ status: "rejected",
108
+ code: DUPLICATE_KEY_CODE,
109
+ reason: `E11000 duplicate key error collection: ${change.collection} index: ${index.field}_1 ` +
110
+ `dup key: { ${index.field}: "${String(value)}" }`,
111
+ };
112
+ }
113
+ }
114
+ const hlc = serverHlc(now);
115
+ const stored = await store.put({
116
+ documentId: change.documentId,
117
+ collection: change.collection,
118
+ document: { ...payload, _id: change.documentId, updatedAt: timestamp },
119
+ version: currentVersion + 1,
120
+ deleted: false,
121
+ updatedAt: timestamp,
122
+ hlc,
123
+ });
124
+ return {
125
+ mutationId: change.mutationId,
126
+ status: "accepted",
127
+ version: stored.version,
128
+ document: stored.document,
129
+ hlc,
130
+ };
131
+ }
132
+ export async function handlePull(request, options) {
133
+ if (request.protocolVersion !== PROTOCOL_VERSION) {
134
+ throw new ProtocolError(400, `Unsupported protocolVersion ${String(request.protocolVersion)}; this server speaks ${PROTOCOL_VERSION}`);
135
+ }
136
+ const now = options.now ?? (() => Date.now());
137
+ const after = request.cursor ? Number(request.cursor) : 0;
138
+ if (Number.isNaN(after))
139
+ throw new ProtocolError(400, `Malformed cursor "${String(request.cursor)}"`);
140
+ const limit = Math.min(Math.max(request.limit ?? 100, 1), 1000);
141
+ const page = await options.store.listChanges(after, limit, request.collections);
142
+ const changes = page.map((d) => ({
143
+ collection: d.collection,
144
+ documentId: d.documentId,
145
+ document: d.deleted ? null : d.document,
146
+ version: d.version,
147
+ deleted: d.deleted,
148
+ updatedAt: d.updatedAt,
149
+ hlc: d.hlc,
150
+ }));
151
+ const last = page[page.length - 1];
152
+ const nextCursor = last ? String(last.seq) : (request.cursor ?? null);
153
+ return {
154
+ protocolVersion: PROTOCOL_VERSION,
155
+ serverTime: new Date(now()).toISOString(),
156
+ serverHlc: serverHlc(now()),
157
+ changes,
158
+ nextCursor,
159
+ hasMore: last ? await options.store.hasMoreAfter(last.seq, request.collections) : false,
160
+ };
161
+ }
162
+ //# 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"}
@@ -0,0 +1,13 @@
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 { createAnchorRouter } from "./adapters/express.js";
11
+ export { createAnchorRouteHandlers } from "./adapters/nextjs.js";
12
+ export { AnchorSyncService, createAnchorSyncService, ANCHOR_SYNC_SERVICE, NESTJS_CONTROLLER_EXAMPLE, } from "./adapters/nestjs.js";
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","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;AAQpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,25 @@
1
+ import type { ChangeResult } from "anchordb";
2
+ import type { ServerStore, StoredServerDocument } from "./types.js";
3
+ /**
4
+ * In-memory store — the default, so a backend runs with no database installed.
5
+ *
6
+ * This is the reference implementation of the `ServerStore` contract; `MongooseStore` is held to
7
+ * the same conformance suite. Suitable for development, tests and the demos, not for production.
8
+ */
9
+ export declare class MemoryStore implements ServerStore {
10
+ readonly name = "memory";
11
+ private readonly docs;
12
+ private readonly idempotency;
13
+ private seq;
14
+ private key;
15
+ get(collection: string, documentId: string): Promise<StoredServerDocument | null>;
16
+ put(doc: Omit<StoredServerDocument, "seq">): Promise<StoredServerDocument>;
17
+ listChanges(afterSeq: number, limit: number, collections?: string[]): Promise<StoredServerDocument[]>;
18
+ hasMoreAfter(seq: number, collections?: string[]): Promise<boolean>;
19
+ getIdempotent(clientId: string, mutationId: string): Promise<ChangeResult | null>;
20
+ setIdempotent(clientId: string, mutationId: string, result: ChangeResult): Promise<void>;
21
+ findByUniqueField(collection: string, field: string, value: unknown, exceptDocumentId: string): Promise<StoredServerDocument | null>;
22
+ all(collection?: string): StoredServerDocument[];
23
+ reset(): void;
24
+ }
25
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../src/store/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEpE;;;;;GAKG;AACH,qBAAa,WAAY,YAAW,WAAW;IAC7C,QAAQ,CAAC,IAAI,YAAY;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2C;IAChE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmC;IAC/D,OAAO,CAAC,GAAG,CAAK;IAEhB,OAAO,CAAC,GAAG;IAIL,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAIjF,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,oBAAoB,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAM1E,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAOrG,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAMnE,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAIjF,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxF,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,OAAO,EACd,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAUvC,GAAG,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAIhD,KAAK,IAAI,IAAI;CAKd"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * In-memory store — the default, so a backend runs with no database installed.
3
+ *
4
+ * This is the reference implementation of the `ServerStore` contract; `MongooseStore` is held to
5
+ * the same conformance suite. Suitable for development, tests and the demos, not for production.
6
+ */
7
+ export class MemoryStore {
8
+ constructor() {
9
+ this.name = "memory";
10
+ this.docs = new Map();
11
+ this.idempotency = new Map();
12
+ this.seq = 0;
13
+ }
14
+ key(collection, documentId) {
15
+ return `${collection}/${documentId}`;
16
+ }
17
+ async get(collection, documentId) {
18
+ return this.docs.get(this.key(collection, documentId)) ?? null;
19
+ }
20
+ async put(doc) {
21
+ const stored = { ...doc, seq: ++this.seq };
22
+ this.docs.set(this.key(doc.collection, doc.documentId), stored);
23
+ return stored;
24
+ }
25
+ async listChanges(afterSeq, limit, collections) {
26
+ return [...this.docs.values()]
27
+ .filter((d) => d.seq > afterSeq && (!collections || collections.includes(d.collection)))
28
+ .sort((a, b) => a.seq - b.seq)
29
+ .slice(0, limit);
30
+ }
31
+ async hasMoreAfter(seq, collections) {
32
+ return [...this.docs.values()].some((d) => d.seq > seq && (!collections || collections.includes(d.collection)));
33
+ }
34
+ async getIdempotent(clientId, mutationId) {
35
+ return this.idempotency.get(`${clientId}:${mutationId}`) ?? null;
36
+ }
37
+ async setIdempotent(clientId, mutationId, result) {
38
+ this.idempotency.set(`${clientId}:${mutationId}`, result);
39
+ }
40
+ async findByUniqueField(collection, field, value, exceptDocumentId) {
41
+ for (const doc of this.docs.values()) {
42
+ if (doc.collection !== collection || doc.deleted)
43
+ continue;
44
+ if (doc.documentId === exceptDocumentId)
45
+ continue;
46
+ if (doc.document[field] === value)
47
+ return doc;
48
+ }
49
+ return null;
50
+ }
51
+ // ---- helpers for tests and demos ----
52
+ all(collection) {
53
+ return [...this.docs.values()].filter((d) => !collection || d.collection === collection);
54
+ }
55
+ reset() {
56
+ this.docs.clear();
57
+ this.idempotency.clear();
58
+ this.seq = 0;
59
+ }
60
+ }
61
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../../src/store/memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import type { ChangeResult } from "anchordb";
2
+ import type { ServerStore, StoredServerDocument } from "./types.js";
3
+ /**
4
+ * MongoDB store, driven through Mongoose.
5
+ *
6
+ * `mongoose` is a *peer* dependency and is passed in rather than imported, for two reasons: the
7
+ * package must install cleanly for people using MemoryStore, and a backend must use its own single
8
+ * mongoose instance (two copies in one process is a well-known source of "MissingSchemaError" and
9
+ * duplicate connection bugs).
10
+ *
11
+ * ## The schemas
12
+ *
13
+ * `anchorSchemas()` returns plain definition objects rather than constructed schemas, so a backend
14
+ * can extend them, add its own indexes, or point them at different collection names. They are the
15
+ * shape the sync protocol needs; your *domain* collections keep whatever schema you already have.
16
+ */
17
+ export interface MongooseLike {
18
+ Schema: new (definition: unknown, options?: unknown) => unknown;
19
+ model: (name: string, schema?: unknown, collection?: string) => MongooseModel;
20
+ models: Record<string, MongooseModel | undefined>;
21
+ }
22
+ export interface MongooseModel {
23
+ findOne(filter: unknown): {
24
+ lean(): {
25
+ exec(): Promise<Record<string, unknown> | null>;
26
+ };
27
+ };
28
+ find(filter: unknown): {
29
+ sort(spec: unknown): {
30
+ limit(n: number): {
31
+ lean(): {
32
+ exec(): Promise<Array<Record<string, unknown>>>;
33
+ };
34
+ };
35
+ lean(): {
36
+ exec(): Promise<Array<Record<string, unknown>>>;
37
+ };
38
+ };
39
+ };
40
+ findOneAndUpdate(filter: unknown, update: unknown, options: unknown): {
41
+ lean(): {
42
+ exec(): Promise<unknown>;
43
+ };
44
+ };
45
+ countDocuments(filter: unknown): {
46
+ exec(): Promise<number>;
47
+ };
48
+ create(doc: unknown): Promise<unknown>;
49
+ }
50
+ /**
51
+ * Schema definitions for the two collections the protocol needs.
52
+ *
53
+ * Timestamps are `Date`, which Mongoose casts from the ISO-8601 UTC strings the client sends — one
54
+ * of the reasons the wire format uses strings rather than a custom encoding.
55
+ */
56
+ export declare function anchorSchemas(): {
57
+ document: Record<string, unknown>;
58
+ idempotency: Record<string, unknown>;
59
+ };
60
+ export interface MongooseStoreOptions {
61
+ mongoose: MongooseLike;
62
+ documentModelName?: string;
63
+ idempotencyModelName?: string;
64
+ /** Collection names, if you want them to differ from the model names. */
65
+ documentCollection?: string;
66
+ idempotencyCollection?: string;
67
+ }
68
+ export declare class MongooseStore implements ServerStore {
69
+ readonly name = "mongoose";
70
+ private readonly documents;
71
+ private readonly idempotency;
72
+ private readonly counters;
73
+ constructor(options: MongooseStoreOptions);
74
+ get(collection: string, documentId: string): Promise<StoredServerDocument | null>;
75
+ /**
76
+ * Allocate the next sequence number atomically.
77
+ *
78
+ * `findOneAndUpdate` with `$inc` is a single atomic operation in MongoDB, so two concurrent
79
+ * pushes cannot receive the same seq. Reading-then-writing a counter would race, and duplicate
80
+ * sequence numbers would make pull cursors skip changes.
81
+ */
82
+ private nextSeq;
83
+ put(doc: Omit<StoredServerDocument, "seq">): Promise<StoredServerDocument>;
84
+ listChanges(afterSeq: number, limit: number, collections?: string[]): Promise<StoredServerDocument[]>;
85
+ hasMoreAfter(seq: number, collections?: string[]): Promise<boolean>;
86
+ getIdempotent(clientId: string, mutationId: string): Promise<ChangeResult | null>;
87
+ setIdempotent(clientId: string, mutationId: string, result: ChangeResult): Promise<void>;
88
+ findByUniqueField(collection: string, field: string, value: unknown, exceptDocumentId: string): Promise<StoredServerDocument | null>;
89
+ }
90
+ //# sourceMappingURL=mongoose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mongoose.d.ts","sourceRoot":"","sources":["../../../src/store/mongoose.ts"],"names":[],"mappings":"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"}