@regular-software/sync-hono 0.3.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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @regular-software/sync-hono
2
+
3
+ Hono adapter for regular software sync servers.
4
+
5
+ This is a 0.x package; APIs may change between minor versions.
@@ -0,0 +1,24 @@
1
+ import { Hono, type Context } from "hono";
2
+ import type { SyncRequest, SyncResult } from "@regular-software/sync-protocol";
3
+ export type SyncHonoEngine<TContext = undefined> = {
4
+ syncSince(request: SyncRequest, context: TContext): SyncResult | Promise<SyncResult>;
5
+ getVersion?(): number;
6
+ };
7
+ export type SyncContextResolver<TContext> = (context: Context) => TContext | Promise<TContext>;
8
+ export type SyncEventListener = (version: number) => void;
9
+ export declare class SyncEventHub {
10
+ private readonly listeners;
11
+ private lastVersion;
12
+ subscribe(listener: SyncEventListener): () => void;
13
+ publish(version: number): void;
14
+ }
15
+ export declare function createSyncHono<TContext = undefined>(options: {
16
+ engine: SyncHonoEngine<TContext>;
17
+ getContext?: SyncContextResolver<TContext>;
18
+ events?: SyncEventHub;
19
+ pollIntervalMs?: number;
20
+ }): {
21
+ app: Hono;
22
+ events: SyncEventHub;
23
+ stop(): void;
24
+ };
package/dist/index.js ADDED
@@ -0,0 +1,115 @@
1
+ // packages/hono/src/index.ts
2
+ import { Hono } from "hono";
3
+ var SyncEventHub = class {
4
+ listeners = /* @__PURE__ */ new Set();
5
+ lastVersion = 0;
6
+ subscribe(listener) {
7
+ this.listeners.add(listener);
8
+ return () => this.listeners.delete(listener);
9
+ }
10
+ publish(version) {
11
+ if (version <= this.lastVersion) {
12
+ return;
13
+ }
14
+ this.lastVersion = version;
15
+ for (const listener of this.listeners) {
16
+ listener(version);
17
+ }
18
+ }
19
+ };
20
+ function createSyncHono(options) {
21
+ const events = options.events ?? new SyncEventHub();
22
+ const app = new Hono();
23
+ app.get("/sync", async (context) => {
24
+ const version = Number(context.req.query("version"));
25
+ const replicaId = context.req.query("replicaId");
26
+ const schemaVersion = Number(context.req.query("schemaVersion"));
27
+ const schemaFingerprint = context.req.query("schemaFingerprint");
28
+ if (!Number.isSafeInteger(version) || version < 0 || !Number.isSafeInteger(schemaVersion) || schemaVersion <= 0 || schemaFingerprint === void 0) {
29
+ return context.json({ error: "Invalid synchronization request" }, 400);
30
+ }
31
+ try {
32
+ const resolvedContext = options.getContext ? await options.getContext(context) : void 0;
33
+ return context.json(
34
+ await options.engine.syncSince(
35
+ {
36
+ version,
37
+ ...replicaId ? { replicaId } : {},
38
+ schemaVersion,
39
+ schemaFingerprint
40
+ },
41
+ resolvedContext
42
+ )
43
+ );
44
+ } catch (error) {
45
+ if (error instanceof Error && error.name === "SyncSchemaMismatchError") {
46
+ const expected = error.expected;
47
+ return context.json({ error: error.message, expected }, 409);
48
+ }
49
+ throw error;
50
+ }
51
+ });
52
+ app.get("/sync-events", (context) => {
53
+ const encoder = new TextEncoder();
54
+ let closed = false;
55
+ let unsubscribe = () => {
56
+ };
57
+ let heartbeat;
58
+ let controller;
59
+ const close = () => {
60
+ if (closed) {
61
+ return;
62
+ }
63
+ closed = true;
64
+ unsubscribe();
65
+ if (heartbeat) {
66
+ clearInterval(heartbeat);
67
+ }
68
+ controller?.close();
69
+ };
70
+ const stream = new ReadableStream({
71
+ start(nextController) {
72
+ controller = nextController;
73
+ unsubscribe = events.subscribe((version) => {
74
+ if (!closed) {
75
+ controller?.enqueue(encoder.encode(`data: ${version}
76
+
77
+ `));
78
+ }
79
+ });
80
+ heartbeat = setInterval(() => {
81
+ if (!closed) {
82
+ controller?.enqueue(encoder.encode(": heartbeat\n\n"));
83
+ }
84
+ }, 3e4);
85
+ context.req.raw.signal.addEventListener("abort", close, { once: true });
86
+ },
87
+ cancel: close
88
+ });
89
+ return new Response(stream, {
90
+ headers: {
91
+ "Cache-Control": "no-cache",
92
+ Connection: "keep-alive",
93
+ "Content-Type": "text/event-stream"
94
+ }
95
+ });
96
+ });
97
+ const pollIntervalMs = options.pollIntervalMs ?? 1e3;
98
+ const poll = options.engine.getVersion && pollIntervalMs > 0 ? setInterval(() => {
99
+ events.publish(options.engine.getVersion?.() ?? 0);
100
+ }, pollIntervalMs) : void 0;
101
+ poll?.unref?.();
102
+ return {
103
+ app,
104
+ events,
105
+ stop() {
106
+ if (poll) {
107
+ clearInterval(poll);
108
+ }
109
+ }
110
+ };
111
+ }
112
+ export {
113
+ SyncEventHub,
114
+ createSyncHono
115
+ };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@regular-software/sync-hono",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "Hono adapter for regular software sync servers.",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": ["dist", "README.md"],
18
+ "repository": {"type": "git", "url": "https://github.com/regular-software/sync.git", "directory": "packages/hono"},
19
+ "bugs": {"url": "https://github.com/regular-software/sync/issues"},
20
+ "homepage": "https://github.com/regular-software/sync#readme",
21
+ "engines": {"node": ">=20"},
22
+ "publishConfig": {"access": "public"},
23
+ "dependencies": {
24
+ "@regular-software/sync-protocol": "0.3.0",
25
+ "hono": "^4.13.5"
26
+ }
27
+ }