@banou/ripple 0.0.4 → 0.0.8
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 +141 -3
- package/_headers +14 -0
- package/api-Jq4OSTp1-B4uxQLLk.js +338 -0
- package/app.webmanifest +50 -0
- package/assets/empty-BvRk9kiK.js +0 -0
- package/assets/libtorrent-DS_9Yo8M.js +6 -0
- package/assets/sintel.torrent +0 -0
- package/assets/worker-SJZDGuG-.js +23 -0
- package/build-DEOZ3A7n.js +1846 -0
- package/index.js +44658 -0
- package/jassub/default.woff2 +0 -0
- package/jassub/fonts.js +1 -0
- package/jassub/jassub-assets.js +0 -0
- package/jassub/jassub-worker-modern.wasm +0 -0
- package/jassub/jassub-worker.js +1 -0
- package/jassub/jassub-worker.wasm +0 -0
- package/jassub/worker.js +148 -0
- package/libav-jspi.wasm +0 -0
- package/libav-worker.js +4824 -0
- package/libav.wasm +0 -0
- package/package.json +14 -81
- package/remote-BLaEXLm6.js +209 -0
- package/sw.js +192 -0
- package/build/app.webmanifest +0 -65
- package/build/assets/index-be95001c.js +0 -211
- package/build/assets/index-d5b720ec.js +0 -109
- package/build/assets/io-worker-bde41815.js +0 -17681
- package/build/index-3576f032.js +0 -14881
- package/build/index-65b2197a.js +0 -116
- package/build/index-9a320571.js +0 -107
- package/build/index.js +0 -33826
- package/build/io-worker-190cc8d7.js +0 -2862
- package/build/jassub-worker-modern.wasm +0 -0
- package/build/jassub-worker.js +0 -15
- package/build/libav-worker.js +0 -1565
- package/build/libav.wasm +0 -0
- package/build/shared-worker.js +0 -13
- package/build/worker.js +0 -38
- /package/{build/assets → assets}/logo.png +0 -0
- /package/{build/assets → assets}/logo.svg +0 -0
- /package/{build/index.html → index.html} +0 -0
package/libav.wasm
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,81 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@banou/ripple",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
],
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
"dev": "vite --port 4560",
|
|
16
|
-
"__dev": "shx rm -rf ./build && shx mkdir ./build && npm run copy-libav && npm run copy-libass && concurrently \"vite --port 4560\" \"vite build --mode development --watch\"",
|
|
17
|
-
"_dev": "shx rm -rf ./build && shx mkdir ./build && npm run copy-libav && npm run copy-html && npm run copy-libass && concurrently \"serve -s -C -p 4560 build\" \"vite build --mode development --watch\"",
|
|
18
|
-
"serve": "serve -s -C -p 4560 build",
|
|
19
|
-
"build": "shx rm -rf build/ && vite build && npm run copy-libav && npm run copy-libass && npm run copy-html",
|
|
20
|
-
"copy-libav": "shx cp node_modules/libav-wasm/build/worker.js build/libav-worker.js && shx cp node_modules/libav-wasm/build/libav.wasm build/libav.wasm",
|
|
21
|
-
"copy-libass": "shx cp node_modules/jassub/dist/jassub-worker.js build/jassub-worker.js && shx cp node_modules/jassub/dist/jassub-worker-modern.wasm build/jassub-worker-modern.wasm",
|
|
22
|
-
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
23
|
-
"preview": "vite preview",
|
|
24
|
-
"copy-html": "shx cp src/index.html build/index.html && shx cp src/app.webmanifest build/app.webmanifest && copyfiles -u 2 ./src/assets/* build/assets",
|
|
25
|
-
"copy-media-player": "copyfiles -u 4 ./node_modules/@banou/media-player/build/* build/tmp && shx rm build/tmp/index.js && shx mv build/tmp/* build && shx rm -rf build/tmp",
|
|
26
|
-
"build-deps": "npm run build-deps-media-player && npm run build-deps-fkn-lib",
|
|
27
|
-
"build-deps-local": "npm run build-deps-media-player && npm run build-deps-fkn-lib-local",
|
|
28
|
-
"build-deps-media-player": "cd node_modules/@banou/media-player && npm i && npm run build && cd ../../..",
|
|
29
|
-
"build-deps-fkn-lib": "cd node_modules/@fkn/lib && npm i && npm run build && cd ../../..",
|
|
30
|
-
"build-deps-fkn-lib-local": "cd node_modules/@fkn/lib && npm i && npm run build-dev && cd ../../..",
|
|
31
|
-
"remove-build-deps": "shx rm -rf node_modules/@banou/media-player"
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"@banou/media-player": "^0.0.1",
|
|
35
|
-
"@emotion/react": "^11.11.1",
|
|
36
|
-
"@fkn/lib": "^0.0.2",
|
|
37
|
-
"@hookform/resolvers": "^3.3.2",
|
|
38
|
-
"@radix-ui/react-hover-card": "^1.0.7",
|
|
39
|
-
"@vitejs/plugin-react": "^4.0.4",
|
|
40
|
-
"chart.js": "^4.4.0",
|
|
41
|
-
"dexie": "^4.0.0-alpha.4",
|
|
42
|
-
"idb-keyval": "^6.2.1",
|
|
43
|
-
"osra": "^0.0.12",
|
|
44
|
-
"parse-torrent": "^11.0.14",
|
|
45
|
-
"queue-utils": "^0.0.0",
|
|
46
|
-
"react": "^18.2.0",
|
|
47
|
-
"react-chartjs-2": "^5.2.0",
|
|
48
|
-
"react-dom": "^18.2.0",
|
|
49
|
-
"react-dropzone": "^14.2.3",
|
|
50
|
-
"react-feather": "^2.0.10",
|
|
51
|
-
"react-hook-form": "^7.48.2",
|
|
52
|
-
"react-router": "^6.16.0",
|
|
53
|
-
"react-router-dom": "^6.16.0",
|
|
54
|
-
"rxdb": "^14.17.1",
|
|
55
|
-
"rxjs": "^7.8.1",
|
|
56
|
-
"webtorrent": "^2.1.25",
|
|
57
|
-
"xstate": "^5.0.0-beta.47",
|
|
58
|
-
"zod": "^3.22.4"
|
|
59
|
-
},
|
|
60
|
-
"devDependencies": {
|
|
61
|
-
"@rollup/plugin-inject": "^5.0.3",
|
|
62
|
-
"@types/react": "^18.2.15",
|
|
63
|
-
"@types/react-dom": "^18.2.7",
|
|
64
|
-
"@types/webtorrent": "^0.109.3",
|
|
65
|
-
"@types/wicg-file-system-access": "^2023.10.1",
|
|
66
|
-
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
67
|
-
"@typescript-eslint/parser": "^6.0.0",
|
|
68
|
-
"concurrently": "^8.2.2",
|
|
69
|
-
"copyfiles": "^2.4.1",
|
|
70
|
-
"eslint": "^8.45.0",
|
|
71
|
-
"eslint-plugin-react-hooks": "^4.6.0",
|
|
72
|
-
"eslint-plugin-react-refresh": "^0.4.3",
|
|
73
|
-
"node-stdlib-browser": "^1.2.0",
|
|
74
|
-
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
75
|
-
"serve": "^14.2.1",
|
|
76
|
-
"shx": "^0.3.4",
|
|
77
|
-
"typescript": "^5.2.2",
|
|
78
|
-
"vite": "^4.4.5",
|
|
79
|
-
"vite-plugin-top-level-await": "^1.3.0"
|
|
80
|
-
}
|
|
81
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@banou/ripple",
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/banou26/ripple.git"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"fkn",
|
|
11
|
+
"fkn-type:app"
|
|
12
|
+
],
|
|
13
|
+
"main": "index.js"
|
|
14
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { a as e } from "./build-DEOZ3A7n.js";
|
|
2
|
+
//#region node_modules/@banou/media-player/dist/remote/index.js
|
|
3
|
+
var t = [
|
|
4
|
+
"loadstart",
|
|
5
|
+
"loadedmetadata",
|
|
6
|
+
"loadeddata",
|
|
7
|
+
"durationchange",
|
|
8
|
+
"timeupdate",
|
|
9
|
+
"play",
|
|
10
|
+
"playing",
|
|
11
|
+
"pause",
|
|
12
|
+
"seeking",
|
|
13
|
+
"seeked",
|
|
14
|
+
"progress",
|
|
15
|
+
"ratechange",
|
|
16
|
+
"volumechange",
|
|
17
|
+
"ended",
|
|
18
|
+
"waiting",
|
|
19
|
+
"stalled",
|
|
20
|
+
"suspend",
|
|
21
|
+
"canplay",
|
|
22
|
+
"canplaythrough",
|
|
23
|
+
"emptied",
|
|
24
|
+
"resize",
|
|
25
|
+
"error"
|
|
26
|
+
], n = [
|
|
27
|
+
"currentTime",
|
|
28
|
+
"volume",
|
|
29
|
+
"muted",
|
|
30
|
+
"playbackRate"
|
|
31
|
+
], r = [
|
|
32
|
+
"play",
|
|
33
|
+
"pause",
|
|
34
|
+
"load"
|
|
35
|
+
], i = (e) => [
|
|
36
|
+
"loadstart",
|
|
37
|
+
...e.readyState >= 1 ? ["loadedmetadata", "durationchange"] : [],
|
|
38
|
+
...e.readyState >= 2 ? ["loadeddata"] : [],
|
|
39
|
+
...e.readyState >= 3 ? ["canplay"] : [],
|
|
40
|
+
"volumechange",
|
|
41
|
+
"ratechange",
|
|
42
|
+
"progress",
|
|
43
|
+
"resize",
|
|
44
|
+
e.paused ? "pause" : "playing",
|
|
45
|
+
...e.ended ? ["ended"] : [],
|
|
46
|
+
...e.error ? ["error"] : []
|
|
47
|
+
], a = (e) => {
|
|
48
|
+
if (!e) return [];
|
|
49
|
+
let t = [];
|
|
50
|
+
for (let n = 0; n < e.length; n++) t.push([e.start(n), e.end(n)]);
|
|
51
|
+
return t;
|
|
52
|
+
}, o = (e) => ({
|
|
53
|
+
currentTime: s(e.currentTime),
|
|
54
|
+
duration: s(e.duration),
|
|
55
|
+
paused: e.paused,
|
|
56
|
+
ended: e.ended ?? !1,
|
|
57
|
+
seeking: e.seeking,
|
|
58
|
+
readyState: e.readyState,
|
|
59
|
+
volume: e.volume ?? 1,
|
|
60
|
+
muted: e.muted ?? !1,
|
|
61
|
+
playbackRate: e.playbackRate ?? 1,
|
|
62
|
+
src: e.src ?? "",
|
|
63
|
+
currentSrc: e.currentSrc ?? "",
|
|
64
|
+
buffered: a(e.buffered),
|
|
65
|
+
seekable: a(e.seekable),
|
|
66
|
+
error: e.error ? {
|
|
67
|
+
code: e.error.code,
|
|
68
|
+
message: e.error.message
|
|
69
|
+
} : null,
|
|
70
|
+
videoWidth: e.videoWidth ?? 0,
|
|
71
|
+
videoHeight: e.videoHeight ?? 0
|
|
72
|
+
}), s = (e) => Number.isNaN(e) ? 0 : e, c = {
|
|
73
|
+
currentTime: 0,
|
|
74
|
+
duration: 0,
|
|
75
|
+
paused: !0,
|
|
76
|
+
ended: !1,
|
|
77
|
+
seeking: !1,
|
|
78
|
+
readyState: 0,
|
|
79
|
+
volume: 1,
|
|
80
|
+
muted: !1,
|
|
81
|
+
playbackRate: 1,
|
|
82
|
+
src: "",
|
|
83
|
+
currentSrc: "",
|
|
84
|
+
buffered: [],
|
|
85
|
+
seekable: [],
|
|
86
|
+
error: null,
|
|
87
|
+
videoWidth: 0,
|
|
88
|
+
videoHeight: 0
|
|
89
|
+
}, l = 8, u = 32, d = (e, t = {}) => {
|
|
90
|
+
h(t.origin);
|
|
91
|
+
let n = typeof window < "u" && window.parent !== window;
|
|
92
|
+
return !t.transport && !n ? () => {} : m(t).serve(e, t);
|
|
93
|
+
}, f = /* @__PURE__ */ new WeakMap(), p = /* @__PURE__ */ new Map(), m = (a) => {
|
|
94
|
+
let s = a.channel ?? "banou-media-player", d = a.transport, m = `${s}|${a.origin ?? "*"}`, h = d ? f.get(d) ?? (f.set(d, /* @__PURE__ */ new Map()), f.get(d)) : p, g = h.get(m);
|
|
95
|
+
if (g) return g;
|
|
96
|
+
let v = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Map(), b = (e) => y.get(e) ?? y.set(e, /* @__PURE__ */ new Set()).get(e), x = (e, t) => {
|
|
97
|
+
y.get(e)?.delete(t);
|
|
98
|
+
try {
|
|
99
|
+
t.onmessage = null, t.close();
|
|
100
|
+
} catch {}
|
|
101
|
+
}, S = (e, t, n) => {
|
|
102
|
+
for (let r of n ? [n] : [...b(e)]) try {
|
|
103
|
+
r.postMessage(t);
|
|
104
|
+
} catch {
|
|
105
|
+
x(e, r);
|
|
106
|
+
}
|
|
107
|
+
}, C = (e) => {
|
|
108
|
+
let t = v.get(e);
|
|
109
|
+
return t ? o(t) : void 0;
|
|
110
|
+
}, w = (e, t) => {
|
|
111
|
+
if (!y.get(e)?.size) return;
|
|
112
|
+
let n = C(e);
|
|
113
|
+
n && S(e, {
|
|
114
|
+
event: t,
|
|
115
|
+
snapshot: n
|
|
116
|
+
});
|
|
117
|
+
}, T = {
|
|
118
|
+
subscribe: (e, t) => {
|
|
119
|
+
if (typeof e != "string") throw TypeError("a player id must be a string");
|
|
120
|
+
if (typeof MessagePort < "u" && !(t instanceof MessagePort)) throw TypeError("subscribe takes a MessagePort");
|
|
121
|
+
if (!y.has(e) && y.size >= u) throw RangeError("too many players on this channel");
|
|
122
|
+
let n = b(e);
|
|
123
|
+
if (n.size >= l) throw RangeError("too many embedders for this player");
|
|
124
|
+
n.add(t), t.onmessage = () => x(e, t), t.start?.();
|
|
125
|
+
let r = C(e);
|
|
126
|
+
r && S(e, {
|
|
127
|
+
event: "loadstart",
|
|
128
|
+
snapshot: r
|
|
129
|
+
}, t);
|
|
130
|
+
},
|
|
131
|
+
set: (e, t, r) => {
|
|
132
|
+
let i = v.get(e);
|
|
133
|
+
if (!(!i || !n.includes(t))) try {
|
|
134
|
+
i[t] = r;
|
|
135
|
+
} catch {}
|
|
136
|
+
},
|
|
137
|
+
call: async (e, t) => {
|
|
138
|
+
let n = v.get(e);
|
|
139
|
+
if (!(!n || !r.includes(t))) try {
|
|
140
|
+
t === "play" ? await n.play() : t === "pause" ? n.pause() : n.load?.();
|
|
141
|
+
} finally {
|
|
142
|
+
w(e, "timeupdate");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}, E = new AbortController();
|
|
146
|
+
E.signal.addEventListener("abort", () => {
|
|
147
|
+
h.delete(m);
|
|
148
|
+
for (let [e, t] of y) for (let n of [...t]) x(e, n);
|
|
149
|
+
y.clear(), v.clear();
|
|
150
|
+
}, { once: !0 }), e(T, {
|
|
151
|
+
transport: a.transport ?? {
|
|
152
|
+
emit: window.parent,
|
|
153
|
+
receive: _(() => window.parent, a.origin)
|
|
154
|
+
},
|
|
155
|
+
origin: a.origin,
|
|
156
|
+
key: s,
|
|
157
|
+
unregisterSignal: E.signal
|
|
158
|
+
});
|
|
159
|
+
let D = { serve: (e, n) => {
|
|
160
|
+
let r = n.id ?? "default", a = t.map((t) => [t, () => {
|
|
161
|
+
v.get(r) === e && w(r, t);
|
|
162
|
+
}]);
|
|
163
|
+
for (let [t, n] of a) e.addEventListener(t, n);
|
|
164
|
+
let s = v.has(r);
|
|
165
|
+
v.set(r, e), s && S(r, {
|
|
166
|
+
event: "emptied",
|
|
167
|
+
snapshot: { ...c }
|
|
168
|
+
});
|
|
169
|
+
for (let t of i(o(e))) w(r, t);
|
|
170
|
+
let l = () => {
|
|
171
|
+
for (let [t, n] of a) e.removeEventListener(t, n);
|
|
172
|
+
v.get(r) === e && (v.delete(r), S(r, {
|
|
173
|
+
event: "emptied",
|
|
174
|
+
snapshot: { ...c }
|
|
175
|
+
}));
|
|
176
|
+
}, u = () => {
|
|
177
|
+
l(), E.abort();
|
|
178
|
+
};
|
|
179
|
+
return n.signal && (n.signal.aborted ? u() : n.signal.addEventListener("abort", u, { once: !0 })), l;
|
|
180
|
+
} };
|
|
181
|
+
return h.set(m, D), D;
|
|
182
|
+
}, h = (e) => {
|
|
183
|
+
if (e === void 0 || e === "*") return;
|
|
184
|
+
if (e === "null") throw TypeError("an opaque origin (\"null\") cannot be spoken to; leave `origin` unset and the sender check stands alone");
|
|
185
|
+
let t;
|
|
186
|
+
try {
|
|
187
|
+
t = new URL(e);
|
|
188
|
+
} catch {
|
|
189
|
+
throw TypeError(`\`origin\` must be an origin like https://example.com, not ${e}`);
|
|
190
|
+
}
|
|
191
|
+
if (t.origin !== e) throw TypeError(`\`origin\` must be an origin like ${t.origin}, not ${e}`);
|
|
192
|
+
}, g = (e) => {
|
|
193
|
+
let t = e && e !== "*" ? e : void 0;
|
|
194
|
+
return {
|
|
195
|
+
target: () => t && t !== "null" ? t : "*",
|
|
196
|
+
admits: (e) => t ? e === t : (e && (t = e), !0)
|
|
197
|
+
};
|
|
198
|
+
}, _ = (e, t, n = g(t)) => (t) => {
|
|
199
|
+
let r = (r) => {
|
|
200
|
+
r.source === e() && n.admits(r.origin) && t(r.data, {
|
|
201
|
+
source: r.source,
|
|
202
|
+
origin: r.origin,
|
|
203
|
+
receiveTransport: window
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
return window.addEventListener("message", r), () => window.removeEventListener("message", r);
|
|
207
|
+
};
|
|
208
|
+
//#endregion
|
|
209
|
+
export { d as exposePlayer };
|
package/sw.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Copied by the build rather than bundled, so it cannot import. `scripts/build-sw.mjs` replaces the
|
|
2
|
+
// three @stamp lines below and refuses to ship a worker where any of them is still a placeholder.
|
|
3
|
+
|
|
4
|
+
const PREFIX = new URL('/__ripple-stream/', self.location.href).href
|
|
5
|
+
|
|
6
|
+
const live = new Map()
|
|
7
|
+
|
|
8
|
+
/* Stamped at build time. These are the dev defaults and must never reach production: a worker whose
|
|
9
|
+
BUILD never changes is never reinstalled, so the update this feature announces would never fire. */
|
|
10
|
+
const MODE = "cache" // @stamp:mode
|
|
11
|
+
const BUILD = "bb553bfd251c2ac2" // @stamp:build
|
|
12
|
+
const MANIFEST = [["/api-Jq4OSTp1-B4uxQLLk.js","c10b405839d5d665"],["/assets/empty-BvRk9kiK.js","e3b0c44298fc1c14"],["/assets/libtorrent-DS_9Yo8M.js","9cdbeb18165a6fa6"],["/assets/worker-SJZDGuG-.js","5d60edb0c4b932c0"],["/build-DEOZ3A7n.js","ed3b894978c81b60"],["/remote-BLaEXLm6.js","23f487ac31e13c9f"]] // @stamp:manifest
|
|
13
|
+
|
|
14
|
+
const CACHE = 'ripple-assets-' + BUILD
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Which requests this worker may answer from its cache, and the whole of it.
|
|
18
|
+
*
|
|
19
|
+
* ONLY hash-named JavaScript chunks. Never the document, never /index.js, never /sw.js. That
|
|
20
|
+
* narrowness is the design, not a shortcut:
|
|
21
|
+
*
|
|
22
|
+
* - A hashed path names its BYTES, so a hit from any build is correct by construction. There is no
|
|
23
|
+
* stale-content failure available here, only a hit or a miss.
|
|
24
|
+
* - Everything else stays on the network, which means a bad build is always replaceable by the next
|
|
25
|
+
* deploy. A worker that served the document from cache could pin somebody to a broken build with
|
|
26
|
+
* no page JavaScript left to run the update check, and nothing short of clearing site data would
|
|
27
|
+
* get them out.
|
|
28
|
+
*
|
|
29
|
+
* It exists because a deploy rotates these filenames and Cloudflare answers the old path with the
|
|
30
|
+
* SPA fallback: 200, text/html, which the browser then runs as a module worker. Measured against
|
|
31
|
+
* production, an earlier build's chunk is byte-identical to one that never existed.
|
|
32
|
+
*/
|
|
33
|
+
const HASHED = /^\/(?:assets\/)?[A-Za-z0-9_.$-]+-[A-Za-z0-9_-]{8,}\.js$/
|
|
34
|
+
|
|
35
|
+
self.addEventListener('install', (event) => {
|
|
36
|
+
// PURGE is the remote kill switch: ship src/sw.mode as `purge` and the next deploy's worker takes
|
|
37
|
+
// over at once, deletes every cache, and answers nothing. It needs no cooperation from the page.
|
|
38
|
+
if (MODE === 'purge') { self.skipWaiting(); return }
|
|
39
|
+
/*
|
|
40
|
+
* No skipWaiting. A new worker installs and WAITS, so a page keeps the build it loaded with until
|
|
41
|
+
* somebody presses Update. That is the opposite of what this worker used to do, and it is the
|
|
42
|
+
* point: taking over immediately is what would swap the chunk set under a running engine.
|
|
43
|
+
*/
|
|
44
|
+
event.waitUntil(
|
|
45
|
+
caches.open(CACHE)
|
|
46
|
+
.then((cache) => cache.addAll(MANIFEST.map(([path]) => path)))
|
|
47
|
+
// A failed precache must not fail the install. The worker still routes and still fills the
|
|
48
|
+
// cache on demand; a rejected install would leave the origin with no worker at all, which is
|
|
49
|
+
// strictly worse than one with a cold cache.
|
|
50
|
+
.catch(() => undefined),
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
self.addEventListener('activate', (event) => {
|
|
55
|
+
event.waitUntil((async () => {
|
|
56
|
+
if (MODE === 'purge') {
|
|
57
|
+
for (const key of await caches.keys()) await caches.delete(key)
|
|
58
|
+
await self.clients.claim()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
// every generation but this one: a hashed chunk is immutable, so old generations are only ever
|
|
62
|
+
// wasted room, never wrong answers
|
|
63
|
+
for (const key of await caches.keys()) {
|
|
64
|
+
if (key.startsWith('ripple-assets-') && key !== CACHE) await caches.delete(key)
|
|
65
|
+
}
|
|
66
|
+
await self.clients.claim()
|
|
67
|
+
})())
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
self.addEventListener('message', (event) => {
|
|
71
|
+
const data = event.data
|
|
72
|
+
if (!data) return
|
|
73
|
+
if (data.type === 'stream-open') {
|
|
74
|
+
const port = event.ports[0]
|
|
75
|
+
if (!port) return
|
|
76
|
+
live.set(data.id, { name: data.name, size: data.size, port, resolvePull: null })
|
|
77
|
+
port.postMessage({ type: 'stream-ready' })
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
// Answered only while the download is still live: a worker killed mid-download comes back with an empty map.
|
|
81
|
+
if (data.type === 'ping' && live.has(data.id) && event.source) {
|
|
82
|
+
event.source.postMessage({ type: 'pong', id: data.id })
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
// Pressed Update. Only a WAITING worker ever receives this, and taking over fires controllerchange
|
|
86
|
+
// in every open page, which is what reloads them all together.
|
|
87
|
+
if (data.type === 'take-over') self.skipWaiting()
|
|
88
|
+
if (data.type === 'which-build' && event.source) {
|
|
89
|
+
event.source.postMessage({ type: 'build', build: BUILD, mode: MODE })
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const disposition = (name) => {
|
|
94
|
+
const safe = String(name || 'download').replace(/[\r\n"\\]/g, '_')
|
|
95
|
+
return `attachment; filename="${safe}"; filename*=UTF-8''${encodeURIComponent(name || 'download')}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
self.addEventListener('fetch', (event) => {
|
|
99
|
+
const url = event.request.url
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* The cache route. Checked BEFORE the stream prefix bails out, and deliberately after nothing:
|
|
103
|
+
* the two never overlap, since a stream URL is under /__ripple-stream/ and can never match a
|
|
104
|
+
* hashed .js path.
|
|
105
|
+
*
|
|
106
|
+
* Cache first, network second, and a miss simply proxies, which is byte-identical to not
|
|
107
|
+
* intercepting at all. Only same-origin GETs, so a request to any other host is untouched.
|
|
108
|
+
*/
|
|
109
|
+
if (MODE !== 'purge' && event.request.method === 'GET' && !url.startsWith(PREFIX)) {
|
|
110
|
+
const path = new URL(url).pathname
|
|
111
|
+
if (new URL(url).origin === self.location.origin && HASHED.test(path)) {
|
|
112
|
+
event.respondWith((async () => {
|
|
113
|
+
const cache = await caches.open(CACHE)
|
|
114
|
+
const hit = await cache.match(path)
|
|
115
|
+
if (hit) return hit
|
|
116
|
+
const fresh = await fetch(event.request)
|
|
117
|
+
// An HTML answer here IS the bug this worker exists for: the chunk is gone and Cloudflare
|
|
118
|
+
// served the SPA fallback with a 200. Never store that, or the cache would preserve the
|
|
119
|
+
// failure across the very reload meant to cure it.
|
|
120
|
+
const type = fresh.headers.get('content-type') || ''
|
|
121
|
+
if (fresh.ok && type.includes('javascript')) await cache.put(path, fresh.clone())
|
|
122
|
+
return fresh
|
|
123
|
+
})())
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!url.startsWith(PREFIX)) return
|
|
129
|
+
|
|
130
|
+
const id = url.slice(PREFIX.length).split('/')[0]
|
|
131
|
+
const entry = live.get(id)
|
|
132
|
+
// Answered rather than left to fall through: an unclaimed synthetic URL would be served the app's own HTML.
|
|
133
|
+
if (!entry) {
|
|
134
|
+
event.respondWith(new Response('Unknown download', { status: 404 }))
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const finish = () => {
|
|
139
|
+
if (entry.done) return
|
|
140
|
+
entry.done = true
|
|
141
|
+
live.delete(id)
|
|
142
|
+
const resolve = entry.resolvePull
|
|
143
|
+
entry.resolvePull = null
|
|
144
|
+
if (resolve) resolve()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const body = new ReadableStream({
|
|
148
|
+
start(controller) {
|
|
149
|
+
entry.port.onmessage = (e) => {
|
|
150
|
+
const msg = e.data
|
|
151
|
+
if (!msg || entry.done) return
|
|
152
|
+
if (msg.type === 'chunk') {
|
|
153
|
+
controller.enqueue(msg.data)
|
|
154
|
+
const resolve = entry.resolvePull
|
|
155
|
+
entry.resolvePull = null
|
|
156
|
+
if (resolve) resolve()
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if (msg.type === 'end') {
|
|
160
|
+
controller.close()
|
|
161
|
+
finish()
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
if (msg.type === 'abort') {
|
|
165
|
+
controller.error(new Error(msg.reason || 'aborted'))
|
|
166
|
+
finish()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
// Returning a promise that only settles once the chunk arrives is what creates the backpressure.
|
|
171
|
+
pull() {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
entry.resolvePull = resolve
|
|
174
|
+
entry.port.postMessage({ type: 'pull' })
|
|
175
|
+
})
|
|
176
|
+
},
|
|
177
|
+
cancel(reason) {
|
|
178
|
+
entry.port.postMessage({ type: 'cancel', reason: String(reason) })
|
|
179
|
+
finish()
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const headers = {
|
|
184
|
+
'Content-Type': 'application/octet-stream',
|
|
185
|
+
'Content-Disposition': disposition(entry.name),
|
|
186
|
+
'Cache-Control': 'no-store',
|
|
187
|
+
}
|
|
188
|
+
// Only when it is known exactly: a zip's length is not known until it has been written.
|
|
189
|
+
if (entry.size > 0) headers['Content-Length'] = String(entry.size)
|
|
190
|
+
|
|
191
|
+
event.respondWith(new Response(body, { headers }))
|
|
192
|
+
})
|
package/build/app.webmanifest
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"short_name": "Ripple",
|
|
3
|
-
"name": "Ripple - A torrent web app",
|
|
4
|
-
"description": "A torrent app on the web",
|
|
5
|
-
"icons": [
|
|
6
|
-
{
|
|
7
|
-
"src": "./assets/logo.png",
|
|
8
|
-
"sizes": "512x512",
|
|
9
|
-
"type": "image/png"
|
|
10
|
-
}
|
|
11
|
-
],
|
|
12
|
-
"id": "/?source=pwa",
|
|
13
|
-
"start_url": "/?source=pwa",
|
|
14
|
-
"background_color": "#3367D6",
|
|
15
|
-
"display": "standalone",
|
|
16
|
-
"scope": "/",
|
|
17
|
-
"theme_color": "#3367D6",
|
|
18
|
-
"shortcuts": [
|
|
19
|
-
{
|
|
20
|
-
"name": "Pause all torrents",
|
|
21
|
-
"short_name": "Pause all",
|
|
22
|
-
"description": "Pause all currently downloading torrents",
|
|
23
|
-
"url": "/?source=pwa&pauseAll=true"
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"protocol_handlers": [
|
|
27
|
-
{
|
|
28
|
-
"protocol": "magnet",
|
|
29
|
-
"url": "/protocol-handler?magnet=%s"
|
|
30
|
-
}
|
|
31
|
-
],
|
|
32
|
-
"file_handlers": [
|
|
33
|
-
{
|
|
34
|
-
"action": "/file-handler",
|
|
35
|
-
"accept": {
|
|
36
|
-
"application/x-bittorrent": [".torrent"],
|
|
37
|
-
"x-scheme-handler/magnet": ["magnet:?xt=urn:btih:"]
|
|
38
|
-
},
|
|
39
|
-
"icons": [
|
|
40
|
-
{
|
|
41
|
-
"src": "csv-icon.png",
|
|
42
|
-
"sizes": "256x256",
|
|
43
|
-
"type": "image/png"
|
|
44
|
-
}
|
|
45
|
-
]
|
|
46
|
-
}
|
|
47
|
-
],
|
|
48
|
-
"share_target": {
|
|
49
|
-
"action": "/share-target",
|
|
50
|
-
"method": "POST",
|
|
51
|
-
"enctype": "multipart/form-data",
|
|
52
|
-
"params": {
|
|
53
|
-
"files": [
|
|
54
|
-
{
|
|
55
|
-
"name": "file",
|
|
56
|
-
"accept": [
|
|
57
|
-
"application/vnd.ripple+json",
|
|
58
|
-
"application/json",
|
|
59
|
-
".ripple"
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|