sh3-core 0.8.0 → 0.8.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.
- package/dist/api.d.ts +2 -1
- package/dist/api.js +1 -1
- package/dist/app/admin/SystemView.svelte +149 -11
- package/dist/documents/backends.d.ts +8 -0
- package/dist/documents/backends.js +87 -0
- package/dist/documents/backends.test.d.ts +1 -0
- package/dist/documents/backends.test.js +33 -0
- package/dist/documents/browse.d.ts +12 -0
- package/dist/documents/browse.js +19 -0
- package/dist/documents/browse.test.d.ts +1 -0
- package/dist/documents/browse.test.js +41 -0
- package/dist/documents/http-backend.d.ts +4 -0
- package/dist/documents/http-backend.js +14 -0
- package/dist/documents/sync/index.d.ts +1 -2
- package/dist/documents/sync/index.js +0 -2
- package/dist/documents/sync/observer.d.ts +3 -0
- package/dist/documents/sync/observer.js +45 -0
- package/dist/documents/sync/registry.d.ts +3 -0
- package/dist/documents/sync/registry.js +8 -1
- package/dist/documents/sync/registry.test.js +11 -0
- package/dist/documents/types.d.ts +18 -0
- package/dist/documents/types.js +6 -1
- package/dist/layout/inspection.d.ts +17 -0
- package/dist/layout/inspection.js +53 -0
- package/dist/shards/activate-browse.test.d.ts +1 -0
- package/dist/shards/activate-browse.test.js +36 -0
- package/dist/shards/activate-sync-registry.test.d.ts +1 -0
- package/dist/shards/activate-sync-registry.test.js +42 -0
- package/dist/shards/activate-tenantid.test.d.ts +1 -0
- package/dist/shards/activate-tenantid.test.js +21 -0
- package/dist/shards/activate.svelte.d.ts +12 -0
- package/dist/shards/activate.svelte.js +33 -3
- package/dist/shards/types.d.ts +33 -0
- package/dist/shell-shard/manifest.js +1 -1
- package/dist/shell-shard/shellShard.svelte.js +52 -4
- package/dist/shell-shard/verbs/index.js +3 -1
- package/dist/shell-shard/verbs/views.d.ts +2 -0
- package/dist/shell-shard/verbs/views.js +103 -2
- package/dist/verbs/types.d.ts +19 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -17,9 +17,10 @@ export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
|
|
|
17
17
|
export { launchApp, returnToHome, unregisterApp } from './apps/lifecycle';
|
|
18
18
|
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, } from './layout/inspection';
|
|
19
19
|
export type { DocumentHandle, DocumentHandleOptions, DocumentFormat, DocumentMeta, DocumentChange, AutosaveController, } from './documents/types';
|
|
20
|
+
export { PERMISSION_DOCUMENTS_BROWSE } from './documents/types';
|
|
21
|
+
export type { BrowseCapability } from './documents/browse';
|
|
20
22
|
export type { SyncHandle, SyncScope, ManifestEntry, ApplyEntry, ApplyOpts, ApplyOutcome, ApplyBatchResult, ConflictPolicy, ConflictResolution, ConflictContext, JournalEntry, ChangePage, GrantRecord, } from './documents/sync/types';
|
|
21
23
|
export { PERMISSION_DOCUMENTS_SYNC, ScopeNotGrantedError, ScopeRevokedError, TenantMismatchError, } from './documents/sync/types';
|
|
22
|
-
export { createSyncRegistry } from './documents/sync/registry';
|
|
23
24
|
export type { SyncRegistry } from './documents/sync/registry';
|
|
24
25
|
export { default as SyncGrantPicker } from './documents/sync/components/SyncGrantPicker.svelte';
|
|
25
26
|
export { default as DocumentSyncExplorer } from './documents/sync/components/DocumentSyncExplorer.svelte';
|
package/dist/api.js
CHANGED
|
@@ -29,8 +29,8 @@ export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
|
|
|
29
29
|
export { launchApp, returnToHome, unregisterApp } from './apps/lifecycle';
|
|
30
30
|
// Layout inspection / mutation for advanced shards (diagnostic, etc.).
|
|
31
31
|
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, } from './layout/inspection';
|
|
32
|
+
export { PERMISSION_DOCUMENTS_BROWSE } from './documents/types';
|
|
32
33
|
export { PERMISSION_DOCUMENTS_SYNC, ScopeNotGrantedError, ScopeRevokedError, TenantMismatchError, } from './documents/sync/types';
|
|
33
|
-
export { createSyncRegistry } from './documents/sync/registry';
|
|
34
34
|
export { default as SyncGrantPicker } from './documents/sync/components/SyncGrantPicker.svelte';
|
|
35
35
|
export { default as DocumentSyncExplorer } from './documents/sync/components/DocumentSyncExplorer.svelte';
|
|
36
36
|
// Shard introspection — read-only reactive maps exposing which shards are
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* Admin System view — server status and
|
|
3
|
+
* Admin System view — server status, restart, and package-bundle cache policy.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const SNAP_POINTS: Array<{ value: number; label: string }> = [
|
|
7
|
+
{ value: 0, label: 'Off (no-store)' },
|
|
8
|
+
{ value: 5, label: '5s (dev)' },
|
|
9
|
+
{ value: 60, label: '1 min' },
|
|
10
|
+
{ value: 3600, label: '1 hour' },
|
|
11
|
+
{ value: 86400, label: '1 day' },
|
|
12
|
+
{ value: 31536000, label: '1 year' },
|
|
13
|
+
];
|
|
14
|
+
|
|
6
15
|
let version = $state('...');
|
|
7
16
|
let restarting = $state(false);
|
|
8
17
|
let restartError = $state<string | null>(null);
|
|
9
18
|
|
|
19
|
+
let cacheMaxAge = $state(31536000);
|
|
20
|
+
let loadedMaxAge = $state(31536000);
|
|
21
|
+
let savingCache = $state(false);
|
|
22
|
+
let cacheError = $state<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const dirty = $derived(cacheMaxAge !== loadedMaxAge);
|
|
25
|
+
const humanized = $derived(humanize(cacheMaxAge));
|
|
26
|
+
|
|
27
|
+
function humanize(sec: number): string {
|
|
28
|
+
if (sec === 0) return '0 seconds (off — browsers never cache)';
|
|
29
|
+
if (sec < 60) return `${sec} seconds`;
|
|
30
|
+
if (sec < 3600) return `${Math.round(sec / 60)} minutes`;
|
|
31
|
+
if (sec < 86400) return `${Math.round(sec / 3600)} hours`;
|
|
32
|
+
if (sec < 31536000) return `${Math.round(sec / 86400)} days`;
|
|
33
|
+
return `${Math.round(sec / 31536000)} years`;
|
|
34
|
+
}
|
|
35
|
+
|
|
10
36
|
async function fetchVersion() {
|
|
11
37
|
try {
|
|
12
38
|
const res = await fetch('/api/version');
|
|
@@ -17,6 +43,43 @@
|
|
|
17
43
|
} catch { /* ignore */ }
|
|
18
44
|
}
|
|
19
45
|
|
|
46
|
+
async function fetchSettings() {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch('/api/admin/settings', { credentials: 'include' });
|
|
49
|
+
if (res.ok) {
|
|
50
|
+
const body = await res.json();
|
|
51
|
+
const age = body.packages?.cacheMaxAge ?? 31536000;
|
|
52
|
+
cacheMaxAge = age;
|
|
53
|
+
loadedMaxAge = age;
|
|
54
|
+
}
|
|
55
|
+
} catch { /* ignore */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function saveCache() {
|
|
59
|
+
savingCache = true;
|
|
60
|
+
cacheError = null;
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch('/api/admin/settings', {
|
|
63
|
+
method: 'PUT',
|
|
64
|
+
credentials: 'include',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ packages: { cacheMaxAge } }),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const body = await res.json().catch(() => ({}));
|
|
70
|
+
cacheError = body.error || 'Save failed';
|
|
71
|
+
} else {
|
|
72
|
+
const body = await res.json();
|
|
73
|
+
loadedMaxAge = body.packages?.cacheMaxAge ?? cacheMaxAge;
|
|
74
|
+
cacheMaxAge = loadedMaxAge;
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
cacheError = 'Network error';
|
|
78
|
+
} finally {
|
|
79
|
+
savingCache = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
20
83
|
async function restart() {
|
|
21
84
|
restarting = true;
|
|
22
85
|
restartError = null;
|
|
@@ -38,6 +101,7 @@
|
|
|
38
101
|
}
|
|
39
102
|
|
|
40
103
|
fetchVersion();
|
|
104
|
+
fetchSettings();
|
|
41
105
|
</script>
|
|
42
106
|
|
|
43
107
|
<div class="admin-system">
|
|
@@ -50,24 +114,98 @@
|
|
|
50
114
|
</div>
|
|
51
115
|
</div>
|
|
52
116
|
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
117
|
+
<section class="admin-system-section">
|
|
118
|
+
<h3>Package bundle cache</h3>
|
|
119
|
+
<p class="admin-system-hint">
|
|
120
|
+
Controls the <code>Cache-Control</code> header on <code>/packages/:id/client.js</code>.
|
|
121
|
+
Set to <strong>0</strong> during development so drop-in bundle replacements take effect on F5.
|
|
122
|
+
Use a long value in production.
|
|
123
|
+
</p>
|
|
124
|
+
|
|
125
|
+
<input
|
|
126
|
+
type="range"
|
|
127
|
+
min="0"
|
|
128
|
+
max="31536000"
|
|
129
|
+
step="1"
|
|
130
|
+
bind:value={cacheMaxAge}
|
|
131
|
+
disabled={savingCache}
|
|
132
|
+
/>
|
|
133
|
+
<div class="admin-system-readout">
|
|
134
|
+
<code>{cacheMaxAge}</code> — {humanized}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="admin-system-snaps">
|
|
138
|
+
{#each SNAP_POINTS as snap (snap.value)}
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
class="admin-snap"
|
|
142
|
+
class:active={cacheMaxAge === snap.value}
|
|
143
|
+
onclick={() => (cacheMaxAge = snap.value)}
|
|
144
|
+
disabled={savingCache}
|
|
145
|
+
>
|
|
146
|
+
{snap.label}
|
|
147
|
+
</button>
|
|
148
|
+
{/each}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="admin-system-actions">
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
class="admin-btn"
|
|
155
|
+
onclick={saveCache}
|
|
156
|
+
disabled={!dirty || savingCache}
|
|
157
|
+
>
|
|
158
|
+
{savingCache ? 'Saving...' : 'Save'}
|
|
159
|
+
</button>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
class="admin-btn-ghost"
|
|
163
|
+
onclick={() => (cacheMaxAge = loadedMaxAge)}
|
|
164
|
+
disabled={!dirty || savingCache}
|
|
165
|
+
>
|
|
166
|
+
Reset
|
|
167
|
+
</button>
|
|
168
|
+
{#if cacheError}
|
|
169
|
+
<div class="admin-error">{cacheError}</div>
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
</section>
|
|
173
|
+
|
|
174
|
+
<section class="admin-system-section">
|
|
175
|
+
<h3>Server control</h3>
|
|
176
|
+
<div class="admin-system-actions">
|
|
177
|
+
<button type="button" class="admin-btn-danger" onclick={restart} disabled={restarting}>
|
|
178
|
+
{restarting ? 'Restarting...' : 'Restart server'}
|
|
179
|
+
</button>
|
|
180
|
+
{#if restartError}
|
|
181
|
+
<div class="admin-error">{restartError}</div>
|
|
182
|
+
{/if}
|
|
183
|
+
</div>
|
|
184
|
+
</section>
|
|
61
185
|
</div>
|
|
62
186
|
|
|
63
187
|
<style>
|
|
64
188
|
.admin-system { padding: 24px; font-family: system-ui, sans-serif; color: var(--shell-fg); }
|
|
65
189
|
.admin-system h2 { margin: 0 0 16px; font-size: 18px; }
|
|
190
|
+
.admin-system h3 { margin: 0 0 8px; font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--shell-fg-subtle); }
|
|
66
191
|
.admin-system-info { margin-bottom: 24px; }
|
|
67
192
|
.admin-system-row { display: flex; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--shell-border, #3a3a5c); font-size: 13px; }
|
|
68
193
|
.admin-system-label { color: var(--shell-fg-subtle); min-width: 140px; }
|
|
69
|
-
.admin-system-
|
|
70
|
-
.admin-
|
|
194
|
+
.admin-system-section { margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid var(--shell-border, #3a3a5c); }
|
|
195
|
+
.admin-system-section:last-child { border-bottom: none; }
|
|
196
|
+
.admin-system-hint { font-size: 13px; color: var(--shell-fg-subtle); margin: 0 0 12px; }
|
|
197
|
+
.admin-system-readout { font-size: 13px; margin: 8px 0 12px; }
|
|
198
|
+
.admin-system-snaps { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
|
199
|
+
.admin-snap { padding: 4px 10px; font-size: 12px; background: transparent; border: 1px solid var(--shell-border, #3a3a5c); color: var(--shell-fg); cursor: pointer; border-radius: var(--shell-radius-sm, 3px); }
|
|
200
|
+
.admin-snap.active { background: var(--shell-accent, #4a7bd4); color: var(--shell-bg); border-color: var(--shell-accent, #4a7bd4); }
|
|
201
|
+
.admin-snap:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
202
|
+
.admin-system-actions { display: flex; flex-direction: row; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
203
|
+
.admin-btn { padding: 8px 16px; background: var(--shell-accent, #4a7bd4); color: var(--shell-bg); border: 1px solid var(--shell-accent, #4a7bd4); font-weight: 600; cursor: pointer; border-radius: var(--shell-radius-sm, 3px); }
|
|
204
|
+
.admin-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
205
|
+
.admin-btn-ghost { padding: 8px 16px; background: transparent; color: var(--shell-fg); border: 1px solid var(--shell-border, #3a3a5c); cursor: pointer; border-radius: var(--shell-radius-sm, 3px); }
|
|
206
|
+
.admin-btn-ghost:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
207
|
+
.admin-btn-danger { padding: 8px 16px; background: transparent; color: var(--shell-error, #d32f2f); border: 1px solid var(--shell-error, #d32f2f); font-weight: 600; cursor: pointer; border-radius: var(--shell-radius-sm, 3px); }
|
|
71
208
|
.admin-btn-danger:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
72
209
|
.admin-error { color: var(--shell-error, #d32f2f); font-size: 13px; }
|
|
210
|
+
input[type="range"] { width: 100%; max-width: 480px; }
|
|
73
211
|
</style>
|
|
@@ -6,6 +6,10 @@ export declare class MemoryDocumentBackend implements DocumentBackend {
|
|
|
6
6
|
delete(tenantId: string, shardId: string, path: string): Promise<void>;
|
|
7
7
|
list(tenantId: string, shardId: string): Promise<DocumentMeta[]>;
|
|
8
8
|
exists(tenantId: string, shardId: string, path: string): Promise<boolean>;
|
|
9
|
+
listAllShards(tenantId: string): Promise<string[]>;
|
|
10
|
+
listAllDocuments(tenantId: string): Promise<Array<DocumentMeta & {
|
|
11
|
+
shardId: string;
|
|
12
|
+
}>>;
|
|
9
13
|
}
|
|
10
14
|
export declare class IndexedDBDocumentBackend implements DocumentBackend {
|
|
11
15
|
#private;
|
|
@@ -14,4 +18,8 @@ export declare class IndexedDBDocumentBackend implements DocumentBackend {
|
|
|
14
18
|
delete(tenantId: string, shardId: string, path: string): Promise<void>;
|
|
15
19
|
list(tenantId: string, shardId: string): Promise<DocumentMeta[]>;
|
|
16
20
|
exists(tenantId: string, shardId: string, path: string): Promise<boolean>;
|
|
21
|
+
listAllShards(tenantId: string): Promise<string[]>;
|
|
22
|
+
listAllDocuments(tenantId: string): Promise<Array<DocumentMeta & {
|
|
23
|
+
shardId: string;
|
|
24
|
+
}>>;
|
|
17
25
|
}
|
|
@@ -62,6 +62,36 @@ export class MemoryDocumentBackend {
|
|
|
62
62
|
async exists(tenantId, shardId, path) {
|
|
63
63
|
return __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").has(compositeKey(tenantId, shardId, path));
|
|
64
64
|
}
|
|
65
|
+
async listAllShards(tenantId) {
|
|
66
|
+
const prefix = `${tenantId}/`;
|
|
67
|
+
const shards = new Set();
|
|
68
|
+
for (const key of __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f").keys()) {
|
|
69
|
+
if (!key.startsWith(prefix))
|
|
70
|
+
continue;
|
|
71
|
+
const rest = key.slice(prefix.length);
|
|
72
|
+
const slash = rest.indexOf('/');
|
|
73
|
+
if (slash < 0)
|
|
74
|
+
continue;
|
|
75
|
+
shards.add(rest.slice(0, slash));
|
|
76
|
+
}
|
|
77
|
+
return [...shards];
|
|
78
|
+
}
|
|
79
|
+
async listAllDocuments(tenantId) {
|
|
80
|
+
const prefix = `${tenantId}/`;
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const [key, entry] of __classPrivateFieldGet(this, _MemoryDocumentBackend_store, "f")) {
|
|
83
|
+
if (!key.startsWith(prefix))
|
|
84
|
+
continue;
|
|
85
|
+
const rest = key.slice(prefix.length);
|
|
86
|
+
const slash = rest.indexOf('/');
|
|
87
|
+
if (slash < 0)
|
|
88
|
+
continue;
|
|
89
|
+
const shardId = rest.slice(0, slash);
|
|
90
|
+
const path = rest.slice(slash + 1);
|
|
91
|
+
out.push({ shardId, path, size: entry.size, lastModified: entry.lastModified });
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
65
95
|
}
|
|
66
96
|
_MemoryDocumentBackend_store = new WeakMap();
|
|
67
97
|
// ---------------------------------------------------------------------------
|
|
@@ -126,6 +156,63 @@ export class IndexedDBDocumentBackend {
|
|
|
126
156
|
const result = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_tx).call(this, 'readonly', (s) => s.getKey(key));
|
|
127
157
|
return result !== undefined;
|
|
128
158
|
}
|
|
159
|
+
async listAllShards(tenantId) {
|
|
160
|
+
const prefix = `${tenantId}/`;
|
|
161
|
+
const db = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_db).call(this);
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
const tx = db.transaction(IDB_STORE, 'readonly');
|
|
164
|
+
const store = tx.objectStore(IDB_STORE);
|
|
165
|
+
const range = IDBKeyRange.bound(prefix, prefix + '\uffff', false, false);
|
|
166
|
+
const req = store.openKeyCursor(range);
|
|
167
|
+
const shards = new Set();
|
|
168
|
+
req.onsuccess = () => {
|
|
169
|
+
const cursor = req.result;
|
|
170
|
+
if (cursor) {
|
|
171
|
+
const rest = cursor.key.slice(prefix.length);
|
|
172
|
+
const slash = rest.indexOf('/');
|
|
173
|
+
if (slash >= 0)
|
|
174
|
+
shards.add(rest.slice(0, slash));
|
|
175
|
+
cursor.continue();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
resolve([...shards]);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
req.onerror = () => reject(req.error);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async listAllDocuments(tenantId) {
|
|
185
|
+
const prefix = `${tenantId}/`;
|
|
186
|
+
const db = await __classPrivateFieldGet(this, _IndexedDBDocumentBackend_instances, "m", _IndexedDBDocumentBackend_db).call(this);
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const tx = db.transaction(IDB_STORE, 'readonly');
|
|
189
|
+
const store = tx.objectStore(IDB_STORE);
|
|
190
|
+
const range = IDBKeyRange.bound(prefix, prefix + '\uffff', false, false);
|
|
191
|
+
const req = store.openCursor(range);
|
|
192
|
+
const out = [];
|
|
193
|
+
req.onsuccess = () => {
|
|
194
|
+
const cursor = req.result;
|
|
195
|
+
if (cursor) {
|
|
196
|
+
const rest = cursor.key.slice(prefix.length);
|
|
197
|
+
const slash = rest.indexOf('/');
|
|
198
|
+
if (slash >= 0) {
|
|
199
|
+
const entry = cursor.value;
|
|
200
|
+
out.push({
|
|
201
|
+
shardId: rest.slice(0, slash),
|
|
202
|
+
path: rest.slice(slash + 1),
|
|
203
|
+
size: entry.size,
|
|
204
|
+
lastModified: entry.lastModified,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
cursor.continue();
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
resolve(out);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
req.onerror = () => reject(req.error);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
129
216
|
}
|
|
130
217
|
_IndexedDBDocumentBackend_dbPromise = new WeakMap(), _IndexedDBDocumentBackend_instances = new WeakSet(), _IndexedDBDocumentBackend_db = function _IndexedDBDocumentBackend_db() {
|
|
131
218
|
if (!__classPrivateFieldGet(this, _IndexedDBDocumentBackend_dbPromise, "f")) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from './backends';
|
|
3
|
+
describe('DocumentBackend tenant-wide primitives', () => {
|
|
4
|
+
it('listAllShards returns every shard that has content for a tenant', async () => {
|
|
5
|
+
const be = new MemoryDocumentBackend();
|
|
6
|
+
await be.write('t1', 'shard-a', 'x.txt', 'a');
|
|
7
|
+
await be.write('t1', 'shard-b', 'y.txt', 'b');
|
|
8
|
+
await be.write('t1', 'shard-b', 'nested/z.txt', 'bb');
|
|
9
|
+
await be.write('t2', 'shard-c', 'z.txt', 'c');
|
|
10
|
+
const shards = await be.listAllShards('t1');
|
|
11
|
+
expect(shards.sort()).toEqual(['shard-a', 'shard-b']);
|
|
12
|
+
const other = await be.listAllShards('t2');
|
|
13
|
+
expect(other).toEqual(['shard-c']);
|
|
14
|
+
});
|
|
15
|
+
it('listAllDocuments returns docs with shardId attached across the tenant', async () => {
|
|
16
|
+
const be = new MemoryDocumentBackend();
|
|
17
|
+
await be.write('t1', 'shard-a', 'x.txt', 'a');
|
|
18
|
+
await be.write('t1', 'shard-b', 'nested/y.txt', 'bb');
|
|
19
|
+
await be.write('t2', 'shard-c', 'z.txt', 'c');
|
|
20
|
+
const docs = await be.listAllDocuments('t1');
|
|
21
|
+
expect(docs).toHaveLength(2);
|
|
22
|
+
expect(docs.map((d) => `${d.shardId}/${d.path}`).sort()).toEqual([
|
|
23
|
+
'shard-a/x.txt',
|
|
24
|
+
'shard-b/nested/y.txt',
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
it('listAllDocuments returns an empty array for an unknown tenant', async () => {
|
|
28
|
+
const be = new MemoryDocumentBackend();
|
|
29
|
+
await be.write('t1', 'shard-a', 'x.txt', 'a');
|
|
30
|
+
expect(await be.listAllDocuments('ghost')).toEqual([]);
|
|
31
|
+
expect(await be.listAllShards('ghost')).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DocumentBackend, DocumentChange, DocumentMeta } from './types';
|
|
2
|
+
export interface BrowseCapability {
|
|
3
|
+
/** Every document in the tenant across all shards, each tagged with its owning shardId. */
|
|
4
|
+
listDocuments(): Promise<Array<DocumentMeta & {
|
|
5
|
+
shardId: string;
|
|
6
|
+
}>>;
|
|
7
|
+
/** Subscribe to tenant-wide document changes. Returns an unsubscribe. */
|
|
8
|
+
watchDocuments(callback: (change: DocumentChange) => void): () => void;
|
|
9
|
+
/** Enumerate shard ids with at least one document in the tenant. */
|
|
10
|
+
listShards(): Promise<string[]>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createBrowseCapability(tenantId: string, backend: DocumentBackend): BrowseCapability;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* BrowseCapability — tenant-wide document observation surface.
|
|
3
|
+
*
|
|
4
|
+
* Exposed on ShardContext as `ctx.browse` when the shard declares the
|
|
5
|
+
* 'documents:browse' permission. Read-only: writes still flow through
|
|
6
|
+
* the owning shard's own ctx.documents() handle.
|
|
7
|
+
*/
|
|
8
|
+
import { documentChanges } from './notifications';
|
|
9
|
+
export function createBrowseCapability(tenantId, backend) {
|
|
10
|
+
return {
|
|
11
|
+
listDocuments: () => backend.listAllDocuments(tenantId),
|
|
12
|
+
listShards: () => backend.listAllShards(tenantId),
|
|
13
|
+
watchDocuments: (callback) => documentChanges.subscribe((change) => {
|
|
14
|
+
if (change.tenantId !== tenantId)
|
|
15
|
+
return;
|
|
16
|
+
callback(change);
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from './backends';
|
|
3
|
+
import { createBrowseCapability } from './browse';
|
|
4
|
+
import { documentChanges } from './notifications';
|
|
5
|
+
describe('BrowseCapability', () => {
|
|
6
|
+
it('lists documents tenant-wide with shardId attached', async () => {
|
|
7
|
+
const be = new MemoryDocumentBackend();
|
|
8
|
+
await be.write('t1', 'a', 'x.txt', '1');
|
|
9
|
+
await be.write('t1', 'b', 'y.txt', '22');
|
|
10
|
+
const browse = createBrowseCapability('t1', be);
|
|
11
|
+
const docs = await browse.listDocuments();
|
|
12
|
+
expect(docs.map((d) => d.shardId).sort()).toEqual(['a', 'b']);
|
|
13
|
+
});
|
|
14
|
+
it('listShards enumerates tenant shards', async () => {
|
|
15
|
+
const be = new MemoryDocumentBackend();
|
|
16
|
+
await be.write('t1', 'a', 'x.txt', '1');
|
|
17
|
+
await be.write('t1', 'b', 'y.txt', '2');
|
|
18
|
+
const browse = createBrowseCapability('t1', be);
|
|
19
|
+
expect((await browse.listShards()).sort()).toEqual(['a', 'b']);
|
|
20
|
+
});
|
|
21
|
+
it('watchDocuments fires with shardId on tenant-wide emits; filters other tenants', () => {
|
|
22
|
+
const be = new MemoryDocumentBackend();
|
|
23
|
+
const browse = createBrowseCapability('t1', be);
|
|
24
|
+
const cb = vi.fn();
|
|
25
|
+
const unsub = browse.watchDocuments(cb);
|
|
26
|
+
documentChanges.emit({ type: 'create', path: 'f.txt', tenantId: 't1', shardId: 's1' });
|
|
27
|
+
documentChanges.emit({ type: 'create', path: 'g.txt', tenantId: 't2', shardId: 's2' });
|
|
28
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
29
|
+
expect(cb).toHaveBeenCalledWith(expect.objectContaining({ shardId: 's1', path: 'f.txt', tenantId: 't1' }));
|
|
30
|
+
unsub();
|
|
31
|
+
});
|
|
32
|
+
it('watchDocuments unsubscribe stops callbacks', () => {
|
|
33
|
+
const be = new MemoryDocumentBackend();
|
|
34
|
+
const browse = createBrowseCapability('t1', be);
|
|
35
|
+
const cb = vi.fn();
|
|
36
|
+
const unsub = browse.watchDocuments(cb);
|
|
37
|
+
unsub();
|
|
38
|
+
documentChanges.emit({ type: 'create', path: 'f.txt', tenantId: 't1', shardId: 's1' });
|
|
39
|
+
expect(cb).not.toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -19,4 +19,8 @@ export declare class HttpDocumentBackend implements DocumentBackend {
|
|
|
19
19
|
delete(tenantId: string, shardId: string, path: string): Promise<void>;
|
|
20
20
|
list(tenantId: string, shardId: string): Promise<DocumentMeta[]>;
|
|
21
21
|
exists(tenantId: string, shardId: string, path: string): Promise<boolean>;
|
|
22
|
+
listAllShards(tenantId: string): Promise<string[]>;
|
|
23
|
+
listAllDocuments(tenantId: string): Promise<Array<DocumentMeta & {
|
|
24
|
+
shardId: string;
|
|
25
|
+
}>>;
|
|
22
26
|
}
|
|
@@ -70,6 +70,20 @@ export class HttpDocumentBackend {
|
|
|
70
70
|
const res = await fetch(url, { method: 'HEAD' });
|
|
71
71
|
return res.ok;
|
|
72
72
|
}
|
|
73
|
+
async listAllShards(tenantId) {
|
|
74
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/_shards`;
|
|
75
|
+
const res = await fetch(url);
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
throw new Error(`listAllShards failed: ${res.status}`);
|
|
78
|
+
return res.json();
|
|
79
|
+
}
|
|
80
|
+
async listAllDocuments(tenantId) {
|
|
81
|
+
const url = `${__classPrivateFieldGet(this, _HttpDocumentBackend_baseUrl, "f")}/api/docs/${tenantId}/_all`;
|
|
82
|
+
const res = await fetch(url);
|
|
83
|
+
if (!res.ok)
|
|
84
|
+
throw new Error(`listAllDocuments failed: ${res.status}`);
|
|
85
|
+
return res.json();
|
|
86
|
+
}
|
|
73
87
|
}
|
|
74
88
|
_HttpDocumentBackend_baseUrl = new WeakMap(), _HttpDocumentBackend_apiKey = new WeakMap(), _HttpDocumentBackend_instances = new WeakSet(), _HttpDocumentBackend_authHeaders = function _HttpDocumentBackend_authHeaders() {
|
|
75
89
|
if (!__classPrivateFieldGet(this, _HttpDocumentBackend_apiKey, "f"))
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export type { SyncScope, SyncHandle, ManifestEntry, ApplyEntry, ApplyOpts, ApplyOutcome, ApplyBatchResult, ConflictPolicy, ConflictResolution, ConflictContext, JournalEntry, ChangePage, GrantRecord, } from './types';
|
|
2
2
|
export { PERMISSION_DOCUMENTS_SYNC, SYNC_RESERVED_SHARD_ID, ScopeNotGrantedError, ScopeRevokedError, TenantMismatchError, } from './types';
|
|
3
|
-
export {
|
|
4
|
-
export { getSyncBundle } from './singleton';
|
|
3
|
+
export type { SyncRegistry } from './registry';
|
|
5
4
|
export { default as SyncGrantPicker } from './components/SyncGrantPicker.svelte';
|
|
6
5
|
export { default as DocumentSyncExplorer } from './components/DocumentSyncExplorer.svelte';
|
|
@@ -6,7 +6,5 @@
|
|
|
6
6
|
* it is imported directly by SyncGrantPicker.svelte only.
|
|
7
7
|
*/
|
|
8
8
|
export { PERMISSION_DOCUMENTS_SYNC, SYNC_RESERVED_SHARD_ID, ScopeNotGrantedError, ScopeRevokedError, TenantMismatchError, } from './types';
|
|
9
|
-
export { createSyncRegistry } from './registry';
|
|
10
|
-
export { getSyncBundle } from './singleton';
|
|
11
9
|
export { default as SyncGrantPicker } from './components/SyncGrantPicker.svelte';
|
|
12
10
|
export { default as DocumentSyncExplorer } from './components/DocumentSyncExplorer.svelte';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Observer factory for ctx.syncRegistry.
|
|
3
|
+
*
|
|
4
|
+
* Returns a lazy accessor that resolves to the same SyncRegistry instance
|
|
5
|
+
* backed by the per-tenant sync bundle. Observer-class shards (e.g. the
|
|
6
|
+
* file-explorer) and connector shards share one view of grants and
|
|
7
|
+
* conflicts.
|
|
8
|
+
*
|
|
9
|
+
* Gated upstream on the 'documents:browse' permission — granting remains
|
|
10
|
+
* exclusive to <SyncGrantPicker />.
|
|
11
|
+
*/
|
|
12
|
+
import { getSyncBundle } from './singleton';
|
|
13
|
+
export function createSyncRegistryAccessor(backend, tenantId) {
|
|
14
|
+
let cached = null;
|
|
15
|
+
let initPromise = null;
|
|
16
|
+
function resolve() {
|
|
17
|
+
if (cached)
|
|
18
|
+
return Promise.resolve(cached);
|
|
19
|
+
if (!initPromise) {
|
|
20
|
+
initPromise = getSyncBundle(backend, tenantId).then(({ registry }) => {
|
|
21
|
+
cached = registry;
|
|
22
|
+
return registry;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return initPromise;
|
|
26
|
+
}
|
|
27
|
+
return () => {
|
|
28
|
+
const proxy = {
|
|
29
|
+
async list(connectorId) {
|
|
30
|
+
return (await resolve()).list(connectorId);
|
|
31
|
+
},
|
|
32
|
+
async revoke(connectorId, scope) {
|
|
33
|
+
return (await resolve()).revoke(connectorId, scope);
|
|
34
|
+
},
|
|
35
|
+
listConflicts: (async (shardId) => {
|
|
36
|
+
const r = await resolve();
|
|
37
|
+
return shardId === undefined ? r.listConflicts() : r.listConflicts(shardId);
|
|
38
|
+
}),
|
|
39
|
+
async listAllConnectorIds() {
|
|
40
|
+
return (await resolve()).listAllConnectorIds();
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return proxy;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -4,7 +4,10 @@ export declare function __grantInternal(backend: DocumentBackend, tenantId: stri
|
|
|
4
4
|
export interface SyncRegistry {
|
|
5
5
|
list(connectorId?: string): Promise<GrantRecord[]>;
|
|
6
6
|
revoke(connectorId: string, scope: SyncScope): Promise<void>;
|
|
7
|
+
/** Per-shard conflict enumeration. */
|
|
7
8
|
listConflicts(shardId: string): Promise<ConflictResolution[]>;
|
|
9
|
+
/** Tenant-wide conflict enumeration (fans out over every shard). */
|
|
10
|
+
listConflicts(): Promise<ConflictResolution[]>;
|
|
8
11
|
listAllConnectorIds(): Promise<string[]>;
|
|
9
12
|
}
|
|
10
13
|
export declare function createSyncRegistry(backend: DocumentBackend, tenantId: string): SyncRegistry;
|
|
@@ -56,7 +56,14 @@ export function createSyncRegistry(backend, tenantId) {
|
|
|
56
56
|
await writeJson(backend, tenantId, grantPath(connectorId), next);
|
|
57
57
|
},
|
|
58
58
|
async listConflicts(shardId) {
|
|
59
|
-
|
|
59
|
+
if (shardId !== undefined)
|
|
60
|
+
return conflicts.listConflicts(shardId);
|
|
61
|
+
const shards = await backend.listAllShards(tenantId);
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const s of shards) {
|
|
64
|
+
out.push(...(await conflicts.listConflicts(s)));
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
60
67
|
},
|
|
61
68
|
async listAllConnectorIds() {
|
|
62
69
|
const paths = await listJsonPaths(backend, tenantId, GRANTS_PREFIX);
|
|
@@ -39,4 +39,15 @@ describe('syncRegistry', () => {
|
|
|
39
39
|
await __grantInternal(backend, 'tenant-a', 'conn-A', { kind: 'tenant' });
|
|
40
40
|
expect((await reg.list('conn-A')).length).toBe(1);
|
|
41
41
|
});
|
|
42
|
+
it('listConflicts() with no args returns conflicts across all shards', async () => {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
// Seed conflict artifacts directly via the backend (matches
|
|
45
|
+
// ConflictManager's artifact naming).
|
|
46
|
+
await backend.write('tenant-a', 'shard-a', 'doc.md.sync-conflict-connA-111', 'remote-a');
|
|
47
|
+
await backend.write('tenant-a', 'shard-b', 'other.md.sync-conflict-connB-222', 'remote-b');
|
|
48
|
+
const all = await reg.listConflicts();
|
|
49
|
+
expect(all.map((c) => c.shardId).sort()).toEqual(['shard-a', 'shard-b']);
|
|
50
|
+
expect((_a = all.find((c) => c.shardId === 'shard-a')) === null || _a === void 0 ? void 0 : _a.path).toBe('doc.md');
|
|
51
|
+
expect((_b = all.find((c) => c.shardId === 'shard-b')) === null || _b === void 0 ? void 0 : _b.path).toBe('other.md');
|
|
52
|
+
});
|
|
42
53
|
});
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest permission string: grants tenant-wide document observation
|
|
3
|
+
* (`ctx.browse`) and sync registry visibility (`ctx.syncRegistry`).
|
|
4
|
+
* Read-only; writes still flow through the shard's own `ctx.documents()`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PERMISSION_DOCUMENTS_BROWSE = "documents:browse";
|
|
1
7
|
/**
|
|
2
8
|
* Format hint for document content. Determines whether reads return a string
|
|
3
9
|
* (`text`) or an `ArrayBuffer` (`binary`).
|
|
@@ -50,6 +56,18 @@ export interface DocumentBackend {
|
|
|
50
56
|
list(tenantId: string, shardId: string): Promise<DocumentMeta[]>;
|
|
51
57
|
/** Return true if the document at `path` exists. */
|
|
52
58
|
exists(tenantId: string, shardId: string, path: string): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* List every shard id that currently has at least one document stored
|
|
61
|
+
* for this tenant. Tenant-wide observation primitive.
|
|
62
|
+
*/
|
|
63
|
+
listAllShards(tenantId: string): Promise<string[]>;
|
|
64
|
+
/**
|
|
65
|
+
* List every document in the tenant across all shards. Each entry
|
|
66
|
+
* carries the owning `shardId`. Tenant-wide observation primitive.
|
|
67
|
+
*/
|
|
68
|
+
listAllDocuments(tenantId: string): Promise<Array<DocumentMeta & {
|
|
69
|
+
shardId: string;
|
|
70
|
+
}>>;
|
|
53
71
|
}
|
|
54
72
|
/**
|
|
55
73
|
* Shard-facing document handle returned by `ctx.documents()`. Binds
|
package/dist/documents/types.js
CHANGED
|
@@ -9,4 +9,9 @@
|
|
|
9
9
|
* The document zone is a parallel subsystem — it does not extend
|
|
10
10
|
* ZoneName or ZoneSchema. Shards access it via `ctx.documents()`.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Manifest permission string: grants tenant-wide document observation
|
|
14
|
+
* (`ctx.browse`) and sync registry visibility (`ctx.syncRegistry`).
|
|
15
|
+
* Read-only; writes still flow through the shard's own `ctx.documents()`.
|
|
16
|
+
*/
|
|
17
|
+
export const PERMISSION_DOCUMENTS_BROWSE = 'documents:browse';
|