@doubling/compound-sync 1.12.3 → 1.12.5
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 +17 -0
- package/auth-persistence.d.ts +22 -0
- package/auth-persistence.js +79 -83
- package/config.d.ts +13 -0
- package/config.js +55 -54
- package/files.d.ts +221 -0
- package/files.js +373 -438
- package/folder-chain.d.ts +44 -0
- package/folder-chain.js +52 -59
- package/manifest.d.ts +38 -0
- package/manifest.js +125 -0
- package/org-sync.d.ts +16 -0
- package/org-sync.js +1196 -972
- package/package.json +26 -3
- package/paths.d.ts +1 -1
- package/pre-mint-app-check.d.ts +6 -0
- package/pre-mint-app-check.js +46 -39
- package/push-outcome.d.ts +9 -0
- package/push-outcome.js +11 -0
- package/setup-auth-with-pre-mint.d.ts +24 -0
- package/setup-auth-with-pre-mint.js +48 -55
- package/storage-helpers.d.ts +14 -0
- package/storage-helpers.js +43 -107
- package/sync-state.d.ts +4 -0
- package/sync-state.js +30 -16
- package/sync.d.ts +18 -0
- package/sync.js +464 -515
- package/team-registry.d.ts +41 -0
- package/team-registry.js +91 -70
- package/yjs-binding-state.d.ts +4 -0
- package/yjs-binding-state.js +25 -22
- package/yjs-file-binding.d.ts +32 -0
- package/yjs-file-binding.js +217 -204
- package/yjs-firestore-update-store.d.ts +13 -0
- package/yjs-firestore-update-store.js +99 -103
- package/yjs-provider.d.ts +43 -0
- package/yjs-provider.js +79 -75
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface TeamData {
|
|
2
|
+
name?: string;
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
export type TeamCallback = (teamId: string, teamData: TeamData) => void;
|
|
6
|
+
export interface TeamRegistryOptions {
|
|
7
|
+
onSetup: TeamCallback;
|
|
8
|
+
onTeardown: TeamCallback;
|
|
9
|
+
}
|
|
10
|
+
export type TeamDocChange = {
|
|
11
|
+
type: 'added';
|
|
12
|
+
id: string;
|
|
13
|
+
data: TeamData;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'modified';
|
|
16
|
+
id: string;
|
|
17
|
+
data: TeamData;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'removed';
|
|
20
|
+
id: string;
|
|
21
|
+
};
|
|
22
|
+
export declare class TeamRegistry {
|
|
23
|
+
private readonly teams;
|
|
24
|
+
private readonly teamIdsByName;
|
|
25
|
+
private readonly onSetup;
|
|
26
|
+
private readonly onTeardown;
|
|
27
|
+
constructor({ onSetup, onTeardown }: TeamRegistryOptions);
|
|
28
|
+
bootstrap(teamId: string, teamData: TeamData): boolean;
|
|
29
|
+
add(teamId: string, teamData: TeamData): boolean;
|
|
30
|
+
modify(teamId: string, teamData: TeamData): boolean;
|
|
31
|
+
remove(teamId: string): boolean;
|
|
32
|
+
applyChange(change: TeamDocChange): boolean;
|
|
33
|
+
has(teamId: string): boolean;
|
|
34
|
+
get(teamId: string): TeamData | undefined;
|
|
35
|
+
getIdByName(teamName: string): string | undefined;
|
|
36
|
+
size(): number;
|
|
37
|
+
entries(): IterableIterator<[string, TeamData]>;
|
|
38
|
+
names(): string[];
|
|
39
|
+
teamIdsByNameMap(): Map<string, string>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=team-registry.d.ts.map
|
package/team-registry.js
CHANGED
|
@@ -17,78 +17,99 @@
|
|
|
17
17
|
// });
|
|
18
18
|
// // For each docChange from onSnapshot on the teams collection:
|
|
19
19
|
// registry.applyChange({ type: 'added', id, data });
|
|
20
|
-
|
|
21
20
|
export class TeamRegistry {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Idempotently register a team. Safe to call with the same team
|
|
30
|
-
// multiple times; only the first call triggers onSetup.
|
|
31
|
-
add(teamId, teamData) {
|
|
32
|
-
if (this.teams.has(teamId)) return false;
|
|
33
|
-
this.teams.set(teamId, teamData);
|
|
34
|
-
if (teamData && teamData.name) {
|
|
35
|
-
this.teamIdsByName.set(teamData.name, teamId);
|
|
21
|
+
teams = new Map();
|
|
22
|
+
teamIdsByName = new Map();
|
|
23
|
+
onSetup;
|
|
24
|
+
onTeardown;
|
|
25
|
+
constructor({ onSetup, onTeardown }) {
|
|
26
|
+
this.onSetup = onSetup;
|
|
27
|
+
this.onTeardown = onTeardown;
|
|
36
28
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const prev = this.teams.get(teamId);
|
|
49
|
-
if (!prev) {
|
|
50
|
-
// Treat modify-of-unknown as an add. Firestore emits both
|
|
51
|
-
// 'added' and 'modified' for the same doc under certain
|
|
52
|
-
// listener-restart scenarios, so we want this path resilient.
|
|
53
|
-
return this.add(teamId, teamData);
|
|
29
|
+
// Register a team in-memory without wiring listeners. Used during
|
|
30
|
+
// startup so manifest reconcile can resolve team-scoped paths before
|
|
31
|
+
// any file onSnapshot handlers run (DOU-55 ordering).
|
|
32
|
+
bootstrap(teamId, teamData) {
|
|
33
|
+
if (this.teams.has(teamId))
|
|
34
|
+
return false;
|
|
35
|
+
this.teams.set(teamId, teamData);
|
|
36
|
+
if (teamData && teamData.name) {
|
|
37
|
+
this.teamIdsByName.set(teamData.name, teamId);
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
54
40
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
// Idempotently register a team. Safe to call with the same team
|
|
42
|
+
// multiple times; only the first call triggers onSetup.
|
|
43
|
+
add(teamId, teamData) {
|
|
44
|
+
if (this.teams.has(teamId))
|
|
45
|
+
return false;
|
|
46
|
+
this.teams.set(teamId, teamData);
|
|
47
|
+
if (teamData && teamData.name) {
|
|
48
|
+
this.teamIdsByName.set(teamData.name, teamId);
|
|
49
|
+
}
|
|
50
|
+
this.onSetup(teamId, teamData);
|
|
51
|
+
return true;
|
|
64
52
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
53
|
+
// Update a team's metadata. If the team's name changed, treat as a
|
|
54
|
+
// rename: teardown the old listener, register a new one (which
|
|
55
|
+
// creates a new local folder under the new name). The old folder
|
|
56
|
+
// is intentionally NOT renamed or removed; the user can clean it
|
|
57
|
+
// up manually. This is conservative; a rename-aware implementation
|
|
58
|
+
// is a follow-up.
|
|
59
|
+
modify(teamId, teamData) {
|
|
60
|
+
const prev = this.teams.get(teamId);
|
|
61
|
+
if (!prev) {
|
|
62
|
+
// Treat modify-of-unknown as an add. Firestore emits both
|
|
63
|
+
// 'added' and 'modified' for the same doc under certain
|
|
64
|
+
// listener-restart scenarios, so we want this path resilient.
|
|
65
|
+
return this.add(teamId, teamData);
|
|
66
|
+
}
|
|
67
|
+
this.teams.set(teamId, teamData);
|
|
68
|
+
const renamed = prev.name !== (teamData && teamData.name);
|
|
69
|
+
if (renamed) {
|
|
70
|
+
if (prev.name)
|
|
71
|
+
this.teamIdsByName.delete(prev.name);
|
|
72
|
+
if (teamData && teamData.name) {
|
|
73
|
+
this.teamIdsByName.set(teamData.name, teamId);
|
|
74
|
+
}
|
|
75
|
+
this.onTeardown(teamId, prev);
|
|
76
|
+
this.onSetup(teamId, teamData);
|
|
77
|
+
}
|
|
78
|
+
return renamed;
|
|
79
|
+
}
|
|
80
|
+
// Remove a team (e.g., user lost membership). Tears down the
|
|
81
|
+
// listener; leaves any local folder alone.
|
|
82
|
+
remove(teamId) {
|
|
83
|
+
const prev = this.teams.get(teamId);
|
|
84
|
+
if (!prev)
|
|
85
|
+
return false;
|
|
86
|
+
this.teams.delete(teamId);
|
|
87
|
+
if (prev.name)
|
|
88
|
+
this.teamIdsByName.delete(prev.name);
|
|
89
|
+
this.onTeardown(teamId, prev);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
// Convenience: dispatch a Firestore docChange into add/modify/remove.
|
|
93
|
+
applyChange(change) {
|
|
94
|
+
if (change.type === 'added')
|
|
95
|
+
return this.add(change.id, change.data);
|
|
96
|
+
if (change.type === 'modified')
|
|
97
|
+
return this.modify(change.id, change.data);
|
|
98
|
+
return this.remove(change.id);
|
|
99
|
+
}
|
|
100
|
+
// Read-only accessors for the daemon's existing call sites.
|
|
101
|
+
has(teamId) { return this.teams.has(teamId); }
|
|
102
|
+
get(teamId) { return this.teams.get(teamId); }
|
|
103
|
+
getIdByName(teamName) { return this.teamIdsByName.get(teamName); }
|
|
104
|
+
size() { return this.teams.size; }
|
|
105
|
+
entries() { return this.teams.entries(); }
|
|
106
|
+
names() {
|
|
107
|
+
return [...this.teams.values()].map((t) => t.name).filter((n) => Boolean(n));
|
|
108
|
+
}
|
|
109
|
+
// The team-name -> team-id map the daemon hands to scopeFromLocalPath
|
|
110
|
+
// (which takes a Map<teamName, teamId> for routing local paths back
|
|
111
|
+
// to their owning team). Exposed as the live map, not a copy, so
|
|
112
|
+
// updates propagate without a re-publish step.
|
|
113
|
+
teamIdsByNameMap() { return this.teamIdsByName; }
|
|
94
114
|
}
|
|
115
|
+
//# sourceMappingURL=team-registry.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function loadBindingState(localPath: string, fileId: string): Uint8Array | null;
|
|
2
|
+
export declare function saveBindingState(localPath: string, fileId: string, encoded: Uint8Array): void;
|
|
3
|
+
export declare function deleteBindingState(localPath: string, fileId: string): void;
|
|
4
|
+
//# sourceMappingURL=yjs-binding-state.d.ts.map
|
package/yjs-binding-state.js
CHANGED
|
@@ -15,47 +15,50 @@
|
|
|
15
15
|
// `<localPath>/.compound-yjs-binding/<fileId>.yjs`. The binary blob is
|
|
16
16
|
// the output of Y.encodeStateAsUpdate(doc); the daemon applies it via
|
|
17
17
|
// Y.applyUpdate(doc, blob) on restore.
|
|
18
|
-
|
|
19
18
|
import fs from 'node:fs';
|
|
20
19
|
import path from 'node:path';
|
|
21
|
-
|
|
22
20
|
const STATE_DIR = '.compound-yjs-binding';
|
|
23
|
-
|
|
24
21
|
function statePathFor(localPath, fileId) {
|
|
25
|
-
|
|
22
|
+
return path.join(localPath, STATE_DIR, `${fileId}.yjs`);
|
|
26
23
|
}
|
|
27
|
-
|
|
28
24
|
// Load the encoded Y.Doc state for a file. Returns null if no state
|
|
29
25
|
// exists yet (first run, or this fileId hasn't been synced before) or
|
|
30
26
|
// if the file is unreadable for any reason (corruption, permissions).
|
|
31
27
|
// Treating unreadable as "no state" is safe: the binding falls back to
|
|
32
28
|
// its first-run behavior, which writes ytext to disk on initial flush.
|
|
33
29
|
export function loadBindingState(localPath, fileId) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
try {
|
|
31
|
+
return fs.readFileSync(statePathFor(localPath, fileId));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
39
36
|
}
|
|
40
|
-
|
|
41
37
|
// Persist the encoded Y.Doc state for a file. Atomic via write-to-tmp +
|
|
42
38
|
// rename so a crash mid-write can't leave a half-written .yjs file that
|
|
43
39
|
// would corrupt the next startup. Best-effort: a failed save costs us a
|
|
44
40
|
// cold-start reconciliation next launch, never data.
|
|
45
41
|
export function saveBindingState(localPath, fileId, encoded) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
const finalPath = statePathFor(localPath, fileId);
|
|
43
|
+
const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
fs.mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
46
|
+
fs.writeFileSync(tmpPath, encoded);
|
|
47
|
+
fs.renameSync(tmpPath, finalPath);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
try {
|
|
51
|
+
fs.unlinkSync(tmpPath);
|
|
52
|
+
}
|
|
53
|
+
catch { /* ignore */ }
|
|
54
|
+
}
|
|
55
55
|
}
|
|
56
|
-
|
|
57
56
|
// Best-effort cleanup when a file is deleted from the workspace, so
|
|
58
57
|
// stale .yjs blobs don't accumulate. Failing to delete is not fatal.
|
|
59
58
|
export function deleteBindingState(localPath, fileId) {
|
|
60
|
-
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(statePathFor(localPath, fileId));
|
|
61
|
+
}
|
|
62
|
+
catch { /* ignore */ }
|
|
61
63
|
}
|
|
64
|
+
//# sourceMappingURL=yjs-binding-state.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import { type YjsUpdateStore } from './yjs-provider.js';
|
|
3
|
+
export interface YjsFileBindingOptions {
|
|
4
|
+
debounceMs?: number;
|
|
5
|
+
onDiskWrite: (text: string) => void;
|
|
6
|
+
onDocStateChange?: ((encoded: Uint8Array) => void) | null;
|
|
7
|
+
initialDocState?: Uint8Array | null;
|
|
8
|
+
currentDiskText?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export declare class YjsFileBinding {
|
|
11
|
+
readonly doc: Y.Doc;
|
|
12
|
+
readonly ytext: Y.Text;
|
|
13
|
+
private readonly store;
|
|
14
|
+
private readonly provider;
|
|
15
|
+
private readonly debounceMs;
|
|
16
|
+
private readonly onDiskWrite;
|
|
17
|
+
private readonly onDocStateChange;
|
|
18
|
+
private readonly initialDocState;
|
|
19
|
+
private readonly currentDiskText;
|
|
20
|
+
private flushTimer;
|
|
21
|
+
private lastDiskText;
|
|
22
|
+
private observer;
|
|
23
|
+
private stopped;
|
|
24
|
+
constructor(store: YjsUpdateStore, { debounceMs, onDiskWrite, onDocStateChange, initialDocState, currentDiskText, }: YjsFileBindingOptions);
|
|
25
|
+
start(): Promise<void>;
|
|
26
|
+
private _applyTextDiff;
|
|
27
|
+
private _emitDocState;
|
|
28
|
+
private _flushNow;
|
|
29
|
+
applyDiskText(newText: string): void;
|
|
30
|
+
stop(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=yjs-file-binding.d.ts.map
|