@hraness/sponge 0.2.5 → 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 +23 -10
- package/bin/sponge.js +8 -7
- package/dist/contracts/index.js +24 -6
- package/dist/contracts/validate.js +106 -29
- package/dist/migrations/008.d.ts +3 -0
- package/dist/migrations/008.js +5 -0
- package/dist/migrations/009.d.ts +3 -0
- package/dist/migrations/009.js +8 -0
- package/dist/migrations/010.d.ts +3 -0
- package/dist/migrations/010.js +11 -0
- package/dist/migrations/index.d.ts +15 -0
- package/dist/migrations/index.js +39 -0
- package/dist/src/acquisition/capture.js +87 -28
- package/dist/src/acquisition/cleanup.d.ts +0 -4
- package/dist/src/acquisition/cleanup.js +3 -5
- package/dist/src/acquisition/errors.js +17 -2
- package/dist/src/acquisition/files.d.ts +0 -1
- package/dist/src/acquisition/files.js +14 -11
- package/dist/src/acquisition/index.d.ts +2 -3
- package/dist/src/acquisition/index.js +1 -2
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/normalize.js +57 -18
- package/dist/src/acquisition/pins.js +22 -4
- package/dist/src/acquisition/process.js +67 -29
- package/dist/src/acquisition/runtime.d.ts +5 -7
- package/dist/src/acquisition/runtime.js +129 -68
- package/dist/src/acquisition/verify-helper.js +10 -3
- package/dist/src/annotations/quote.js +40 -12
- package/dist/src/annotations/store.d.ts +0 -1
- package/dist/src/annotations/store.js +66 -14
- package/dist/src/app/article.d.ts +2 -1
- package/dist/src/app/article.js +66 -70
- package/dist/src/app/bundle-files.d.ts +0 -4
- package/dist/src/app/bundle-files.js +11 -14
- package/dist/src/app/markdown.js +18 -6
- package/dist/src/app/source.d.ts +0 -4
- package/dist/src/app/source.js +18 -14
- package/dist/src/cli/arguments.d.ts +2 -8
- package/dist/src/cli/arguments.js +67 -80
- package/dist/src/cli/commands.d.ts +42 -0
- package/dist/src/cli/commands.js +795 -0
- package/dist/src/cli/config.js +12 -9
- package/dist/src/cli/digest.d.ts +1 -1
- package/dist/src/cli/digest.js +39 -17
- package/dist/src/cli/import.js +25 -4
- package/dist/src/cli/indexing.d.ts +2 -0
- package/dist/src/cli/indexing.js +26 -9
- package/dist/src/cli/main.js +47 -386
- package/dist/src/cli/portability.d.ts +0 -2
- package/dist/src/cli/portability.js +13 -8
- package/dist/src/cli/queue.js +12 -8
- package/dist/src/cli/runtimes.d.ts +1 -3
- package/dist/src/cli/runtimes.js +13 -6
- package/dist/src/cli/terminal.js +1 -1
- package/dist/src/cli/worker.d.ts +7 -0
- package/dist/src/cli/worker.js +54 -15
- package/dist/src/derivations/kinds.d.ts +34 -0
- package/dist/src/derivations/kinds.js +14 -0
- package/dist/src/derivations/types.d.ts +48 -0
- package/dist/src/derivations/types.js +1 -0
- package/dist/src/digest/derivation.d.ts +25 -0
- package/dist/src/digest/derivation.js +124 -0
- package/dist/src/digest/editorial.d.ts +10 -46
- package/dist/src/digest/editorial.js +73 -59
- package/dist/src/digest/endpoint.js +63 -27
- package/dist/src/digest/pipeline.d.ts +53 -32
- package/dist/src/digest/pipeline.js +357 -125
- package/dist/src/digest/schema.d.ts +44 -17
- package/dist/src/digest/schema.js +132 -44
- package/dist/src/errors.d.ts +46 -0
- package/dist/src/errors.js +117 -0
- package/dist/src/identity/query-policy.d.ts +0 -5
- package/dist/src/identity/query-policy.js +61 -9
- package/dist/src/identity/uri.d.ts +6 -5
- package/dist/src/identity/uri.js +37 -18
- package/dist/src/identity/visibility.d.ts +11 -1
- package/dist/src/identity/visibility.js +81 -17
- package/dist/src/importers/apply.js +104 -36
- package/dist/src/importers/custody.js +27 -9
- package/dist/src/importers/inspect.js +125 -38
- package/dist/src/importers/jungle-digest.d.ts +31 -24
- package/dist/src/importers/jungle-digest.js +217 -115
- package/dist/src/importers/jungle-store.d.ts +20 -11
- package/dist/src/importers/jungle-store.js +136 -41
- package/dist/src/importers/jungle.d.ts +32 -16
- package/dist/src/importers/jungle.js +145 -32
- package/dist/src/importers/manifest.js +57 -21
- package/dist/src/importers/verify-helper.js +18 -4
- package/dist/src/indexing/image.js +29 -10
- package/dist/src/indexing/index.d.ts +37 -6
- package/dist/src/indexing/index.js +360 -124
- package/dist/src/indexing/jobs.js +156 -47
- package/dist/src/indexing/prune-run.d.ts +2 -0
- package/dist/src/indexing/prune-run.js +69 -26
- package/dist/src/indexing/prune.d.ts +12 -3
- package/dist/src/indexing/prune.js +85 -35
- package/dist/src/indexing/runner.d.ts +2 -1
- package/dist/src/indexing/runner.js +83 -40
- package/dist/src/indexing/upgrade.d.ts +9 -0
- package/dist/src/indexing/upgrade.js +79 -0
- package/dist/src/inference/asr/bridge.js +6 -2
- package/dist/src/inference/asr/protocol.js +109 -19
- package/dist/src/inference/asr/runtime.js +58 -21
- package/dist/src/inference/asr/windows.js +9 -5
- package/dist/src/inference/embedding/index.d.ts +2 -6
- package/dist/src/inference/embedding/index.js +89 -29
- package/dist/src/ingest/attempt.d.ts +29 -0
- package/dist/src/ingest/attempt.js +55 -0
- package/dist/src/media/capture-helper.js +7 -2
- package/dist/src/media/capture.d.ts +2 -1
- package/dist/src/media/capture.js +124 -43
- package/dist/src/media/handler.d.ts +4 -2
- package/dist/src/media/handler.js +88 -70
- package/dist/src/media/transcribe.js +63 -21
- package/dist/src/metadata/article.js +49 -17
- package/dist/src/models/artifacts.d.ts +10 -8
- package/dist/src/models/artifacts.js +237 -106
- package/dist/src/models/catalog.d.ts +2 -5
- package/dist/src/models/catalog.js +94 -25
- package/dist/src/models/download.js +55 -15
- package/dist/src/models/runtime.d.ts +0 -2
- package/dist/src/models/runtime.js +69 -33
- package/dist/src/pdf/capture-helper.js +3 -1
- package/dist/src/pdf/capture.d.ts +2 -1
- package/dist/src/pdf/capture.js +95 -27
- package/dist/src/pdf/errors.js +12 -1
- package/dist/src/pdf/handler.d.ts +2 -1
- package/dist/src/pdf/handler.js +70 -71
- package/dist/src/pdf/index.d.ts +1 -3
- package/dist/src/pdf/index.js +0 -2
- package/dist/src/pdf/input.js +12 -7
- package/dist/src/pdf/normalize.js +80 -21
- package/dist/src/pdf/source-identity.js +3 -2
- package/dist/src/pdf/source.js +12 -8
- package/dist/src/pdf/verify-helper.js +15 -3
- package/dist/src/portability/backup.d.ts +2 -9
- package/dist/src/portability/backup.js +108 -22
- package/dist/src/portability/database.js +51 -17
- package/dist/src/portability/export.js +35 -9
- package/dist/src/portability/files.d.ts +1 -4
- package/dist/src/portability/files.js +127 -67
- package/dist/src/portability/gc.js +65 -22
- package/dist/src/publish/component.js +1 -1
- package/dist/src/publish/feed.d.ts +2 -2
- package/dist/src/publish/feed.js +75 -29
- package/dist/src/publish/identity.d.ts +5 -1
- package/dist/src/publish/identity.js +31 -11
- package/dist/src/publish/index.d.ts +2 -3
- package/dist/src/publish/index.js +34 -21
- package/dist/src/publish/projection.d.ts +2 -9
- package/dist/src/publish/projection.js +21 -14
- package/dist/src/search/extension.js +3 -1
- package/dist/src/search/hybrid.d.ts +3 -8
- package/dist/src/search/hybrid.js +45 -11
- package/dist/src/search/lexical.js +18 -4
- package/dist/src/search/query.js +56 -25
- package/dist/src/search/vector.js +37 -10
- package/dist/src/service/launch-agent.d.ts +5 -3
- package/dist/src/service/launch-agent.js +272 -83
- package/dist/src/service/lifetime.js +47 -14
- package/dist/src/service/loop.d.ts +3 -3
- package/dist/src/service/loop.js +47 -19
- package/dist/src/store/annotations.d.ts +32 -0
- package/dist/src/store/annotations.js +44 -0
- package/dist/src/store/backups.d.ts +18 -0
- package/dist/src/store/backups.js +110 -0
- package/dist/src/store/blobs.d.ts +24 -0
- package/dist/src/store/blobs.js +139 -0
- package/dist/src/store/changes.d.ts +5 -12
- package/dist/src/store/changes.js +108 -56
- package/dist/src/store/context.d.ts +15 -0
- package/dist/src/store/context.js +19 -0
- package/dist/src/store/derivations.d.ts +42 -0
- package/dist/src/store/derivations.js +104 -0
- package/dist/src/store/files.d.ts +0 -2
- package/dist/src/store/files.js +19 -24
- package/dist/src/store/ids.d.ts +30 -0
- package/dist/src/store/ids.js +34 -0
- package/dist/src/store/imports.d.ts +26 -0
- package/dist/src/store/imports.js +57 -0
- package/dist/src/store/index.d.ts +3 -186
- package/dist/src/store/index.js +2 -809
- package/dist/src/store/lease.d.ts +20 -0
- package/dist/src/store/lease.js +62 -0
- package/dist/src/store/library.d.ts +75 -0
- package/dist/src/store/library.js +174 -0
- package/dist/src/store/open.d.ts +7 -0
- package/dist/src/store/open.js +183 -0
- package/dist/src/store/queue.d.ts +65 -0
- package/dist/src/store/queue.js +327 -0
- package/dist/src/store/revisions.d.ts +12 -0
- package/dist/src/store/revisions.js +189 -0
- package/dist/src/store/search-index.d.ts +63 -0
- package/dist/src/store/search-index.js +190 -0
- package/dist/src/store/sync-ready.d.ts +6 -0
- package/dist/src/store/sync-ready.js +121 -0
- package/dist/src/store/types.d.ts +1 -5
- package/dist/src/store/types.js +1 -8
- package/dist/src/util/canonical.d.ts +2 -0
- package/dist/src/util/canonical.js +17 -0
- package/dist/src/util/exact.d.ts +2 -0
- package/dist/src/util/exact.js +9 -0
- package/dist/src/util/fsync.d.ts +4 -0
- package/dist/src/util/fsync.js +22 -0
- package/dist/src/util/hash.d.ts +4 -0
- package/dist/src/util/hash.js +8 -0
- package/dist/src/worker/basic.d.ts +0 -1
- package/dist/src/worker/basic.js +23 -7
- package/dist/src/worker/custody.d.ts +4 -1
- package/dist/src/worker/custody.js +111 -38
- package/dist/src/worker/engine.d.ts +2 -1
- package/dist/src/worker/engine.js +42 -20
- package/dist/src/worker/identity.js +13 -5
- package/dist/src/worker/journal.d.ts +1 -1
- package/dist/src/worker/journal.js +54 -24
- package/dist/src/worker/lock.d.ts +18 -0
- package/dist/src/worker/lock.js +196 -0
- package/dist/src/worker/runtime-gc.d.ts +4 -3
- package/dist/src/worker/runtime-gc.js +68 -41
- package/dist/src/worker/supervisor-child.js +19 -6
- package/dist/src/worker/supervisor.js +41 -8
- package/native/runtime-manifest.json +4 -4
- package/package.json +10 -2
- package/dist/src/digest/store.d.ts +0 -57
- package/dist/src/digest/store.js +0 -77
- package/dist/src/store/foundation.d.ts +0 -7
- package/dist/src/store/foundation.js +0 -7
|
@@ -4,13 +4,46 @@
|
|
|
4
4
|
* Shareable URIs also drop `clickTrackers` and every credential-like field, so a published URI
|
|
5
5
|
* never carries a capability. See docs/decisions/sync-and-publishing.md.
|
|
6
6
|
*/
|
|
7
|
-
export const campaignFields = new Set([
|
|
8
|
-
|
|
7
|
+
export const campaignFields = new Set([
|
|
8
|
+
'utm_source',
|
|
9
|
+
'utm_medium',
|
|
10
|
+
'utm_campaign',
|
|
11
|
+
'utm_term',
|
|
12
|
+
'utm_content',
|
|
13
|
+
'utm_id',
|
|
14
|
+
]);
|
|
15
|
+
export const clickTrackers = new Set([
|
|
16
|
+
'fbclid',
|
|
17
|
+
'gclid',
|
|
18
|
+
'dclid',
|
|
19
|
+
'msclkid',
|
|
20
|
+
'mc_cid',
|
|
21
|
+
'mc_eid',
|
|
22
|
+
'igshid',
|
|
23
|
+
]);
|
|
9
24
|
/** Credential-like query keys, compared case-insensitively. Presence makes a capture private by default. */
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
const secretFields = new Set([
|
|
26
|
+
'token',
|
|
27
|
+
'key',
|
|
28
|
+
'sig',
|
|
29
|
+
'signature',
|
|
30
|
+
'auth',
|
|
31
|
+
'access_token',
|
|
32
|
+
'code',
|
|
33
|
+
'rlkey',
|
|
34
|
+
'session',
|
|
35
|
+
'password',
|
|
36
|
+
'secret',
|
|
37
|
+
'api_key',
|
|
38
|
+
'apikey',
|
|
39
|
+
'auth_token',
|
|
40
|
+
'id_token',
|
|
41
|
+
'refresh_token',
|
|
42
|
+
'client_secret',
|
|
43
|
+
]);
|
|
44
|
+
const secretPrefixes = ['x-amz-', 'x-goog-'];
|
|
12
45
|
/** The decoded, lowercased key of one raw `a=b` field, or undefined when it does not decode. */
|
|
13
|
-
|
|
46
|
+
function fieldKey(field) {
|
|
14
47
|
try {
|
|
15
48
|
return decodeURIComponent(field.split('=', 1)[0].replaceAll('+', ' ')).toLowerCase();
|
|
16
49
|
}
|
|
@@ -18,12 +51,20 @@ export function fieldKey(field) {
|
|
|
18
51
|
return undefined;
|
|
19
52
|
}
|
|
20
53
|
}
|
|
21
|
-
export function isSecretField(key) {
|
|
54
|
+
export function isSecretField(key) {
|
|
55
|
+
return secretFields.has(key) || secretPrefixes.some((p) => key.startsWith(p));
|
|
56
|
+
}
|
|
22
57
|
/** Filter raw fields: URLSearchParams mutation rewrites unknown signed query bytes. */
|
|
23
58
|
export function filterQuery(search, drop) {
|
|
24
59
|
if (!search)
|
|
25
60
|
return '';
|
|
26
|
-
const fields = search
|
|
61
|
+
const fields = search
|
|
62
|
+
.slice(1)
|
|
63
|
+
.split('&')
|
|
64
|
+
.filter((field) => {
|
|
65
|
+
const key = fieldKey(field);
|
|
66
|
+
return key === undefined || !drop(key);
|
|
67
|
+
});
|
|
27
68
|
return fields.length ? '?' + fields.join('&') : '';
|
|
28
69
|
}
|
|
29
70
|
/**
|
|
@@ -42,11 +83,22 @@ export function isCapabilityUrl(locator) {
|
|
|
42
83
|
return false;
|
|
43
84
|
if (url.username || url.password)
|
|
44
85
|
return true;
|
|
45
|
-
if (url.search &&
|
|
86
|
+
if (url.search &&
|
|
87
|
+
url.search
|
|
88
|
+
.slice(1)
|
|
89
|
+
.split('&')
|
|
90
|
+
.some((field) => {
|
|
91
|
+
const key = fieldKey(field);
|
|
92
|
+
return key !== undefined && isSecretField(key);
|
|
93
|
+
}))
|
|
46
94
|
return true;
|
|
47
95
|
const host = url.hostname.replace(/^www\./, ''), path = url.pathname;
|
|
48
96
|
// Google Docs, Sheets, Slides, Forms, Drawings and Drive files shared "anyone with the link".
|
|
49
|
-
if ((host === 'docs.google.com' &&
|
|
97
|
+
if (((host === 'docs.google.com' &&
|
|
98
|
+
/^\/(document|spreadsheets|presentation|forms|drawings)\/d\/[^/]+/.test(path) &&
|
|
99
|
+
!/^\/[^/]+\/d\/e\//.test(path)) ||
|
|
100
|
+
(host === 'drive.google.com' && /^\/(file\/d|drive\/folders)\/[^/]+/.test(path))) &&
|
|
101
|
+
url.searchParams.get('usp') === 'sharing')
|
|
50
102
|
return true;
|
|
51
103
|
// Dropbox shared links are capabilities with or without rlkey.
|
|
52
104
|
if ((host === 'dropbox.com' || host === 'dl.dropboxusercontent.com') && /^\/(s|sh|scl)\//.test(path))
|
|
@@ -3,15 +3,13 @@ export interface ItemUri {
|
|
|
3
3
|
uri: string;
|
|
4
4
|
role: 'primary' | 'alias';
|
|
5
5
|
resolvable: boolean;
|
|
6
|
-
origin: 'locator' | 'revision';
|
|
6
|
+
origin: 'locator' | 'revision' | 'metadata' | 'user';
|
|
7
7
|
}
|
|
8
8
|
/** `capability`: a public-realm or imported link that grants access by itself (see query-policy.ts). */
|
|
9
9
|
export type AccessClass = 'public' | 'account' | 'local' | 'imported' | 'capability';
|
|
10
10
|
export declare const maxUriBytes = 8192;
|
|
11
11
|
/** Same-work identity drops fragments, click trackers and credential-like fields that the source key keeps for capture fidelity. */
|
|
12
12
|
export declare function shareableWebUri(locator: string): string;
|
|
13
|
-
/** Provider identifiers name the same work across URL spellings, hosts and mirrors. */
|
|
14
|
-
export declare function providerUris(href: string): string[];
|
|
15
13
|
export declare function isResolvable(uri: string): boolean;
|
|
16
14
|
/**
|
|
17
15
|
* Derive an item's URIs from its first source and, once captured, its source PDF. Total: a
|
|
@@ -24,7 +22,10 @@ export declare function deriveUris(input: {
|
|
|
24
22
|
locator: string;
|
|
25
23
|
sourcePdfSha256?: string;
|
|
26
24
|
}): ItemUri[];
|
|
27
|
-
/**
|
|
28
|
-
|
|
25
|
+
/**
|
|
26
|
+
* How the first source was reached decides the default visibility; see docs/decisions/sync-and-publishing.md.
|
|
27
|
+
* An imported local file with a recorded web address (`webUri`, origin 'metadata') is classed by that address instead.
|
|
28
|
+
*/
|
|
29
|
+
export declare function accessClass(realm: string, locator: string, webUri?: string): AccessClass;
|
|
29
30
|
/** Public only when the source was public. Imports keep their source's publication decision. */
|
|
30
31
|
export declare function defaultVisibility(access: AccessClass, importedPrivacy?: string): 'public' | 'private';
|
package/dist/src/identity/uri.js
CHANGED
|
@@ -6,21 +6,27 @@ export function shareableWebUri(locator) {
|
|
|
6
6
|
const url = new URL(normalizeWebLocator(locator));
|
|
7
7
|
url.hash = '';
|
|
8
8
|
if (url.search)
|
|
9
|
-
url.search = filterQuery(url.search, key => clickTrackers.has(key) || isSecretField(key));
|
|
9
|
+
url.search = filterQuery(url.search, (key) => clickTrackers.has(key) || isSecretField(key));
|
|
10
10
|
return url.href;
|
|
11
11
|
}
|
|
12
12
|
const youtubeId = /^[A-Za-z0-9_-]{11}$/;
|
|
13
|
-
const decoded = (value) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const decoded = (value) => {
|
|
14
|
+
try {
|
|
15
|
+
return decodeURIComponent(value);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
19
21
|
/** Provider identifiers name the same work across URL spellings, hosts and mirrors. */
|
|
20
|
-
|
|
22
|
+
function providerUris(href) {
|
|
21
23
|
const url = new URL(href), host = url.hostname.replace(/^(www|m|mobile)\./, ''), parts = url.pathname.split('/').filter(Boolean), out = [];
|
|
22
24
|
if (host === 'youtube.com' || host === 'music.youtube.com') {
|
|
23
|
-
const id = url.pathname === '/watch'
|
|
25
|
+
const id = url.pathname === '/watch'
|
|
26
|
+
? url.searchParams.get('v')
|
|
27
|
+
: ['shorts', 'live', 'embed'].includes(parts[0])
|
|
28
|
+
? parts[1]
|
|
29
|
+
: undefined;
|
|
24
30
|
if (id && youtubeId.test(id))
|
|
25
31
|
out.push('youtube:' + id);
|
|
26
32
|
}
|
|
@@ -29,7 +35,11 @@ export function providerUris(href) {
|
|
|
29
35
|
if ((host === 'x.com' || host === 'twitter.com') && parts[1] === 'status' && /^\d{1,25}$/.test(parts[2] ?? ''))
|
|
30
36
|
out.push('x:status/' + parts[2]);
|
|
31
37
|
if (host === 'arxiv.org' && ['abs', 'pdf'].includes(parts[0]) && parts[1]) {
|
|
32
|
-
const id = parts
|
|
38
|
+
const id = parts
|
|
39
|
+
.slice(1)
|
|
40
|
+
.join('/')
|
|
41
|
+
.replace(/\.pdf$/, '')
|
|
42
|
+
.replace(/v\d+$/, '');
|
|
33
43
|
if (/^(\d{4}\.\d{4,5}|[a-z-]+(\.[A-Z]{2})?\/\d{7})$/.test(id))
|
|
34
44
|
out.push('arxiv:' + id);
|
|
35
45
|
}
|
|
@@ -40,7 +50,9 @@ export function providerUris(href) {
|
|
|
40
50
|
}
|
|
41
51
|
return out;
|
|
42
52
|
}
|
|
43
|
-
export function isResolvable(uri) {
|
|
53
|
+
export function isResolvable(uri) {
|
|
54
|
+
return /^(https?:|doi:|arxiv:|isbn:|youtube:|x:)/.test(uri);
|
|
55
|
+
}
|
|
44
56
|
/**
|
|
45
57
|
* Derive an item's URIs from its first source and, once captured, its source PDF. Total: a
|
|
46
58
|
* locator that cannot be parsed, carries userinfo or is too long yields only `sponge:item/<id>`.
|
|
@@ -48,8 +60,10 @@ export function isResolvable(uri) { return /^(https?:|doi:|arxiv:|isbn:|youtube:
|
|
|
48
60
|
* `urn:sha256:<file hash>`, which matches the same file anywhere but is not resolvable.
|
|
49
61
|
*/
|
|
50
62
|
export function deriveUris(input) {
|
|
51
|
-
const out = [], add = (uri, origin) => {
|
|
52
|
-
|
|
63
|
+
const out = [], add = (uri, origin) => {
|
|
64
|
+
if (uri.length >= 3 && Buffer.byteLength(uri) <= maxUriBytes && !out.some((u) => u.uri === uri))
|
|
65
|
+
out.push({ uri, role: out.length ? 'alias' : 'primary', resolvable: isResolvable(uri), origin });
|
|
66
|
+
};
|
|
53
67
|
if (/^https?:/i.test(input.locator)) {
|
|
54
68
|
let web;
|
|
55
69
|
try {
|
|
@@ -64,7 +78,9 @@ export function deriveUris(input) {
|
|
|
64
78
|
for (const uri of providerUris(web))
|
|
65
79
|
add(uri, 'locator');
|
|
66
80
|
}
|
|
67
|
-
catch {
|
|
81
|
+
catch {
|
|
82
|
+
/* a provider alias is optional */
|
|
83
|
+
}
|
|
68
84
|
}
|
|
69
85
|
}
|
|
70
86
|
if (input.sourcePdfSha256)
|
|
@@ -73,10 +89,13 @@ export function deriveUris(input) {
|
|
|
73
89
|
add('sponge:item/' + input.itemId, 'locator');
|
|
74
90
|
return out;
|
|
75
91
|
}
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
/**
|
|
93
|
+
* How the first source was reached decides the default visibility; see docs/decisions/sync-and-publishing.md.
|
|
94
|
+
* An imported local file with a recorded web address (`webUri`, origin 'metadata') is classed by that address instead.
|
|
95
|
+
*/
|
|
96
|
+
export function accessClass(realm, locator, webUri) {
|
|
97
|
+
if (/^(file|local):/i.test(locator))
|
|
98
|
+
return realm.startsWith('import:') && webUri && /^https?:/i.test(webUri) ? accessClass(realm, webUri) : 'local';
|
|
80
99
|
if (realm.startsWith('connection:'))
|
|
81
100
|
return 'account';
|
|
82
101
|
// A guard on top of the source rule: a link that is itself a credential is never public by default.
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import { type AccessClass, type ItemUri } from './uri.js';
|
|
3
3
|
export type Visibility = 'public' | 'private';
|
|
4
|
+
/** The recorded web address of a capture that has none of its own (origin 'metadata', primary), if any. */
|
|
5
|
+
export declare function metadataUri(db: DatabaseSync, itemId: string): string | undefined;
|
|
6
|
+
/** Whether the capture itself (its locator or revision) yields a web URI. */
|
|
7
|
+
export declare function hasWebUri(db: DatabaseSync, itemId: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Record `uri` (already a shareable web URI) as the primary of a capture without a web URI of its own; derived URIs become
|
|
10
|
+
* aliases and a previously recorded primary is replaced. Idempotent. Derived state like every item URI, not a logged field.
|
|
11
|
+
*/
|
|
12
|
+
export declare function setMetadataUri(db: DatabaseSync, itemId: string, uri: string): void;
|
|
4
13
|
/** Recompute derived URIs; metadata and user URIs are left alone. Idempotent. */
|
|
5
14
|
export declare function syncUris(db: DatabaseSync, itemId: string): void;
|
|
6
15
|
/**
|
|
@@ -20,5 +29,6 @@ export interface VisibilityState {
|
|
|
20
29
|
override: Visibility | null;
|
|
21
30
|
effective: Visibility;
|
|
22
31
|
}
|
|
23
|
-
|
|
32
|
+
/** `webUri` previews a metadata URI not yet recorded; by default the recorded one counts. */
|
|
33
|
+
export declare function visibility(db: DatabaseSync, itemId: string, webUri?: string | undefined): VisibilityState;
|
|
24
34
|
export declare function setVisibility(db: DatabaseSync, itemId: string, value: Visibility | null): VisibilityState;
|
|
@@ -1,24 +1,71 @@
|
|
|
1
|
-
import { accessClass, defaultVisibility, deriveUris } from './uri.js';
|
|
1
|
+
import { accessClass, defaultVisibility, deriveUris, maxUriBytes } from './uri.js';
|
|
2
2
|
import { mutate } from '../store/changes.js';
|
|
3
|
-
import { StoreError } from '../store/types.js';
|
|
4
3
|
import { text } from '../store/files.js';
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { SpongeError } from '../errors.js';
|
|
5
|
+
const fail = (code = 'invalid-input') => {
|
|
6
|
+
throw new SpongeError(code);
|
|
7
|
+
};
|
|
8
|
+
function firstSource(db, itemId) {
|
|
9
|
+
return db.prepare('SELECT realm_key,locator FROM source_refs WHERE item_id=? ORDER BY rowid LIMIT 1').get(itemId);
|
|
10
|
+
}
|
|
11
|
+
/** The recorded web address of a capture that has none of its own (origin 'metadata', primary), if any. */
|
|
12
|
+
export function metadataUri(db, itemId) {
|
|
13
|
+
const r = db
|
|
14
|
+
.prepare("SELECT uri FROM item_uris WHERE item_id=? AND origin='metadata' AND role='primary'")
|
|
15
|
+
.get(itemId);
|
|
16
|
+
return r && /^https?:/.test(String(r.uri)) ? String(r.uri) : undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Whether the capture itself (its locator or revision) yields a web URI. */
|
|
19
|
+
export function hasWebUri(db, itemId) {
|
|
20
|
+
return !!db
|
|
21
|
+
.prepare("SELECT 1 FROM item_uris WHERE item_id=? AND origin IN ('locator','revision') AND (uri LIKE 'http:%' OR uri LIKE 'https:%')")
|
|
22
|
+
.get(itemId);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Record `uri` (already a shareable web URI) as the primary of a capture without a web URI of its own; derived URIs become
|
|
26
|
+
* aliases and a previously recorded primary is replaced. Idempotent. Derived state like every item URI, not a logged field.
|
|
27
|
+
*/
|
|
28
|
+
export function setMetadataUri(db, itemId, uri) {
|
|
29
|
+
text(uri, maxUriBytes);
|
|
30
|
+
if (!/^https?:/.test(uri) || hasWebUri(db, itemId))
|
|
31
|
+
fail();
|
|
32
|
+
if (metadataUri(db, itemId) === uri)
|
|
33
|
+
return;
|
|
34
|
+
db.prepare("DELETE FROM item_uris WHERE item_id=? AND origin='metadata' AND role='primary'").run(itemId);
|
|
35
|
+
db.prepare("UPDATE item_uris SET role='alias' WHERE item_id=?").run(itemId);
|
|
36
|
+
db.prepare("INSERT INTO item_uris VALUES(?,?,'primary',1,'metadata') ON CONFLICT(item_id,uri) DO UPDATE SET role='primary',resolvable=1,origin='metadata'").run(itemId, uri);
|
|
37
|
+
}
|
|
7
38
|
/** Recompute derived URIs; metadata and user URIs are left alone. Idempotent. */
|
|
8
39
|
export function syncUris(db, itemId) {
|
|
9
40
|
const source = firstSource(db, itemId);
|
|
10
41
|
if (!source)
|
|
11
42
|
return;
|
|
12
|
-
const pdf = db
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
43
|
+
const pdf = db
|
|
44
|
+
.prepare("SELECT ra.sha256 FROM items i JOIN revision_assets ra ON ra.revision_id=i.current_revision WHERE i.id=? AND ra.relationship='source-pdf' ORDER BY ra.ordinal LIMIT 1")
|
|
45
|
+
.get(itemId);
|
|
46
|
+
const uris = deriveUris({
|
|
47
|
+
itemId,
|
|
48
|
+
locator: String(source.locator),
|
|
49
|
+
...(pdf ? { sourcePdfSha256: String(pdf.sha256) } : {}),
|
|
50
|
+
});
|
|
51
|
+
// A capture without a web URI keeps a recorded web address as its primary; its derived URIs are then aliases.
|
|
52
|
+
const kept = !uris.some((u) => /^https?:/.test(u.uri)) && metadataUri(db, itemId), rows = kept ? uris.map((u) => ({ ...u, role: 'alias' })) : uris;
|
|
53
|
+
const current = db
|
|
54
|
+
.prepare("SELECT uri,role,resolvable,origin FROM item_uris WHERE item_id=? AND origin IN ('locator','revision') ORDER BY uri")
|
|
55
|
+
.all(itemId)
|
|
56
|
+
.map((r) => `${r.uri}|${r.role}|${r.resolvable}|${r.origin}`)
|
|
57
|
+
.join('\n');
|
|
58
|
+
const next = rows
|
|
59
|
+
.map((u) => `${u.uri}|${u.role}|${Number(u.resolvable)}|${u.origin}`)
|
|
60
|
+
.sort()
|
|
61
|
+
.join('\n');
|
|
16
62
|
if (current === next)
|
|
17
63
|
return;
|
|
18
64
|
db.prepare("DELETE FROM item_uris WHERE item_id=? AND origin IN ('locator','revision')").run(itemId);
|
|
19
65
|
// A derived primary replaces any other primary; keep later metadata/user URIs as aliases.
|
|
20
|
-
|
|
21
|
-
|
|
66
|
+
if (!kept)
|
|
67
|
+
db.prepare("UPDATE item_uris SET role='alias' WHERE item_id=?").run(itemId);
|
|
68
|
+
for (const u of rows)
|
|
22
69
|
db.prepare('INSERT INTO item_uris VALUES(?,?,?,?,?) ON CONFLICT(item_id,uri) DO UPDATE SET role=excluded.role,resolvable=excluded.resolvable,origin=excluded.origin').run(itemId, u.uri, u.role, Number(u.resolvable), u.origin);
|
|
23
70
|
}
|
|
24
71
|
/**
|
|
@@ -26,7 +73,10 @@ export function syncUris(db, itemId) {
|
|
|
26
73
|
* one hostile locator leaves that item without URIs instead of making the library unopenable.
|
|
27
74
|
*/
|
|
28
75
|
export function backfillUris(db) {
|
|
29
|
-
const missing = db
|
|
76
|
+
const missing = db
|
|
77
|
+
.prepare("SELECT i.id FROM items i WHERE NOT EXISTS (SELECT 1 FROM item_uris u WHERE u.item_id=i.id AND u.role='primary') AND EXISTS (SELECT 1 FROM source_refs r WHERE r.item_id=i.id)")
|
|
78
|
+
.all()
|
|
79
|
+
.map((r) => String(r.id));
|
|
30
80
|
let skipped = 0;
|
|
31
81
|
for (const id of missing) {
|
|
32
82
|
db.exec('SAVEPOINT backfill_uri');
|
|
@@ -42,26 +92,40 @@ export function backfillUris(db) {
|
|
|
42
92
|
return { filled: missing.length - skipped, skipped };
|
|
43
93
|
}
|
|
44
94
|
export function itemUris(db, itemId) {
|
|
45
|
-
return db
|
|
95
|
+
return db
|
|
96
|
+
.prepare("SELECT uri,role,resolvable,origin FROM item_uris WHERE item_id=? ORDER BY role='alias',uri")
|
|
97
|
+
.all(itemId)
|
|
98
|
+
.map((r) => ({
|
|
99
|
+
uri: String(r.uri),
|
|
100
|
+
role: r.role,
|
|
101
|
+
resolvable: !!r.resolvable,
|
|
102
|
+
origin: r.origin,
|
|
103
|
+
}));
|
|
46
104
|
}
|
|
47
105
|
/** Items sharing any URI with `uri`: the local half of "who else is reading this". */
|
|
48
106
|
export function itemsForUri(db, uri) {
|
|
49
107
|
text(uri, 8192);
|
|
50
|
-
return db
|
|
108
|
+
return db
|
|
109
|
+
.prepare('SELECT DISTINCT u.item_id FROM item_uris u JOIN items i ON i.id=u.item_id WHERE u.uri=? AND i.deleted_at IS NULL ORDER BY u.item_id')
|
|
110
|
+
.all(uri)
|
|
111
|
+
.map((r) => String(r.item_id));
|
|
51
112
|
}
|
|
52
|
-
|
|
113
|
+
/** `webUri` previews a metadata URI not yet recorded; by default the recorded one counts. */
|
|
114
|
+
export function visibility(db, itemId, webUri = metadataUri(db, itemId)) {
|
|
53
115
|
const item = db.prepare('SELECT visibility,deleted_at FROM items WHERE id=?').get(itemId) ?? fail('unavailable'), source = firstSource(db, itemId);
|
|
54
|
-
const access = source ? accessClass(String(source.realm_key), String(source.locator)) : 'local';
|
|
116
|
+
const access = source ? accessClass(String(source.realm_key), String(source.locator), webUri) : 'local';
|
|
55
117
|
const reading = db.prepare('SELECT state_json FROM reading_state WHERE item_id=?').get(itemId);
|
|
56
118
|
let importedPrivacy;
|
|
57
119
|
try {
|
|
58
|
-
importedPrivacy = reading
|
|
120
|
+
importedPrivacy = reading
|
|
121
|
+
? JSON.parse(String(reading.state_json))?.sourcePrivacy
|
|
122
|
+
: undefined;
|
|
59
123
|
}
|
|
60
124
|
catch {
|
|
61
125
|
importedPrivacy = undefined;
|
|
62
126
|
}
|
|
63
127
|
const base = defaultVisibility(access, importedPrivacy), override = (item.visibility ?? null);
|
|
64
|
-
return { access, default: base, override, effective: item.deleted_at ? 'private' : override ?? base };
|
|
128
|
+
return { access, default: base, override, effective: item.deleted_at ? 'private' : (override ?? base) };
|
|
65
129
|
}
|
|
66
130
|
export function setVisibility(db, itemId, value) {
|
|
67
131
|
if (value !== null && value !== 'public' && value !== 'private')
|
|
@@ -1,62 +1,108 @@
|
|
|
1
|
-
import { hash } from '../
|
|
2
|
-
import {
|
|
1
|
+
import { sha256 as hash } from '../util/hash.js';
|
|
2
|
+
import { runAttempt } from '../ingest/attempt.js';
|
|
3
3
|
import { bundleBytes, bundleText } from '../app/bundle-files.js';
|
|
4
4
|
import { localizeMarkdown } from '../app/markdown.js';
|
|
5
5
|
import { selectedMetadata } from '../metadata/article.js';
|
|
6
6
|
import { drainQueue } from '../worker/engine.js';
|
|
7
7
|
import { readImportManifest, importBundleRoot } from './manifest.js';
|
|
8
8
|
import { inspectImportBundle, releasedBundleVerifier } from './inspect.js';
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { SpongeError, publicCode } from '../errors.js';
|
|
10
|
+
const errorCode = (error) => {
|
|
11
|
+
const code = publicCode(error);
|
|
12
|
+
return [
|
|
13
|
+
'unsupported',
|
|
14
|
+
'invalid-input',
|
|
15
|
+
'integrity',
|
|
16
|
+
'limit-exceeded',
|
|
17
|
+
'cancelled',
|
|
18
|
+
'cleanup-required',
|
|
19
|
+
'runtime-missing',
|
|
20
|
+
'owner-busy',
|
|
21
|
+
'unavailable',
|
|
22
|
+
].includes(code)
|
|
23
|
+
? code
|
|
24
|
+
: 'unclassified';
|
|
25
|
+
};
|
|
26
|
+
function promote(context, bundle, namespace, entry, sourceHash) {
|
|
11
27
|
const { store, attempt, signal, checkpoint } = context;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const source = store.sourceForJob(attempt.jobId);
|
|
28
|
+
return runAttempt(store, attempt, errorCode, async () => {
|
|
29
|
+
const source = store.queue.source(attempt.jobId);
|
|
16
30
|
if (!source)
|
|
17
|
-
throw
|
|
31
|
+
throw new SpongeError('integrity');
|
|
18
32
|
await checkpoint('extracting');
|
|
19
33
|
const markdown = await bundleText(bundle.root, bundle.markdown), localized = localizeMarkdown(bundle.kind === 'pdf' ? markdown.replace(/^<!-- pdf-page: ([1-9][0-9]*) -->$/gm, '## Page $1') : markdown, bundle.assets), assets = [];
|
|
20
34
|
for (const asset of bundle.assets) {
|
|
21
35
|
if (signal.aborted)
|
|
22
|
-
throw
|
|
23
|
-
const stored = await store.
|
|
36
|
+
throw new SpongeError('cancelled');
|
|
37
|
+
const stored = await store.blobs.put(attempt, asset.sha256, asset.bytes, bundleBytes(bundle.root, asset));
|
|
24
38
|
assets.push({ ...stored, mediaType: asset.mediaType, relationship: asset.relationship });
|
|
25
39
|
}
|
|
26
40
|
if (signal.aborted)
|
|
27
|
-
throw
|
|
41
|
+
throw new SpongeError('cancelled');
|
|
28
42
|
await checkpoint('promoting');
|
|
29
43
|
const title = selectedMetadata(bundle.metadata, 'title')?.value;
|
|
30
|
-
store.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
store.revisions.commit({
|
|
45
|
+
...attempt,
|
|
46
|
+
itemId: source.itemId,
|
|
47
|
+
bundleHash: importBundleHash(sourceHash),
|
|
48
|
+
markdown: localized.markdown,
|
|
49
|
+
title: typeof title === 'string' ? title : undefined,
|
|
50
|
+
extractor: { name: 'sponge-wordcell-import', version: '1' },
|
|
51
|
+
status: bundle.status === 'partial' || localized.missingImages ? 'partial' : 'complete',
|
|
52
|
+
metadata: [
|
|
53
|
+
...bundle.metadata,
|
|
54
|
+
...Object.entries({ importNamespace: namespace, externalSourceId: entry.id, sourcePrivacy: entry.privacy }).map(([field, value]) => ({
|
|
55
|
+
field,
|
|
56
|
+
value,
|
|
57
|
+
source: bundle.kind === 'pdf' ? 'pdf' : 'html',
|
|
58
|
+
observedAt: new Date().toISOString(),
|
|
59
|
+
rule: 'sponge-import-v1',
|
|
60
|
+
})),
|
|
61
|
+
],
|
|
62
|
+
assets,
|
|
63
|
+
warnings: bundle.status === 'partial' || localized.missingImages ? ['partial'] : [],
|
|
64
|
+
importedState: {
|
|
65
|
+
namespace,
|
|
66
|
+
sourceKey: entry.id,
|
|
67
|
+
sourceHash,
|
|
68
|
+
importerVersion: '1',
|
|
69
|
+
notes: entry.notes,
|
|
70
|
+
readingState: entry.readingState,
|
|
71
|
+
privacy: entry.privacy,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return { ok: true, itemId: source.itemId };
|
|
75
|
+
});
|
|
41
76
|
}
|
|
42
77
|
/** One verified bundle: a receipt short-circuits, a dry run stops after verification, otherwise the capture is promoted through the queue. */
|
|
43
78
|
export async function importBundleEntry(store, root, namespace, entry, verify, dryRun, signal) {
|
|
44
|
-
const bundle = await inspectImportBundle(await importBundleRoot(root, entry), verify), sourceHash = hash(JSON.stringify([bundle.importHash, entry])), receipt = store.
|
|
79
|
+
const bundle = await inspectImportBundle(await importBundleRoot(root, entry), verify), sourceHash = hash(JSON.stringify([bundle.importHash, entry])), receipt = store.imports.receipt(namespace, entry.id, sourceHash);
|
|
45
80
|
if (signal?.aborted)
|
|
46
|
-
throw
|
|
81
|
+
throw new SpongeError('cancelled');
|
|
47
82
|
const base = { sourceHash, historicalIntegrity: bundle.historicalIntegrity };
|
|
48
83
|
if (receipt)
|
|
49
84
|
return { ...base, status: 'duplicate', itemId: receipt.itemId };
|
|
50
85
|
if (dryRun)
|
|
51
86
|
return { ...base, status: 'ready' };
|
|
52
|
-
const submission = store.
|
|
53
|
-
|
|
87
|
+
const submission = store.queue.submit({
|
|
88
|
+
kind: bundle.kind,
|
|
89
|
+
sourceKey: hash(JSON.stringify([namespace, entry.id])),
|
|
90
|
+
locator: bundle.sourceUrl,
|
|
91
|
+
realm: 'import:' + namespace,
|
|
92
|
+
idempotencyKey: 'import:' + hash(JSON.stringify([namespace, entry.id, sourceHash])),
|
|
93
|
+
intent: 'refresh',
|
|
94
|
+
});
|
|
95
|
+
const job = store.queue.list().find((row) => row.id === submission.jobId);
|
|
54
96
|
if (job && ['failed', 'needs-auth', 'needs-permission', 'unsupported'].includes(String(job.state)))
|
|
55
|
-
store.
|
|
56
|
-
const results = await drainQueue(store, {
|
|
97
|
+
store.queue.retry(submission.jobId, { automatic: false });
|
|
98
|
+
const results = await drainQueue(store, {
|
|
99
|
+
jobId: submission.jobId,
|
|
100
|
+
signal,
|
|
101
|
+
handlers: { [bundle.kind]: (context) => promote(context, bundle, namespace, entry, sourceHash) },
|
|
102
|
+
});
|
|
57
103
|
const result = results[0];
|
|
58
104
|
if (!result || !result.ok)
|
|
59
|
-
throw
|
|
105
|
+
throw new SpongeError(result && !result.ok ? result.code : 'integrity');
|
|
60
106
|
return { ...base, status: 'imported', itemId: submission.itemId };
|
|
61
107
|
}
|
|
62
108
|
/** The revision a bundle import committed: content-addressed by its source hash. */
|
|
@@ -64,20 +110,42 @@ export const importBundleHash = (sourceHash) => hash(JSON.stringify(['wordcell-i
|
|
|
64
110
|
/** Additive bounded batches. Every applied entry is reverified; source bundles are read-only. */
|
|
65
111
|
export async function importManifest(store, path, options) {
|
|
66
112
|
const { root, manifest, sha256 } = await readImportManifest(path), limit = options.limit ?? 20, offset = options.offset ?? 0;
|
|
67
|
-
if (!Number.isSafeInteger(limit) ||
|
|
68
|
-
|
|
69
|
-
|
|
113
|
+
if (!Number.isSafeInteger(limit) ||
|
|
114
|
+
limit < 1 ||
|
|
115
|
+
limit > 100 ||
|
|
116
|
+
!Number.isSafeInteger(offset) ||
|
|
117
|
+
offset < 0 ||
|
|
118
|
+
offset > manifest.entries.length)
|
|
119
|
+
throw new SpongeError('invalid-input');
|
|
120
|
+
const dryRun = options.dryRun !== false, report = {
|
|
121
|
+
schemaVersion: 1,
|
|
122
|
+
manifestSha256: sha256,
|
|
123
|
+
namespace: manifest.namespace,
|
|
124
|
+
dryRun,
|
|
125
|
+
entries: [],
|
|
126
|
+
nextOffset: offset + limit < manifest.entries.length ? offset + limit : null,
|
|
127
|
+
};
|
|
70
128
|
const verify = options.verify ?? releasedBundleVerifier(options.runtime, store.root, options.signal);
|
|
71
129
|
for (const entry of manifest.entries.slice(offset, offset + limit)) {
|
|
72
130
|
if (options.signal?.aborted)
|
|
73
|
-
throw
|
|
131
|
+
throw new SpongeError('cancelled');
|
|
74
132
|
try {
|
|
75
|
-
const result = await importBundleEntry(store, root, manifest.namespace, entry, verify, dryRun, options.signal), base = {
|
|
133
|
+
const result = await importBundleEntry(store, root, manifest.namespace, entry, verify, dryRun, options.signal), base = {
|
|
134
|
+
id: entry.id,
|
|
135
|
+
sourceHash: result.sourceHash,
|
|
136
|
+
historicalIntegrity: result.historicalIntegrity,
|
|
137
|
+
privacy: entry.privacy,
|
|
138
|
+
};
|
|
76
139
|
report.entries.push({ ...base, status: result.status, ...(result.itemId ? { itemId: result.itemId } : {}) });
|
|
77
140
|
}
|
|
78
141
|
catch (error) {
|
|
79
142
|
const code = errorCode(error);
|
|
80
|
-
report.entries.push({
|
|
143
|
+
report.entries.push({
|
|
144
|
+
id: entry.id,
|
|
145
|
+
status: code === 'unsupported' ? 'unsupported' : 'failed',
|
|
146
|
+
code,
|
|
147
|
+
privacy: entry.privacy,
|
|
148
|
+
});
|
|
81
149
|
if (code === 'cleanup-required' || code === 'owner-busy' || code === 'cancelled') {
|
|
82
150
|
report.nextOffset = offset + report.entries.length - 1;
|
|
83
151
|
break;
|