turbo-rails 2.0.0.pre.beta.1 → 2.0.0.pre.beta.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/turbo.js +119 -60
- data/app/assets/javascripts/turbo.min.js +6 -6
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/javascript/turbo/index.js +2 -0
- data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
- data/app/models/concerns/turbo/broadcastable.rb +5 -3
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d87101284e042e02ec6ba74cdf085367415619ee0c0f1c8b4013ad18022c00c7
|
4
|
+
data.tar.gz: 1d514a6ece035de00be6c8af254662476343db15827ca02b27fbcd7caab14d5f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b81a8c738ec5f9ff1c3584a4598e116eb5e54557295717e8eaa6ae582d66408c0554477a1bbf7a623bc8d409e45db1e984892c7a784aea2047163c719fb97a7
|
7
|
+
data.tar.gz: 1f86ba10f8bd0b76a722eac75490bc309e292e5dd04c7a488b153792ae9f00c9aef4cedf6dc28534569366c7089667abeaa8ac7ad9458ed50a82be58fec13f42
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/*!
|
2
|
-
Turbo 8.0.0-beta.
|
2
|
+
Turbo 8.0.0-beta.2
|
3
3
|
Copyright © 2023 37signals LLC
|
4
4
|
*/
|
5
5
|
(function(prototype) {
|
@@ -485,12 +485,31 @@ async function around(callback, reader) {
|
|
485
485
|
return [ before, after ];
|
486
486
|
}
|
487
487
|
|
488
|
-
|
488
|
+
class LimitedSet extends Set {
|
489
|
+
constructor(maxSize) {
|
490
|
+
super();
|
491
|
+
this.maxSize = maxSize;
|
492
|
+
}
|
493
|
+
add(value) {
|
494
|
+
if (this.size >= this.maxSize) {
|
495
|
+
const iterator = this.values();
|
496
|
+
const oldestValue = iterator.next().value;
|
497
|
+
this.delete(oldestValue);
|
498
|
+
}
|
499
|
+
super.add(value);
|
500
|
+
}
|
501
|
+
}
|
502
|
+
|
503
|
+
const recentRequests = new LimitedSet(20);
|
504
|
+
|
505
|
+
const nativeFetch = window.fetch;
|
506
|
+
|
507
|
+
function fetchWithTurboHeaders(url, options = {}) {
|
489
508
|
const modifiedHeaders = new Headers(options.headers || {});
|
490
509
|
const requestUID = uuid();
|
491
|
-
|
510
|
+
recentRequests.add(requestUID);
|
492
511
|
modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
|
493
|
-
return
|
512
|
+
return nativeFetch(url, {
|
494
513
|
...options,
|
495
514
|
headers: modifiedHeaders
|
496
515
|
});
|
@@ -609,7 +628,7 @@ class FetchRequest {
|
|
609
628
|
await this.#allowRequestToBeIntercepted(fetchOptions);
|
610
629
|
try {
|
611
630
|
this.delegate.requestStarted(this);
|
612
|
-
const response = await
|
631
|
+
const response = await fetchWithTurboHeaders(this.url.href, fetchOptions);
|
613
632
|
return await this.receive(response);
|
614
633
|
} catch (error) {
|
615
634
|
if (error.name !== "AbortError") {
|
@@ -848,6 +867,7 @@ class FormSubmission {
|
|
848
867
|
this.state = FormSubmissionState.waiting;
|
849
868
|
this.submitter?.setAttribute("disabled", "");
|
850
869
|
this.setSubmitsWith();
|
870
|
+
markAsBusy(this.formElement);
|
851
871
|
dispatch("turbo:submit-start", {
|
852
872
|
target: this.formElement,
|
853
873
|
detail: {
|
@@ -895,6 +915,7 @@ class FormSubmission {
|
|
895
915
|
this.state = FormSubmissionState.stopped;
|
896
916
|
this.submitter?.removeAttribute("disabled");
|
897
917
|
this.resetSubmitterText();
|
918
|
+
clearBusyState(this.formElement);
|
898
919
|
dispatch("turbo:submit-end", {
|
899
920
|
target: this.formElement,
|
900
921
|
detail: {
|
@@ -1176,6 +1197,12 @@ class View {
|
|
1176
1197
|
this.element.removeAttribute("data-turbo-preview");
|
1177
1198
|
}
|
1178
1199
|
}
|
1200
|
+
markVisitDirection(direction) {
|
1201
|
+
this.element.setAttribute("data-turbo-visit-direction", direction);
|
1202
|
+
}
|
1203
|
+
unmarkVisitDirection() {
|
1204
|
+
this.element.removeAttribute("data-turbo-visit-direction");
|
1205
|
+
}
|
1179
1206
|
async renderSnapshot(renderer) {
|
1180
1207
|
await renderer.render();
|
1181
1208
|
}
|
@@ -1480,14 +1507,14 @@ class FrameRenderer extends Renderer {
|
|
1480
1507
|
return true;
|
1481
1508
|
}
|
1482
1509
|
async render() {
|
1483
|
-
await
|
1510
|
+
await nextRepaint();
|
1484
1511
|
this.preservingPermanentElements((() => {
|
1485
1512
|
this.loadFrameElement();
|
1486
1513
|
}));
|
1487
1514
|
this.scrollFrameIntoView();
|
1488
|
-
await
|
1515
|
+
await nextRepaint();
|
1489
1516
|
this.focusFirstAutofocusableElement();
|
1490
|
-
await
|
1517
|
+
await nextRepaint();
|
1491
1518
|
this.activateScriptElements();
|
1492
1519
|
}
|
1493
1520
|
loadFrameElement() {
|
@@ -1846,6 +1873,12 @@ const SystemStatusCode = {
|
|
1846
1873
|
contentTypeMismatch: -2
|
1847
1874
|
};
|
1848
1875
|
|
1876
|
+
const Direction = {
|
1877
|
+
advance: "forward",
|
1878
|
+
restore: "back",
|
1879
|
+
replace: "none"
|
1880
|
+
};
|
1881
|
+
|
1849
1882
|
class Visit {
|
1850
1883
|
identifier=uuid();
|
1851
1884
|
timingMetrics={};
|
@@ -1861,7 +1894,7 @@ class Visit {
|
|
1861
1894
|
this.delegate = delegate;
|
1862
1895
|
this.location = location;
|
1863
1896
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
1864
|
-
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse} = {
|
1897
|
+
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse, direction: direction} = {
|
1865
1898
|
...defaultOptions,
|
1866
1899
|
...options
|
1867
1900
|
};
|
@@ -1878,6 +1911,7 @@ class Visit {
|
|
1878
1911
|
this.scrolled = !willRender;
|
1879
1912
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
1880
1913
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
1914
|
+
this.direction = direction || Direction[action];
|
1881
1915
|
}
|
1882
1916
|
get adapter() {
|
1883
1917
|
return this.delegate.adapter;
|
@@ -2098,7 +2132,7 @@ class Visit {
|
|
2098
2132
|
this.finishRequest();
|
2099
2133
|
}
|
2100
2134
|
performScroll() {
|
2101
|
-
if (!this.scrolled && !this.view.forceReloaded && !this.view.
|
2135
|
+
if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
|
2102
2136
|
if (this.action == "restore") {
|
2103
2137
|
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
|
2104
2138
|
} else {
|
@@ -2162,9 +2196,7 @@ class Visit {
|
|
2162
2196
|
}
|
2163
2197
|
async render(callback) {
|
2164
2198
|
this.cancelRender();
|
2165
|
-
await
|
2166
|
-
this.frame = requestAnimationFrame((() => resolve()));
|
2167
|
-
}));
|
2199
|
+
this.frame = await nextRepaint();
|
2168
2200
|
await callback();
|
2169
2201
|
delete this.frame;
|
2170
2202
|
}
|
@@ -2386,6 +2418,7 @@ class History {
|
|
2386
2418
|
restorationData={};
|
2387
2419
|
started=false;
|
2388
2420
|
pageLoaded=false;
|
2421
|
+
currentIndex=0;
|
2389
2422
|
constructor(delegate) {
|
2390
2423
|
this.delegate = delegate;
|
2391
2424
|
}
|
@@ -2393,6 +2426,7 @@ class History {
|
|
2393
2426
|
if (!this.started) {
|
2394
2427
|
addEventListener("popstate", this.onPopState, false);
|
2395
2428
|
addEventListener("load", this.onPageLoad, false);
|
2429
|
+
this.currentIndex = history.state?.turbo?.restorationIndex || 0;
|
2396
2430
|
this.started = true;
|
2397
2431
|
this.replace(new URL(window.location.href));
|
2398
2432
|
}
|
@@ -2411,9 +2445,11 @@ class History {
|
|
2411
2445
|
this.update(history.replaceState, location, restorationIdentifier);
|
2412
2446
|
}
|
2413
2447
|
update(method, location, restorationIdentifier = uuid()) {
|
2448
|
+
if (method === history.pushState) ++this.currentIndex;
|
2414
2449
|
const state = {
|
2415
2450
|
turbo: {
|
2416
|
-
restorationIdentifier: restorationIdentifier
|
2451
|
+
restorationIdentifier: restorationIdentifier,
|
2452
|
+
restorationIndex: this.currentIndex
|
2417
2453
|
}
|
2418
2454
|
};
|
2419
2455
|
method.call(history, state, "", location.href);
|
@@ -2448,9 +2484,11 @@ class History {
|
|
2448
2484
|
const {turbo: turbo} = event.state || {};
|
2449
2485
|
if (turbo) {
|
2450
2486
|
this.location = new URL(window.location.href);
|
2451
|
-
const {restorationIdentifier: restorationIdentifier} = turbo;
|
2487
|
+
const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo;
|
2452
2488
|
this.restorationIdentifier = restorationIdentifier;
|
2453
|
-
this.
|
2489
|
+
const direction = restorationIndex > this.currentIndex ? "forward" : "back";
|
2490
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
|
2491
|
+
this.currentIndex = restorationIndex;
|
2454
2492
|
}
|
2455
2493
|
}
|
2456
2494
|
};
|
@@ -2725,7 +2763,7 @@ async function withAutofocusFromFragment(fragment, callback) {
|
|
2725
2763
|
elementWithAutofocus.id = willAutofocusId;
|
2726
2764
|
}
|
2727
2765
|
callback();
|
2728
|
-
await
|
2766
|
+
await nextRepaint();
|
2729
2767
|
const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
|
2730
2768
|
if (hasNoActiveElement && willAutofocusId) {
|
2731
2769
|
const elementToAutofocus = document.getElementById(willAutofocusId);
|
@@ -3663,7 +3701,10 @@ class PageView extends View {
|
|
3663
3701
|
return this.snapshotCache.get(location);
|
3664
3702
|
}
|
3665
3703
|
isPageRefresh(visit) {
|
3666
|
-
return !visit || this.lastRenderedLocation.
|
3704
|
+
return !visit || this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace";
|
3705
|
+
}
|
3706
|
+
shouldPreserveScrollPosition(visit) {
|
3707
|
+
return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition;
|
3667
3708
|
}
|
3668
3709
|
get snapshot() {
|
3669
3710
|
return PageSnapshot.fromElement(this.element);
|
@@ -3672,24 +3713,25 @@ class PageView extends View {
|
|
3672
3713
|
|
3673
3714
|
class Preloader {
|
3674
3715
|
selector="a[data-turbo-preload]";
|
3675
|
-
constructor(delegate) {
|
3716
|
+
constructor(delegate, snapshotCache) {
|
3676
3717
|
this.delegate = delegate;
|
3677
|
-
|
3678
|
-
get snapshotCache() {
|
3679
|
-
return this.delegate.navigator.view.snapshotCache;
|
3718
|
+
this.snapshotCache = snapshotCache;
|
3680
3719
|
}
|
3681
3720
|
start() {
|
3682
3721
|
if (document.readyState === "loading") {
|
3683
|
-
|
3684
|
-
this.preloadOnLoadLinksForView(document.body);
|
3685
|
-
}));
|
3722
|
+
document.addEventListener("DOMContentLoaded", this.#preloadAll);
|
3686
3723
|
} else {
|
3687
3724
|
this.preloadOnLoadLinksForView(document.body);
|
3688
3725
|
}
|
3689
3726
|
}
|
3727
|
+
stop() {
|
3728
|
+
document.removeEventListener("DOMContentLoaded", this.#preloadAll);
|
3729
|
+
}
|
3690
3730
|
preloadOnLoadLinksForView(element) {
|
3691
3731
|
for (const link of element.querySelectorAll(this.selector)) {
|
3692
|
-
this.
|
3732
|
+
if (this.delegate.shouldPreloadLink(link)) {
|
3733
|
+
this.preloadURL(link);
|
3734
|
+
}
|
3693
3735
|
}
|
3694
3736
|
}
|
3695
3737
|
async preloadURL(link) {
|
@@ -3697,33 +3739,27 @@ class Preloader {
|
|
3697
3739
|
if (this.snapshotCache.has(location)) {
|
3698
3740
|
return;
|
3699
3741
|
}
|
3700
|
-
|
3701
|
-
|
3702
|
-
headers: {
|
3703
|
-
"Sec-Purpose": "prefetch",
|
3704
|
-
Accept: "text/html"
|
3705
|
-
}
|
3706
|
-
});
|
3707
|
-
const responseText = await response.text();
|
3708
|
-
const snapshot = PageSnapshot.fromHTMLString(responseText);
|
3709
|
-
this.snapshotCache.put(location, snapshot);
|
3710
|
-
} catch (_) {}
|
3742
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, link);
|
3743
|
+
await fetchRequest.perform();
|
3711
3744
|
}
|
3712
|
-
|
3713
|
-
|
3714
|
-
class LimitedSet extends Set {
|
3715
|
-
constructor(maxSize) {
|
3716
|
-
super();
|
3717
|
-
this.maxSize = maxSize;
|
3745
|
+
prepareRequest(fetchRequest) {
|
3746
|
+
fetchRequest.headers["Sec-Purpose"] = "prefetch";
|
3718
3747
|
}
|
3719
|
-
|
3720
|
-
|
3721
|
-
const
|
3722
|
-
const
|
3723
|
-
this.
|
3724
|
-
}
|
3725
|
-
super.add(value);
|
3748
|
+
async requestSucceededWithResponse(fetchRequest, fetchResponse) {
|
3749
|
+
try {
|
3750
|
+
const responseHTML = await fetchResponse.responseHTML;
|
3751
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
3752
|
+
this.snapshotCache.put(fetchRequest.url, snapshot);
|
3753
|
+
} catch (_) {}
|
3726
3754
|
}
|
3755
|
+
requestStarted(fetchRequest) {}
|
3756
|
+
requestErrored(fetchRequest) {}
|
3757
|
+
requestFinished(fetchRequest) {}
|
3758
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
3759
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
3760
|
+
#preloadAll=() => {
|
3761
|
+
this.preloadOnLoadLinksForView(document.body);
|
3762
|
+
};
|
3727
3763
|
}
|
3728
3764
|
|
3729
3765
|
class Cache {
|
@@ -3750,7 +3786,6 @@ class Cache {
|
|
3750
3786
|
class Session {
|
3751
3787
|
navigator=new Navigator(this);
|
3752
3788
|
history=new History(this);
|
3753
|
-
preloader=new Preloader(this);
|
3754
3789
|
view=new PageView(this, document.documentElement);
|
3755
3790
|
adapter=new BrowserAdapter(this);
|
3756
3791
|
pageObserver=new PageObserver(this);
|
@@ -3763,12 +3798,15 @@ class Session {
|
|
3763
3798
|
frameRedirector=new FrameRedirector(this, document.documentElement);
|
3764
3799
|
streamMessageRenderer=new StreamMessageRenderer;
|
3765
3800
|
cache=new Cache(this);
|
3766
|
-
recentRequests=new LimitedSet(20);
|
3767
3801
|
drive=true;
|
3768
3802
|
enabled=true;
|
3769
3803
|
progressBarDelay=500;
|
3770
3804
|
started=false;
|
3771
3805
|
formMode="on";
|
3806
|
+
constructor(recentRequests) {
|
3807
|
+
this.recentRequests = recentRequests;
|
3808
|
+
this.preloader = new Preloader(this, this.view.snapshotCache);
|
3809
|
+
}
|
3772
3810
|
start() {
|
3773
3811
|
if (!this.started) {
|
3774
3812
|
this.pageObserver.start();
|
@@ -3799,6 +3837,7 @@ class Session {
|
|
3799
3837
|
this.streamObserver.stop();
|
3800
3838
|
this.frameRedirector.stop();
|
3801
3839
|
this.history.stop();
|
3840
|
+
this.preloader.stop();
|
3802
3841
|
this.started = false;
|
3803
3842
|
}
|
3804
3843
|
}
|
@@ -3847,11 +3886,24 @@ class Session {
|
|
3847
3886
|
get restorationIdentifier() {
|
3848
3887
|
return this.history.restorationIdentifier;
|
3849
3888
|
}
|
3850
|
-
|
3889
|
+
shouldPreloadLink(element) {
|
3890
|
+
const isUnsafe = element.hasAttribute("data-turbo-method");
|
3891
|
+
const isStream = element.hasAttribute("data-turbo-stream");
|
3892
|
+
const frameTarget = element.getAttribute("data-turbo-frame");
|
3893
|
+
const frame = frameTarget == "_top" ? null : document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
|
3894
|
+
if (isUnsafe || isStream || frame instanceof FrameElement) {
|
3895
|
+
return false;
|
3896
|
+
} else {
|
3897
|
+
const location = new URL(element.href);
|
3898
|
+
return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation);
|
3899
|
+
}
|
3900
|
+
}
|
3901
|
+
historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
|
3851
3902
|
if (this.enabled) {
|
3852
3903
|
this.navigator.startVisit(location, restorationIdentifier, {
|
3853
3904
|
action: "restore",
|
3854
|
-
historyChanged: true
|
3905
|
+
historyChanged: true,
|
3906
|
+
direction: direction
|
3855
3907
|
});
|
3856
3908
|
} else {
|
3857
3909
|
this.adapter.pageInvalidated({
|
@@ -3889,6 +3941,7 @@ class Session {
|
|
3889
3941
|
visitStarted(visit) {
|
3890
3942
|
if (!visit.acceptsStreamResponse) {
|
3891
3943
|
markAsBusy(document.documentElement);
|
3944
|
+
this.view.markVisitDirection(visit.direction);
|
3892
3945
|
}
|
3893
3946
|
extendURLWithDeprecatedProperties(visit.location);
|
3894
3947
|
if (!visit.silent) {
|
@@ -3896,6 +3949,7 @@ class Session {
|
|
3896
3949
|
}
|
3897
3950
|
}
|
3898
3951
|
visitCompleted(visit) {
|
3952
|
+
this.view.unmarkVisitDirection();
|
3899
3953
|
clearBusyState(document.documentElement);
|
3900
3954
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
3901
3955
|
}
|
@@ -4086,7 +4140,7 @@ const deprecatedLocationPropertyDescriptors = {
|
|
4086
4140
|
}
|
4087
4141
|
};
|
4088
4142
|
|
4089
|
-
const session = new Session;
|
4143
|
+
const session = new Session(recentRequests);
|
4090
4144
|
|
4091
4145
|
const {cache: cache, navigator: navigator$1} = session;
|
4092
4146
|
|
@@ -4139,7 +4193,7 @@ var Turbo = Object.freeze({
|
|
4139
4193
|
PageRenderer: PageRenderer,
|
4140
4194
|
PageSnapshot: PageSnapshot,
|
4141
4195
|
FrameRenderer: FrameRenderer,
|
4142
|
-
fetch:
|
4196
|
+
fetch: fetchWithTurboHeaders,
|
4143
4197
|
start: start,
|
4144
4198
|
registerAdapter: registerAdapter,
|
4145
4199
|
visit: visit,
|
@@ -4806,11 +4860,14 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
4806
4860
|
}
|
4807
4861
|
})();
|
4808
4862
|
|
4809
|
-
window.Turbo =
|
4863
|
+
window.Turbo = {
|
4864
|
+
...Turbo,
|
4865
|
+
StreamActions: StreamActions
|
4866
|
+
};
|
4810
4867
|
|
4811
4868
|
start();
|
4812
4869
|
|
4813
|
-
var
|
4870
|
+
var Turbo$1 = Object.freeze({
|
4814
4871
|
__proto__: null,
|
4815
4872
|
FetchEnctype: FetchEnctype,
|
4816
4873
|
FetchMethod: FetchMethod,
|
@@ -4828,7 +4885,7 @@ var turbo_es2017Esm = Object.freeze({
|
|
4828
4885
|
clearCache: clearCache,
|
4829
4886
|
connectStreamSource: connectStreamSource,
|
4830
4887
|
disconnectStreamSource: disconnectStreamSource,
|
4831
|
-
fetch:
|
4888
|
+
fetch: fetchWithTurboHeaders,
|
4832
4889
|
fetchEnctypeFromString: fetchEnctypeFromString,
|
4833
4890
|
fetchMethodFromString: fetchMethodFromString,
|
4834
4891
|
isSafe: isSafe,
|
@@ -4979,6 +5036,8 @@ function isBodyInit(body) {
|
|
4979
5036
|
return body instanceof FormData || body instanceof URLSearchParams;
|
4980
5037
|
}
|
4981
5038
|
|
5039
|
+
window.Turbo = Turbo$1;
|
5040
|
+
|
4982
5041
|
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
|
4983
5042
|
|
4984
5043
|
var adapters = {
|
@@ -5514,4 +5573,4 @@ var index = Object.freeze({
|
|
5514
5573
|
getConfig: getConfig
|
5515
5574
|
});
|
5516
5575
|
|
5517
|
-
export {
|
5576
|
+
export { Turbo$1 as Turbo, cable };
|