turbo-rails 0.8.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +44 -11
- data/app/assets/javascripts/turbo.js +334 -108
- data/app/assets/javascripts/turbo.min.js +6 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -0
- data/app/channels/turbo/streams_channel.rb +1 -1
- data/app/models/concerns/turbo/broadcastable.rb +24 -6
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/turbo/engine.rb +1 -1
- data/lib/turbo/version.rb +1 -1
- metadata +19 -4
@@ -14,6 +14,31 @@
|
|
14
14
|
Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
|
15
15
|
})();
|
16
16
|
|
17
|
+
(function(prototype) {
|
18
|
+
if (typeof prototype.requestSubmit == "function") return;
|
19
|
+
prototype.requestSubmit = function(submitter) {
|
20
|
+
if (submitter) {
|
21
|
+
validateSubmitter(submitter, this);
|
22
|
+
submitter.click();
|
23
|
+
} else {
|
24
|
+
submitter = document.createElement("input");
|
25
|
+
submitter.type = "submit";
|
26
|
+
submitter.hidden = true;
|
27
|
+
this.appendChild(submitter);
|
28
|
+
submitter.click();
|
29
|
+
this.removeChild(submitter);
|
30
|
+
}
|
31
|
+
};
|
32
|
+
function validateSubmitter(submitter, form) {
|
33
|
+
submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
|
34
|
+
submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
|
35
|
+
submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
|
36
|
+
}
|
37
|
+
function raise(errorConstructor, message, name) {
|
38
|
+
throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name);
|
39
|
+
}
|
40
|
+
})(HTMLFormElement.prototype);
|
41
|
+
|
17
42
|
const submittersByForm = new WeakMap;
|
18
43
|
|
19
44
|
function findSubmitterFromClickTarget(target) {
|
@@ -160,6 +185,11 @@ function getAnchor(url) {
|
|
160
185
|
}
|
161
186
|
}
|
162
187
|
|
188
|
+
function getAction(form, submitter) {
|
189
|
+
const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action;
|
190
|
+
return expandURL(action);
|
191
|
+
}
|
192
|
+
|
163
193
|
function getExtension(url) {
|
164
194
|
return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
|
165
195
|
}
|
@@ -173,6 +203,10 @@ function isPrefixedBy(baseURL, url) {
|
|
173
203
|
return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
|
174
204
|
}
|
175
205
|
|
206
|
+
function locationIsVisitable(location, rootLocation) {
|
207
|
+
return isPrefixedBy(location, rootLocation) && isHTML(location);
|
208
|
+
}
|
209
|
+
|
176
210
|
function getRequestURL(url) {
|
177
211
|
const anchor = getAnchor(url);
|
178
212
|
return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
|
@@ -308,6 +342,31 @@ function uuid() {
|
|
308
342
|
})).join("");
|
309
343
|
}
|
310
344
|
|
345
|
+
function getAttribute(attributeName, ...elements) {
|
346
|
+
for (const value of elements.map((element => element === null || element === void 0 ? void 0 : element.getAttribute(attributeName)))) {
|
347
|
+
if (typeof value == "string") return value;
|
348
|
+
}
|
349
|
+
return null;
|
350
|
+
}
|
351
|
+
|
352
|
+
function markAsBusy(...elements) {
|
353
|
+
for (const element of elements) {
|
354
|
+
if (element.localName == "turbo-frame") {
|
355
|
+
element.setAttribute("busy", "");
|
356
|
+
}
|
357
|
+
element.setAttribute("aria-busy", "true");
|
358
|
+
}
|
359
|
+
}
|
360
|
+
|
361
|
+
function clearBusyState(...elements) {
|
362
|
+
for (const element of elements) {
|
363
|
+
if (element.localName == "turbo-frame") {
|
364
|
+
element.removeAttribute("busy");
|
365
|
+
}
|
366
|
+
element.removeAttribute("aria-busy");
|
367
|
+
}
|
368
|
+
}
|
369
|
+
|
311
370
|
var FetchMethod;
|
312
371
|
|
313
372
|
(function(FetchMethod) {
|
@@ -344,12 +403,8 @@ class FetchRequest {
|
|
344
403
|
this.delegate = delegate;
|
345
404
|
this.method = method;
|
346
405
|
this.headers = this.defaultHeaders;
|
347
|
-
|
348
|
-
|
349
|
-
} else {
|
350
|
-
this.body = body;
|
351
|
-
this.url = location;
|
352
|
-
}
|
406
|
+
this.body = body;
|
407
|
+
this.url = location;
|
353
408
|
this.target = target;
|
354
409
|
}
|
355
410
|
get location() {
|
@@ -407,7 +462,7 @@ class FetchRequest {
|
|
407
462
|
credentials: "same-origin",
|
408
463
|
headers: this.headers,
|
409
464
|
redirect: "follow",
|
410
|
-
body: this.body,
|
465
|
+
body: this.isIdempotent ? null : this.body,
|
411
466
|
signal: this.abortSignal,
|
412
467
|
referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
|
413
468
|
};
|
@@ -429,7 +484,7 @@ class FetchRequest {
|
|
429
484
|
cancelable: true,
|
430
485
|
detail: {
|
431
486
|
fetchOptions: fetchOptions,
|
432
|
-
url: this.url
|
487
|
+
url: this.url,
|
433
488
|
resume: this.resolveRequestPromise
|
434
489
|
},
|
435
490
|
target: this.target
|
@@ -438,20 +493,6 @@ class FetchRequest {
|
|
438
493
|
}
|
439
494
|
}
|
440
495
|
|
441
|
-
function mergeFormDataEntries(url, entries) {
|
442
|
-
const currentSearchParams = new URLSearchParams(url.search);
|
443
|
-
for (const [name, value] of entries) {
|
444
|
-
if (value instanceof File) continue;
|
445
|
-
if (currentSearchParams.has(name)) {
|
446
|
-
currentSearchParams.delete(name);
|
447
|
-
url.searchParams.set(name, value);
|
448
|
-
} else {
|
449
|
-
url.searchParams.append(name, value);
|
450
|
-
}
|
451
|
-
}
|
452
|
-
return url;
|
453
|
-
}
|
454
|
-
|
455
496
|
class AppearanceObserver {
|
456
497
|
constructor(delegate, element) {
|
457
498
|
this.started = false;
|
@@ -553,9 +594,16 @@ class FormSubmission {
|
|
553
594
|
this.formElement = formElement;
|
554
595
|
this.submitter = submitter;
|
555
596
|
this.formData = buildFormData(formElement, submitter);
|
597
|
+
this.location = expandURL(this.action);
|
598
|
+
if (this.method == FetchMethod.get) {
|
599
|
+
mergeFormDataEntries(this.location, [ ...this.body.entries() ]);
|
600
|
+
}
|
556
601
|
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
|
557
602
|
this.mustRedirect = mustRedirect;
|
558
603
|
}
|
604
|
+
static confirmMethod(message, element) {
|
605
|
+
return confirm(message);
|
606
|
+
}
|
559
607
|
get method() {
|
560
608
|
var _a;
|
561
609
|
const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
|
@@ -566,9 +614,6 @@ class FormSubmission {
|
|
566
614
|
const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null;
|
567
615
|
return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
|
568
616
|
}
|
569
|
-
get location() {
|
570
|
-
return expandURL(this.action);
|
571
|
-
}
|
572
617
|
get body() {
|
573
618
|
if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
|
574
619
|
return new URLSearchParams(this.stringFormData);
|
@@ -586,8 +631,20 @@ class FormSubmission {
|
|
586
631
|
get stringFormData() {
|
587
632
|
return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
|
588
633
|
}
|
634
|
+
get confirmationMessage() {
|
635
|
+
return this.formElement.getAttribute("data-turbo-confirm");
|
636
|
+
}
|
637
|
+
get needsConfirmation() {
|
638
|
+
return this.confirmationMessage !== null;
|
639
|
+
}
|
589
640
|
async start() {
|
590
641
|
const {initialized: initialized, requesting: requesting} = FormSubmissionState;
|
642
|
+
if (this.needsConfirmation) {
|
643
|
+
const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
|
644
|
+
if (!answer) {
|
645
|
+
return;
|
646
|
+
}
|
647
|
+
}
|
591
648
|
if (this.state == initialized) {
|
592
649
|
this.state = requesting;
|
593
650
|
return this.fetchRequest.perform();
|
@@ -611,7 +668,9 @@ class FormSubmission {
|
|
611
668
|
}
|
612
669
|
}
|
613
670
|
requestStarted(request) {
|
671
|
+
var _a;
|
614
672
|
this.state = FormSubmissionState.waiting;
|
673
|
+
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
|
615
674
|
dispatch("turbo:submit-start", {
|
616
675
|
target: this.formElement,
|
617
676
|
detail: {
|
@@ -656,7 +715,9 @@ class FormSubmission {
|
|
656
715
|
this.delegate.formSubmissionErrored(this, error);
|
657
716
|
}
|
658
717
|
requestFinished(request) {
|
718
|
+
var _a;
|
659
719
|
this.state = FormSubmissionState.stopped;
|
720
|
+
(_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
|
660
721
|
dispatch("turbo:submit-end", {
|
661
722
|
target: this.formElement,
|
662
723
|
detail: Object.assign({
|
@@ -700,6 +761,16 @@ function responseSucceededWithoutRedirect(response) {
|
|
700
761
|
return response.statusCode == 200 && !response.redirected;
|
701
762
|
}
|
702
763
|
|
764
|
+
function mergeFormDataEntries(url, entries) {
|
765
|
+
const searchParams = new URLSearchParams;
|
766
|
+
for (const [name, value] of entries) {
|
767
|
+
if (value instanceof File) continue;
|
768
|
+
searchParams.append(name, value);
|
769
|
+
}
|
770
|
+
url.search = searchParams.toString();
|
771
|
+
return url;
|
772
|
+
}
|
773
|
+
|
703
774
|
class Snapshot {
|
704
775
|
constructor(element) {
|
705
776
|
this.element = element;
|
@@ -742,9 +813,10 @@ class FormInterceptor {
|
|
742
813
|
constructor(delegate, element) {
|
743
814
|
this.submitBubbled = event => {
|
744
815
|
const form = event.target;
|
745
|
-
if (form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
|
816
|
+
if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
|
746
817
|
const submitter = event.submitter || undefined;
|
747
|
-
|
818
|
+
const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
|
819
|
+
if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
|
748
820
|
event.preventDefault();
|
749
821
|
event.stopImmediatePropagation();
|
750
822
|
this.delegate.formSubmissionIntercepted(form, submitter);
|
@@ -955,10 +1027,11 @@ function createPlaceholderForPermanentElement(permanentElement) {
|
|
955
1027
|
}
|
956
1028
|
|
957
1029
|
class Renderer {
|
958
|
-
constructor(currentSnapshot, newSnapshot, isPreview) {
|
1030
|
+
constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
|
959
1031
|
this.currentSnapshot = currentSnapshot;
|
960
1032
|
this.newSnapshot = newSnapshot;
|
961
1033
|
this.isPreview = isPreview;
|
1034
|
+
this.willRender = willRender;
|
962
1035
|
this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
|
963
1036
|
resolve: resolve,
|
964
1037
|
reject: reject
|
@@ -1340,7 +1413,9 @@ var VisitState;
|
|
1340
1413
|
|
1341
1414
|
const defaultOptions = {
|
1342
1415
|
action: "advance",
|
1343
|
-
historyChanged: false
|
1416
|
+
historyChanged: false,
|
1417
|
+
visitCachedSnapshot: () => {},
|
1418
|
+
willRender: true
|
1344
1419
|
};
|
1345
1420
|
|
1346
1421
|
var SystemStatusCode;
|
@@ -1363,13 +1438,16 @@ class Visit {
|
|
1363
1438
|
this.delegate = delegate;
|
1364
1439
|
this.location = location;
|
1365
1440
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
1366
|
-
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response} = Object.assign(Object.assign({}, defaultOptions), options);
|
1441
|
+
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender} = Object.assign(Object.assign({}, defaultOptions), options);
|
1367
1442
|
this.action = action;
|
1368
1443
|
this.historyChanged = historyChanged;
|
1369
1444
|
this.referrer = referrer;
|
1370
1445
|
this.snapshotHTML = snapshotHTML;
|
1371
1446
|
this.response = response;
|
1372
1447
|
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
1448
|
+
this.visitCachedSnapshot = visitCachedSnapshot;
|
1449
|
+
this.willRender = willRender;
|
1450
|
+
this.scrolled = !willRender;
|
1373
1451
|
}
|
1374
1452
|
get adapter() {
|
1375
1453
|
return this.delegate.adapter;
|
@@ -1468,7 +1546,7 @@ class Visit {
|
|
1468
1546
|
this.cacheSnapshot();
|
1469
1547
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1470
1548
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
1471
|
-
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
|
1549
|
+
await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
|
1472
1550
|
this.adapter.visitRendered(this);
|
1473
1551
|
this.complete();
|
1474
1552
|
} else {
|
@@ -1505,7 +1583,7 @@ class Visit {
|
|
1505
1583
|
this.adapter.visitRendered(this);
|
1506
1584
|
} else {
|
1507
1585
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1508
|
-
await this.view.renderPage(snapshot, isPreview);
|
1586
|
+
await this.view.renderPage(snapshot, isPreview, this.willRender);
|
1509
1587
|
this.adapter.visitRendered(this);
|
1510
1588
|
if (!isPreview) {
|
1511
1589
|
this.complete();
|
@@ -1515,7 +1593,8 @@ class Visit {
|
|
1515
1593
|
}
|
1516
1594
|
}
|
1517
1595
|
followRedirect() {
|
1518
|
-
|
1596
|
+
var _a;
|
1597
|
+
if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
|
1519
1598
|
this.adapter.visitProposedToLocation(this.redirectedToLocation, {
|
1520
1599
|
action: "replace",
|
1521
1600
|
response: this.response
|
@@ -1537,34 +1616,41 @@ class Visit {
|
|
1537
1616
|
requestPreventedHandlingResponse(request, response) {}
|
1538
1617
|
async requestSucceededWithResponse(request, response) {
|
1539
1618
|
const responseHTML = await response.responseHTML;
|
1619
|
+
const {redirected: redirected, statusCode: statusCode} = response;
|
1540
1620
|
if (responseHTML == undefined) {
|
1541
1621
|
this.recordResponse({
|
1542
|
-
statusCode: SystemStatusCode.contentTypeMismatch
|
1622
|
+
statusCode: SystemStatusCode.contentTypeMismatch,
|
1623
|
+
redirected: redirected
|
1543
1624
|
});
|
1544
1625
|
} else {
|
1545
1626
|
this.redirectedToLocation = response.redirected ? response.location : undefined;
|
1546
1627
|
this.recordResponse({
|
1547
|
-
statusCode:
|
1548
|
-
responseHTML: responseHTML
|
1628
|
+
statusCode: statusCode,
|
1629
|
+
responseHTML: responseHTML,
|
1630
|
+
redirected: redirected
|
1549
1631
|
});
|
1550
1632
|
}
|
1551
1633
|
}
|
1552
1634
|
async requestFailedWithResponse(request, response) {
|
1553
1635
|
const responseHTML = await response.responseHTML;
|
1636
|
+
const {redirected: redirected, statusCode: statusCode} = response;
|
1554
1637
|
if (responseHTML == undefined) {
|
1555
1638
|
this.recordResponse({
|
1556
|
-
statusCode: SystemStatusCode.contentTypeMismatch
|
1639
|
+
statusCode: SystemStatusCode.contentTypeMismatch,
|
1640
|
+
redirected: redirected
|
1557
1641
|
});
|
1558
1642
|
} else {
|
1559
1643
|
this.recordResponse({
|
1560
|
-
statusCode:
|
1561
|
-
responseHTML: responseHTML
|
1644
|
+
statusCode: statusCode,
|
1645
|
+
responseHTML: responseHTML,
|
1646
|
+
redirected: redirected
|
1562
1647
|
});
|
1563
1648
|
}
|
1564
1649
|
}
|
1565
1650
|
requestErrored(request, error) {
|
1566
1651
|
this.recordResponse({
|
1567
|
-
statusCode: SystemStatusCode.networkFailure
|
1652
|
+
statusCode: SystemStatusCode.networkFailure,
|
1653
|
+
redirected: false
|
1568
1654
|
});
|
1569
1655
|
}
|
1570
1656
|
requestFinished() {
|
@@ -1622,12 +1708,12 @@ class Visit {
|
|
1622
1708
|
} else if (this.action == "restore") {
|
1623
1709
|
return !this.hasCachedSnapshot();
|
1624
1710
|
} else {
|
1625
|
-
return
|
1711
|
+
return this.willRender;
|
1626
1712
|
}
|
1627
1713
|
}
|
1628
1714
|
cacheSnapshot() {
|
1629
1715
|
if (!this.snapshotCached) {
|
1630
|
-
this.view.cacheSnapshot();
|
1716
|
+
this.view.cacheSnapshot().then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
|
1631
1717
|
this.snapshotCached = true;
|
1632
1718
|
}
|
1633
1719
|
}
|
@@ -1664,10 +1750,10 @@ class BrowserAdapter {
|
|
1664
1750
|
this.navigator.startVisit(location, uuid(), options);
|
1665
1751
|
}
|
1666
1752
|
visitStarted(visit) {
|
1753
|
+
visit.loadCachedSnapshot();
|
1667
1754
|
visit.issueRequest();
|
1668
1755
|
visit.changeHistory();
|
1669
1756
|
visit.goToSamePageAnchor();
|
1670
|
-
visit.loadCachedSnapshot();
|
1671
1757
|
}
|
1672
1758
|
visitRequestStarted(visit) {
|
1673
1759
|
this.progressBar.setValue(0);
|
@@ -1775,7 +1861,7 @@ class FormSubmitObserver {
|
|
1775
1861
|
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
1776
1862
|
const submitter = event.submitter || undefined;
|
1777
1863
|
if (form) {
|
1778
|
-
const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
|
1864
|
+
const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
|
1779
1865
|
if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
|
1780
1866
|
event.preventDefault();
|
1781
1867
|
this.delegate.formSubmitted(form, submitter);
|
@@ -1819,12 +1905,11 @@ class FrameRedirector {
|
|
1819
1905
|
linkClickIntercepted(element, url) {
|
1820
1906
|
const frame = this.findFrameElement(element);
|
1821
1907
|
if (frame) {
|
1822
|
-
frame.
|
1823
|
-
frame.src = url;
|
1908
|
+
frame.delegate.linkClickIntercepted(element, url);
|
1824
1909
|
}
|
1825
1910
|
}
|
1826
1911
|
shouldInterceptFormSubmission(element, submitter) {
|
1827
|
-
return this.
|
1912
|
+
return this.shouldSubmit(element, submitter);
|
1828
1913
|
}
|
1829
1914
|
formSubmissionIntercepted(element, submitter) {
|
1830
1915
|
const frame = this.findFrameElement(element, submitter);
|
@@ -1833,6 +1918,13 @@ class FrameRedirector {
|
|
1833
1918
|
frame.delegate.formSubmissionIntercepted(element, submitter);
|
1834
1919
|
}
|
1835
1920
|
}
|
1921
|
+
shouldSubmit(form, submitter) {
|
1922
|
+
var _a;
|
1923
|
+
const action = getAction(form, submitter);
|
1924
|
+
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
1925
|
+
const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
|
1926
|
+
return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
|
1927
|
+
}
|
1836
1928
|
shouldRedirect(element, submitter) {
|
1837
1929
|
const frame = this.findFrameElement(element, submitter);
|
1838
1930
|
return frame ? frame != element.closest("turbo-frame") : false;
|
@@ -1988,7 +2080,11 @@ class Navigator {
|
|
1988
2080
|
}
|
1989
2081
|
proposeVisit(location, options = {}) {
|
1990
2082
|
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
|
1991
|
-
|
2083
|
+
if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
|
2084
|
+
this.delegate.visitProposedToLocation(location, options);
|
2085
|
+
} else {
|
2086
|
+
window.location.href = location.toString();
|
2087
|
+
}
|
1992
2088
|
}
|
1993
2089
|
}
|
1994
2090
|
startVisit(locatable, restorationIdentifier, options = {}) {
|
@@ -2001,13 +2097,7 @@ class Navigator {
|
|
2001
2097
|
submitForm(form, submitter) {
|
2002
2098
|
this.stop();
|
2003
2099
|
this.formSubmission = new FormSubmission(this, form, submitter, true);
|
2004
|
-
|
2005
|
-
this.proposeVisit(this.formSubmission.fetchRequest.url, {
|
2006
|
-
action: this.getActionForFormSubmission(this.formSubmission)
|
2007
|
-
});
|
2008
|
-
} else {
|
2009
|
-
this.formSubmission.start();
|
2010
|
-
}
|
2100
|
+
this.formSubmission.start();
|
2011
2101
|
}
|
2012
2102
|
stop() {
|
2013
2103
|
if (this.formSubmission) {
|
@@ -2040,11 +2130,14 @@ class Navigator {
|
|
2040
2130
|
if (formSubmission.method != FetchMethod.get) {
|
2041
2131
|
this.view.clearSnapshotCache();
|
2042
2132
|
}
|
2043
|
-
const {statusCode: statusCode} = fetchResponse;
|
2133
|
+
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
2134
|
+
const action = this.getActionForFormSubmission(formSubmission);
|
2044
2135
|
const visitOptions = {
|
2136
|
+
action: action,
|
2045
2137
|
response: {
|
2046
2138
|
statusCode: statusCode,
|
2047
|
-
responseHTML: responseHTML
|
2139
|
+
responseHTML: responseHTML,
|
2140
|
+
redirected: redirected
|
2048
2141
|
}
|
2049
2142
|
};
|
2050
2143
|
this.proposeVisit(fetchResponse.location, visitOptions);
|
@@ -2095,7 +2188,7 @@ class Navigator {
|
|
2095
2188
|
}
|
2096
2189
|
getActionForFormSubmission(formSubmission) {
|
2097
2190
|
const {formElement: formElement, submitter: submitter} = formSubmission;
|
2098
|
-
const action =
|
2191
|
+
const action = getAttribute("data-turbo-action", submitter, formElement);
|
2099
2192
|
return isAction(action) ? action : "advance";
|
2100
2193
|
}
|
2101
2194
|
}
|
@@ -2295,7 +2388,9 @@ class PageRenderer extends Renderer {
|
|
2295
2388
|
this.mergeHead();
|
2296
2389
|
}
|
2297
2390
|
async render() {
|
2298
|
-
this.
|
2391
|
+
if (this.willRender) {
|
2392
|
+
this.replaceBody();
|
2393
|
+
}
|
2299
2394
|
}
|
2300
2395
|
finishRendering() {
|
2301
2396
|
super.finishRendering();
|
@@ -2431,8 +2526,8 @@ class PageView extends View {
|
|
2431
2526
|
this.snapshotCache = new SnapshotCache(10);
|
2432
2527
|
this.lastRenderedLocation = new URL(location.href);
|
2433
2528
|
}
|
2434
|
-
renderPage(snapshot, isPreview = false) {
|
2435
|
-
const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
|
2529
|
+
renderPage(snapshot, isPreview = false, willRender = true) {
|
2530
|
+
const renderer = new PageRenderer(this.snapshot, snapshot, isPreview, willRender);
|
2436
2531
|
return this.render(renderer);
|
2437
2532
|
}
|
2438
2533
|
renderError(snapshot) {
|
@@ -2447,7 +2542,9 @@ class PageView extends View {
|
|
2447
2542
|
this.delegate.viewWillCacheSnapshot();
|
2448
2543
|
const {snapshot: snapshot, lastRenderedLocation: location} = this;
|
2449
2544
|
await nextEventLoopTick();
|
2450
|
-
|
2545
|
+
const cachedSnapshot = snapshot.clone();
|
2546
|
+
this.snapshotCache.put(location, cachedSnapshot);
|
2547
|
+
return cachedSnapshot;
|
2451
2548
|
}
|
2452
2549
|
}
|
2453
2550
|
getCachedSnapshotForLocation(location) {
|
@@ -2552,7 +2649,7 @@ class Session {
|
|
2552
2649
|
});
|
2553
2650
|
}
|
2554
2651
|
willFollowLinkToLocation(link, location) {
|
2555
|
-
return this.elementDriveEnabled(link) &&
|
2652
|
+
return this.elementDriveEnabled(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location);
|
2556
2653
|
}
|
2557
2654
|
followedLinkToLocation(link, location) {
|
2558
2655
|
const action = this.getActionForLink(link);
|
@@ -2561,14 +2658,23 @@ class Session {
|
|
2561
2658
|
});
|
2562
2659
|
}
|
2563
2660
|
convertLinkWithMethodClickToFormSubmission(link) {
|
2564
|
-
var _a;
|
2565
2661
|
const linkMethod = link.getAttribute("data-turbo-method");
|
2566
2662
|
if (linkMethod) {
|
2567
2663
|
const form = document.createElement("form");
|
2568
2664
|
form.method = linkMethod;
|
2569
2665
|
form.action = link.getAttribute("href") || "undefined";
|
2570
2666
|
form.hidden = true;
|
2571
|
-
(
|
2667
|
+
if (link.hasAttribute("data-turbo-confirm")) {
|
2668
|
+
form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
|
2669
|
+
}
|
2670
|
+
const frame = this.getTargetFrameForLink(link);
|
2671
|
+
if (frame) {
|
2672
|
+
form.setAttribute("data-turbo-frame", frame);
|
2673
|
+
form.addEventListener("turbo:submit-start", (() => form.remove()));
|
2674
|
+
} else {
|
2675
|
+
form.addEventListener("submit", (() => form.remove()));
|
2676
|
+
}
|
2677
|
+
document.body.appendChild(form);
|
2572
2678
|
return dispatch("submit", {
|
2573
2679
|
cancelable: true,
|
2574
2680
|
target: form
|
@@ -2600,7 +2706,8 @@ class Session {
|
|
2600
2706
|
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
2601
2707
|
}
|
2602
2708
|
willSubmitForm(form, submitter) {
|
2603
|
-
|
2709
|
+
const action = getAction(form, submitter);
|
2710
|
+
return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter)) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
|
2604
2711
|
}
|
2605
2712
|
formSubmitted(form, submitter) {
|
2606
2713
|
this.navigator.submitForm(form, submitter);
|
@@ -2667,6 +2774,7 @@ class Session {
|
|
2667
2774
|
});
|
2668
2775
|
}
|
2669
2776
|
notifyApplicationAfterVisitingLocation(location, action) {
|
2777
|
+
markAsBusy(document.documentElement);
|
2670
2778
|
return dispatch("turbo:visit", {
|
2671
2779
|
detail: {
|
2672
2780
|
url: location.href,
|
@@ -2690,6 +2798,7 @@ class Session {
|
|
2690
2798
|
return dispatch("turbo:render");
|
2691
2799
|
}
|
2692
2800
|
notifyApplicationAfterPageLoad(timing = {}) {
|
2801
|
+
clearBusyState(document.documentElement);
|
2693
2802
|
return dispatch("turbo:load", {
|
2694
2803
|
detail: {
|
2695
2804
|
url: this.location.href,
|
@@ -2737,8 +2846,16 @@ class Session {
|
|
2737
2846
|
const action = link.getAttribute("data-turbo-action");
|
2738
2847
|
return isAction(action) ? action : "advance";
|
2739
2848
|
}
|
2740
|
-
|
2741
|
-
|
2849
|
+
getTargetFrameForLink(link) {
|
2850
|
+
const frame = link.getAttribute("data-turbo-frame");
|
2851
|
+
if (frame) {
|
2852
|
+
return frame;
|
2853
|
+
} else {
|
2854
|
+
const container = link.closest("turbo-frame");
|
2855
|
+
if (container) {
|
2856
|
+
return container.id;
|
2857
|
+
}
|
2858
|
+
}
|
2742
2859
|
}
|
2743
2860
|
get snapshot() {
|
2744
2861
|
return this.view.snapshot;
|
@@ -2793,6 +2910,10 @@ function setProgressBarDelay(delay) {
|
|
2793
2910
|
session.setProgressBarDelay(delay);
|
2794
2911
|
}
|
2795
2912
|
|
2913
|
+
function setConfirmMethod(confirmMethod) {
|
2914
|
+
FormSubmission.confirmMethod = confirmMethod;
|
2915
|
+
}
|
2916
|
+
|
2796
2917
|
var Turbo = Object.freeze({
|
2797
2918
|
__proto__: null,
|
2798
2919
|
navigator: navigator$1,
|
@@ -2806,11 +2927,14 @@ var Turbo = Object.freeze({
|
|
2806
2927
|
disconnectStreamSource: disconnectStreamSource,
|
2807
2928
|
renderStreamMessage: renderStreamMessage,
|
2808
2929
|
clearCache: clearCache,
|
2809
|
-
setProgressBarDelay: setProgressBarDelay
|
2930
|
+
setProgressBarDelay: setProgressBarDelay,
|
2931
|
+
setConfirmMethod: setConfirmMethod
|
2810
2932
|
});
|
2811
2933
|
|
2812
2934
|
class FrameController {
|
2813
2935
|
constructor(element) {
|
2936
|
+
this.fetchResponseLoaded = fetchResponse => {};
|
2937
|
+
this.currentFetchRequest = null;
|
2814
2938
|
this.resolveVisitPromise = () => {};
|
2815
2939
|
this.connected = false;
|
2816
2940
|
this.hasBeenLoaded = false;
|
@@ -2865,11 +2989,10 @@ class FrameController {
|
|
2865
2989
|
this.currentURL = this.sourceURL;
|
2866
2990
|
if (this.sourceURL) {
|
2867
2991
|
try {
|
2868
|
-
this.element.loaded = this.visit(this.sourceURL);
|
2992
|
+
this.element.loaded = this.visit(expandURL(this.sourceURL));
|
2869
2993
|
this.appearanceObserver.stop();
|
2870
2994
|
await this.element.loaded;
|
2871
2995
|
this.hasBeenLoaded = true;
|
2872
|
-
session.frameLoaded(this.element);
|
2873
2996
|
} catch (error) {
|
2874
2997
|
this.currentURL = previousURL;
|
2875
2998
|
throw error;
|
@@ -2878,7 +3001,7 @@ class FrameController {
|
|
2878
3001
|
}
|
2879
3002
|
}
|
2880
3003
|
async loadResponse(fetchResponse) {
|
2881
|
-
if (fetchResponse.redirected) {
|
3004
|
+
if (fetchResponse.redirected || fetchResponse.succeeded && fetchResponse.isHTML) {
|
2882
3005
|
this.sourceURL = fetchResponse.response.url;
|
2883
3006
|
}
|
2884
3007
|
try {
|
@@ -2886,14 +3009,18 @@ class FrameController {
|
|
2886
3009
|
if (html) {
|
2887
3010
|
const {body: body} = parseHTMLDocument(html);
|
2888
3011
|
const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
|
2889
|
-
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
|
3012
|
+
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
|
2890
3013
|
if (this.view.renderPromise) await this.view.renderPromise;
|
2891
3014
|
await this.view.render(renderer);
|
2892
3015
|
session.frameRendered(fetchResponse, this.element);
|
3016
|
+
session.frameLoaded(this.element);
|
3017
|
+
this.fetchResponseLoaded(fetchResponse);
|
2893
3018
|
}
|
2894
3019
|
} catch (error) {
|
2895
3020
|
console.error(error);
|
2896
3021
|
this.view.invalidate();
|
3022
|
+
} finally {
|
3023
|
+
this.fetchResponseLoaded = () => {};
|
2897
3024
|
}
|
2898
3025
|
}
|
2899
3026
|
elementAppearedInViewport(element) {
|
@@ -2919,19 +3046,15 @@ class FrameController {
|
|
2919
3046
|
}
|
2920
3047
|
this.reloadable = false;
|
2921
3048
|
this.formSubmission = new FormSubmission(this, element, submitter);
|
2922
|
-
|
2923
|
-
|
2924
|
-
|
2925
|
-
const {fetchRequest: fetchRequest} = this.formSubmission;
|
2926
|
-
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
|
2927
|
-
this.formSubmission.start();
|
2928
|
-
}
|
3049
|
+
const {fetchRequest: fetchRequest} = this.formSubmission;
|
3050
|
+
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
|
3051
|
+
this.formSubmission.start();
|
2929
3052
|
}
|
2930
3053
|
prepareHeadersForRequest(headers, request) {
|
2931
3054
|
headers["Turbo-Frame"] = this.id;
|
2932
3055
|
}
|
2933
3056
|
requestStarted(request) {
|
2934
|
-
this.element
|
3057
|
+
markAsBusy(this.element);
|
2935
3058
|
}
|
2936
3059
|
requestPreventedHandlingResponse(request, response) {
|
2937
3060
|
this.resolveVisitPromise();
|
@@ -2949,14 +3072,14 @@ class FrameController {
|
|
2949
3072
|
this.resolveVisitPromise();
|
2950
3073
|
}
|
2951
3074
|
requestFinished(request) {
|
2952
|
-
this.element
|
3075
|
+
clearBusyState(this.element);
|
2953
3076
|
}
|
2954
|
-
formSubmissionStarted(
|
2955
|
-
|
2956
|
-
frame.setAttribute("busy", "");
|
3077
|
+
formSubmissionStarted({formElement: formElement}) {
|
3078
|
+
markAsBusy(formElement, this.findFrameElement(formElement));
|
2957
3079
|
}
|
2958
3080
|
formSubmissionSucceededWithResponse(formSubmission, response) {
|
2959
3081
|
const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
|
3082
|
+
this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
|
2960
3083
|
frame.delegate.loadResponse(response);
|
2961
3084
|
}
|
2962
3085
|
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
@@ -2965,9 +3088,8 @@ class FrameController {
|
|
2965
3088
|
formSubmissionErrored(formSubmission, error) {
|
2966
3089
|
console.error(error);
|
2967
3090
|
}
|
2968
|
-
formSubmissionFinished(
|
2969
|
-
|
2970
|
-
frame.removeAttribute("busy");
|
3091
|
+
formSubmissionFinished({formElement: formElement}) {
|
3092
|
+
clearBusyState(formElement, this.findFrameElement(formElement));
|
2971
3093
|
}
|
2972
3094
|
allowsImmediateRender(snapshot, resume) {
|
2973
3095
|
return true;
|
@@ -2975,10 +3097,14 @@ class FrameController {
|
|
2975
3097
|
viewRenderedSnapshot(snapshot, isPreview) {}
|
2976
3098
|
viewInvalidated() {}
|
2977
3099
|
async visit(url) {
|
2978
|
-
|
3100
|
+
var _a;
|
3101
|
+
const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
|
3102
|
+
(_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
|
3103
|
+
this.currentFetchRequest = request;
|
2979
3104
|
return new Promise((resolve => {
|
2980
3105
|
this.resolveVisitPromise = () => {
|
2981
3106
|
this.resolveVisitPromise = () => {};
|
3107
|
+
this.currentFetchRequest = null;
|
2982
3108
|
resolve();
|
2983
3109
|
};
|
2984
3110
|
request.perform();
|
@@ -2986,12 +3112,36 @@ class FrameController {
|
|
2986
3112
|
}
|
2987
3113
|
navigateFrame(element, url, submitter) {
|
2988
3114
|
const frame = this.findFrameElement(element, submitter);
|
3115
|
+
this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
|
2989
3116
|
frame.setAttribute("reloadable", "");
|
2990
3117
|
frame.src = url;
|
2991
3118
|
}
|
3119
|
+
proposeVisitIfNavigatedWithAction(frame, element, submitter) {
|
3120
|
+
const action = getAttribute("data-turbo-action", submitter, element, frame);
|
3121
|
+
if (isAction(action)) {
|
3122
|
+
const {visitCachedSnapshot: visitCachedSnapshot} = new SnapshotSubstitution(frame);
|
3123
|
+
frame.delegate.fetchResponseLoaded = fetchResponse => {
|
3124
|
+
if (frame.src) {
|
3125
|
+
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
3126
|
+
const responseHTML = frame.ownerDocument.documentElement.outerHTML;
|
3127
|
+
const response = {
|
3128
|
+
statusCode: statusCode,
|
3129
|
+
redirected: redirected,
|
3130
|
+
responseHTML: responseHTML
|
3131
|
+
};
|
3132
|
+
session.visit(frame.src, {
|
3133
|
+
action: action,
|
3134
|
+
response: response,
|
3135
|
+
visitCachedSnapshot: visitCachedSnapshot,
|
3136
|
+
willRender: false
|
3137
|
+
});
|
3138
|
+
}
|
3139
|
+
};
|
3140
|
+
}
|
3141
|
+
}
|
2992
3142
|
findFrameElement(element, submitter) {
|
2993
3143
|
var _a;
|
2994
|
-
const id =
|
3144
|
+
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
2995
3145
|
return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
|
2996
3146
|
}
|
2997
3147
|
async extractForeignFrameElement(container) {
|
@@ -3011,8 +3161,15 @@ class FrameController {
|
|
3011
3161
|
}
|
3012
3162
|
return new FrameElement;
|
3013
3163
|
}
|
3164
|
+
formActionIsVisitable(form, submitter) {
|
3165
|
+
const action = getAction(form, submitter);
|
3166
|
+
return locationIsVisitable(expandURL(action), this.rootLocation);
|
3167
|
+
}
|
3014
3168
|
shouldInterceptNavigation(element, submitter) {
|
3015
|
-
const id =
|
3169
|
+
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
3170
|
+
if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
|
3171
|
+
return false;
|
3172
|
+
}
|
3016
3173
|
if (!this.enabled || id == "_top") {
|
3017
3174
|
return false;
|
3018
3175
|
}
|
@@ -3068,6 +3225,24 @@ class FrameController {
|
|
3068
3225
|
get isActive() {
|
3069
3226
|
return this.element.isActive && this.connected;
|
3070
3227
|
}
|
3228
|
+
get rootLocation() {
|
3229
|
+
var _a;
|
3230
|
+
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
3231
|
+
const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
|
3232
|
+
return expandURL(root);
|
3233
|
+
}
|
3234
|
+
}
|
3235
|
+
|
3236
|
+
class SnapshotSubstitution {
|
3237
|
+
constructor(element) {
|
3238
|
+
this.visitCachedSnapshot = ({element: element}) => {
|
3239
|
+
var _a;
|
3240
|
+
const {id: id, clone: clone} = this;
|
3241
|
+
(_a = element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
|
3242
|
+
};
|
3243
|
+
this.clone = element.cloneNode(true);
|
3244
|
+
this.id = element.id;
|
3245
|
+
}
|
3071
3246
|
}
|
3072
3247
|
|
3073
3248
|
function getFrameElementById(id) {
|
@@ -3090,6 +3265,7 @@ function activateElement(element, currentURL) {
|
|
3090
3265
|
}
|
3091
3266
|
if (element instanceof FrameElement) {
|
3092
3267
|
element.connectedCallback();
|
3268
|
+
element.disconnectedCallback();
|
3093
3269
|
return element;
|
3094
3270
|
}
|
3095
3271
|
}
|
@@ -3274,6 +3450,7 @@ var turbo_es2017Esm = Object.freeze({
|
|
3274
3450
|
registerAdapter: registerAdapter,
|
3275
3451
|
renderStreamMessage: renderStreamMessage,
|
3276
3452
|
session: session,
|
3453
|
+
setConfirmMethod: setConfirmMethod,
|
3277
3454
|
setProgressBarDelay: setProgressBarDelay,
|
3278
3455
|
start: start,
|
3279
3456
|
visit: visit
|
@@ -3356,8 +3533,6 @@ const now = () => (new Date).getTime();
|
|
3356
3533
|
|
3357
3534
|
const secondsSince = time => (now() - time) / 1e3;
|
3358
3535
|
|
3359
|
-
const clamp = (number, min, max) => Math.max(min, Math.min(max, number));
|
3360
|
-
|
3361
3536
|
class ConnectionMonitor {
|
3362
3537
|
constructor(connection) {
|
3363
3538
|
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
@@ -3370,7 +3545,7 @@ class ConnectionMonitor {
|
|
3370
3545
|
delete this.stoppedAt;
|
3371
3546
|
this.startPolling();
|
3372
3547
|
addEventListener("visibilitychange", this.visibilityDidChange);
|
3373
|
-
logger.log(`ConnectionMonitor started.
|
3548
|
+
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
3374
3549
|
}
|
3375
3550
|
}
|
3376
3551
|
stop() {
|
@@ -3411,24 +3586,29 @@ class ConnectionMonitor {
|
|
3411
3586
|
}), this.getPollInterval());
|
3412
3587
|
}
|
3413
3588
|
getPollInterval() {
|
3414
|
-
const {
|
3415
|
-
const
|
3416
|
-
|
3589
|
+
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
3590
|
+
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
3591
|
+
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
3592
|
+
const jitter = jitterMax * Math.random();
|
3593
|
+
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
3417
3594
|
}
|
3418
3595
|
reconnectIfStale() {
|
3419
3596
|
if (this.connectionIsStale()) {
|
3420
|
-
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts},
|
3597
|
+
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
3421
3598
|
this.reconnectAttempts++;
|
3422
3599
|
if (this.disconnectedRecently()) {
|
3423
|
-
logger.log(
|
3600
|
+
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
3424
3601
|
} else {
|
3425
3602
|
logger.log("ConnectionMonitor reopening");
|
3426
3603
|
this.connection.reopen();
|
3427
3604
|
}
|
3428
3605
|
}
|
3429
3606
|
}
|
3607
|
+
get refreshedAt() {
|
3608
|
+
return this.pingedAt ? this.pingedAt : this.startedAt;
|
3609
|
+
}
|
3430
3610
|
connectionIsStale() {
|
3431
|
-
return secondsSince(this.
|
3611
|
+
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
3432
3612
|
}
|
3433
3613
|
disconnectedRecently() {
|
3434
3614
|
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
@@ -3445,14 +3625,10 @@ class ConnectionMonitor {
|
|
3445
3625
|
}
|
3446
3626
|
}
|
3447
3627
|
|
3448
|
-
ConnectionMonitor.pollInterval = {
|
3449
|
-
min: 3,
|
3450
|
-
max: 30,
|
3451
|
-
multiplier: 5
|
3452
|
-
};
|
3453
|
-
|
3454
3628
|
ConnectionMonitor.staleThreshold = 6;
|
3455
3629
|
|
3630
|
+
ConnectionMonitor.reconnectionBackoffRate = .15;
|
3631
|
+
|
3456
3632
|
var INTERNAL = {
|
3457
3633
|
message_types: {
|
3458
3634
|
welcome: "welcome",
|
@@ -3595,6 +3771,7 @@ Connection.prototype.events = {
|
|
3595
3771
|
return this.monitor.recordPing();
|
3596
3772
|
|
3597
3773
|
case message_types.confirmation:
|
3774
|
+
this.subscriptions.confirmSubscription(identifier);
|
3598
3775
|
return this.subscriptions.notify(identifier, "connected");
|
3599
3776
|
|
3600
3777
|
case message_types.rejection:
|
@@ -3662,9 +3839,47 @@ class Subscription {
|
|
3662
3839
|
}
|
3663
3840
|
}
|
3664
3841
|
|
3842
|
+
class SubscriptionGuarantor {
|
3843
|
+
constructor(subscriptions) {
|
3844
|
+
this.subscriptions = subscriptions;
|
3845
|
+
this.pendingSubscriptions = [];
|
3846
|
+
}
|
3847
|
+
guarantee(subscription) {
|
3848
|
+
if (this.pendingSubscriptions.indexOf(subscription) == -1) {
|
3849
|
+
logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
|
3850
|
+
this.pendingSubscriptions.push(subscription);
|
3851
|
+
} else {
|
3852
|
+
logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
|
3853
|
+
}
|
3854
|
+
this.startGuaranteeing();
|
3855
|
+
}
|
3856
|
+
forget(subscription) {
|
3857
|
+
logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
|
3858
|
+
this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
|
3859
|
+
}
|
3860
|
+
startGuaranteeing() {
|
3861
|
+
this.stopGuaranteeing();
|
3862
|
+
this.retrySubscribing();
|
3863
|
+
}
|
3864
|
+
stopGuaranteeing() {
|
3865
|
+
clearTimeout(this.retryTimeout);
|
3866
|
+
}
|
3867
|
+
retrySubscribing() {
|
3868
|
+
this.retryTimeout = setTimeout((() => {
|
3869
|
+
if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
|
3870
|
+
this.pendingSubscriptions.map((subscription => {
|
3871
|
+
logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
|
3872
|
+
this.subscriptions.subscribe(subscription);
|
3873
|
+
}));
|
3874
|
+
}
|
3875
|
+
}), 500);
|
3876
|
+
}
|
3877
|
+
}
|
3878
|
+
|
3665
3879
|
class Subscriptions {
|
3666
3880
|
constructor(consumer) {
|
3667
3881
|
this.consumer = consumer;
|
3882
|
+
this.guarantor = new SubscriptionGuarantor(this);
|
3668
3883
|
this.subscriptions = [];
|
3669
3884
|
}
|
3670
3885
|
create(channelName, mixin) {
|
@@ -3679,7 +3894,7 @@ class Subscriptions {
|
|
3679
3894
|
this.subscriptions.push(subscription);
|
3680
3895
|
this.consumer.ensureActiveConnection();
|
3681
3896
|
this.notify(subscription, "initialized");
|
3682
|
-
this.
|
3897
|
+
this.subscribe(subscription);
|
3683
3898
|
return subscription;
|
3684
3899
|
}
|
3685
3900
|
remove(subscription) {
|
@@ -3697,6 +3912,7 @@ class Subscriptions {
|
|
3697
3912
|
}));
|
3698
3913
|
}
|
3699
3914
|
forget(subscription) {
|
3915
|
+
this.guarantor.forget(subscription);
|
3700
3916
|
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
3701
3917
|
return subscription;
|
3702
3918
|
}
|
@@ -3704,7 +3920,7 @@ class Subscriptions {
|
|
3704
3920
|
return this.subscriptions.filter((s => s.identifier === identifier));
|
3705
3921
|
}
|
3706
3922
|
reload() {
|
3707
|
-
return this.subscriptions.map((subscription => this.
|
3923
|
+
return this.subscriptions.map((subscription => this.subscribe(subscription)));
|
3708
3924
|
}
|
3709
3925
|
notifyAll(callbackName, ...args) {
|
3710
3926
|
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
@@ -3718,6 +3934,15 @@ class Subscriptions {
|
|
3718
3934
|
}
|
3719
3935
|
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
3720
3936
|
}
|
3937
|
+
subscribe(subscription) {
|
3938
|
+
if (this.sendCommand(subscription, "subscribe")) {
|
3939
|
+
this.guarantor.guarantee(subscription);
|
3940
|
+
}
|
3941
|
+
}
|
3942
|
+
confirmSubscription(identifier) {
|
3943
|
+
logger.log(`Subscription confirmed ${identifier}`);
|
3944
|
+
this.findAll(identifier).map((subscription => this.guarantor.forget(subscription)));
|
3945
|
+
}
|
3721
3946
|
sendCommand(subscription, command) {
|
3722
3947
|
const {identifier: identifier} = subscription;
|
3723
3948
|
return this.consumer.send({
|
@@ -3788,6 +4013,7 @@ var index = Object.freeze({
|
|
3788
4013
|
INTERNAL: INTERNAL,
|
3789
4014
|
Subscription: Subscription,
|
3790
4015
|
Subscriptions: Subscriptions,
|
4016
|
+
SubscriptionGuarantor: SubscriptionGuarantor,
|
3791
4017
|
adapters: adapters,
|
3792
4018
|
createWebSocketURL: createWebSocketURL,
|
3793
4019
|
logger: logger,
|