@mieweb/pulsevault 0.2.0 → 0.4.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/CHANGELOG.md +107 -1
- package/OPERATIONS.md +24 -8
- package/PROTOCOL.md +187 -105
- package/README.md +103 -42
- package/dist/app.d.ts +28 -12
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +17 -4
- package/dist/app.js.map +1 -1
- package/dist/core.d.ts +18 -8
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +91 -23
- package/dist/core.js.map +1 -1
- package/dist/lib/authorize.d.ts +7 -1
- package/dist/lib/authorize.d.ts.map +1 -1
- package/dist/lib/capabilities.d.ts +30 -0
- package/dist/lib/capabilities.d.ts.map +1 -0
- package/dist/lib/capabilities.js +23 -0
- package/dist/lib/capabilities.js.map +1 -0
- package/dist/lib/capability-token.d.ts +41 -7
- package/dist/lib/capability-token.d.ts.map +1 -1
- package/dist/lib/capability-token.js +68 -13
- package/dist/lib/capability-token.js.map +1 -1
- package/dist/lib/deeplinks.d.ts +0 -13
- package/dist/lib/deeplinks.d.ts.map +1 -1
- package/dist/lib/deeplinks.js +0 -5
- package/dist/lib/deeplinks.js.map +1 -1
- package/dist/lib/options.d.ts +6 -1
- package/dist/lib/options.d.ts.map +1 -1
- package/dist/lib/options.js +9 -4
- package/dist/lib/options.js.map +1 -1
- package/dist/lib/protocol.d.ts +31 -0
- package/dist/lib/protocol.d.ts.map +1 -0
- package/dist/lib/protocol.js +55 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/pulsevaultTus.d.ts +7 -2
- package/dist/lib/pulsevaultTus.d.ts.map +1 -1
- package/dist/lib/pulsevaultTus.js +80 -6
- package/dist/lib/pulsevaultTus.js.map +1 -1
- package/dist/lib/retention.d.ts +54 -0
- package/dist/lib/retention.d.ts.map +1 -0
- package/dist/lib/retention.js +119 -0
- package/dist/lib/retention.js.map +1 -0
- package/dist/lib/view-links.d.ts +49 -0
- package/dist/lib/view-links.d.ts.map +1 -0
- package/dist/lib/view-links.js +35 -0
- package/dist/lib/view-links.js.map +1 -0
- package/dist/routes/pulsevault.d.ts +2 -2
- package/dist/routes/pulsevault.d.ts.map +1 -1
- package/dist/routes/pulsevault.js +51 -30
- package/dist/routes/pulsevault.js.map +1 -1
- package/dist/storage/local.d.ts +5 -1
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +55 -1
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/s3.d.ts +5 -1
- package/dist/storage/s3.d.ts.map +1 -1
- package/dist/storage/s3.js +52 -5
- package/dist/storage/s3.js.map +1 -1
- package/dist/storage/types.d.ts +34 -6
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +16 -4
- package/protocol/openapi.json +1058 -0
- package/protocol/schemas/beat-manifest.schema.json +54 -0
- package/protocol/schemas/capabilities.schema.json +65 -0
- package/protocol/schemas/capability-token.schema.json +36 -0
- package/protocol/schemas/deep-link.schema.json +29 -0
- package/protocol/schemas/pulse-client.schema.json +8 -0
- package/protocol/schemas/upload-metadata.schema.json +45 -0
- package/protocol/schemas/view-link.schema.json +23 -0
package/dist/lib/deeplinks.d.ts
CHANGED
|
@@ -16,19 +16,6 @@ export type UploadLinkOptions = {
|
|
|
16
16
|
* step. Generate with `crypto.randomUUID()`.
|
|
17
17
|
*/
|
|
18
18
|
artifactId: string;
|
|
19
|
-
/**
|
|
20
|
-
* Per-session override of the deployment-wide `uploadUnit` advertised via
|
|
21
|
-
* `GET /capabilities` (PROTOCOL.md §8). Omit to let the client fall back to
|
|
22
|
-
* whatever `/capabilities` reports — the same behavior as before this field
|
|
23
|
-
* existed. Set this when an operator wants "segment" and "merged" sessions
|
|
24
|
-
* live at the same time (e.g. different pairing flows, a/b testing, a
|
|
25
|
-
* gradual rollout) rather than one fixed value for the whole deployment:
|
|
26
|
-
* `/capabilities` can only ever report one current value, and a client
|
|
27
|
-
* that reads it separately from opening the link is racing whatever the
|
|
28
|
-
* server was serving at that moment, not the value this specific session
|
|
29
|
-
* was paired under.
|
|
30
|
-
*/
|
|
31
|
-
uploadUnit?: 'segment' | 'merged';
|
|
32
19
|
};
|
|
33
20
|
/**
|
|
34
21
|
* Build a `pulsecam://` deep link that opens the Pulse app directly on the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deeplinks.d.ts","sourceRoot":"","sources":["../../src/lib/deeplinks.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,gJAAgJ;IAChJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"deeplinks.d.ts","sourceRoot":"","sources":["../../src/lib/deeplinks.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,gJAAgJ;IAChJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AA4BF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAqB/D"}
|
package/dist/lib/deeplinks.js
CHANGED
|
@@ -48,9 +48,6 @@ export function buildUploadLink(opts) {
|
|
|
48
48
|
throw new Error(`buildUploadLink: \`server\` must be https:// (got "${opts.server}") — ` +
|
|
49
49
|
'the only exception is http://localhost or a private IP literal for local development.');
|
|
50
50
|
}
|
|
51
|
-
if (opts.uploadUnit !== undefined && opts.uploadUnit !== 'segment' && opts.uploadUnit !== 'merged') {
|
|
52
|
-
throw new Error(`buildUploadLink: \`uploadUnit\` must be "segment" or "merged" (got "${opts.uploadUnit}")`);
|
|
53
|
-
}
|
|
54
51
|
const params = new URLSearchParams({
|
|
55
52
|
v: LINK_VERSION,
|
|
56
53
|
artifactId: opts.artifactId,
|
|
@@ -58,8 +55,6 @@ export function buildUploadLink(opts) {
|
|
|
58
55
|
});
|
|
59
56
|
if (opts.token)
|
|
60
57
|
params.set('token', opts.token);
|
|
61
|
-
if (opts.uploadUnit)
|
|
62
|
-
params.set('uploadUnit', opts.uploadUnit);
|
|
63
58
|
return `pulsecam://?${params.toString()}`;
|
|
64
59
|
}
|
|
65
60
|
//# sourceMappingURL=deeplinks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deeplinks.js","sourceRoot":"","sources":["../../src/lib/deeplinks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deeplinks.js","sourceRoot":"","sources":["../../src/lib/deeplinks.ts"],"names":[],"mappings":"AAoBA,qFAAqF;AACrF,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB;;;;;;;;GAQG;AACH,SAAS,kBAAkB,CAAC,GAAQ;IAClC,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC1B,OAAO,CACL,IAAI,KAAK,WAAW;QACpB,IAAI,KAAK,WAAW;QACpB,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;QAChC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC;QACjC,qCAAqC,CAAC,IAAI,CAAC,IAAI,CAAC;QAChD,mDAAmD,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,0BAA0B;QAC5F,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,aAAa;KAChD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,IAAuB;IACrD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,mDAAmD,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,sDAAsD,IAAI,CAAC,MAAM,OAAO;YACtE,uFAAuF,CAC1F,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,CAAC,EAAE,YAAY;QACf,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAC,CAAC;IACH,IAAI,IAAI,CAAC,KAAK;QAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAChD,OAAO,eAAe,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC5C,CAAC"}
|
package/dist/lib/options.d.ts
CHANGED
|
@@ -23,7 +23,12 @@ export declare function normalizeAllowedExtensions(raw: PulseVaultAllowedExtensi
|
|
|
23
23
|
/** Shared by the Fastify plugin's `prefix` and the core's `basePath` — same shape, different option name. */
|
|
24
24
|
export declare function validateBasePath(basePath: string, optionName: string): void;
|
|
25
25
|
export declare function validateMaxUploadSize(maxUploadSize: number): void;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Fail at boot on options that no longer exist, instead of silently ignoring
|
|
28
|
+
* them. Shared by every entry point (Fastify plugin, core) so the message
|
|
29
|
+
* can't drift between surfaces.
|
|
30
|
+
*/
|
|
31
|
+
export declare function rejectRemovedOptions(opts: object): void;
|
|
27
32
|
export declare function validateAllowedExtensions(allowedExtensions: PulseVaultAllowedExtensionsInput | undefined): void;
|
|
28
33
|
/**
|
|
29
34
|
* One-time (not per-request) warning for the now-deprecated per-kind project
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/lib/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAErE,iHAAiH;AACjH,KAAK,uBAAuB,GAAG;IAC7B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG,SAAS,MAAM,EAAE,GAAG,uBAAuB,CAAC;AAE3F,MAAM,MAAM,2BAA2B,GAAG;IACxC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B,CAAC;AAWF;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,gCAAgC,GAAG,SAAS,GAChD,2BAA2B,CAyB7B;AAED,6GAA6G;AAC7G,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAM3E;AAED,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAIjE;AAED,wBAAgB,
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/lib/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAErE,iHAAiH;AACjH,KAAK,uBAAuB,GAAG;IAC7B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG,SAAS,MAAM,EAAE,GAAG,uBAAuB,CAAC;AAE3F,MAAM,MAAM,2BAA2B,GAAG;IACxC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B,CAAC;AAWF;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,gCAAgC,GAAG,SAAS,GAChD,2BAA2B,CAyB7B;AAED,6GAA6G;AAC7G,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAM3E;AAED,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAIjE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMvD;AAED,wBAAgB,yBAAyB,CACvC,iBAAiB,EAAE,gCAAgC,GAAG,SAAS,GAC9D,IAAI,CAiBN;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,IAAI,EAAE;IACtD,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC,GAAG,IAAI,CAaP;AAED,8GAA8G;AAC9G,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,yBAAyB,GAAG,SAAS,EAC9C,aAAa,EAAE,yBAAyB,GAAG,SAAS,GACnD,yBAAyB,GAAG,SAAS,CASvC;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,0BAA0B,GAAG,SAAS,EAC/C,aAAa,EAAE,0BAA0B,GAAG,SAAS,GACpD,0BAA0B,GAAG,SAAS,CASxC"}
|
package/dist/lib/options.js
CHANGED
|
@@ -3,7 +3,7 @@ const DEFAULT_PROJECT_EXTENSIONS = ['.pulse', '.zip'];
|
|
|
3
3
|
// WebVTT only — it's what the Pulse app uploads, and it carries word-level
|
|
4
4
|
// inline cue timestamps (`<00:00:01.500>word`) for karaoke rendering.
|
|
5
5
|
const DEFAULT_CAPTIONS_EXTENSIONS = ['.vtt'];
|
|
6
|
-
// The
|
|
6
|
+
// The pulse's poster frame (the draft's first-clip thumbnail).
|
|
7
7
|
const DEFAULT_THUMBNAIL_EXTENSIONS = ['.jpg', '.jpeg', '.png'];
|
|
8
8
|
const EXTENSION_REGEX = /^\.[^.\s/\\]+$/;
|
|
9
9
|
/**
|
|
@@ -48,9 +48,14 @@ export function validateMaxUploadSize(maxUploadSize) {
|
|
|
48
48
|
throw new TypeError('`maxUploadSize` must be a positive number (use Infinity for no cap)');
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Fail at boot on options that no longer exist, instead of silently ignoring
|
|
53
|
+
* them. Shared by every entry point (Fastify plugin, core) so the message
|
|
54
|
+
* can't drift between surfaces.
|
|
55
|
+
*/
|
|
56
|
+
export function rejectRemovedOptions(opts) {
|
|
57
|
+
if (opts.uploadUnit !== undefined) {
|
|
58
|
+
throw new TypeError('`uploadUnit` was removed — delete the option. A pulse uploads as one video; beats are timestamps within it (see CHANGELOG).');
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
export function validateAllowedExtensions(allowedExtensions) {
|
package/dist/lib/options.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/lib/options.ts"],"names":[],"mappings":"AAoBA,MAAM,wBAAwB,GAAsB,CAAC,MAAM,CAAC,CAAC;AAC7D,MAAM,0BAA0B,GAAsB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACzE,2EAA2E;AAC3E,sEAAsE;AACtE,MAAM,2BAA2B,GAAsB,CAAC,MAAM,CAAC,CAAC;AAChE
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/lib/options.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
/** Spec revision this release implements, e.g. `"2.1"`. */
|
|
3
|
+
export declare const PROTOCOL_REVISION: string;
|
|
4
|
+
/** Protocol major this release implements (the `Protocol-Version` response header). */
|
|
5
|
+
export declare const PROTOCOL_VERSION: number;
|
|
6
|
+
export declare const MIN_SUPPORTED_PROTOCOL_VERSION: number;
|
|
7
|
+
export declare const MAX_SUPPORTED_PROTOCOL_VERSION: number;
|
|
8
|
+
/**
|
|
9
|
+
* What a client says about itself in the `Pulse-Client` request header (PROTOCOL.md §7):
|
|
10
|
+
* `<product>/<version> [(<details>)]; protocol=<min>[-<max>]`, e.g.
|
|
11
|
+
* `Pulse/2.1.0 (45; ios); protocol=1-2`. Optional: a client that doesn't send it is treated as
|
|
12
|
+
* before this header existed.
|
|
13
|
+
*/
|
|
14
|
+
export type PulseClient = {
|
|
15
|
+
raw: string;
|
|
16
|
+
protocolMin?: number;
|
|
17
|
+
protocolMax?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function parsePulseClient(value: string | string[] | undefined): PulseClient | null;
|
|
20
|
+
/**
|
|
21
|
+
* The `426 Upgrade Required` body for a client whose newest protocol is older than this server's
|
|
22
|
+
* oldest, or `null` if the client is fine (or didn't say).
|
|
23
|
+
*/
|
|
24
|
+
export declare function outdatedClientRejection(req: IncomingMessage): {
|
|
25
|
+
error: string;
|
|
26
|
+
minSupportedVersion: number;
|
|
27
|
+
maxSupportedVersion: number;
|
|
28
|
+
} | null;
|
|
29
|
+
/** The client app version from `Upload-Metadata.appVersion`, trimmed and capped. */
|
|
30
|
+
export declare function normalizeAppVersion(value: string | null | undefined): string | undefined;
|
|
31
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/lib/protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAgBjD,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,QAAwB,CAAC;AACvD,uFAAuF;AACvF,eAAO,MAAM,gBAAgB,QAA0C,CAAC;AACxE,eAAO,MAAM,8BAA8B,QAAoB,CAAC;AAChE,eAAO,MAAM,8BAA8B,QAAoB,CAAC;AAUhE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,WAAW,GAAG,IAAI,CAQzF;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,eAAe,GACnB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAcpF;AAQD,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMxF"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const { pulseProtocol } = require('../../package.json');
|
|
4
|
+
/** Spec revision this release implements, e.g. `"2.1"`. */
|
|
5
|
+
export const PROTOCOL_REVISION = pulseProtocol.version;
|
|
6
|
+
/** Protocol major this release implements (the `Protocol-Version` response header). */
|
|
7
|
+
export const PROTOCOL_VERSION = Number(PROTOCOL_REVISION.split('.')[0]);
|
|
8
|
+
export const MIN_SUPPORTED_PROTOCOL_VERSION = pulseProtocol.min;
|
|
9
|
+
export const MAX_SUPPORTED_PROTOCOL_VERSION = pulseProtocol.max;
|
|
10
|
+
if (!/^\d+\.\d+$/.test(PROTOCOL_REVISION) ||
|
|
11
|
+
MIN_SUPPORTED_PROTOCOL_VERSION > PROTOCOL_VERSION ||
|
|
12
|
+
MAX_SUPPORTED_PROTOCOL_VERSION < PROTOCOL_VERSION) {
|
|
13
|
+
throw new Error(`package.json pulseProtocol is inconsistent: ${JSON.stringify(pulseProtocol)}`);
|
|
14
|
+
}
|
|
15
|
+
export function parsePulseClient(value) {
|
|
16
|
+
const raw = (Array.isArray(value) ? value[0] : value)?.trim();
|
|
17
|
+
if (!raw)
|
|
18
|
+
return null;
|
|
19
|
+
const match = raw.match(/(?:^|;)\s*protocol=(\d+)(?:-(\d+))?\s*(?:;|$)/);
|
|
20
|
+
if (!match)
|
|
21
|
+
return { raw };
|
|
22
|
+
const protocolMin = Number(match[1]);
|
|
23
|
+
const protocolMax = match[2] === undefined ? protocolMin : Number(match[2]);
|
|
24
|
+
return protocolMax < protocolMin ? { raw } : { raw, protocolMin, protocolMax };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The `426 Upgrade Required` body for a client whose newest protocol is older than this server's
|
|
28
|
+
* oldest, or `null` if the client is fine (or didn't say).
|
|
29
|
+
*/
|
|
30
|
+
export function outdatedClientRejection(req) {
|
|
31
|
+
const client = parsePulseClient(req.headers['pulse-client']);
|
|
32
|
+
if (client?.protocolMax === undefined || client.protocolMax >= MIN_SUPPORTED_PROTOCOL_VERSION) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const range = MIN_SUPPORTED_PROTOCOL_VERSION === MAX_SUPPORTED_PROTOCOL_VERSION
|
|
36
|
+
? `${MIN_SUPPORTED_PROTOCOL_VERSION}`
|
|
37
|
+
: `${MIN_SUPPORTED_PROTOCOL_VERSION}–${MAX_SUPPORTED_PROTOCOL_VERSION}`;
|
|
38
|
+
return {
|
|
39
|
+
error: `This server needs a client that speaks upload protocol ${range}. Update the app and try again.`,
|
|
40
|
+
minSupportedVersion: MIN_SUPPORTED_PROTOCOL_VERSION,
|
|
41
|
+
maxSupportedVersion: MAX_SUPPORTED_PROTOCOL_VERSION,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Defensive cap on the stored `appVersion` metadata: a version string, never a document.
|
|
46
|
+
* Capped by code point so a truncated value can't end in half a surrogate pair.
|
|
47
|
+
*/
|
|
48
|
+
const MAX_APP_VERSION_LENGTH = 64;
|
|
49
|
+
/** The client app version from `Upload-Metadata.appVersion`, trimmed and capped. */
|
|
50
|
+
export function normalizeAppVersion(value) {
|
|
51
|
+
return (Array.from((value ?? '').trim())
|
|
52
|
+
.slice(0, MAX_APP_VERSION_LENGTH)
|
|
53
|
+
.join('') || undefined);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/lib/protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,14 +30,19 @@ export type PulseVaultOnUploadComplete = (request: PulseVaultRequest, ctx: {
|
|
|
30
30
|
/**
|
|
31
31
|
* Fired at low-frequency, audit-worthy moments — never per chunk — so an
|
|
32
32
|
* operator can wire one hook to get both ops metrics and a compliance audit
|
|
33
|
-
* trail without hand-rolling both from the lower-level hooks.
|
|
33
|
+
* trail without hand-rolling both from the lower-level hooks. `remove` fires
|
|
34
|
+
* after PulseVault removed an artifact: a `DELETE /artifacts/:id` or a TUS
|
|
35
|
+
* `DELETE` (`reason: "deleted"`), or the `retention` sweep (`reason: "abandoned"`)
|
|
36
|
+
* — so a host keeping its own index of artifacts can drop it.
|
|
34
37
|
*/
|
|
35
38
|
export type PulseVaultArtifactEvent = {
|
|
36
|
-
phase: 'authorize' | 'complete' | 'reject';
|
|
39
|
+
phase: 'authorize' | 'complete' | 'reject' | 'remove';
|
|
37
40
|
artifactId: string;
|
|
38
41
|
kind: UploadKind;
|
|
39
42
|
size?: number;
|
|
40
43
|
reason?: string;
|
|
44
|
+
/** The uploading app's version, from `Upload-Metadata.appVersion` (`complete`/`reject` only). */
|
|
45
|
+
appVersion?: string;
|
|
41
46
|
};
|
|
42
47
|
export type PulseVaultOnArtifactEvent = (event: PulseVaultArtifactEvent) => void | Promise<void>;
|
|
43
48
|
export type PulsevaultTusOptions = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pulsevaultTus.d.ts","sourceRoot":"","sources":["../../src/lib/pulsevaultTus.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"pulsevaultTus.d.ts","sourceRoot":"","sources":["../../src/lib/pulsevaultTus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAKrD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAiB,MAAM,cAAc,CAAC;AAC5F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,oBAAoB,yCAAgD,CAAC;AAElF,MAAM,MAAM,0BAA0B,GAAG,CACvC,OAAO,EAAE,iBAAiB,EAC1B,GAAG,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAC1E,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;;;;GAOG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AACF,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjG,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,iBAAiB,EAAE;QACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;QACzB,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;QAC3B,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;KAC9B,CAAC;IACF;;;;;OAKG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C,uGAAuG;IACvG,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;IAC9C,uCAAuC;IACvC,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C,sFAAsF;IACtF,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAM5D;AAgDD,6FAA6F;AAC7F,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AA2DD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,oBAAoB,UAyLtE"}
|
|
@@ -2,6 +2,7 @@ import { Server } from '@tus/server';
|
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { isUuid } from './uuid.js';
|
|
5
|
+
import { normalizeAppVersion } from './protocol.js';
|
|
5
6
|
import { statusCodeOf } from './errors.js';
|
|
6
7
|
import { consoleLogger } from './request.js';
|
|
7
8
|
import { parseUploadKind } from '../storage/types.js';
|
|
@@ -19,6 +20,51 @@ export function tusError(status, body) {
|
|
|
19
20
|
body,
|
|
20
21
|
});
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The datastore tus drives, with termination routed through PulseVault's own removal. A TUS
|
|
25
|
+
* DELETE (the client cancelling, or discarding what a failed run created) takes tus's per-upload
|
|
26
|
+
* lock and calls the datastore's `remove` — which on its own drops only tus's bytes and offset
|
|
27
|
+
* record: PulseVault's sidecar stays, so the artifactId 409s on every later create, and on S3 a
|
|
28
|
+
* finished upload isn't removed at all (aborting its completed multipart upload fails first).
|
|
29
|
+
* So both run: the datastore's removal, then `storage.remove`, which deletes the whole artifact,
|
|
30
|
+
* in flight or finished. It 404s only when neither found anything to remove.
|
|
31
|
+
*/
|
|
32
|
+
function withArtifactRemoval(storage, onRemoved) {
|
|
33
|
+
const { datastore } = storage;
|
|
34
|
+
if (!storage.remove)
|
|
35
|
+
return datastore;
|
|
36
|
+
const removeArtifact = storage.remove.bind(storage);
|
|
37
|
+
return new Proxy(datastore, {
|
|
38
|
+
get(target, prop) {
|
|
39
|
+
if (prop === 'remove') {
|
|
40
|
+
return async (id) => {
|
|
41
|
+
const artifactId = artifactIdFromUploadId(id);
|
|
42
|
+
const kind = artifactId ? await resolveKind(storage, artifactId) : undefined;
|
|
43
|
+
// The datastore's own removal first — the upload's bytes and tus's records, which a
|
|
44
|
+
// custom adapter's `remove` may not know about — then the artifact's.
|
|
45
|
+
let uploadRemoved = false;
|
|
46
|
+
let uploadError;
|
|
47
|
+
try {
|
|
48
|
+
await target.remove(id);
|
|
49
|
+
uploadRemoved = true;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
// Not there, or (S3) already completed: `storage.remove` below still removes it.
|
|
53
|
+
uploadError = err;
|
|
54
|
+
}
|
|
55
|
+
const artifactRemoved = artifactId ? await removeArtifact(artifactId) : false;
|
|
56
|
+
if (artifactId && kind && artifactRemoved) {
|
|
57
|
+
await onRemoved?.(artifactId, kind);
|
|
58
|
+
}
|
|
59
|
+
if (!uploadRemoved && !artifactRemoved)
|
|
60
|
+
throw uploadError;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const value = Reflect.get(target, prop, target);
|
|
64
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
22
68
|
/** Parse the artifactId UUID from a tus upload id of the form `<kind>/<artifactId><ext>`. */
|
|
23
69
|
export function artifactIdFromUploadId(id) {
|
|
24
70
|
const [, nameWithExt] = id.split('/');
|
|
@@ -62,16 +108,20 @@ function parseUploadMetadata(metadata) {
|
|
|
62
108
|
const name = Array.from((metadata?.name ?? '').trim())
|
|
63
109
|
.slice(0, MAX_ARTIFACT_NAME_LENGTH)
|
|
64
110
|
.join('') || undefined;
|
|
65
|
-
|
|
111
|
+
// The uploading app's version (PROTOCOL.md §4), trimmed and capped like `name`.
|
|
112
|
+
const appVersion = normalizeAppVersion(metadata?.appVersion);
|
|
113
|
+
return { artifactId, filename, kind, relatedTo, checksum, name, appVersion };
|
|
66
114
|
}
|
|
67
115
|
export function createPulsevaultTusServer(options) {
|
|
68
116
|
const { storage, tusPath, maxSize, allowedExtensions, validatePayload, onUploadComplete, onArtifactEvent, logger = consoleLogger, } = options;
|
|
69
117
|
const server = new Server({
|
|
70
118
|
path: tusPath,
|
|
71
|
-
datastore: storage
|
|
119
|
+
datastore: withArtifactRemoval(storage, async (artifactId, kind) => {
|
|
120
|
+
await onArtifactEvent?.({ phase: 'remove', artifactId, kind, reason: 'deleted' });
|
|
121
|
+
}),
|
|
72
122
|
maxSize,
|
|
73
123
|
namingFunction: async (_req, metadata) => {
|
|
74
|
-
const { artifactId, filename, kind, relatedTo, checksum, name } = parseUploadMetadata(metadata);
|
|
124
|
+
const { artifactId, filename, kind, relatedTo, checksum, name, appVersion } = parseUploadMetadata(metadata);
|
|
75
125
|
if (!isUuid(artifactId)) {
|
|
76
126
|
throw tusError(400, 'Upload-Metadata must include a valid `artifactId` UUID.\n');
|
|
77
127
|
}
|
|
@@ -89,7 +139,16 @@ export function createPulsevaultTusServer(options) {
|
|
|
89
139
|
store.relatedTo = relatedTo;
|
|
90
140
|
store.checksum = checksum;
|
|
91
141
|
}
|
|
92
|
-
return storage.reserveUpload({
|
|
142
|
+
return storage.reserveUpload({
|
|
143
|
+
artifactId,
|
|
144
|
+
filename,
|
|
145
|
+
ext,
|
|
146
|
+
kind,
|
|
147
|
+
relatedTo,
|
|
148
|
+
checksum,
|
|
149
|
+
name,
|
|
150
|
+
appVersion,
|
|
151
|
+
});
|
|
93
152
|
},
|
|
94
153
|
// Relative Location (RFC 7231 §7.1.2) so the upload URL is correct behind
|
|
95
154
|
// any TLS-terminating proxy without trusting spoofable X-Forwarded-*
|
|
@@ -120,6 +179,8 @@ export function createPulsevaultTusServer(options) {
|
|
|
120
179
|
}
|
|
121
180
|
const size = upload.size ?? 0;
|
|
122
181
|
const uploadId = upload.id;
|
|
182
|
+
// Reported on the complete/reject events so an operator can see which app build sent it.
|
|
183
|
+
const appVersion = normalizeAppVersion(upload.metadata?.appVersion);
|
|
123
184
|
// Resolve kind/checksum: prefer the context value (set during the same
|
|
124
185
|
// request's namingFunction for single-request uploads), fall back to a
|
|
125
186
|
// storage lookup (cheap in-memory cache hit) for chunked uploads, or —
|
|
@@ -154,7 +215,14 @@ export function createPulsevaultTusServer(options) {
|
|
|
154
215
|
catch (rmErr) {
|
|
155
216
|
logger.error({ err: rmErr, artifactId }, 'pulsevault failed to remove rejected upload');
|
|
156
217
|
}
|
|
157
|
-
await onArtifactEvent?.({
|
|
218
|
+
await onArtifactEvent?.({
|
|
219
|
+
phase: 'reject',
|
|
220
|
+
artifactId,
|
|
221
|
+
kind,
|
|
222
|
+
size,
|
|
223
|
+
reason: message,
|
|
224
|
+
...(appVersion ? { appVersion } : {}),
|
|
225
|
+
});
|
|
158
226
|
// 4xx rejection reasons are the client's business (e.g. "Checksum
|
|
159
227
|
// mismatch: …"); 5xx means *our* side broke — log the real error and
|
|
160
228
|
// return a generic body so internals (adapter wiring, stack detail)
|
|
@@ -195,7 +263,13 @@ export function createPulsevaultTusServer(options) {
|
|
|
195
263
|
throw tusError(500, 'Upload completion hook failed\n');
|
|
196
264
|
}
|
|
197
265
|
}
|
|
198
|
-
await onArtifactEvent?.({
|
|
266
|
+
await onArtifactEvent?.({
|
|
267
|
+
phase: 'complete',
|
|
268
|
+
artifactId,
|
|
269
|
+
kind,
|
|
270
|
+
size,
|
|
271
|
+
...(appVersion ? { appVersion } : {}),
|
|
272
|
+
});
|
|
199
273
|
return {};
|
|
200
274
|
},
|
|
201
275
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pulsevaultTus.js","sourceRoot":"","sources":["../../src/lib/pulsevaultTus.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"pulsevaultTus.js","sourceRoot":"","sources":["../../src/lib/pulsevaultTus.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type PulseVaultLogger } from './request.js';
|
|
2
|
+
import type { PulseVaultArtifactRecord, PulseVaultStorage } from '../storage/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Opt-in cleanup of what abandoned uploads leave behind. A client that dies mid-upload (an app
|
|
5
|
+
* killed, a phone that never comes back) leaves an unfinished upload, and the related artifacts
|
|
6
|
+
* it finished before its video — captions, a beat manifest, a thumbnail — belong to a video that
|
|
7
|
+
* will never arrive. Nothing else removes them.
|
|
8
|
+
*/
|
|
9
|
+
export type PulseVaultRetentionOptions = {
|
|
10
|
+
/**
|
|
11
|
+
* Remove an upload still unfinished this long after its last activity (the local adapter) or
|
|
12
|
+
* after it started (S3), and a finished related artifact — anything but a video — whose
|
|
13
|
+
* `relatedTo` artifact isn't finished (or is gone) this long after it finished. Finished videos
|
|
14
|
+
* are never removed. Keep it well above how long an upload may take — at least your capability
|
|
15
|
+
* tokens' lifetime, past which no upload can continue anyway.
|
|
16
|
+
*/
|
|
17
|
+
abandonedAfterSeconds: number;
|
|
18
|
+
/** How often to sweep. Defaults to 3600 (an hour); at most 24 days. */
|
|
19
|
+
sweepIntervalSeconds?: number;
|
|
20
|
+
};
|
|
21
|
+
export type SweepAbandonedUploadsOptions = {
|
|
22
|
+
/** See `PulseVaultRetentionOptions.abandonedAfterSeconds`. */
|
|
23
|
+
abandonedAfterSeconds: number;
|
|
24
|
+
/** The time to measure from, in ms since the epoch. Defaults to now. */
|
|
25
|
+
now?: number;
|
|
26
|
+
/** Called after each removal — e.g. to update the host's own index of artifacts. */
|
|
27
|
+
onRemoved?: (record: PulseVaultArtifactRecord) => void | Promise<void>;
|
|
28
|
+
/** Stops the sweep between artifacts (shutdown). */
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
/** Where a failure to check or remove one artifact is logged. Defaults to `console`. */
|
|
31
|
+
logger?: PulseVaultLogger;
|
|
32
|
+
};
|
|
33
|
+
/** Fail at boot on a retention setting that can't work, rather than sweeping wrongly later. */
|
|
34
|
+
export declare function validateRetentionOptions(retention: PulseVaultRetentionOptions | undefined, storage: PulseVaultStorage): void;
|
|
35
|
+
/**
|
|
36
|
+
* One sweep (see `PulseVaultRetentionOptions`): removes, through `storage.remove`, every upload
|
|
37
|
+
* unfinished for longer than `abandonedAfterSeconds`, and every finished related artifact (not a
|
|
38
|
+
* video) whose `relatedTo` artifact still isn't finished that long after it finished. Resolves
|
|
39
|
+
* the removed artifactIds. An artifact that can't be checked or removed is logged and skipped;
|
|
40
|
+
* the rest of the sweep carries on. Safe to run from several instances at once (a removal that
|
|
41
|
+
* finds nothing is a no-op), and usable on its own from a cron job instead of the `retention`
|
|
42
|
+
* option.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sweepAbandonedUploads(storage: PulseVaultStorage, opts: SweepAbandonedUploadsOptions): Promise<string[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Run `sweepAbandonedUploads` every `sweepIntervalSeconds` (first sweep one interval after start).
|
|
47
|
+
* The timer doesn't keep the process alive. `stop` ends it and resolves once a sweep in progress
|
|
48
|
+
* has stopped, so storage can be shut down safely after it. A failed sweep is logged and the next
|
|
49
|
+
* one tries again.
|
|
50
|
+
*/
|
|
51
|
+
export declare function startRetentionSweep(storage: PulseVaultStorage, retention: PulseVaultRetentionOptions, logger: PulseVaultLogger, onRemoved?: (record: PulseVaultArtifactRecord) => void | Promise<void>): {
|
|
52
|
+
stop: () => Promise<void>;
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=retention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/lib/retention.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,KAAK,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAEvF;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC;;;;;;OAMG;IACH,qBAAqB,EAAE,MAAM,CAAC;IAC9B,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,8DAA8D;IAC9D,qBAAqB,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,oDAAoD;IACpD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,wFAAwF;IACxF,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AASF,+FAA+F;AAC/F,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,0BAA0B,GAAG,SAAS,EACjD,OAAO,EAAE,iBAAiB,GACzB,IAAI,CAmBN;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAAC,MAAM,EAAE,CAAC,CAgDnB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,iBAAiB,EAC1B,SAAS,EAAE,0BAA0B,EACrC,MAAM,EAAE,gBAAgB,EACxB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACrE;IAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAiC/B"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { consoleLogger } from './request.js';
|
|
2
|
+
const DEFAULT_SWEEP_INTERVAL_SECONDS = 3600;
|
|
3
|
+
/** The longest delay a Node timer takes; anything above fires after 1 ms instead. */
|
|
4
|
+
const MAX_SWEEP_INTERVAL_SECONDS = Math.floor((2 ** 31 - 1) / 1000);
|
|
5
|
+
const isPositiveSeconds = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
6
|
+
/** Fail at boot on a retention setting that can't work, rather than sweeping wrongly later. */
|
|
7
|
+
export function validateRetentionOptions(retention, storage) {
|
|
8
|
+
if (retention === undefined)
|
|
9
|
+
return;
|
|
10
|
+
const { abandonedAfterSeconds, sweepIntervalSeconds } = retention;
|
|
11
|
+
if (!isPositiveSeconds(abandonedAfterSeconds)) {
|
|
12
|
+
throw new TypeError('`retention.abandonedAfterSeconds` must be a positive number of seconds');
|
|
13
|
+
}
|
|
14
|
+
if (sweepIntervalSeconds !== undefined &&
|
|
15
|
+
(!isPositiveSeconds(sweepIntervalSeconds) || sweepIntervalSeconds > MAX_SWEEP_INTERVAL_SECONDS)) {
|
|
16
|
+
throw new TypeError(`\`retention.sweepIntervalSeconds\` must be a positive number of seconds, at most ${MAX_SWEEP_INTERVAL_SECONDS} (24 days)`);
|
|
17
|
+
}
|
|
18
|
+
if (typeof storage.listArtifacts !== 'function' || typeof storage.remove !== 'function') {
|
|
19
|
+
throw new TypeError('`retention` needs a storage adapter with `listArtifacts` and `remove` (both built-in adapters have them)');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One sweep (see `PulseVaultRetentionOptions`): removes, through `storage.remove`, every upload
|
|
24
|
+
* unfinished for longer than `abandonedAfterSeconds`, and every finished related artifact (not a
|
|
25
|
+
* video) whose `relatedTo` artifact still isn't finished that long after it finished. Resolves
|
|
26
|
+
* the removed artifactIds. An artifact that can't be checked or removed is logged and skipped;
|
|
27
|
+
* the rest of the sweep carries on. Safe to run from several instances at once (a removal that
|
|
28
|
+
* finds nothing is a no-op), and usable on its own from a cron job instead of the `retention`
|
|
29
|
+
* option.
|
|
30
|
+
*/
|
|
31
|
+
export async function sweepAbandonedUploads(storage, opts) {
|
|
32
|
+
const { listArtifacts, remove } = storage;
|
|
33
|
+
if (typeof listArtifacts !== 'function' || typeof remove !== 'function') {
|
|
34
|
+
throw new TypeError('sweepAbandonedUploads needs `storage.listArtifacts` and `storage.remove`');
|
|
35
|
+
}
|
|
36
|
+
// A cutoff that isn't a number would make every artifact look old enough to remove.
|
|
37
|
+
if (!isPositiveSeconds(opts.abandonedAfterSeconds)) {
|
|
38
|
+
throw new TypeError('sweepAbandonedUploads: `abandonedAfterSeconds` must be a positive number');
|
|
39
|
+
}
|
|
40
|
+
const now = opts.now ?? Date.now();
|
|
41
|
+
if (!Number.isFinite(now))
|
|
42
|
+
throw new TypeError('sweepAbandonedUploads: `now` must be a number');
|
|
43
|
+
const cutoff = now - opts.abandonedAfterSeconds * 1000;
|
|
44
|
+
const logger = opts.logger ?? consoleLogger;
|
|
45
|
+
// Whether an artifact is finished, looked up once per sweep.
|
|
46
|
+
const finished = new Map();
|
|
47
|
+
const isFinished = (artifactId) => {
|
|
48
|
+
let known = finished.get(artifactId);
|
|
49
|
+
if (!known) {
|
|
50
|
+
known = storage.resolve(artifactId).then((resolved) => resolved !== null);
|
|
51
|
+
finished.set(artifactId, known);
|
|
52
|
+
}
|
|
53
|
+
return known;
|
|
54
|
+
};
|
|
55
|
+
const removed = [];
|
|
56
|
+
for await (const record of listArtifacts.call(storage, { changedBefore: cutoff })) {
|
|
57
|
+
if (opts.signal?.aborted)
|
|
58
|
+
break;
|
|
59
|
+
// Only a record known to be older than the cutoff: an adapter's bad timestamp keeps it.
|
|
60
|
+
if (!(Number.isFinite(record.updatedAt) && record.updatedAt < cutoff))
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const abandoned = !record.ready ||
|
|
64
|
+
(record.kind !== 'video' &&
|
|
65
|
+
record.relatedTo !== undefined &&
|
|
66
|
+
!(await isFinished(record.relatedTo)));
|
|
67
|
+
if (abandoned && (await remove.call(storage, record.artifactId))) {
|
|
68
|
+
removed.push(record.artifactId);
|
|
69
|
+
await opts.onRemoved?.(record);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
logger.error({ err, artifactId: record.artifactId }, 'pulsevault retention could not remove an artifact');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return removed;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Run `sweepAbandonedUploads` every `sweepIntervalSeconds` (first sweep one interval after start).
|
|
80
|
+
* The timer doesn't keep the process alive. `stop` ends it and resolves once a sweep in progress
|
|
81
|
+
* has stopped, so storage can be shut down safely after it. A failed sweep is logged and the next
|
|
82
|
+
* one tries again.
|
|
83
|
+
*/
|
|
84
|
+
export function startRetentionSweep(storage, retention, logger, onRemoved) {
|
|
85
|
+
const intervalMs = (retention.sweepIntervalSeconds ?? DEFAULT_SWEEP_INTERVAL_SECONDS) * 1000;
|
|
86
|
+
const stopping = new AbortController();
|
|
87
|
+
let inFlight = null;
|
|
88
|
+
const timer = setInterval(() => {
|
|
89
|
+
// One sweep at a time: a slow one (a large bucket) isn't overlapped by the next.
|
|
90
|
+
if (inFlight)
|
|
91
|
+
return;
|
|
92
|
+
inFlight = sweepAbandonedUploads(storage, {
|
|
93
|
+
abandonedAfterSeconds: retention.abandonedAfterSeconds,
|
|
94
|
+
onRemoved,
|
|
95
|
+
signal: stopping.signal,
|
|
96
|
+
logger,
|
|
97
|
+
})
|
|
98
|
+
.then((removed) => {
|
|
99
|
+
if (removed.length > 0) {
|
|
100
|
+
logger.info({ removed }, 'pulsevault removed abandoned uploads');
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
.catch((err) => {
|
|
104
|
+
logger.error({ err }, 'pulsevault retention sweep failed');
|
|
105
|
+
})
|
|
106
|
+
.finally(() => {
|
|
107
|
+
inFlight = null;
|
|
108
|
+
});
|
|
109
|
+
}, intervalMs);
|
|
110
|
+
timer.unref?.();
|
|
111
|
+
return {
|
|
112
|
+
stop: async () => {
|
|
113
|
+
clearInterval(timer);
|
|
114
|
+
stopping.abort();
|
|
115
|
+
await inFlight;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=retention.js.map
|