sh3-core 0.8.2 → 0.9.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/dist/api.d.ts +3 -6
- package/dist/api.js +1 -3
- package/dist/apps/types.d.ts +3 -5
- package/dist/documents/backends.d.ts +2 -0
- package/dist/documents/backends.js +6 -0
- package/dist/documents/handle.js +13 -5
- package/dist/documents/handle.test.js +55 -0
- package/dist/documents/http-backend.d.ts +11 -4
- package/dist/documents/http-backend.js +37 -11
- package/dist/documents/index.d.ts +2 -1
- package/dist/documents/index.js +1 -1
- package/dist/documents/sync-types.d.ts +45 -0
- package/dist/documents/sync-types.js +11 -0
- package/dist/documents/types.d.ts +40 -2
- package/dist/documents/types.js +3 -2
- package/dist/keys/ConsentDialog.svelte +4 -4
- package/dist/keys/consent.test.js +4 -3
- package/dist/keys/types.d.ts +4 -2
- package/dist/server-shard/types.d.ts +55 -8
- package/dist/shards/activate.svelte.js +4 -29
- package/dist/shards/types.d.ts +0 -15
- package/dist/shell/views/KeysAndPeers.svelte +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -10
- package/dist/documents/journal-hook.d.ts +0 -6
- package/dist/documents/journal-hook.js +0 -16
- package/dist/documents/sync/activate-integration.test.d.ts +0 -1
- package/dist/documents/sync/activate-integration.test.js +0 -37
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte +0 -99
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte.d.ts +0 -15
- package/dist/documents/sync/components/SyncGrantPicker.svelte +0 -70
- package/dist/documents/sync/components/SyncGrantPicker.svelte.d.ts +0 -12
- package/dist/documents/sync/conflicts.d.ts +0 -30
- package/dist/documents/sync/conflicts.js +0 -77
- package/dist/documents/sync/conflicts.test.d.ts +0 -1
- package/dist/documents/sync/conflicts.test.js +0 -71
- package/dist/documents/sync/engine.d.ts +0 -19
- package/dist/documents/sync/engine.js +0 -188
- package/dist/documents/sync/engine.test.d.ts +0 -1
- package/dist/documents/sync/engine.test.js +0 -169
- package/dist/documents/sync/handle.d.ts +0 -11
- package/dist/documents/sync/handle.js +0 -79
- package/dist/documents/sync/handle.test.js +0 -56
- package/dist/documents/sync/hash.d.ts +0 -1
- package/dist/documents/sync/hash.js +0 -13
- package/dist/documents/sync/hash.test.d.ts +0 -1
- package/dist/documents/sync/hash.test.js +0 -20
- package/dist/documents/sync/index.d.ts +0 -5
- package/dist/documents/sync/index.js +0 -10
- package/dist/documents/sync/journal.d.ts +0 -30
- package/dist/documents/sync/journal.js +0 -179
- package/dist/documents/sync/journal.test.d.ts +0 -1
- package/dist/documents/sync/journal.test.js +0 -87
- package/dist/documents/sync/observer.d.ts +0 -3
- package/dist/documents/sync/observer.js +0 -45
- package/dist/documents/sync/registry.d.ts +0 -13
- package/dist/documents/sync/registry.js +0 -73
- package/dist/documents/sync/registry.test.d.ts +0 -1
- package/dist/documents/sync/registry.test.js +0 -53
- package/dist/documents/sync/serialization.d.ts +0 -5
- package/dist/documents/sync/serialization.js +0 -24
- package/dist/documents/sync/serialization.test.d.ts +0 -1
- package/dist/documents/sync/serialization.test.js +0 -26
- package/dist/documents/sync/singleton.d.ts +0 -11
- package/dist/documents/sync/singleton.js +0 -26
- package/dist/documents/sync/tombstones.d.ts +0 -19
- package/dist/documents/sync/tombstones.js +0 -58
- package/dist/documents/sync/tombstones.test.d.ts +0 -1
- package/dist/documents/sync/tombstones.test.js +0 -37
- package/dist/documents/sync/types.d.ts +0 -116
- package/dist/documents/sync/types.js +0 -27
- package/dist/documents/sync/write-hook.test.d.ts +0 -1
- package/dist/documents/sync/write-hook.test.js +0 -36
- package/dist/server-sync.d.ts +0 -6
- package/dist/server-sync.js +0 -634
- package/dist/server-sync.js.map +0 -7
- package/dist/shards/activate-sync-registry.test.d.ts +0 -1
- package/dist/shards/activate-sync-registry.test.js +0 -42
- package/dist/testing.d.ts +0 -3
- package/dist/testing.js +0 -77
- package/dist/testing.js.map +0 -7
- /package/dist/documents/{sync/handle.test.d.ts → handle.test.d.ts} +0 -0
|
@@ -23,12 +23,8 @@ import { fetchEnvState, putEnvState } from '../env/client';
|
|
|
23
23
|
import { isAdmin as checkIsAdmin } from '../auth/index';
|
|
24
24
|
import { createZoneManager } from '../state/manage';
|
|
25
25
|
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
26
|
-
import { PERMISSION_DOCUMENTS_SYNC } from '../documents/sync/types';
|
|
27
26
|
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
28
27
|
import { createBrowseCapability } from '../documents/browse';
|
|
29
|
-
import { getSyncBundle } from '../documents/sync/singleton';
|
|
30
|
-
import { createSyncHandle } from '../documents/sync/handle';
|
|
31
|
-
import { createSyncRegistryAccessor } from '../documents/sync/observer';
|
|
32
28
|
import { createShardKeysApi } from '../keys/client';
|
|
33
29
|
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
34
30
|
import { subscribe } from '../keys/revocation-bus.svelte';
|
|
@@ -74,7 +70,7 @@ export function registerShard(shard) {
|
|
|
74
70
|
* @throws If the shard is not registered, or if a manifest view has no factory after activation.
|
|
75
71
|
*/
|
|
76
72
|
export async function activateShard(id) {
|
|
77
|
-
var _a, _b, _c, _d, _e
|
|
73
|
+
var _a, _b, _c, _d, _e;
|
|
78
74
|
const shard = registeredShards.get(id);
|
|
79
75
|
if (!shard) {
|
|
80
76
|
throw new Error(`Cannot activate shard "${id}": not registered`);
|
|
@@ -143,31 +139,10 @@ export async function activateShard(id) {
|
|
|
143
139
|
browse: ((_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_BROWSE))
|
|
144
140
|
? createBrowseCapability(getTenantId(), getDocumentBackend())
|
|
145
141
|
: undefined,
|
|
146
|
-
|
|
147
|
-
? createSyncRegistryAccessor(getDocumentBackend(), getTenantId())
|
|
148
|
-
: undefined,
|
|
149
|
-
sync: ((_d = shard.manifest.permissions) === null || _d === void 0 ? void 0 : _d.includes(PERMISSION_DOCUMENTS_SYNC))
|
|
150
|
-
? () => {
|
|
151
|
-
const backend = getDocumentBackend();
|
|
152
|
-
const tenantId = getTenantId();
|
|
153
|
-
const bundlePromise = getSyncBundle(backend, tenantId);
|
|
154
|
-
const handlePromise = bundlePromise.then(({ engine, registry }) => createSyncHandle({ tenantId, connectorId: id, engine, registry }));
|
|
155
|
-
return {
|
|
156
|
-
connectorId: id,
|
|
157
|
-
grantedScopes: async () => (await handlePromise).grantedScopes(),
|
|
158
|
-
getManifest: async (scope) => (await handlePromise).getManifest(scope),
|
|
159
|
-
changesSince: async (scope, cursor) => (await handlePromise).changesSince(scope, cursor),
|
|
160
|
-
ack: async (scope, cursor) => (await handlePromise).ack(scope, cursor),
|
|
161
|
-
apply: async (scope, entry, opts) => (await handlePromise).apply(scope, entry, opts),
|
|
162
|
-
applyBatch: async (scope, manifest, opts) => (await handlePromise).applyBatch(scope, manifest, opts),
|
|
163
|
-
forget: async (scope, path) => (await handlePromise).forget(scope, path),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
: undefined,
|
|
167
|
-
keys: ((_e = shard.manifest.permissions) === null || _e === void 0 ? void 0 : _e.includes(PERMISSION_KEYS_MINT))
|
|
142
|
+
keys: ((_c = shard.manifest.permissions) === null || _c === void 0 ? void 0 : _c.includes(PERMISSION_KEYS_MINT))
|
|
168
143
|
? createShardKeysApi({
|
|
169
144
|
shardId: id,
|
|
170
|
-
shardPermissions: (
|
|
145
|
+
shardPermissions: (_d = shard.manifest.permissions) !== null && _d !== void 0 ? _d : [],
|
|
171
146
|
})
|
|
172
147
|
: undefined,
|
|
173
148
|
};
|
|
@@ -204,7 +179,7 @@ export async function activateShard(id) {
|
|
|
204
179
|
console.warn(`[sh3] Failed to hydrate env state for shard "${id}":`, err instanceof Error ? err.message : err);
|
|
205
180
|
}
|
|
206
181
|
}
|
|
207
|
-
void ((
|
|
182
|
+
void ((_e = shard.autostart) === null || _e === void 0 ? void 0 : _e.call(shard, ctx));
|
|
208
183
|
}
|
|
209
184
|
/**
|
|
210
185
|
* Deactivate an active shard. Calls `shard.deactivate`, flushes and disposes
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -2,8 +2,6 @@ import type { StateZones } from '../state/zones.svelte';
|
|
|
2
2
|
import type { ZoneSchema, ZoneManager } from '../state/types';
|
|
3
3
|
import type { DocumentHandle, DocumentHandleOptions } from '../documents/types';
|
|
4
4
|
import type { BrowseCapability } from '../documents/browse';
|
|
5
|
-
import type { SyncHandle } from '../documents/sync/types';
|
|
6
|
-
import type { SyncRegistry } from '../documents/sync/registry';
|
|
7
5
|
import type { EnvState } from '../env/types';
|
|
8
6
|
import type { Verb } from '../verbs/types';
|
|
9
7
|
import type { ShardContextKeys } from '../keys/types';
|
|
@@ -214,12 +212,6 @@ export interface ShardContext {
|
|
|
214
212
|
* `if (ctx.zones)` before use.
|
|
215
213
|
*/
|
|
216
214
|
zones?: ZoneManager;
|
|
217
|
-
/**
|
|
218
|
-
* Cross-shard document sync API. Only present when the shard's
|
|
219
|
-
* manifest declares the `'documents:sync'` permission. Check with
|
|
220
|
-
* `if (ctx.sync)` before use.
|
|
221
|
-
*/
|
|
222
|
-
sync?: () => SyncHandle;
|
|
223
215
|
/**
|
|
224
216
|
* Tenant-wide document browse API. Read-only enumeration and change
|
|
225
217
|
* subscription across every shard's documents for the active tenant.
|
|
@@ -228,13 +220,6 @@ export interface ShardContext {
|
|
|
228
220
|
* owning shard's own `ctx.documents()` handle.
|
|
229
221
|
*/
|
|
230
222
|
browse?: BrowseCapability;
|
|
231
|
-
/**
|
|
232
|
-
* Sync registry observer. Read-only list/revoke/conflict enumeration
|
|
233
|
-
* for explorer-class shards. Only present when the shard declares
|
|
234
|
-
* `'documents:browse'`. Granting still happens exclusively via
|
|
235
|
-
* `<SyncGrantPicker />`.
|
|
236
|
-
*/
|
|
237
|
-
syncRegistry?: () => SyncRegistry;
|
|
238
223
|
/**
|
|
239
224
|
* Mint/list/revoke keys minted by this shard. Only available when the
|
|
240
225
|
* manifest declares the `keys:mint` permission.
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
<div class="keys-peers-info">
|
|
71
71
|
<span class="keys-peers-label">{row.label}</span>
|
|
72
72
|
<span class="keys-peers-meta">
|
|
73
|
-
Minted by: {row.mintedByShardId ?? 'system'}{row.
|
|
73
|
+
Minted by: {row.mintedByShardId ?? 'system'}{row.peerRole || row.peerId ? ` \u00B7 Peer: ${row.peerRole ?? '—'}${row.peerId ? ` (${row.peerId})` : ''}` : ''}
|
|
74
74
|
</span>
|
|
75
75
|
<span class="keys-peers-meta">
|
|
76
76
|
Scopes: {row.scopes.join(', ')}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.9.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.9.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sh3-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -26,18 +26,10 @@
|
|
|
26
26
|
"./build": {
|
|
27
27
|
"types": "./dist/build.d.ts",
|
|
28
28
|
"default": "./dist/build.js"
|
|
29
|
-
},
|
|
30
|
-
"./server-sync": {
|
|
31
|
-
"types": "./dist/server-sync.d.ts",
|
|
32
|
-
"default": "./dist/server-sync.js"
|
|
33
|
-
},
|
|
34
|
-
"./testing": {
|
|
35
|
-
"types": "./dist/testing.d.ts",
|
|
36
|
-
"default": "./dist/testing.js"
|
|
37
29
|
}
|
|
38
30
|
},
|
|
39
31
|
"scripts": {
|
|
40
|
-
"build": "node --import tsx scripts/sync-version.ts && svelte-package -i src -o dist && node --import tsx scripts/
|
|
32
|
+
"build": "node --import tsx scripts/sync-version.ts && svelte-package -i src -o dist && node --import tsx scripts/generate-api-docs.ts",
|
|
41
33
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
42
34
|
"pack": "npm run build && npm pack",
|
|
43
35
|
"test": "vitest run --passWithNoTests",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { JournalEntry } from './sync/types';
|
|
2
|
-
type Appender = (entry: Omit<JournalEntry, 'seq' | 'ts'>) => Promise<void>;
|
|
3
|
-
export declare function setJournalAppender(fn: Appender): void;
|
|
4
|
-
export declare function clearJournalAppender(): void;
|
|
5
|
-
export declare function notifyJournal(entry: Omit<JournalEntry, 'seq' | 'ts'>): Promise<void>;
|
|
6
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Journal appender hook — lets the sync engine subscribe to regular
|
|
3
|
-
* shard writes/deletes without creating an import cycle between the
|
|
4
|
-
* document handle and the sync subsystem.
|
|
5
|
-
*/
|
|
6
|
-
let appender = null;
|
|
7
|
-
export function setJournalAppender(fn) {
|
|
8
|
-
appender = fn;
|
|
9
|
-
}
|
|
10
|
-
export function clearJournalAppender() {
|
|
11
|
-
appender = null;
|
|
12
|
-
}
|
|
13
|
-
export async function notifyJournal(entry) {
|
|
14
|
-
if (appender)
|
|
15
|
-
await appender(entry);
|
|
16
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { MemoryDocumentBackend } from '../backends';
|
|
3
|
-
import { __setDocumentBackend, __setTenantId } from '../config';
|
|
4
|
-
import { __resetShardRegistryForTest, registerShard, activateShard } from '../../shards/activate.svelte';
|
|
5
|
-
import { __resetSyncBundlesForTest } from './singleton';
|
|
6
|
-
import { PERMISSION_DOCUMENTS_SYNC } from './types';
|
|
7
|
-
describe('ctx.sync() gating', () => {
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
__resetShardRegistryForTest();
|
|
10
|
-
__resetSyncBundlesForTest();
|
|
11
|
-
__setDocumentBackend(new MemoryDocumentBackend());
|
|
12
|
-
__setTenantId('tenant-a');
|
|
13
|
-
});
|
|
14
|
-
it('is undefined without documents:sync permission', async () => {
|
|
15
|
-
let captured;
|
|
16
|
-
const shard = {
|
|
17
|
-
manifest: { id: 's-none', version: '0', views: [] },
|
|
18
|
-
activate: async (ctx) => { captured = ctx; },
|
|
19
|
-
};
|
|
20
|
-
registerShard(shard);
|
|
21
|
-
await activateShard('s-none');
|
|
22
|
-
expect(captured.sync).toBeUndefined();
|
|
23
|
-
});
|
|
24
|
-
it('is a function when documents:sync is declared', async () => {
|
|
25
|
-
let captured;
|
|
26
|
-
const shard = {
|
|
27
|
-
manifest: { id: 's-sync', version: '0', views: [], permissions: [PERMISSION_DOCUMENTS_SYNC] },
|
|
28
|
-
activate: async (ctx) => { captured = ctx; },
|
|
29
|
-
};
|
|
30
|
-
registerShard(shard);
|
|
31
|
-
await activateShard('s-sync');
|
|
32
|
-
expect(typeof captured.sync).toBe('function');
|
|
33
|
-
const h = captured.sync();
|
|
34
|
-
expect(h.connectorId).toBe('s-sync');
|
|
35
|
-
expect(await h.grantedScopes()).toEqual([]);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
import SyncGrantPicker from './SyncGrantPicker.svelte';
|
|
4
|
-
import { createSyncRegistry, type SyncRegistry } from '../registry';
|
|
5
|
-
import { getDocumentBackend, getTenantId } from '../../config';
|
|
6
|
-
import type { GrantRecord, SyncScope, ConflictResolution } from '../types';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
/** Optional connector-specific filter; if omitted, shows everything. */
|
|
10
|
-
connectorId?: string;
|
|
11
|
-
/** Shard IDs whose conflict artifacts should be listed. */
|
|
12
|
-
conflictShardIds?: string[];
|
|
13
|
-
/** Pending grant request, if any — embeds the picker when set. */
|
|
14
|
-
pendingRequest?: { connectorId: string; scope: SyncScope };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let { connectorId, conflictShardIds = [], pendingRequest }: Props = $props();
|
|
18
|
-
|
|
19
|
-
let registry: SyncRegistry | null = $state(null);
|
|
20
|
-
let grants: GrantRecord[] = $state([]);
|
|
21
|
-
let conflicts: ConflictResolution[] = $state([]);
|
|
22
|
-
|
|
23
|
-
async function refresh() {
|
|
24
|
-
if (!registry) return;
|
|
25
|
-
grants = await registry.list(connectorId);
|
|
26
|
-
const all: ConflictResolution[] = [];
|
|
27
|
-
for (const shardId of conflictShardIds) {
|
|
28
|
-
all.push(...await registry.listConflicts(shardId));
|
|
29
|
-
}
|
|
30
|
-
conflicts = all;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
onMount(async () => {
|
|
34
|
-
registry = createSyncRegistry(getDocumentBackend(), getTenantId());
|
|
35
|
-
await refresh();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
async function revoke(record: GrantRecord) {
|
|
39
|
-
if (!registry) return;
|
|
40
|
-
await registry.revoke(record.connectorId, record.scope);
|
|
41
|
-
await refresh();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function describeScope(s: SyncScope): string {
|
|
45
|
-
if (s.kind === 'tenant') return 'entire tenant';
|
|
46
|
-
if (s.kind === 'shard') return `shard:${s.shardId}`;
|
|
47
|
-
return `shard:${s.shardId}/${s.prefix}`;
|
|
48
|
-
}
|
|
49
|
-
</script>
|
|
50
|
-
|
|
51
|
-
<section class="document-sync-explorer" part="container">
|
|
52
|
-
<h2 part="title">Document Sync</h2>
|
|
53
|
-
|
|
54
|
-
{#if pendingRequest}
|
|
55
|
-
<SyncGrantPicker
|
|
56
|
-
connectorId={pendingRequest.connectorId}
|
|
57
|
-
scope={pendingRequest.scope}
|
|
58
|
-
onGranted={refresh}
|
|
59
|
-
/>
|
|
60
|
-
{/if}
|
|
61
|
-
|
|
62
|
-
<h3 part="subtitle">Granted scopes</h3>
|
|
63
|
-
{#if grants.length === 0}
|
|
64
|
-
<p part="empty">No scopes granted yet.</p>
|
|
65
|
-
{:else}
|
|
66
|
-
<ul part="grants">
|
|
67
|
-
{#each grants as g}
|
|
68
|
-
<li>
|
|
69
|
-
<span part="grant-connector">{g.connectorId}</span>
|
|
70
|
-
<span part="grant-scope">{describeScope(g.scope)}</span>
|
|
71
|
-
<button type="button" onclick={() => revoke(g)} part="revoke">Revoke</button>
|
|
72
|
-
</li>
|
|
73
|
-
{/each}
|
|
74
|
-
</ul>
|
|
75
|
-
{/if}
|
|
76
|
-
|
|
77
|
-
<h3 part="subtitle">Conflicts</h3>
|
|
78
|
-
{#if conflicts.length === 0}
|
|
79
|
-
<p part="empty">No active conflicts.</p>
|
|
80
|
-
{:else}
|
|
81
|
-
<ul part="conflicts">
|
|
82
|
-
{#each conflicts as c}
|
|
83
|
-
<li>
|
|
84
|
-
<code part="conflict-path">{c.shardId}:{c.path}</code>
|
|
85
|
-
<small part="conflict-artifact">{c.conflictArtifactPath}</small>
|
|
86
|
-
</li>
|
|
87
|
-
{/each}
|
|
88
|
-
</ul>
|
|
89
|
-
{/if}
|
|
90
|
-
</section>
|
|
91
|
-
|
|
92
|
-
<style>
|
|
93
|
-
.document-sync-explorer {
|
|
94
|
-
display: grid;
|
|
95
|
-
gap: 0.75rem;
|
|
96
|
-
}
|
|
97
|
-
ul { list-style: none; padding: 0; margin: 0; }
|
|
98
|
-
li { display: flex; gap: 0.5rem; align-items: center; }
|
|
99
|
-
</style>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { SyncScope } from '../types';
|
|
2
|
-
interface Props {
|
|
3
|
-
/** Optional connector-specific filter; if omitted, shows everything. */
|
|
4
|
-
connectorId?: string;
|
|
5
|
-
/** Shard IDs whose conflict artifacts should be listed. */
|
|
6
|
-
conflictShardIds?: string[];
|
|
7
|
-
/** Pending grant request, if any — embeds the picker when set. */
|
|
8
|
-
pendingRequest?: {
|
|
9
|
-
connectorId: string;
|
|
10
|
-
scope: SyncScope;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
declare const DocumentSyncExplorer: import("svelte").Component<Props, {}, "">;
|
|
14
|
-
type DocumentSyncExplorer = ReturnType<typeof DocumentSyncExplorer>;
|
|
15
|
-
export default DocumentSyncExplorer;
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getDocumentBackend, getTenantId } from '../../config';
|
|
3
|
-
import { __grantInternal } from '../registry';
|
|
4
|
-
import type { SyncScope } from '../types';
|
|
5
|
-
|
|
6
|
-
import type { Snippet } from 'svelte';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
connectorId: string;
|
|
10
|
-
scope: SyncScope;
|
|
11
|
-
onGranted?: () => void;
|
|
12
|
-
onCancel?: () => void;
|
|
13
|
-
rationale?: Snippet;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let { connectorId, scope, onGranted, onCancel, rationale }: Props = $props();
|
|
17
|
-
|
|
18
|
-
let pending = $state(false);
|
|
19
|
-
let error = $state<string | null>(null);
|
|
20
|
-
|
|
21
|
-
function describe(s: SyncScope): string {
|
|
22
|
-
switch (s.kind) {
|
|
23
|
-
case 'tenant': return 'all your documents across every shard';
|
|
24
|
-
case 'shard': return `all documents for shard "${s.shardId}"`;
|
|
25
|
-
case 'path': return `documents under "${s.prefix}" in shard "${s.shardId}"`;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function confirm() {
|
|
30
|
-
pending = true;
|
|
31
|
-
error = null;
|
|
32
|
-
try {
|
|
33
|
-
await __grantInternal(getDocumentBackend(), getTenantId(), connectorId, scope);
|
|
34
|
-
onGranted?.();
|
|
35
|
-
} catch (e) {
|
|
36
|
-
error = e instanceof Error ? e.message : String(e);
|
|
37
|
-
} finally {
|
|
38
|
-
pending = false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
</script>
|
|
42
|
-
|
|
43
|
-
<section class="sync-grant-picker" part="container">
|
|
44
|
-
<header part="header">
|
|
45
|
-
<h3 part="title">Grant sync access</h3>
|
|
46
|
-
</header>
|
|
47
|
-
<p part="summary">
|
|
48
|
-
<strong>{connectorId}</strong> is requesting access to {describe(scope)}.
|
|
49
|
-
</p>
|
|
50
|
-
{#if rationale}{@render rationale()}{/if}
|
|
51
|
-
{#if error}
|
|
52
|
-
<p class="error" part="error">{error}</p>
|
|
53
|
-
{/if}
|
|
54
|
-
<footer part="actions">
|
|
55
|
-
<button type="button" disabled={pending} onclick={() => onCancel?.()} part="cancel">Cancel</button>
|
|
56
|
-
<button type="button" disabled={pending} onclick={confirm} part="confirm">Grant</button>
|
|
57
|
-
</footer>
|
|
58
|
-
</section>
|
|
59
|
-
|
|
60
|
-
<style>
|
|
61
|
-
.sync-grant-picker {
|
|
62
|
-
display: grid;
|
|
63
|
-
gap: 0.75rem;
|
|
64
|
-
padding: 1rem;
|
|
65
|
-
border: 1px solid var(--sh3-border, #444);
|
|
66
|
-
border-radius: 6px;
|
|
67
|
-
}
|
|
68
|
-
.error { color: var(--sh3-error, #c00); }
|
|
69
|
-
footer { display: flex; gap: 0.5rem; justify-content: flex-end; }
|
|
70
|
-
</style>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { SyncScope } from '../types';
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
interface Props {
|
|
4
|
-
connectorId: string;
|
|
5
|
-
scope: SyncScope;
|
|
6
|
-
onGranted?: () => void;
|
|
7
|
-
onCancel?: () => void;
|
|
8
|
-
rationale?: Snippet;
|
|
9
|
-
}
|
|
10
|
-
declare const SyncGrantPicker: import("svelte").Component<Props, {}, "">;
|
|
11
|
-
type SyncGrantPicker = ReturnType<typeof SyncGrantPicker>;
|
|
12
|
-
export default SyncGrantPicker;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { DocumentBackend } from '../types';
|
|
2
|
-
import type { ConflictPolicy, ConflictResolution } from './types';
|
|
3
|
-
interface ConflictInput {
|
|
4
|
-
connectorId: string;
|
|
5
|
-
shardId: string;
|
|
6
|
-
path: string;
|
|
7
|
-
localHash: string;
|
|
8
|
-
remoteHash: string;
|
|
9
|
-
remoteContent?: string | ArrayBuffer;
|
|
10
|
-
baseHash?: string;
|
|
11
|
-
}
|
|
12
|
-
export type ConflictAction = {
|
|
13
|
-
action: 'apply-remote';
|
|
14
|
-
asPath?: string;
|
|
15
|
-
} | {
|
|
16
|
-
action: 'skip';
|
|
17
|
-
} | {
|
|
18
|
-
action: 'conflict';
|
|
19
|
-
resolution: ConflictResolution;
|
|
20
|
-
};
|
|
21
|
-
export declare class ConflictManager {
|
|
22
|
-
private backend;
|
|
23
|
-
private tenantId;
|
|
24
|
-
constructor(backend: DocumentBackend, tenantId: string);
|
|
25
|
-
resolve(policy: ConflictPolicy, input: ConflictInput): Promise<ConflictAction>;
|
|
26
|
-
getBaseHash(connectorId: string, shardId: string, path: string): Promise<string | null>;
|
|
27
|
-
setBaseHash(connectorId: string, shardId: string, path: string, hash: string): Promise<void>;
|
|
28
|
-
listConflicts(shardId: string): Promise<ConflictResolution[]>;
|
|
29
|
-
}
|
|
30
|
-
export {};
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Conflict resolution — dispatches the caller-supplied (or default)
|
|
3
|
-
* policy, writes .sync-conflict-* artifacts when required, and tracks
|
|
4
|
-
* per-(connectorId, path) base hashes for three-way comparisons.
|
|
5
|
-
*/
|
|
6
|
-
import { readJson, writeJson } from './serialization';
|
|
7
|
-
const BASES_PREFIX = 'bases/';
|
|
8
|
-
function baseKey(connectorId, shardId, path) {
|
|
9
|
-
return `${BASES_PREFIX}${encodeURIComponent(connectorId)}__${shardId}__${encodeURIComponent(path)}.json`;
|
|
10
|
-
}
|
|
11
|
-
function isArtifactName(name) {
|
|
12
|
-
return /\.sync-conflict-[^.]+-\d+$/.test(name);
|
|
13
|
-
}
|
|
14
|
-
export class ConflictManager {
|
|
15
|
-
constructor(backend, tenantId) {
|
|
16
|
-
this.backend = backend;
|
|
17
|
-
this.tenantId = tenantId;
|
|
18
|
-
}
|
|
19
|
-
async resolve(policy, input) {
|
|
20
|
-
const p = typeof policy === 'function' ? await policy({
|
|
21
|
-
path: input.path, shardId: input.shardId,
|
|
22
|
-
localHash: input.localHash, remoteHash: input.remoteHash, baseHash: input.baseHash,
|
|
23
|
-
}) : policy;
|
|
24
|
-
switch (p) {
|
|
25
|
-
case 'remote-wins': return { action: 'apply-remote' };
|
|
26
|
-
case 'local-wins': return { action: 'skip' };
|
|
27
|
-
case 'keep-both': {
|
|
28
|
-
const asPath = `${input.path}.incoming-${input.connectorId}-${Date.now()}`;
|
|
29
|
-
return { action: 'apply-remote', asPath };
|
|
30
|
-
}
|
|
31
|
-
case 'default':
|
|
32
|
-
default: {
|
|
33
|
-
const ts = Date.now();
|
|
34
|
-
const artifact = `${input.path}.sync-conflict-${input.connectorId}-${ts}`;
|
|
35
|
-
if (input.remoteContent !== undefined) {
|
|
36
|
-
await this.backend.write(this.tenantId, input.shardId, artifact, input.remoteContent);
|
|
37
|
-
}
|
|
38
|
-
const resolution = {
|
|
39
|
-
path: input.path,
|
|
40
|
-
shardId: input.shardId,
|
|
41
|
-
localHash: input.localHash,
|
|
42
|
-
remoteHash: input.remoteHash,
|
|
43
|
-
conflictArtifactPath: artifact,
|
|
44
|
-
base: input.baseHash ? { hash: input.baseHash } : undefined,
|
|
45
|
-
};
|
|
46
|
-
return { action: 'conflict', resolution };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
async getBaseHash(connectorId, shardId, path) {
|
|
51
|
-
return readJson(this.backend, this.tenantId, baseKey(connectorId, shardId, path));
|
|
52
|
-
}
|
|
53
|
-
async setBaseHash(connectorId, shardId, path, hash) {
|
|
54
|
-
await writeJson(this.backend, this.tenantId, baseKey(connectorId, shardId, path), hash);
|
|
55
|
-
}
|
|
56
|
-
async listConflicts(shardId) {
|
|
57
|
-
const metas = await this.backend.list(this.tenantId, shardId);
|
|
58
|
-
const out = [];
|
|
59
|
-
for (const m of metas) {
|
|
60
|
-
const name = m.path;
|
|
61
|
-
if (!isArtifactName(name))
|
|
62
|
-
continue;
|
|
63
|
-
const m2 = /^(.*)\.sync-conflict-([^-]+)-(\d+)$/.exec(name);
|
|
64
|
-
if (!m2)
|
|
65
|
-
continue;
|
|
66
|
-
const originalPath = m2[1];
|
|
67
|
-
out.push({
|
|
68
|
-
path: originalPath,
|
|
69
|
-
shardId,
|
|
70
|
-
localHash: '',
|
|
71
|
-
remoteHash: '',
|
|
72
|
-
conflictArtifactPath: name,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { MemoryDocumentBackend } from '../backends';
|
|
3
|
-
import { ConflictManager } from './conflicts';
|
|
4
|
-
describe('ConflictManager', () => {
|
|
5
|
-
let backend;
|
|
6
|
-
let mgr;
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
backend = new MemoryDocumentBackend();
|
|
9
|
-
mgr = new ConflictManager(backend, 'tenant-a');
|
|
10
|
-
});
|
|
11
|
-
it('remote-wins: returns apply-remote=true', async () => {
|
|
12
|
-
const r = await mgr.resolve('remote-wins', {
|
|
13
|
-
connectorId: 'c1', shardId: 's1', path: 'a.md',
|
|
14
|
-
localHash: 'L', remoteHash: 'R',
|
|
15
|
-
});
|
|
16
|
-
expect(r).toEqual({ action: 'apply-remote' });
|
|
17
|
-
});
|
|
18
|
-
it('local-wins: returns skip', async () => {
|
|
19
|
-
const r = await mgr.resolve('local-wins', {
|
|
20
|
-
connectorId: 'c1', shardId: 's1', path: 'a.md',
|
|
21
|
-
localHash: 'L', remoteHash: 'R',
|
|
22
|
-
});
|
|
23
|
-
expect(r).toEqual({ action: 'skip' });
|
|
24
|
-
});
|
|
25
|
-
it('keep-both: returns apply-remote with renamed path', async () => {
|
|
26
|
-
vi.useFakeTimers().setSystemTime(new Date(10000));
|
|
27
|
-
const r = await mgr.resolve('keep-both', {
|
|
28
|
-
connectorId: 'c1', shardId: 's1', path: 'a.md',
|
|
29
|
-
localHash: 'L', remoteHash: 'R',
|
|
30
|
-
});
|
|
31
|
-
expect(r).toEqual({ action: 'apply-remote', asPath: 'a.md.incoming-c1-10000' });
|
|
32
|
-
vi.useRealTimers();
|
|
33
|
-
});
|
|
34
|
-
it('default: writes .sync-conflict artifact, keeps local, returns resolution', async () => {
|
|
35
|
-
vi.useFakeTimers().setSystemTime(new Date(20000));
|
|
36
|
-
const r = await mgr.resolve('default', {
|
|
37
|
-
connectorId: 'c1', shardId: 's1', path: 'dir/a.md',
|
|
38
|
-
localHash: 'L', remoteHash: 'R', remoteContent: 'REMOTE',
|
|
39
|
-
});
|
|
40
|
-
expect(r.action).toBe('conflict');
|
|
41
|
-
if (r.action !== 'conflict')
|
|
42
|
-
throw new Error('unreachable');
|
|
43
|
-
expect(r.resolution.conflictArtifactPath).toBe('dir/a.md.sync-conflict-c1-20000');
|
|
44
|
-
// Artifact was written to the backend under the target shard.
|
|
45
|
-
const read = await backend.read('tenant-a', 's1', 'dir/a.md.sync-conflict-c1-20000');
|
|
46
|
-
expect(read).toBe('REMOTE');
|
|
47
|
-
vi.useRealTimers();
|
|
48
|
-
});
|
|
49
|
-
it('function policy dispatches to returned action', async () => {
|
|
50
|
-
const r = await mgr.resolve(async () => 'remote-wins', {
|
|
51
|
-
connectorId: 'c1', shardId: 's1', path: 'a.md',
|
|
52
|
-
localHash: 'L', remoteHash: 'R',
|
|
53
|
-
});
|
|
54
|
-
expect(r).toEqual({ action: 'apply-remote' });
|
|
55
|
-
});
|
|
56
|
-
it('stores and retrieves base hash per (connector, shard, path)', async () => {
|
|
57
|
-
await mgr.setBaseHash('c1', 's1', 'a.md', 'H1');
|
|
58
|
-
expect(await mgr.getBaseHash('c1', 's1', 'a.md')).toBe('H1');
|
|
59
|
-
expect(await mgr.getBaseHash('c2', 's1', 'a.md')).toBeNull();
|
|
60
|
-
});
|
|
61
|
-
it('lists .sync-conflict-* artifacts within a shard', async () => {
|
|
62
|
-
await backend.write('tenant-a', 's1', 'a.md.sync-conflict-c1-1', 'x');
|
|
63
|
-
await backend.write('tenant-a', 's1', 'b.md.sync-conflict-c2-2', 'y');
|
|
64
|
-
await backend.write('tenant-a', 's1', 'c.md', 'regular');
|
|
65
|
-
const list = await mgr.listConflicts('s1');
|
|
66
|
-
expect(list.map((c) => c.conflictArtifactPath).sort()).toEqual([
|
|
67
|
-
'a.md.sync-conflict-c1-1',
|
|
68
|
-
'b.md.sync-conflict-c2-2',
|
|
69
|
-
]);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { DocumentBackend } from '../types';
|
|
2
|
-
import type { ApplyBatchResult, ApplyEntry, ApplyOpts, ApplyOutcome, ChangePage, ManifestEntry, SyncScope } from './types';
|
|
3
|
-
import { Journal } from './journal';
|
|
4
|
-
export declare class SyncEngine {
|
|
5
|
-
#private;
|
|
6
|
-
private backend;
|
|
7
|
-
private tenantId;
|
|
8
|
-
constructor(backend: DocumentBackend, tenantId: string, opts?: {
|
|
9
|
-
segmentSize?: number;
|
|
10
|
-
});
|
|
11
|
-
init(): Promise<void>;
|
|
12
|
-
get journal(): Journal;
|
|
13
|
-
getManifest(_connectorId: string, scope: SyncScope): Promise<ManifestEntry[]>;
|
|
14
|
-
changesSince(scope: SyncScope, cursor?: string): Promise<ChangePage>;
|
|
15
|
-
ack(connectorId: string, _scope: SyncScope, cursor: string): Promise<void>;
|
|
16
|
-
apply(connectorId: string, scope: SyncScope, entry: ApplyEntry, opts?: ApplyOpts): Promise<ApplyOutcome>;
|
|
17
|
-
applyBatch(connectorId: string, scope: SyncScope, manifest: ApplyEntry[], opts?: ApplyOpts): Promise<ApplyBatchResult>;
|
|
18
|
-
forget(scope: SyncScope, path: string): Promise<void>;
|
|
19
|
-
}
|