@thoughtbot/superglue 1.0.3 → 2.0.0-alpha.10

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.
@@ -42,19 +42,22 @@ __export(lib_exports, {
42
42
  getIn: () => getIn,
43
43
  pageReducer: () => pageReducer,
44
44
  prepareStore: () => prepareStore,
45
+ receiveResponse: () => receiveResponse,
45
46
  removePage: () => removePage,
46
47
  rootReducer: () => rootReducer,
47
48
  saveAndProcessPage: () => saveAndProcessPage,
48
49
  saveResponse: () => saveResponse,
49
50
  setup: () => setup,
50
51
  superglueReducer: () => superglueReducer,
51
- updateFragments: () => updateFragments,
52
+ unproxy: () => unproxy2,
52
53
  urlToPageKey: () => urlToPageKey,
53
54
  useContent: () => useContent,
55
+ useSetFragment: () => useSetFragment,
56
+ useStreamSource: () => useStreamSource,
54
57
  useSuperglue: () => useSuperglue
55
58
  });
56
59
  module.exports = __toCommonJS(lib_exports);
57
- var import_react2 = __toESM(require("react"));
60
+ var import_react4 = __toESM(require("react"));
58
61
 
59
62
  // lib/config.ts
60
63
  var config = {
@@ -228,7 +231,26 @@ function setIn(object, path, value) {
228
231
  return results[0];
229
232
  }
230
233
 
234
+ // lib/utils/limited_set.ts
235
+ var LimitedSet = class extends Set {
236
+ constructor(maxSize) {
237
+ super();
238
+ this.maxSize = maxSize;
239
+ }
240
+ add(value) {
241
+ if (this.size >= this.maxSize) {
242
+ const iterator = this.values();
243
+ const oldestValue = iterator.next().value;
244
+ this.delete(oldestValue);
245
+ }
246
+ super.add(value);
247
+ return this;
248
+ }
249
+ };
250
+
231
251
  // lib/utils/request.ts
252
+ var import_uuid = require("uuid");
253
+ var lastRequestIds = new LimitedSet(20);
232
254
  function isValidResponse(xhr) {
233
255
  return isValidContent(xhr) && !downloadingFile(xhr);
234
256
  }
@@ -284,6 +306,9 @@ function argsForFetch(getState, pathQuery2, {
284
306
  nextHeaders["x-requested-with"] = "XMLHttpRequest";
285
307
  nextHeaders["accept"] = "application/json";
286
308
  nextHeaders["x-superglue-request"] = "true";
309
+ const requestId = (0, import_uuid.v4)();
310
+ lastRequestIds.add(requestId);
311
+ nextHeaders["X-Superglue-Request-Id"] = requestId;
287
312
  if (method != "GET" && method != "HEAD") {
288
313
  nextHeaders["content-type"] = "application/json";
289
314
  }
@@ -323,7 +348,7 @@ function argsForFetch(getState, pathQuery2, {
323
348
  return [fetchPath.toString(), { ...options, ...rest }];
324
349
  }
325
350
  function extractJSON(rsp) {
326
- return rsp.clone().json().then((json) => {
351
+ return rsp.json().then((json) => {
327
352
  return { rsp, json };
328
353
  }).catch((e) => {
329
354
  e.response = rsp;
@@ -477,7 +502,6 @@ var handleGraft = (0, import_toolkit.createAction)(
477
502
  var superglueError = (0, import_toolkit.createAction)(
478
503
  "@@superglue/ERROR"
479
504
  );
480
- var updateFragments = (0, import_toolkit.createAction)("@@superglue/UPDATE_FRAGMENTS");
481
505
  var copyPage = (0, import_toolkit.createAction)(
482
506
  "@@superglue/COPY_PAGE"
483
507
  );
@@ -492,24 +516,291 @@ var beforeRemote = (0, import_toolkit.createAction)("@@superglue/BEFORE_REMOTE")
492
516
  var setCSRFToken = (0, import_toolkit.createAction)("@@superglue/SET_CSRF_TOKEN");
493
517
  var historyChange = (0, import_toolkit.createAction)("@@superglue/HISTORY_CHANGE");
494
518
  var setActivePage = (0, import_toolkit.createAction)("@@superglue/SET_ACTIVE_PAGE");
519
+ var handleFragmentGraft = (0, import_toolkit.createAction)(
520
+ "@@superglue/HANDLE_FRAGMENT_GRAFT",
521
+ ({
522
+ fragmentId,
523
+ response
524
+ }) => {
525
+ return {
526
+ payload: {
527
+ response,
528
+ fragmentId
529
+ }
530
+ };
531
+ }
532
+ );
533
+ var saveFragment = (0, import_toolkit.createAction)(
534
+ "@@superglue/SAVE_FRAGMENT",
535
+ ({ fragmentId, data }) => {
536
+ return {
537
+ payload: {
538
+ fragmentId,
539
+ data
540
+ }
541
+ };
542
+ }
543
+ );
544
+ var receiveResponse = (0, import_toolkit.createAction)(
545
+ "@@superglue/RECEIVE_RESPONSE",
546
+ ({ pageKey, response }) => {
547
+ pageKey = urlToPageKey(pageKey);
548
+ return {
549
+ payload: {
550
+ pageKey,
551
+ response
552
+ }
553
+ };
554
+ }
555
+ );
556
+ var appendToFragment = (0, import_toolkit.createAction)(
557
+ "@@superglue/APPEND_TO_FRAGMENT",
558
+ ({ data, fragmentId }) => {
559
+ return {
560
+ payload: {
561
+ data,
562
+ fragmentId
563
+ }
564
+ };
565
+ }
566
+ );
567
+ var prependToFragment = (0, import_toolkit.createAction)(
568
+ "@@superglue/PREPEND_TO_FRAGMENT",
569
+ ({ data, fragmentId }) => {
570
+ return {
571
+ payload: {
572
+ data,
573
+ fragmentId
574
+ }
575
+ };
576
+ }
577
+ );
578
+
579
+ // lib/utils/proxy.ts
580
+ var ORIGINAL_TARGET = Symbol("@@originalTarget");
581
+ var ARRAY_GETTER_METHODS = /* @__PURE__ */ new Set([
582
+ Symbol.iterator,
583
+ "at",
584
+ "concat",
585
+ "entries",
586
+ "every",
587
+ "filter",
588
+ "find",
589
+ "findIndex",
590
+ "flat",
591
+ "flatMap",
592
+ "forEach",
593
+ "includes",
594
+ "indexOf",
595
+ "join",
596
+ "keys",
597
+ "lastIndexOf",
598
+ "map",
599
+ "reduce",
600
+ "reduceRight",
601
+ "slice",
602
+ "some",
603
+ "toString",
604
+ "values"
605
+ ]);
606
+ var ARRAY_SETTER_METHODS = /* @__PURE__ */ new Set([
607
+ "copyWithin",
608
+ "fill",
609
+ "pop",
610
+ "push",
611
+ "reverse",
612
+ "shift",
613
+ "sort",
614
+ "splice",
615
+ "unshift"
616
+ ]);
617
+ function isArraySetter(prop) {
618
+ return ARRAY_SETTER_METHODS.has(prop);
619
+ }
620
+ function isArrayGetter(prop) {
621
+ return ARRAY_GETTER_METHODS.has(prop);
622
+ }
623
+ function convertToInt(prop) {
624
+ if (typeof prop === "symbol") return null;
625
+ const num = Number(prop);
626
+ return Number.isInteger(num) ? num : null;
627
+ }
628
+ function isFragmentReference(value) {
629
+ return !!value && typeof value === "object" && "__id" in value && typeof value.__id === "string";
630
+ }
631
+ function createArrayProxy(arrayData, fragments, dependencies, proxyCache) {
632
+ if (proxyCache && proxyCache.has(arrayData)) {
633
+ return proxyCache.get(arrayData);
634
+ }
635
+ const proxy = new Proxy(arrayData, {
636
+ get(target, prop) {
637
+ if (prop === ORIGINAL_TARGET) {
638
+ return target;
639
+ }
640
+ if (isArrayGetter(prop)) {
641
+ const method = target[prop];
642
+ if (typeof method === "function") {
643
+ return function(...args) {
644
+ return Reflect.apply(method, proxy, args);
645
+ };
646
+ }
647
+ return method;
648
+ }
649
+ if (isArraySetter(prop)) {
650
+ throw new Error(
651
+ `Cannot mutate proxy array. Use useSetFragment to update state.`
652
+ );
653
+ }
654
+ const index = convertToInt(prop);
655
+ if (index !== null && index >= 0 && index < target.length) {
656
+ const item = target[index];
657
+ if (isFragmentReference(item)) {
658
+ dependencies.add(item.__id);
659
+ const fragmentData = fragments.current[item.__id];
660
+ if (!fragmentData) {
661
+ return void 0;
662
+ }
663
+ return createProxy(fragmentData, fragments, dependencies, proxyCache);
664
+ }
665
+ if (typeof item === "object" && item !== null) {
666
+ if ("$$typeof" in item) {
667
+ return item;
668
+ } else {
669
+ return createProxy(
670
+ item,
671
+ fragments,
672
+ dependencies,
673
+ proxyCache
674
+ );
675
+ }
676
+ }
677
+ return item;
678
+ }
679
+ return Reflect.get(target, prop);
680
+ },
681
+ has(target, prop) {
682
+ if (prop === ORIGINAL_TARGET) {
683
+ return true;
684
+ }
685
+ return Reflect.has(target, prop);
686
+ },
687
+ set() {
688
+ throw new Error(
689
+ "Cannot mutate proxy array. Use useSetFragment to update state."
690
+ );
691
+ },
692
+ deleteProperty() {
693
+ throw new Error(
694
+ "Cannot delete properties on proxy array. Use useSetFragment to update state."
695
+ );
696
+ },
697
+ defineProperty() {
698
+ throw new Error(
699
+ "Cannot define properties on proxy array. Use useSetFragment to update state."
700
+ );
701
+ }
702
+ });
703
+ if (proxyCache) {
704
+ proxyCache.set(arrayData, proxy);
705
+ }
706
+ return proxy;
707
+ }
708
+ function createObjectProxy(objectData, fragments, dependencies, proxyCache) {
709
+ if (proxyCache && proxyCache.has(objectData)) {
710
+ return proxyCache.get(objectData);
711
+ }
712
+ const proxy = new Proxy(objectData, {
713
+ get(target, prop) {
714
+ if (prop === ORIGINAL_TARGET) {
715
+ return target;
716
+ }
717
+ const value = target[prop];
718
+ if (isFragmentReference(value)) {
719
+ dependencies.add(value.__id);
720
+ const fragmentData = fragments.current[value.__id];
721
+ if (!fragmentData) {
722
+ return void 0;
723
+ }
724
+ return createProxy(fragmentData, fragments, dependencies, proxyCache);
725
+ }
726
+ if (typeof value === "object" && value !== null) {
727
+ if ("$$typeof" in value) {
728
+ return value;
729
+ } else if (Array.isArray(value)) {
730
+ return createArrayProxy(value, fragments, dependencies, proxyCache);
731
+ } else {
732
+ return createObjectProxy(value, fragments, dependencies, proxyCache);
733
+ }
734
+ }
735
+ return value;
736
+ },
737
+ has(target, prop) {
738
+ if (prop === ORIGINAL_TARGET) {
739
+ return true;
740
+ }
741
+ return Reflect.has(target, prop);
742
+ },
743
+ set() {
744
+ throw new Error(
745
+ "Cannot mutate proxy object. Use useSetFragment to update state."
746
+ );
747
+ },
748
+ deleteProperty() {
749
+ throw new Error(
750
+ "Cannot delete properties on proxy object. Use useSetFragment to update state."
751
+ );
752
+ },
753
+ defineProperty() {
754
+ throw new Error(
755
+ "Cannot define properties on proxy object. Use useSetFragment to update state."
756
+ );
757
+ }
758
+ });
759
+ if (proxyCache) {
760
+ proxyCache.set(objectData, proxy);
761
+ }
762
+ return proxy;
763
+ }
764
+ function createProxy(content, fragments, dependencies, proxyCache) {
765
+ if (!content || typeof content !== "object") {
766
+ return content;
767
+ }
768
+ if ("$$typeof" in content) {
769
+ return content;
770
+ }
771
+ if (Array.isArray(content)) {
772
+ return createArrayProxy(content, fragments, dependencies, proxyCache);
773
+ }
774
+ return createObjectProxy(content, fragments, dependencies, proxyCache);
775
+ }
776
+ function unproxy(proxy) {
777
+ if (proxy && typeof proxy === "object" && ORIGINAL_TARGET in proxy) {
778
+ return proxy[ORIGINAL_TARGET];
779
+ }
780
+ return proxy;
781
+ }
495
782
 
496
783
  // lib/action_creators/requests.ts
497
784
  function handleFetchErr(err, fetchArgs, dispatch) {
498
785
  dispatch(superglueError({ message: err.message }));
786
+ console.error(err);
499
787
  throw err;
500
788
  }
501
789
  function buildMeta(pageKey, page, state, rsp, fetchArgs) {
502
790
  const { assets: prevAssets } = state;
503
791
  const { assets: nextAssets } = page;
504
- return {
792
+ const meta = {
505
793
  pageKey,
506
794
  page,
507
795
  redirected: rsp.redirected,
508
796
  rsp,
509
797
  fetchArgs,
510
- componentIdentifier: page.componentIdentifier,
511
798
  needsRefresh: needsRefresh(prevAssets, nextAssets)
512
799
  };
800
+ if (page.action !== "handleStreamResponse") {
801
+ meta.componentIdentifier = page.componentIdentifier;
802
+ }
803
+ return meta;
513
804
  }
514
805
  var MismatchedComponentError = class extends Error {
515
806
  constructor(message) {
@@ -517,10 +808,11 @@ var MismatchedComponentError = class extends Error {
517
808
  this.name = "MismatchedComponentError";
518
809
  }
519
810
  };
811
+ var defaultBeforeSave = (prevPage, receivedPage) => receivedPage;
520
812
  var remote = (path, {
521
813
  pageKey: targetPageKey,
522
814
  force = false,
523
- beforeSave = (prevPage, receivedPage) => receivedPage,
815
+ beforeSave = defaultBeforeSave,
524
816
  ...rest
525
817
  } = {}) => {
526
818
  targetPageKey = targetPageKey && urlToPageKey(targetPageKey);
@@ -530,7 +822,7 @@ var remote = (path, {
530
822
  dispatch(beforeRemote({ currentPageKey, fetchArgs }));
531
823
  dispatch(beforeFetch({ fetchArgs }));
532
824
  return fetch(...fetchArgs).then(parseResponse).then(({ rsp, json }) => {
533
- const { superglue, pages = {} } = getState();
825
+ const { superglue, pages = {}, fragments } = getState();
534
826
  let pageKey;
535
827
  if (targetPageKey === void 0) {
536
828
  const isGet = fetchArgs[1].method === "GET";
@@ -539,10 +831,11 @@ var remote = (path, {
539
831
  pageKey = targetPageKey;
540
832
  }
541
833
  const meta = buildMeta(pageKey, json, superglue, rsp, fetchArgs);
542
- const existingId = pages[pageKey]?.componentIdentifier;
543
- const receivedId = json.componentIdentifier;
544
- if (!!existingId && existingId != receivedId && !force) {
545
- const message = `You cannot replace or update an existing page
834
+ if (json.action !== "handleStreamResponse") {
835
+ const existingId = pages[pageKey]?.componentIdentifier;
836
+ const receivedId = json.componentIdentifier;
837
+ if (!!existingId && existingId != receivedId && !force) {
838
+ const message = `You cannot replace or update an existing page
546
839
  located at pages["${currentPageKey}"] that has a componentIdentifier
547
840
  of "${existingId}" with the contents of a page response that has a
548
841
  componentIdentifier of "${receivedId}".
@@ -558,9 +851,27 @@ compatible with the page component associated with "${existingId}".
558
851
  Consider using data-sg-visit, the visit function, or redirect_back to
559
852
  the same page. Or if you're sure you want to proceed, use force: true.
560
853
  `;
561
- throw new MismatchedComponentError(message);
854
+ throw new MismatchedComponentError(message);
855
+ }
856
+ }
857
+ dispatch(
858
+ receiveResponse({
859
+ pageKey,
860
+ response: JSON.parse(JSON.stringify(json))
861
+ })
862
+ );
863
+ const existingPage = createProxy(
864
+ pages[pageKey],
865
+ { current: fragments },
866
+ /* @__PURE__ */ new Set(),
867
+ /* @__PURE__ */ new WeakMap()
868
+ );
869
+ let page = json;
870
+ if (json.action === "savePage" || json.action === "graft") {
871
+ page = JSON.parse(
872
+ JSON.stringify(beforeSave(existingPage, json))
873
+ );
562
874
  }
563
- const page = beforeSave(pages[pageKey], json);
564
875
  return dispatch(saveAndProcessPage(pageKey, page)).then(() => meta);
565
876
  }).catch((e) => handleFetchErr(e, fetchArgs, dispatch));
566
877
  };
@@ -577,6 +888,155 @@ function calculatePageKey(rsp, isGet, currentPageKey) {
577
888
  return pageKey;
578
889
  }
579
890
 
891
+ // lib/action_creators/stream.ts
892
+ var streamPrepend = (fragments, data, options = {}) => {
893
+ return (dispatch) => {
894
+ if (options.saveAs) {
895
+ const { saveAs } = options;
896
+ dispatch(
897
+ saveFragment({
898
+ fragmentId: saveAs,
899
+ data
900
+ })
901
+ );
902
+ fragments.forEach((fragmentId) => {
903
+ dispatch(
904
+ prependToFragment({
905
+ fragmentId,
906
+ data: {
907
+ __id: saveAs
908
+ }
909
+ })
910
+ );
911
+ });
912
+ } else {
913
+ fragments.forEach((fragmentId) => {
914
+ dispatch(
915
+ prependToFragment({
916
+ fragmentId,
917
+ data
918
+ })
919
+ );
920
+ });
921
+ }
922
+ };
923
+ };
924
+ var streamAppend = (fragments, data, options = {}) => {
925
+ return (dispatch) => {
926
+ if (options.saveAs) {
927
+ const { saveAs } = options;
928
+ dispatch(
929
+ saveFragment({
930
+ fragmentId: saveAs,
931
+ data
932
+ })
933
+ );
934
+ fragments.forEach((fragmentId) => {
935
+ dispatch(
936
+ appendToFragment({
937
+ fragmentId,
938
+ data: {
939
+ __id: saveAs
940
+ }
941
+ })
942
+ );
943
+ });
944
+ } else {
945
+ fragments.forEach((fragmentId) => {
946
+ dispatch(
947
+ appendToFragment({
948
+ fragmentId,
949
+ data
950
+ })
951
+ );
952
+ });
953
+ }
954
+ };
955
+ };
956
+ var streamSave = (fragment, data) => {
957
+ return (dispatch) => {
958
+ dispatch(
959
+ saveFragment({
960
+ fragmentId: fragment,
961
+ data
962
+ })
963
+ );
964
+ };
965
+ };
966
+ var handleStreamMessage = (rawMessage) => {
967
+ return (dispatch) => {
968
+ const message = JSON.parse(rawMessage);
969
+ let nextMessage = message;
970
+ if (message.handler !== "refresh") {
971
+ message.fragments.reverse().forEach((fragment) => {
972
+ const { id, path } = fragment;
973
+ const node = getIn(nextMessage, path);
974
+ nextMessage = setIn(nextMessage, path, { __id: id });
975
+ dispatch(
976
+ saveFragment({
977
+ fragmentId: id,
978
+ data: node
979
+ })
980
+ );
981
+ });
982
+ }
983
+ if (nextMessage.action === "handleStreamMessage") {
984
+ if (nextMessage.handler === "append") {
985
+ dispatch(
986
+ streamAppend(
987
+ nextMessage.fragmentIds,
988
+ nextMessage.data,
989
+ nextMessage.options
990
+ )
991
+ );
992
+ }
993
+ if (nextMessage.handler === "prepend") {
994
+ dispatch(
995
+ streamPrepend(
996
+ nextMessage.fragmentIds,
997
+ nextMessage.data,
998
+ nextMessage.options
999
+ )
1000
+ );
1001
+ }
1002
+ if (nextMessage.handler === "save") {
1003
+ dispatch(streamSave(nextMessage.fragmentIds[0], nextMessage.data));
1004
+ }
1005
+ }
1006
+ };
1007
+ };
1008
+ var handleStreamResponse = (response) => {
1009
+ return (dispatch) => {
1010
+ let nextResponse = response;
1011
+ nextResponse.fragments.reverse().forEach((fragment) => {
1012
+ const { id, path } = fragment;
1013
+ const node = getIn(nextResponse, path);
1014
+ nextResponse = setIn(nextResponse, path, { __id: id });
1015
+ dispatch(
1016
+ saveFragment({
1017
+ fragmentId: id,
1018
+ data: node
1019
+ })
1020
+ );
1021
+ });
1022
+ nextResponse.data.forEach((message) => {
1023
+ if (message.handler === "append") {
1024
+ dispatch(
1025
+ streamAppend(message.fragmentIds, message.data, message.options)
1026
+ );
1027
+ }
1028
+ if (message.handler === "prepend") {
1029
+ dispatch(
1030
+ streamPrepend(message.fragmentIds, message.data, message.options)
1031
+ );
1032
+ }
1033
+ if (message.handler === "save") {
1034
+ dispatch(streamSave(message.fragmentIds[0], message.data));
1035
+ }
1036
+ });
1037
+ };
1038
+ };
1039
+
580
1040
  // lib/action_creators/index.ts
581
1041
  function fetchDeferments(pageKey, defers = []) {
582
1042
  pageKey = urlToPageKey(pageKey);
@@ -610,58 +1070,64 @@ function fetchDeferments(pageKey, defers = []) {
610
1070
  return Promise.all(fetches);
611
1071
  };
612
1072
  }
1073
+ function addPlaceholdersToDeferredNodes(existingPage, page) {
1074
+ const { defers = [] } = existingPage;
1075
+ const prevDefers = defers.map(({ path }) => {
1076
+ const node = getIn(existingPage, path);
1077
+ const copy = JSON.stringify(node);
1078
+ return [path, JSON.parse(copy)];
1079
+ });
1080
+ return prevDefers.reduce((memo, [path, node]) => {
1081
+ return setIn(page, path, node);
1082
+ }, page);
1083
+ }
613
1084
  function saveAndProcessPage(pageKey, page) {
614
1085
  return (dispatch, getState) => {
615
1086
  pageKey = urlToPageKey(pageKey);
616
- const { defers = [] } = page;
617
- if ("action" in page) {
618
- const prevPage = getState().pages[pageKey];
619
- dispatch(handleGraft({ pageKey, page }));
620
- const currentPage = getState().pages[pageKey];
621
- currentPage.fragments.forEach((fragment) => {
622
- const { type, path } = fragment;
623
- const currentFragment = getIn(currentPage, path);
624
- const prevFragment = getIn(prevPage, path);
625
- if (!prevFragment) {
626
- dispatch(
627
- updateFragments({
628
- name: type,
629
- pageKey,
630
- value: currentFragment,
631
- path
632
- })
633
- );
634
- } else if (currentFragment !== prevFragment) {
635
- dispatch(
636
- updateFragments({
637
- name: type,
638
- pageKey,
639
- value: currentFragment,
640
- previousValue: prevFragment,
641
- path
642
- })
643
- );
644
- }
645
- });
646
- } else {
647
- dispatch(saveResponse({ pageKey, page }));
648
- const currentPage = getState().pages[pageKey];
649
- currentPage.fragments.forEach((fragment) => {
650
- const { type, path } = fragment;
651
- const currentFragment = getIn(currentPage, path);
1087
+ let nextPage = page;
1088
+ const state = getState();
1089
+ if (page.action === "savePage" && state.pages[pageKey]) {
1090
+ const existingPage = createProxy(
1091
+ state.pages[pageKey],
1092
+ { current: state.fragments },
1093
+ /* @__PURE__ */ new Set(),
1094
+ /* @__PURE__ */ new WeakMap()
1095
+ );
1096
+ nextPage = JSON.parse(
1097
+ JSON.stringify(addPlaceholdersToDeferredNodes(existingPage, nextPage))
1098
+ );
1099
+ }
1100
+ page.fragments.slice().reverse().forEach((fragment) => {
1101
+ const { id, path } = fragment;
1102
+ const node = getIn(nextPage, path);
1103
+ nextPage = setIn(nextPage, path, { __id: id });
1104
+ dispatch(
1105
+ saveFragment({
1106
+ fragmentId: id,
1107
+ data: node
1108
+ })
1109
+ );
1110
+ });
1111
+ if (nextPage.action === "graft") {
1112
+ if (typeof nextPage.fragmentContext === "string") {
652
1113
  dispatch(
653
- updateFragments({
654
- name: type,
655
- pageKey,
656
- value: currentFragment,
657
- path
1114
+ handleFragmentGraft({
1115
+ fragmentId: nextPage.fragmentContext,
1116
+ response: nextPage
658
1117
  })
659
1118
  );
660
- });
1119
+ } else {
1120
+ dispatch(handleGraft({ pageKey, page: nextPage }));
1121
+ }
1122
+ } else if (nextPage.action === "handleStreamResponse") {
1123
+ dispatch(handleStreamResponse(nextPage));
1124
+ return Promise.resolve();
1125
+ } else {
1126
+ dispatch(saveResponse({ pageKey, page: nextPage }));
661
1127
  }
662
1128
  const hasFetch = typeof fetch != "undefined";
663
1129
  if (hasFetch) {
664
- return dispatch(fetchDeferments(pageKey, defers)).then(
1130
+ return dispatch(fetchDeferments(pageKey, nextPage.defers)).then(
665
1131
  () => Promise.resolve()
666
1132
  );
667
1133
  } else {
@@ -671,13 +1137,170 @@ function saveAndProcessPage(pageKey, page) {
671
1137
  }
672
1138
 
673
1139
  // lib/index.tsx
1140
+ var import_react_redux5 = require("react-redux");
1141
+
1142
+ // lib/hooks/useStreamSource.tsx
1143
+ var import_react2 = require("react");
1144
+
1145
+ // lib/hooks/index.ts
674
1146
  var import_react_redux3 = require("react-redux");
1147
+
1148
+ // lib/hooks/useContent.tsx
1149
+ var import_react_redux = require("react-redux");
1150
+ var import_react = require("react");
1151
+ function useContent(fragmentRef) {
1152
+ const superglueState = useSuperglue();
1153
+ const currentPageKey = superglueState.currentPageKey;
1154
+ const dependencies = (0, import_react.useRef)(/* @__PURE__ */ new Set());
1155
+ const fragmentId = typeof fragmentRef === "string" ? fragmentRef : fragmentRef?.__id;
1156
+ const sourceData = (0, import_react_redux.useSelector)((state) => {
1157
+ if (fragmentId) {
1158
+ return state.fragments[fragmentId];
1159
+ } else {
1160
+ return state.pages[currentPageKey].data;
1161
+ }
1162
+ });
1163
+ const trackedFragments = (0, import_react_redux.useSelector)(
1164
+ (state) => state.fragments,
1165
+ (oldFragments, newFragments) => {
1166
+ if (oldFragments === newFragments) {
1167
+ return true;
1168
+ }
1169
+ return Array.from(dependencies.current).every((id) => {
1170
+ const prevVal = oldFragments[id];
1171
+ const nextVal = newFragments[id];
1172
+ return prevVal === nextVal;
1173
+ });
1174
+ }
1175
+ );
1176
+ const store = (0, import_react_redux.useStore)();
1177
+ const proxy = (0, import_react.useMemo)(() => {
1178
+ const proxyCache = /* @__PURE__ */ new WeakMap();
1179
+ if (fragmentId && !sourceData) {
1180
+ return void 0;
1181
+ }
1182
+ return createProxy(
1183
+ sourceData,
1184
+ { current: store.getState().fragments },
1185
+ dependencies.current,
1186
+ proxyCache
1187
+ );
1188
+ }, [sourceData, trackedFragments]);
1189
+ return proxy;
1190
+ }
1191
+ function unproxy2(proxy) {
1192
+ return unproxy(proxy);
1193
+ }
1194
+
1195
+ // lib/hooks/useSetFragment.tsx
1196
+ var import_react_redux2 = require("react-redux");
1197
+ var import_immer = require("immer");
1198
+ var immer = new import_immer.Immer();
1199
+ immer.setAutoFreeze(false);
1200
+ function useSetFragment() {
1201
+ const dispatch = (0, import_react_redux2.useDispatch)();
1202
+ const fragments = (0, import_react_redux2.useSelector)((state) => state.fragments);
1203
+ function setter(fragmentRefOrId, updater) {
1204
+ const fragmentId = typeof fragmentRefOrId === "string" ? fragmentRefOrId : fragmentRefOrId.__id;
1205
+ const currentFragment = fragments[fragmentId];
1206
+ if (currentFragment === void 0) {
1207
+ throw new Error(`Fragment with id "${fragmentId}" not found`);
1208
+ }
1209
+ const updatedFragment = immer.produce(currentFragment, updater);
1210
+ dispatch(
1211
+ saveFragment({
1212
+ fragmentId,
1213
+ data: updatedFragment
1214
+ })
1215
+ );
1216
+ }
1217
+ return setter;
1218
+ }
1219
+
1220
+ // lib/hooks/index.ts
1221
+ function useSuperglue() {
1222
+ return (0, import_react_redux3.useSelector)((state) => state.superglue);
1223
+ }
1224
+
1225
+ // lib/hooks/useStreamSource.tsx
1226
+ var import_lodash = __toESM(require("lodash.debounce"));
1227
+ var StreamActions = class {
1228
+ constructor({
1229
+ remote: remote2,
1230
+ store
1231
+ }) {
1232
+ this.store = store;
1233
+ this.remote = (0, import_lodash.default)(remote2, 300);
1234
+ }
1235
+ refresh(pageKey) {
1236
+ this.remote(pageKey);
1237
+ }
1238
+ prepend(fragments, data, options = {}) {
1239
+ this.store.dispatch(streamPrepend(fragments, data, options));
1240
+ }
1241
+ save(fragment, data) {
1242
+ this.store.dispatch(streamSave(fragment, data));
1243
+ }
1244
+ append(fragments, data, options = {}) {
1245
+ this.store.dispatch(streamAppend(fragments, data, options));
1246
+ }
1247
+ handle(rawMessage, currentPageKey) {
1248
+ const message = JSON.parse(rawMessage);
1249
+ const { superglue } = this.store.getState();
1250
+ const nextPageKey = superglue.currentPageKey;
1251
+ if (message.action === "handleStreamMessage") {
1252
+ if (message.handler === "refresh" && currentPageKey === nextPageKey && !lastRequestIds.has(message.requestId)) {
1253
+ this.refresh(currentPageKey);
1254
+ }
1255
+ if (message.handler !== "refresh") {
1256
+ this.store.dispatch(handleStreamMessage(rawMessage));
1257
+ }
1258
+ }
1259
+ }
1260
+ };
1261
+ var CableContext = (0, import_react2.createContext)({
1262
+ cable: null,
1263
+ streamActions: null
1264
+ });
1265
+ function useStreamSource(channel) {
1266
+ const { cable: cable2, streamActions } = (0, import_react2.useContext)(CableContext);
1267
+ const [connected, setConnected] = (0, import_react2.useState)(false);
1268
+ const { currentPageKey } = useSuperglue();
1269
+ const subscriptionRef = (0, import_react2.useRef)(null);
1270
+ (0, import_react2.useEffect)(() => {
1271
+ if (cable2) {
1272
+ const subscription = cable2.subscriptions.create(channel, {
1273
+ received: (message) => {
1274
+ streamActions?.handle(message, currentPageKey);
1275
+ },
1276
+ connected: () => {
1277
+ setConnected(true);
1278
+ },
1279
+ disconnected: () => setConnected(false)
1280
+ });
1281
+ subscriptionRef.current = subscription;
1282
+ return () => subscription.unsubscribe();
1283
+ } else {
1284
+ subscriptionRef.current = null;
1285
+ setConnected(false);
1286
+ return () => {
1287
+ };
1288
+ }
1289
+ }, [cable2, JSON.stringify(channel), currentPageKey]);
1290
+ return {
1291
+ connected,
1292
+ subscription: subscriptionRef.current
1293
+ };
1294
+ }
1295
+
1296
+ // lib/index.tsx
1297
+ var import_actioncable = require("@rails/actioncable");
675
1298
  var import_history = require("history");
676
1299
 
677
1300
  // lib/components/Navigation.tsx
678
- var import_react = __toESM(require("react"));
679
- var import_react_redux = require("react-redux");
680
- var NavigationContext = (0, import_react.createContext)(
1301
+ var import_react3 = __toESM(require("react"));
1302
+ var import_react_redux4 = require("react-redux");
1303
+ var NavigationContext = (0, import_react3.createContext)(
681
1304
  {}
682
1305
  );
683
1306
  var hasWindow = typeof window !== "undefined";
@@ -694,27 +1317,27 @@ var notFound = (identifier) => {
694
1317
  );
695
1318
  throw error;
696
1319
  };
697
- var NavigationProvider = (0, import_react.forwardRef)(function NavigationProvider2({ history, visit, remote: remote2, mapping }, ref) {
698
- const dispatch = (0, import_react_redux.useDispatch)();
699
- const pages = (0, import_react_redux.useSelector)((state) => state.pages);
700
- const superglue = (0, import_react_redux.useSelector)(
1320
+ var NavigationProvider = (0, import_react3.forwardRef)(function NavigationProvider2({ history, visit, remote: remote2, mapping }, ref) {
1321
+ const dispatch = (0, import_react_redux4.useDispatch)();
1322
+ const pages = (0, import_react_redux4.useSelector)((state) => state.pages);
1323
+ const superglue = (0, import_react_redux4.useSelector)(
701
1324
  (state) => state.superglue
702
1325
  );
703
- const currentPageKey = (0, import_react_redux.useSelector)(
1326
+ const currentPageKey = (0, import_react_redux4.useSelector)(
704
1327
  (state) => state.superglue.currentPageKey
705
1328
  );
706
- const store = (0, import_react_redux.useStore)();
707
- (0, import_react.useEffect)(() => {
1329
+ const store = (0, import_react_redux4.useStore)();
1330
+ (0, import_react3.useEffect)(() => {
708
1331
  return history.listen(onHistoryChange);
709
1332
  }, []);
710
- (0, import_react.useLayoutEffect)(() => {
1333
+ (0, import_react3.useLayoutEffect)(() => {
711
1334
  const state = history.location.state;
712
1335
  if (state && "superglue" in state) {
713
1336
  const { posX, posY } = state;
714
1337
  setWindowScroll(posX, posY);
715
1338
  }
716
1339
  }, [currentPageKey]);
717
- (0, import_react.useImperativeHandle)(
1340
+ (0, import_react3.useImperativeHandle)(
718
1341
  ref,
719
1342
  () => {
720
1343
  return {
@@ -739,7 +1362,6 @@ var NavigationProvider = (0, import_react.forwardRef)(function NavigationProvide
739
1362
  hash: location.hash
740
1363
  },
741
1364
  {
742
- pageKey: nextPageKey,
743
1365
  superglue: true,
744
1366
  posY: window.pageYOffset,
745
1367
  posX: window.pageXOffset
@@ -748,7 +1370,7 @@ var NavigationProvider = (0, import_react.forwardRef)(function NavigationProvide
748
1370
  }
749
1371
  }
750
1372
  if (state && "superglue" in state) {
751
- const { pageKey } = state;
1373
+ const pageKey = urlToPageKey(location.pathname + location.search);
752
1374
  const prevPageKey = store.getState().superglue.currentPageKey;
753
1375
  const containsKey = !!pages[pageKey];
754
1376
  if (containsKey) {
@@ -797,7 +1419,6 @@ var NavigationProvider = (0, import_react.forwardRef)(function NavigationProvide
797
1419
  const historyArgs = [
798
1420
  path,
799
1421
  {
800
- pageKey: nextPageKey,
801
1422
  superglue: true,
802
1423
  posY: 0,
803
1424
  posX: 0
@@ -843,12 +1464,12 @@ var NavigationProvider = (0, import_react.forwardRef)(function NavigationProvide
843
1464
  const { componentIdentifier } = pages[currentPageKey];
844
1465
  const Component = mapping[componentIdentifier];
845
1466
  if (Component) {
846
- return /* @__PURE__ */ import_react.default.createElement(
1467
+ return /* @__PURE__ */ import_react3.default.createElement(
847
1468
  NavigationContext.Provider,
848
1469
  {
849
1470
  value: { pageKey: currentPageKey, search, navigateTo, visit, remote: remote2 }
850
1471
  },
851
- /* @__PURE__ */ import_react.default.createElement(Component, null)
1472
+ /* @__PURE__ */ import_react3.default.createElement(Component, null)
852
1473
  );
853
1474
  } else {
854
1475
  notFound(componentIdentifier);
@@ -856,17 +1477,6 @@ var NavigationProvider = (0, import_react.forwardRef)(function NavigationProvide
856
1477
  });
857
1478
 
858
1479
  // lib/reducers/index.ts
859
- function addPlaceholdersToDeferredNodes(existingPage, page) {
860
- const { defers = [] } = existingPage;
861
- const prevDefers = defers.map(({ path }) => {
862
- const node = getIn(existingPage, path);
863
- const copy = JSON.stringify(node);
864
- return [path, JSON.parse(copy)];
865
- });
866
- return prevDefers.reduce((memo, [path, node]) => {
867
- return setIn(page, path, node);
868
- }, page);
869
- }
870
1480
  function constrainPagesSize(state) {
871
1481
  const { maxPages } = config;
872
1482
  const allPageKeys = Object.keys(state);
@@ -879,14 +1489,10 @@ function constrainPagesSize(state) {
879
1489
  }
880
1490
  function handleSaveResponse(state, pageKey, page) {
881
1491
  state = { ...state };
882
- let nextPage = {
1492
+ const nextPage = {
883
1493
  ...page,
884
1494
  savedAt: Date.now()
885
1495
  };
886
- const existingPage = state[pageKey];
887
- if (existingPage) {
888
- nextPage = addPlaceholdersToDeferredNodes(existingPage, nextPage);
889
- }
890
1496
  constrainPagesSize(state);
891
1497
  state[pageKey] = nextPage;
892
1498
  return state;
@@ -916,7 +1522,7 @@ function appendReceivedFragmentsOntoPage(state, pageKey, receivedFragments) {
916
1522
  nextState[pageKey] = nextPage;
917
1523
  return nextState;
918
1524
  }
919
- function graftNodeOntoPage(state, pageKey, node, pathToNode) {
1525
+ function graftNodeOntoTarget(state, pageKey, node, pathToNode) {
920
1526
  if (!node) {
921
1527
  console.warn(
922
1528
  "There was no node returned in the response. Do you have the correct key path in your props_at?"
@@ -929,6 +1535,17 @@ function graftNodeOntoPage(state, pageKey, node, pathToNode) {
929
1535
  const fullPathToNode = [pageKey, pathToNode].join(".");
930
1536
  return setIn(state, fullPathToNode, node);
931
1537
  }
1538
+ function handleFragmentGraftResponse(state, key, response) {
1539
+ const target = state[key];
1540
+ if (!target) {
1541
+ const error = new Error(
1542
+ `Superglue was looking for ${key} in your fragments, but could not find it.`
1543
+ );
1544
+ throw error;
1545
+ }
1546
+ const { data: receivedNode, path: pathToNode } = response;
1547
+ return graftNodeOntoTarget(state, key, receivedNode, pathToNode);
1548
+ }
932
1549
  function handleGraftResponse(state, pageKey, page) {
933
1550
  const currentPage = state[pageKey];
934
1551
  if (!currentPage) {
@@ -943,7 +1560,7 @@ function handleGraftResponse(state, pageKey, page) {
943
1560
  fragments: receivedFragments = []
944
1561
  } = page;
945
1562
  return [
946
- (nextState) => graftNodeOntoPage(nextState, pageKey, receivedNode, pathToNode),
1563
+ (nextState) => graftNodeOntoTarget(nextState, pageKey, receivedNode, pathToNode),
947
1564
  (nextState) => appendReceivedFragmentsOntoPage(nextState, pageKey, receivedFragments)
948
1565
  ].reduce((memo, fn) => fn(memo), state);
949
1566
  }
@@ -1006,25 +1623,68 @@ function superglueReducer(state = {
1006
1623
  }
1007
1624
  return state;
1008
1625
  }
1626
+ function fragmentReducer(state = {}, action) {
1627
+ if (handleFragmentGraft.match(action)) {
1628
+ const { fragmentId, response } = action.payload;
1629
+ return handleFragmentGraftResponse(state, fragmentId, response);
1630
+ }
1631
+ if (saveFragment.match(action)) {
1632
+ const { fragmentId, data } = action.payload;
1633
+ return {
1634
+ ...state,
1635
+ [fragmentId]: data
1636
+ };
1637
+ }
1638
+ if (appendToFragment.match(action)) {
1639
+ const { data, fragmentId } = action.payload;
1640
+ let targetFragment = state[fragmentId];
1641
+ if (Array.isArray(targetFragment)) {
1642
+ targetFragment = [...targetFragment, data];
1643
+ return {
1644
+ ...state,
1645
+ [fragmentId]: targetFragment
1646
+ };
1647
+ } else {
1648
+ return state;
1649
+ }
1650
+ }
1651
+ if (prependToFragment.match(action)) {
1652
+ const { data, fragmentId } = action.payload;
1653
+ let targetFragment = state[fragmentId];
1654
+ if (Array.isArray(targetFragment)) {
1655
+ targetFragment = [data, ...targetFragment];
1656
+ return {
1657
+ ...state,
1658
+ [fragmentId]: targetFragment
1659
+ };
1660
+ } else {
1661
+ return state;
1662
+ }
1663
+ }
1664
+ return state;
1665
+ }
1009
1666
  var rootReducer = {
1010
1667
  superglue: superglueReducer,
1011
- pages: pageReducer
1668
+ pages: pageReducer,
1669
+ fragments: fragmentReducer
1012
1670
  };
1013
1671
 
1014
- // lib/hooks/index.ts
1015
- var import_react_redux2 = require("react-redux");
1016
- function useSuperglue() {
1017
- return (0, import_react_redux2.useSelector)((state) => state.superglue);
1018
- }
1019
- function useContent() {
1020
- const superglueState = useSuperglue();
1021
- const currentPageKey = superglueState.currentPageKey;
1022
- return (0, import_react_redux2.useSelector)(
1023
- (state) => state.pages[currentPageKey]
1024
- ).data;
1025
- }
1026
-
1027
1672
  // lib/index.tsx
1673
+ function getConfig(name) {
1674
+ if (typeof document !== "undefined") {
1675
+ const element = document.head.querySelector(
1676
+ `meta[name='action-cable-${name}']`
1677
+ );
1678
+ if (element) {
1679
+ return element.getAttribute("content") || "/cable";
1680
+ } else {
1681
+ return "/cable";
1682
+ }
1683
+ } else {
1684
+ return "/cable";
1685
+ }
1686
+ }
1687
+ var cable = (0, import_actioncable.createConsumer)(getConfig("url"));
1028
1688
  var hasWindow2 = typeof window !== "undefined";
1029
1689
  var createHistory = () => {
1030
1690
  if (hasWindow2) {
@@ -1041,6 +1701,9 @@ var prepareStore = (store, initialPage, path) => {
1041
1701
  pageKey: initialPageKey
1042
1702
  })
1043
1703
  );
1704
+ store.dispatch(
1705
+ receiveResponse({ pageKey: initialPageKey, response: initialPage })
1706
+ );
1044
1707
  store.dispatch(saveAndProcessPage(initialPageKey, initialPage));
1045
1708
  store.dispatch(setCSRFToken({ csrfToken }));
1046
1709
  };
@@ -1065,12 +1728,14 @@ var setup = ({
1065
1728
  ujsAttributePrefix: "data-sg",
1066
1729
  store
1067
1730
  });
1731
+ const streamActions = new StreamActions({ remote: remote2, store });
1068
1732
  return {
1069
1733
  visit,
1070
1734
  remote: remote2,
1071
1735
  nextHistory,
1072
1736
  initialPageKey,
1073
- ujs: handlers
1737
+ ujs: handlers,
1738
+ streamActions
1074
1739
  };
1075
1740
  };
1076
1741
  function Application({
@@ -1083,8 +1748,8 @@ function Application({
1083
1748
  mapping,
1084
1749
  ...rest
1085
1750
  }) {
1086
- const navigatorRef = (0, import_react2.useRef)(null);
1087
- const { visit, remote: remote2, nextHistory, initialPageKey, ujs } = (0, import_react2.useMemo)(() => {
1751
+ const navigatorRef = (0, import_react4.useRef)(null);
1752
+ const { visit, remote: remote2, nextHistory, initialPageKey, ujs, streamActions } = (0, import_react4.useMemo)(() => {
1088
1753
  return setup({
1089
1754
  initialPage,
1090
1755
  baseUrl,
@@ -1095,7 +1760,7 @@ function Application({
1095
1760
  navigatorRef
1096
1761
  });
1097
1762
  }, []);
1098
- return /* @__PURE__ */ import_react2.default.createElement("div", { onClick: ujs.onClick, onSubmit: ujs.onSubmit, ...rest }, /* @__PURE__ */ import_react2.default.createElement(import_react_redux3.Provider, { store }, /* @__PURE__ */ import_react2.default.createElement(
1763
+ return /* @__PURE__ */ import_react4.default.createElement("div", { onClick: ujs.onClick, onSubmit: ujs.onSubmit, ...rest }, /* @__PURE__ */ import_react4.default.createElement(import_react_redux5.Provider, { store }, /* @__PURE__ */ import_react4.default.createElement(CableContext.Provider, { value: { streamActions, cable } }, /* @__PURE__ */ import_react4.default.createElement(
1099
1764
  NavigationProvider,
1100
1765
  {
1101
1766
  ref: navigatorRef,
@@ -1105,7 +1770,7 @@ function Application({
1105
1770
  history: nextHistory,
1106
1771
  initialPageKey
1107
1772
  }
1108
- )));
1773
+ ))));
1109
1774
  }
1110
1775
  // Annotate the CommonJS export names for ESM import in node:
1111
1776
  0 && (module.exports = {
@@ -1121,15 +1786,18 @@ function Application({
1121
1786
  getIn,
1122
1787
  pageReducer,
1123
1788
  prepareStore,
1789
+ receiveResponse,
1124
1790
  removePage,
1125
1791
  rootReducer,
1126
1792
  saveAndProcessPage,
1127
1793
  saveResponse,
1128
1794
  setup,
1129
1795
  superglueReducer,
1130
- updateFragments,
1796
+ unproxy,
1131
1797
  urlToPageKey,
1132
1798
  useContent,
1799
+ useSetFragment,
1800
+ useStreamSource,
1133
1801
  useSuperglue
1134
1802
  });
1135
1803
  //# sourceMappingURL=superglue.cjs.map