cosveti-sync 0.0.1

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.
@@ -0,0 +1,126 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import * as collab from '@tiptap/pm/collab';
3
+ import { Step } from '@tiptap/pm/transform';
4
+ import { doSync } from './doSync.js';
5
+ import { SNAPSHOT_DEBOUNCE_MS } from './index.js';
6
+ import { get } from 'svelte/store';
7
+ export function syncExtension(convex, id, syncApi, initialState, opts, isSyncEnabled) {
8
+ const log = opts?.debug ? console.debug : () => { };
9
+ let snapshotTimer;
10
+ const trySubmitSnapshot = (version, content) => {
11
+ if (snapshotTimer) {
12
+ clearTimeout(snapshotTimer);
13
+ }
14
+ snapshotTimer = setTimeout(() => {
15
+ void convex
16
+ .mutation(syncApi.submitSnapshot, { id, version, content })
17
+ .catch(opts?.onSyncError); // Changed to string
18
+ }, opts?.snapshotDebounceMs ?? SNAPSHOT_DEBOUNCE_MS);
19
+ };
20
+ let active = false;
21
+ let pending;
22
+ // let watch: Watch<number | null> | undefined; // REACT CODE
23
+ let serverVersion = undefined;
24
+ let unsubscribe; // Changed to use onUpdate from convex/browser instead of convex/react
25
+ async function trySync(editor) {
26
+ // console.log('Called trySync');
27
+ if (serverVersion === undefined) {
28
+ return;
29
+ }
30
+ if (serverVersion && serverVersion > collab.getVersion(editor.state)) {
31
+ clearTimeout(snapshotTimer);
32
+ snapshotTimer = undefined;
33
+ }
34
+ if (active) {
35
+ if (!pending) {
36
+ let resolve = () => { };
37
+ let reject = () => { };
38
+ const promise = new Promise((res, rej) => {
39
+ resolve = res;
40
+ reject = rej;
41
+ });
42
+ pending = { resolve, reject, promise };
43
+ }
44
+ return pending.promise;
45
+ }
46
+ active = true;
47
+ try {
48
+ if (await doSync(editor, convex, syncApi, id, serverVersion, initialState, opts?.debug)) {
49
+ const version = collab.getVersion(editor.state);
50
+ const content = JSON.stringify(editor.state.doc.toJSON());
51
+ localStorage.setItem(`convex-sync-${id}`, JSON.stringify({
52
+ content: editor.getJSON(),
53
+ version: collab.getVersion(editor.state)
54
+ }));
55
+ if (collab.sendableSteps(editor.state)) {
56
+ throw new Error('Synced but still have sendable steps');
57
+ }
58
+ trySubmitSnapshot(version, content);
59
+ }
60
+ }
61
+ catch (error) {
62
+ if (opts?.onSyncError) {
63
+ opts.onSyncError(error);
64
+ }
65
+ else {
66
+ throw error;
67
+ }
68
+ }
69
+ finally {
70
+ active = false;
71
+ if (pending) {
72
+ const { resolve, reject } = pending;
73
+ pending = undefined;
74
+ trySync(editor).then(resolve, reject);
75
+ }
76
+ }
77
+ }
78
+ return Extension.create({
79
+ name: 'convex-sync',
80
+ onDestroy() {
81
+ log('destroying');
82
+ unsubscribe?.();
83
+ },
84
+ onCreate() {
85
+ if (initialState.restoredSteps?.length) {
86
+ // TODO: verify that restoring local steps works
87
+ console.log('Restoring local steps', initialState.restoredSteps);
88
+ const tr = this.editor.state.tr;
89
+ for (const step of initialState.restoredSteps) {
90
+ tr.step(Step.fromJSON(this.editor.schema, step));
91
+ }
92
+ this.editor.view.dispatch(tr);
93
+ }
94
+ unsubscribe = convex.onUpdate(syncApi.latestVersion, { id }, (newVersion) => {
95
+ // 🔑 CRITICAL: Skip *processing* updates when sync is OFF
96
+ // Subscription remains ACTIVE (Convex still tracks changes)
97
+ if (isSyncEnabled && get(isSyncEnabled) == false) {
98
+ console.log('Sync paused: Ignoring incoming version update (still tracking)');
99
+ return;
100
+ }
101
+ console.log('New version received.');
102
+ serverVersion = newVersion;
103
+ void trySync(this.editor);
104
+ }, opts?.onSyncError);
105
+ // void trySync(this.editor);
106
+ },
107
+ onUpdate() {
108
+ // 🔑 SKIP MUTATION IF SYNC IS OFF (zero impact on incoming updates)
109
+ if (isSyncEnabled && get(isSyncEnabled) == false) {
110
+ console.log('Sync enabled: ' + get(isSyncEnabled));
111
+ return; // <-- Single-line guard
112
+ }
113
+ void trySync(this.editor);
114
+ },
115
+ addProseMirrorPlugins() {
116
+ log('Adding collab plugin', {
117
+ version: initialState.initialVersion
118
+ });
119
+ return [
120
+ collab.collab({
121
+ version: initialState.initialVersion
122
+ })
123
+ ];
124
+ }
125
+ });
126
+ }
@@ -0,0 +1,19 @@
1
+ import type { Content } from '@tiptap/core';
2
+ import type { SyncApi } from '../client/index.js';
3
+ import type { ConvexClient } from 'convex/browser';
4
+ export type UseSyncOptions = {
5
+ onSyncError?: (error: Error) => void;
6
+ snapshotDebounceMs?: number;
7
+ debug?: boolean;
8
+ };
9
+ export type InitialState = {
10
+ initialContent: Content | null;
11
+ initialVersion?: number;
12
+ restoredSteps?: object[];
13
+ };
14
+ export type SyncContext = {
15
+ convex: ConvexClient;
16
+ id: string;
17
+ syncApi: SyncApi;
18
+ opts?: UseSyncOptions;
19
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "cosveti-sync",
3
+ "description": "A convex component for syncing tiptap in a svelte project",
4
+ "version": "0.0.1",
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build && npm run prepack",
8
+ "preview": "vite preview",
9
+ "prepare": "svelte-kit sync || echo ''",
10
+ "prepack": "svelte-kit sync && svelte-package && publint",
11
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13
+ "lint": "prettier --check . && eslint .",
14
+ "test:unit": "vitest",
15
+ "test": "npm run test:unit -- --run",
16
+ "format": "prettier --write ."
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "!dist/**/*.test.*",
21
+ "!dist/**/*.spec.*"
22
+ ],
23
+ "sideEffects": [
24
+ "**/*.css"
25
+ ],
26
+ "svelte": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "type": "module",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "svelte": "./dist/index.js"
33
+ }
34
+ },
35
+ "peerDependencies": {
36
+ "@tiptap/pm": "^3.18.0",
37
+ "@tiptap/core": "^3.18.0",
38
+ "convex": "^1.10.0",
39
+ "svelte": "^5.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "convex-svelte": "^0.0.12",
43
+ "@eslint/compat": "^2.0.1",
44
+ "@eslint/js": "^9.39.2",
45
+ "@sveltejs/adapter-auto": "^7.0.0",
46
+ "@sveltejs/kit": "^2.50.1",
47
+ "@sveltejs/package": "^2.5.7",
48
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
49
+ "@types/node": "^22",
50
+ "@vitest/browser-playwright": "^4.0.18",
51
+ "eslint": "^9.39.2",
52
+ "eslint-config-prettier": "^10.1.8",
53
+ "eslint-plugin-svelte": "^3.14.0",
54
+ "globals": "^17.1.0",
55
+ "playwright": "^1.58.0",
56
+ "prettier": "^3.8.1",
57
+ "prettier-plugin-svelte": "^3.4.1",
58
+ "publint": "^0.3.17",
59
+ "svelte": "^5.48.2",
60
+ "svelte-check": "^4.3.5",
61
+ "typescript": "^5.9.3",
62
+ "typescript-eslint": "^8.53.1",
63
+ "vite": "^7.3.1",
64
+ "vitest": "^4.0.18",
65
+ "vitest-browser-svelte": "^2.0.2"
66
+ },
67
+ "keywords": [
68
+ "svelte",
69
+ "tiptap",
70
+ "convex",
71
+ "collab",
72
+ "prosemirror",
73
+ "sync",
74
+ "editor"
75
+ ],
76
+ "dependencies": {
77
+ "@tiptap/extension-bubble-menu": "^3.18.0",
78
+ "@tiptap/starter-kit": "^3.18.0"
79
+ },
80
+ "license": "Apache-2.0"
81
+ }