@teleporthq/teleport-plugin-next-data-source 0.43.66 → 0.43.68

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.
Files changed (334) hide show
  1. package/__tests__/array-mapper-cache-wiring.test.ts +11 -0
  2. package/__tests__/array-overlap-sql.test.ts +53 -0
  3. package/__tests__/blog-post-page-enrichment.test.ts +214 -0
  4. package/__tests__/browser-row-policy.test.ts +503 -0
  5. package/__tests__/data-source-cache-wrapping.test.ts +18 -2
  6. package/__tests__/data-source-request-locale.test.ts +313 -0
  7. package/__tests__/ecommerce-product-options.test.ts +206 -0
  8. package/__tests__/ecommerce-product-pages.test.ts +82 -0
  9. package/__tests__/ecommerce-product-pre-options.golden.json +505 -0
  10. package/__tests__/ecommerce-product-storefront-contract.test.ts +293 -0
  11. package/__tests__/ecommerce-product-subscriptions.test.ts +121 -0
  12. package/__tests__/fetchers.test.ts +2 -2
  13. package/__tests__/gated-data-provider.test.ts +88 -0
  14. package/__tests__/items-path.test.ts +150 -0
  15. package/__tests__/localized-columns.test.ts +217 -0
  16. package/__tests__/numbered-pagination.test.ts +15 -0
  17. package/__tests__/product-filter-fields.ts +230 -0
  18. package/__tests__/product-filter-sql.fixture.json +7 -0
  19. package/__tests__/product-options-storefront.fixture.json +2510 -0
  20. package/__tests__/product-page-url-fixtures.json +25 -0
  21. package/__tests__/product-price-sort.ts +4 -2
  22. package/__tests__/raw-rows-mode.test.ts +111 -0
  23. package/__tests__/refetch-loading-state.test.ts +23 -0
  24. package/__tests__/rest-api-response-cache.test.ts +434 -0
  25. package/__tests__/search-clear-button.test.ts +122 -0
  26. package/__tests__/table-read-route-protected-tables.test.ts +493 -0
  27. package/dist/cjs/array-overlap-sql.d.ts +21 -0
  28. package/dist/cjs/array-overlap-sql.d.ts.map +1 -0
  29. package/dist/cjs/array-overlap-sql.js +25 -0
  30. package/dist/cjs/array-overlap-sql.js.map +1 -0
  31. package/dist/cjs/content-localization.d.ts +37 -0
  32. package/dist/cjs/content-localization.d.ts.map +1 -0
  33. package/dist/cjs/content-localization.js +29 -0
  34. package/dist/cjs/content-localization.js.map +1 -0
  35. package/dist/cjs/count-effect.d.ts +11 -1
  36. package/dist/cjs/count-effect.d.ts.map +1 -1
  37. package/dist/cjs/count-effect.js +30 -2
  38. package/dist/cjs/count-effect.js.map +1 -1
  39. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  40. package/dist/cjs/data-source-fetchers.js +10 -4
  41. package/dist/cjs/data-source-fetchers.js.map +1 -1
  42. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  43. package/dist/cjs/fetchers/airtable.js +1 -1
  44. package/dist/cjs/fetchers/airtable.js.map +1 -1
  45. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  46. package/dist/cjs/fetchers/clickhouse.js +1 -1
  47. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  48. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  49. package/dist/cjs/fetchers/firestore.js +1 -1
  50. package/dist/cjs/fetchers/firestore.js.map +1 -1
  51. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  52. package/dist/cjs/fetchers/javascript.js +1 -1
  53. package/dist/cjs/fetchers/javascript.js.map +1 -1
  54. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  55. package/dist/cjs/fetchers/mariadb.js +1 -1
  56. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  57. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  58. package/dist/cjs/fetchers/mongodb.js +1 -1
  59. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  60. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  61. package/dist/cjs/fetchers/mysql.js +1 -1
  62. package/dist/cjs/fetchers/mysql.js.map +1 -1
  63. package/dist/cjs/fetchers/postgresql.d.ts +1 -1
  64. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  65. package/dist/cjs/fetchers/postgresql.js +9 -4
  66. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  67. package/dist/cjs/fetchers/raw-query.d.ts +8 -1
  68. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  69. package/dist/cjs/fetchers/raw-query.js +42 -4
  70. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  71. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  72. package/dist/cjs/fetchers/redshift.js +1 -1
  73. package/dist/cjs/fetchers/redshift.js.map +1 -1
  74. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  75. package/dist/cjs/fetchers/rest-api.js +7 -2
  76. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  77. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  78. package/dist/cjs/fetchers/teleport.js +16 -6
  79. package/dist/cjs/fetchers/teleport.js.map +1 -1
  80. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  81. package/dist/cjs/fetchers/turso.js +1 -1
  82. package/dist/cjs/fetchers/turso.js.map +1 -1
  83. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +43 -0
  84. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -0
  85. package/dist/cjs/fetchers/utils/browser-row-policy.js +66 -0
  86. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -0
  87. package/dist/cjs/fetchers/utils/request-sql-guards.d.ts +8 -0
  88. package/dist/cjs/fetchers/utils/request-sql-guards.d.ts.map +1 -0
  89. package/dist/cjs/fetchers/utils/request-sql-guards.js +12 -0
  90. package/dist/cjs/fetchers/utils/request-sql-guards.js.map +1 -0
  91. package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts +40 -0
  92. package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
  93. package/dist/cjs/fetchers/utils/rest-api-response-cache.js +64 -0
  94. package/dist/cjs/fetchers/utils/rest-api-response-cache.js.map +1 -0
  95. package/dist/cjs/fetchers/utils/table-access-guard.d.ts +23 -0
  96. package/dist/cjs/fetchers/utils/table-access-guard.d.ts.map +1 -0
  97. package/dist/cjs/fetchers/utils/table-access-guard.js +34 -0
  98. package/dist/cjs/fetchers/utils/table-access-guard.js.map +1 -0
  99. package/dist/cjs/index.d.ts +5 -0
  100. package/dist/cjs/index.d.ts.map +1 -1
  101. package/dist/cjs/index.js +60 -8
  102. package/dist/cjs/index.js.map +1 -1
  103. package/dist/cjs/items-path.d.ts +10 -0
  104. package/dist/cjs/items-path.d.ts.map +1 -0
  105. package/dist/cjs/items-path.js +86 -0
  106. package/dist/cjs/items-path.js.map +1 -0
  107. package/dist/cjs/loading-state.d.ts +16 -1
  108. package/dist/cjs/loading-state.d.ts.map +1 -1
  109. package/dist/cjs/loading-state.js +69 -3
  110. package/dist/cjs/loading-state.js.map +1 -1
  111. package/dist/cjs/localized-columns.d.ts +56 -0
  112. package/dist/cjs/localized-columns.d.ts.map +1 -0
  113. package/dist/cjs/localized-columns.js +137 -0
  114. package/dist/cjs/localized-columns.js.map +1 -0
  115. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  116. package/dist/cjs/pagination-plugin.js +140 -50
  117. package/dist/cjs/pagination-plugin.js.map +1 -1
  118. package/dist/cjs/product-filter-fields.d.ts +87 -0
  119. package/dist/cjs/product-filter-fields.d.ts.map +1 -0
  120. package/dist/cjs/product-filter-fields.js +161 -0
  121. package/dist/cjs/product-filter-fields.js.map +1 -0
  122. package/dist/cjs/request-locale.d.ts +49 -0
  123. package/dist/cjs/request-locale.d.ts.map +1 -0
  124. package/dist/cjs/request-locale.js +130 -0
  125. package/dist/cjs/request-locale.js.map +1 -0
  126. package/dist/cjs/search-clear.d.ts +31 -0
  127. package/dist/cjs/search-clear.d.ts.map +1 -0
  128. package/dist/cjs/search-clear.js +103 -0
  129. package/dist/cjs/search-clear.js.map +1 -0
  130. package/dist/cjs/transformations/blog-adjacent-posts.d.ts +16 -0
  131. package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -0
  132. package/dist/cjs/transformations/blog-adjacent-posts.js +25 -0
  133. package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -0
  134. package/dist/cjs/transformations/blog-comments.d.ts +30 -0
  135. package/dist/cjs/transformations/blog-comments.d.ts.map +1 -0
  136. package/dist/cjs/transformations/blog-comments.js +34 -0
  137. package/dist/cjs/transformations/blog-comments.js.map +1 -0
  138. package/dist/cjs/transformations/blog-post.d.ts +2 -0
  139. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  140. package/dist/cjs/transformations/blog-post.js +4 -1
  141. package/dist/cjs/transformations/blog-post.js.map +1 -1
  142. package/dist/cjs/transformations/ecommerce-product.d.ts +9 -1
  143. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  144. package/dist/cjs/transformations/ecommerce-product.js +28 -1
  145. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  146. package/dist/cjs/transformations/heading-anchors.d.ts +25 -0
  147. package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -0
  148. package/dist/cjs/transformations/heading-anchors.js +29 -0
  149. package/dist/cjs/transformations/heading-anchors.js.map +1 -0
  150. package/dist/cjs/transformations/index.d.ts +61 -4
  151. package/dist/cjs/transformations/index.d.ts.map +1 -1
  152. package/dist/cjs/transformations/index.js +69 -19
  153. package/dist/cjs/transformations/index.js.map +1 -1
  154. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
  155. package/dist/cjs/transformations/shared-utils.js +1 -1
  156. package/dist/cjs/transformations/shared-utils.js.map +1 -1
  157. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  158. package/dist/cjs/utils.d.ts +11 -1
  159. package/dist/cjs/utils.d.ts.map +1 -1
  160. package/dist/cjs/utils.js +60 -16
  161. package/dist/cjs/utils.js.map +1 -1
  162. package/dist/esm/array-overlap-sql.d.ts +21 -0
  163. package/dist/esm/array-overlap-sql.d.ts.map +1 -0
  164. package/dist/esm/array-overlap-sql.js +21 -0
  165. package/dist/esm/array-overlap-sql.js.map +1 -0
  166. package/dist/esm/content-localization.d.ts +37 -0
  167. package/dist/esm/content-localization.d.ts.map +1 -0
  168. package/dist/esm/content-localization.js +24 -0
  169. package/dist/esm/content-localization.js.map +1 -0
  170. package/dist/esm/count-effect.d.ts +11 -1
  171. package/dist/esm/count-effect.d.ts.map +1 -1
  172. package/dist/esm/count-effect.js +28 -1
  173. package/dist/esm/count-effect.js.map +1 -1
  174. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  175. package/dist/esm/data-source-fetchers.js +11 -5
  176. package/dist/esm/data-source-fetchers.js.map +1 -1
  177. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  178. package/dist/esm/fetchers/airtable.js +1 -1
  179. package/dist/esm/fetchers/airtable.js.map +1 -1
  180. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  181. package/dist/esm/fetchers/clickhouse.js +1 -1
  182. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  183. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  184. package/dist/esm/fetchers/firestore.js +1 -1
  185. package/dist/esm/fetchers/firestore.js.map +1 -1
  186. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  187. package/dist/esm/fetchers/javascript.js +1 -1
  188. package/dist/esm/fetchers/javascript.js.map +1 -1
  189. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  190. package/dist/esm/fetchers/mariadb.js +1 -1
  191. package/dist/esm/fetchers/mariadb.js.map +1 -1
  192. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  193. package/dist/esm/fetchers/mongodb.js +1 -1
  194. package/dist/esm/fetchers/mongodb.js.map +1 -1
  195. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  196. package/dist/esm/fetchers/mysql.js +1 -1
  197. package/dist/esm/fetchers/mysql.js.map +1 -1
  198. package/dist/esm/fetchers/postgresql.d.ts +1 -1
  199. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  200. package/dist/esm/fetchers/postgresql.js +9 -4
  201. package/dist/esm/fetchers/postgresql.js.map +1 -1
  202. package/dist/esm/fetchers/raw-query.d.ts +8 -1
  203. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  204. package/dist/esm/fetchers/raw-query.js +42 -4
  205. package/dist/esm/fetchers/raw-query.js.map +1 -1
  206. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  207. package/dist/esm/fetchers/redshift.js +1 -1
  208. package/dist/esm/fetchers/redshift.js.map +1 -1
  209. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  210. package/dist/esm/fetchers/rest-api.js +7 -2
  211. package/dist/esm/fetchers/rest-api.js.map +1 -1
  212. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  213. package/dist/esm/fetchers/teleport.js +16 -6
  214. package/dist/esm/fetchers/teleport.js.map +1 -1
  215. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  216. package/dist/esm/fetchers/turso.js +1 -1
  217. package/dist/esm/fetchers/turso.js.map +1 -1
  218. package/dist/esm/fetchers/utils/browser-row-policy.d.ts +43 -0
  219. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -0
  220. package/dist/esm/fetchers/utils/browser-row-policy.js +60 -0
  221. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -0
  222. package/dist/esm/fetchers/utils/request-sql-guards.d.ts +8 -0
  223. package/dist/esm/fetchers/utils/request-sql-guards.d.ts.map +1 -0
  224. package/dist/esm/fetchers/utils/request-sql-guards.js +8 -0
  225. package/dist/esm/fetchers/utils/request-sql-guards.js.map +1 -0
  226. package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts +40 -0
  227. package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
  228. package/dist/esm/fetchers/utils/rest-api-response-cache.js +59 -0
  229. package/dist/esm/fetchers/utils/rest-api-response-cache.js.map +1 -0
  230. package/dist/esm/fetchers/utils/table-access-guard.d.ts +23 -0
  231. package/dist/esm/fetchers/utils/table-access-guard.d.ts.map +1 -0
  232. package/dist/esm/fetchers/utils/table-access-guard.js +29 -0
  233. package/dist/esm/fetchers/utils/table-access-guard.js.map +1 -0
  234. package/dist/esm/index.d.ts +5 -0
  235. package/dist/esm/index.d.ts.map +1 -1
  236. package/dist/esm/index.js +55 -7
  237. package/dist/esm/index.js.map +1 -1
  238. package/dist/esm/items-path.d.ts +10 -0
  239. package/dist/esm/items-path.d.ts.map +1 -0
  240. package/dist/esm/items-path.js +58 -0
  241. package/dist/esm/items-path.js.map +1 -0
  242. package/dist/esm/loading-state.d.ts +16 -1
  243. package/dist/esm/loading-state.d.ts.map +1 -1
  244. package/dist/esm/loading-state.js +69 -3
  245. package/dist/esm/loading-state.js.map +1 -1
  246. package/dist/esm/localized-columns.d.ts +56 -0
  247. package/dist/esm/localized-columns.d.ts.map +1 -0
  248. package/dist/esm/localized-columns.js +132 -0
  249. package/dist/esm/localized-columns.js.map +1 -0
  250. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  251. package/dist/esm/pagination-plugin.js +141 -51
  252. package/dist/esm/pagination-plugin.js.map +1 -1
  253. package/dist/esm/product-filter-fields.d.ts +87 -0
  254. package/dist/esm/product-filter-fields.d.ts.map +1 -0
  255. package/dist/esm/product-filter-fields.js +154 -0
  256. package/dist/esm/product-filter-fields.js.map +1 -0
  257. package/dist/esm/request-locale.d.ts +49 -0
  258. package/dist/esm/request-locale.d.ts.map +1 -0
  259. package/dist/esm/request-locale.js +98 -0
  260. package/dist/esm/request-locale.js.map +1 -0
  261. package/dist/esm/search-clear.d.ts +31 -0
  262. package/dist/esm/search-clear.d.ts.map +1 -0
  263. package/dist/esm/search-clear.js +74 -0
  264. package/dist/esm/search-clear.js.map +1 -0
  265. package/dist/esm/transformations/blog-adjacent-posts.d.ts +16 -0
  266. package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -0
  267. package/dist/esm/transformations/blog-adjacent-posts.js +21 -0
  268. package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -0
  269. package/dist/esm/transformations/blog-comments.d.ts +30 -0
  270. package/dist/esm/transformations/blog-comments.d.ts.map +1 -0
  271. package/dist/esm/transformations/blog-comments.js +30 -0
  272. package/dist/esm/transformations/blog-comments.js.map +1 -0
  273. package/dist/esm/transformations/blog-post.d.ts +2 -0
  274. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  275. package/dist/esm/transformations/blog-post.js +4 -1
  276. package/dist/esm/transformations/blog-post.js.map +1 -1
  277. package/dist/esm/transformations/ecommerce-product.d.ts +9 -1
  278. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  279. package/dist/esm/transformations/ecommerce-product.js +29 -2
  280. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  281. package/dist/esm/transformations/heading-anchors.d.ts +25 -0
  282. package/dist/esm/transformations/heading-anchors.d.ts.map +1 -0
  283. package/dist/esm/transformations/heading-anchors.js +25 -0
  284. package/dist/esm/transformations/heading-anchors.js.map +1 -0
  285. package/dist/esm/transformations/index.d.ts +61 -4
  286. package/dist/esm/transformations/index.d.ts.map +1 -1
  287. package/dist/esm/transformations/index.js +69 -19
  288. package/dist/esm/transformations/index.js.map +1 -1
  289. package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
  290. package/dist/esm/transformations/shared-utils.js +1 -1
  291. package/dist/esm/transformations/shared-utils.js.map +1 -1
  292. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  293. package/dist/esm/utils.d.ts +11 -1
  294. package/dist/esm/utils.d.ts.map +1 -1
  295. package/dist/esm/utils.js +60 -16
  296. package/dist/esm/utils.js.map +1 -1
  297. package/package.json +5 -5
  298. package/src/array-overlap-sql.ts +41 -0
  299. package/src/content-localization.ts +53 -0
  300. package/src/count-effect.ts +53 -2
  301. package/src/data-source-fetchers.ts +41 -5
  302. package/src/fetchers/airtable.ts +4 -2
  303. package/src/fetchers/clickhouse.ts +4 -2
  304. package/src/fetchers/firestore.ts +4 -2
  305. package/src/fetchers/javascript.ts +9 -1
  306. package/src/fetchers/mariadb.ts +4 -2
  307. package/src/fetchers/mongodb.ts +4 -2
  308. package/src/fetchers/mysql.ts +4 -2
  309. package/src/fetchers/postgresql.ts +53 -12
  310. package/src/fetchers/raw-query.ts +60 -5
  311. package/src/fetchers/redshift.ts +4 -2
  312. package/src/fetchers/rest-api.ts +27 -5
  313. package/src/fetchers/teleport.ts +85 -22
  314. package/src/fetchers/turso.ts +4 -2
  315. package/src/fetchers/utils/browser-row-policy.ts +212 -0
  316. package/src/fetchers/utils/request-sql-guards.ts +25 -0
  317. package/src/fetchers/utils/rest-api-response-cache.ts +188 -0
  318. package/src/fetchers/utils/table-access-guard.ts +39 -0
  319. package/src/index.ts +79 -11
  320. package/src/items-path.ts +74 -0
  321. package/src/loading-state.ts +134 -3
  322. package/src/localized-columns.ts +146 -0
  323. package/src/pagination-plugin.ts +206 -60
  324. package/src/product-filter-fields.ts +173 -0
  325. package/src/request-locale.ts +113 -0
  326. package/src/search-clear.ts +102 -0
  327. package/src/transformations/blog-adjacent-posts.ts +74 -0
  328. package/src/transformations/blog-comments.ts +132 -0
  329. package/src/transformations/blog-post.ts +38 -5
  330. package/src/transformations/ecommerce-product.ts +568 -5
  331. package/src/transformations/heading-anchors.ts +85 -0
  332. package/src/transformations/index.ts +191 -23
  333. package/src/transformations/shared-utils.ts +6 -0
  334. package/src/utils.ts +109 -19
@@ -0,0 +1,188 @@
1
+ import { DataCache } from '@teleporthq/teleport-shared'
2
+
3
+ /**
4
+ * The REST API data source's response cache: `config.cache` as the editor
5
+ * stores it on the data source.
6
+ */
7
+ export interface RESTAPICacheConfig {
8
+ enabled?: boolean
9
+ ttlSeconds?: number
10
+ }
11
+
12
+ /** A year: past that, the window only ever ends with the next deployment. */
13
+ const MAX_TTL_SECONDS = 31_536_000
14
+
15
+ /**
16
+ * Bounds for the one store every REST API data source in a server process
17
+ * shares. Entries are raw JSON text, so the character budget is close to the
18
+ * memory the cache holds.
19
+ */
20
+ const MAX_ENTRIES = 100
21
+ const MAX_CHARS = 33_554_432
22
+
23
+ /**
24
+ * How long a request may wait on an upstream call another request started.
25
+ * An older call is treated as stuck and a new one is made, so one hung socket
26
+ * cannot hold every later visitor.
27
+ */
28
+ const IN_FLIGHT_JOIN_MS = 10_000
29
+
30
+ /**
31
+ * The window, in whole seconds, a REST API data source caches its upstream
32
+ * response for — or `undefined` when caching is off, which keeps the emitted
33
+ * fetcher exactly what it was before this option existed.
34
+ */
35
+ export const resolveRESTAPICacheTTLSeconds = (
36
+ cache: RESTAPICacheConfig | undefined
37
+ ): number | undefined => {
38
+ if (!cache || typeof cache !== 'object' || cache.enabled !== true) {
39
+ return undefined
40
+ }
41
+
42
+ const { ttlSeconds } = cache
43
+ const seconds =
44
+ typeof ttlSeconds === 'number' && Number.isFinite(ttlSeconds) && ttlSeconds > 0
45
+ ? Math.max(1, Math.floor(ttlSeconds))
46
+ : DataCache.DEFAULT_CACHE_TTL_SECONDS
47
+
48
+ return Math.min(seconds, MAX_TTL_SECONDS)
49
+ }
50
+
51
+ /**
52
+ * Runtime helpers of a cached REST API fetcher.
53
+ *
54
+ * The cache sits under the handler, around the upstream `fetch` only: every
55
+ * caller — the API route, `getStaticProps` through `fetchData`, the count
56
+ * route, the global-state route — asks the same URL, and search, filters, sort
57
+ * and paging are applied to the cached payload afterwards. One upstream call
58
+ * per window therefore serves every page, filter and visitor.
59
+ *
60
+ * - The store lives on `globalThis`, so every data-source module in the
61
+ * process (each API route is its own bundle) shares it. On a serverless host
62
+ * that is one store per warm instance; the `s-maxage` header below is what
63
+ * carries the window to the CDN in front of all of them.
64
+ * - Entries are keyed by the whole upstream request (method, URL, headers,
65
+ * body) and hold the response TEXT: every reader parses its own copy, so a
66
+ * handler that sorts in place can never reorder another request's rows.
67
+ * - Freshness is judged by the READER's window, so two data sources calling
68
+ * the same URL with different windows each get what they asked for.
69
+ * - Only a 2xx JSON response is kept. Errors, non-JSON bodies and network
70
+ * failures reach the caller as before and are retried by the next request.
71
+ *
72
+ * Emitted inside a template literal: no backticks or template placeholders in
73
+ * here, not even in comments.
74
+ */
75
+ export const generateRESTAPIResponseCacheCode = (ttlSeconds: number): string => {
76
+ return `const REST_API_CACHE_TTL_MS = ${ttlSeconds * 1000}
77
+ const REST_API_CACHE_MAX_ENTRIES = ${MAX_ENTRIES}
78
+ const REST_API_CACHE_MAX_CHARS = ${MAX_CHARS}
79
+ const REST_API_IN_FLIGHT_JOIN_MS = ${IN_FLIGHT_JOIN_MS}
80
+
81
+ // One store per server process, shared by every REST API data source module.
82
+ if (!globalThis.__tqRestApiCache) {
83
+ globalThis.__tqRestApiCache = { entries: new Map(), inFlight: new Map(), chars: 0 }
84
+ }
85
+ const restApiCache = globalThis.__tqRestApiCache
86
+
87
+ function restApiCacheKey(url, options) {
88
+ return JSON.stringify([options.method, url, options.headers, options.body === undefined ? null : options.body])
89
+ }
90
+
91
+ function forgetRestApiResponse(key) {
92
+ const entry = restApiCache.entries.get(key)
93
+ if (entry) {
94
+ restApiCache.entries.delete(key)
95
+ restApiCache.chars -= entry.bodyText.length
96
+ }
97
+ }
98
+
99
+ function rememberRestApiResponse(key, entry) {
100
+ forgetRestApiResponse(key)
101
+ if (entry.bodyText.length > REST_API_CACHE_MAX_CHARS) {
102
+ return
103
+ }
104
+ restApiCache.entries.set(key, entry)
105
+ restApiCache.chars += entry.bodyText.length
106
+ // A Map iterates in insertion order and every hit re-inserts its entry, so
107
+ // the first key is always the least recently used one.
108
+ while (
109
+ restApiCache.entries.size > REST_API_CACHE_MAX_ENTRIES ||
110
+ restApiCache.chars > REST_API_CACHE_MAX_CHARS
111
+ ) {
112
+ forgetRestApiResponse(restApiCache.entries.keys().next().value)
113
+ }
114
+ }
115
+
116
+ async function requestRestApi(url, options) {
117
+ const response = await fetch(url, options)
118
+ if (!response.ok) {
119
+ return { ok: false, status: response.status, statusText: response.statusText }
120
+ }
121
+ const bodyText = await response.text()
122
+ // Parsed once here so a body that is not JSON fails this request, as
123
+ // response.json() would, instead of being cached and failing every request
124
+ // until the window ends.
125
+ JSON.parse(bodyText)
126
+ return {
127
+ ok: true,
128
+ status: response.status,
129
+ statusText: response.statusText,
130
+ bodyText,
131
+ fetchedAt: Date.now(),
132
+ }
133
+ }
134
+
135
+ function fetchRestApiWithCache(url, options) {
136
+ const key = restApiCacheKey(url, options)
137
+ const now = Date.now()
138
+
139
+ const cached = restApiCache.entries.get(key)
140
+ if (cached && now >= cached.fetchedAt && now - cached.fetchedAt < REST_API_CACHE_TTL_MS) {
141
+ restApiCache.entries.delete(key)
142
+ restApiCache.entries.set(key, cached)
143
+ return Promise.resolve(cached)
144
+ }
145
+
146
+ // Concurrent misses share one upstream call instead of each making their own.
147
+ const pending = restApiCache.inFlight.get(key)
148
+ if (pending && now - pending.startedAt < REST_API_IN_FLIGHT_JOIN_MS) {
149
+ return pending.promise
150
+ }
151
+
152
+ const flight = { startedAt: now, promise: null }
153
+ const release = () => {
154
+ if (restApiCache.inFlight.get(key) === flight) {
155
+ restApiCache.inFlight.delete(key)
156
+ }
157
+ }
158
+ flight.promise = requestRestApi(url, options).then(
159
+ (result) => {
160
+ release()
161
+ if (result.ok) {
162
+ rememberRestApiResponse(key, result)
163
+ }
164
+ return result
165
+ },
166
+ (error) => {
167
+ release()
168
+ throw error
169
+ }
170
+ )
171
+ restApiCache.inFlight.set(key, flight)
172
+ return flight.promise
173
+ }
174
+
175
+ // Lets the CDN answer this exact URL until the upstream response it was built
176
+ // from expires, so neither layer serves it longer than the window. Only a real
177
+ // GET response gets the header: fetchData, fetchCount and tqWithCache call the
178
+ // handler with a bare { status, json } object.
179
+ function applyRestApiCacheHeaders(req, res, fetchedAt) {
180
+ if (!req || req.method !== 'GET' || !res || typeof res.setHeader !== 'function') {
181
+ return
182
+ }
183
+ const remainingSeconds = Math.floor((fetchedAt + REST_API_CACHE_TTL_MS - Date.now()) / 1000)
184
+ if (remainingSeconds > 0) {
185
+ res.setHeader('Cache-Control', 'public, max-age=0, s-maxage=' + remainingSeconds)
186
+ }
187
+ }`
188
+ }
@@ -0,0 +1,39 @@
1
+ import { SessionCookieResolver, TableAccess } from '@teleporthq/teleport-shared'
2
+ import { generateBrowserRowAccessCall, generateBrowserRowPolicyCode } from './browser-row-policy'
3
+
4
+ /**
5
+ * The money-table guard for a generated per-table read route
6
+ * (`/api/teleport-<table>-<source>` and its `-count` twin).
7
+ *
8
+ * Every table a page binds gets such a route, and the route serves the whole
9
+ * table — plus any single SELECT through `?rawQuery=` — to whoever asks. For a
10
+ * catalogue that is the product; for gift cards and voucher codes it is theft.
11
+ * The guard refuses a protected table (see `TableAccess.PROTECTED_TABLES`)
12
+ * unless the caller is a server segment presenting the app secret or a
13
+ * signed-in member of a trusted role — the generated admin, whose list pages
14
+ * are the one browser reader these tables have. It is followed by the
15
+ * browser row policy (see `browser-row-policy.ts`): unpublished rows and
16
+ * private columns of the public tables, and the admin-only tables.
17
+ */
18
+
19
+ /** Declares the session resolver and the `__ta*` guard once per module. */
20
+ export const generateTableAccessPreamble = (trustedReaderRoles: ReadonlyArray<string>): string => {
21
+ return `${SessionCookieResolver.generateCommonJsSessionTokenResolverCode()}
22
+ ${TableAccess.generateTableAccessHelperCode({ trustedReaderRoles })}
23
+ ${generateBrowserRowPolicyCode()}`
24
+ }
25
+
26
+ /**
27
+ * The early return at the top of a handler, before the client connects:
28
+ * `tableName` is the module's table, `rawQueryExpr` the request's raw statement
29
+ * (or `null` where the handler accepts none).
30
+ */
31
+ export const generateReadGuardCall = (tableName: string, rawQueryExpr: string | null): string => {
32
+ return ` const __access = await __taGuardRead(req, ${JSON.stringify(tableName)}, ${
33
+ rawQueryExpr || 'null'
34
+ })
35
+ if (__access) {
36
+ return res.status(__access.status).json({ success: false, error: __access.message, timestamp: Date.now() })
37
+ }
38
+ ${generateBrowserRowAccessCall(tableName, rawQueryExpr)}`
39
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  FileType,
5
5
  ChunkType,
6
6
  UIDLComponentSEO,
7
+ UIDLDependency,
7
8
  UIDLDynamicReference,
8
9
  } from '@teleporthq/teleport-types'
9
10
  import { UIDLUtils, StringUtils } from '@teleporthq/teleport-shared'
@@ -16,6 +17,7 @@ import {
16
17
  import { createNextArrayMapperPaginationPlugin } from './pagination-plugin'
17
18
  import { buildProductTransformOptions } from './transformations'
18
19
  import { DATA_SOURCE_ISR_REVALIDATE_SECONDS } from './isr'
20
+ import { ensureRouterDeclaration } from './request-locale'
19
21
  import * as types from '@babel/types'
20
22
 
21
23
  interface SearchConfig {
@@ -55,6 +57,41 @@ interface WrapContext {
55
57
  // from user-defined or standard renderPropIdentifier values
56
58
  const WRAPPED_DS_EXPR_PREFIX = '__dsExpr_'
57
59
 
60
+ /**
61
+ * The statements of the component function the `jsx-component` chunk declares
62
+ * (`const Page = (props) => { … }`), or `null` when the chunk holds another
63
+ * shape — a test may hand the plugin a bare element.
64
+ */
65
+ const findComponentBody = (componentChunk: { content: unknown }): types.Statement[] | null => {
66
+ const content = componentChunk.content as types.Node | undefined
67
+ if (content?.type !== 'VariableDeclaration') {
68
+ return null
69
+ }
70
+ const init = content.declarations[0]?.init
71
+ if (
72
+ (init?.type !== 'ArrowFunctionExpression' && init?.type !== 'FunctionExpression') ||
73
+ init.body.type !== 'BlockStatement'
74
+ ) {
75
+ return null
76
+ }
77
+ return init.body.body
78
+ }
79
+
80
+ /**
81
+ * A client-side fetch in a localized project reads `router.locale` (see
82
+ * `extractDataSourceIntoNextAPIFolder`), so the router has to be declared in
83
+ * the component that fetches.
84
+ */
85
+ const ensureRouterForClientFetches = (
86
+ componentChunk: { content: unknown },
87
+ dependencies: Record<string, UIDLDependency>
88
+ ): void => {
89
+ const body = findComponentBody(componentChunk)
90
+ if (body) {
91
+ ensureRouterDeclaration(body, dependencies)
92
+ }
93
+ }
94
+
58
95
  function containsDataSourceDataReference(astNode: any): boolean {
59
96
  if (!astNode || typeof astNode !== 'object') {
60
97
  return false
@@ -490,6 +527,11 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
490
527
 
491
528
  let getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
492
529
 
530
+ const transformOptions = buildProductTransformOptions(options)
531
+ // Whether a provider on this page fetches client-side — in a localized
532
+ // project that fetch reads `router.locale`.
533
+ let wiredClientFetch = false
534
+
493
535
  // Track the first data source info for wrapping dataSourceData expressions
494
536
  let firstDataSourceInfo: DataSourceInfo | null = null
495
537
  // Track fetcher import name for wrapped providers
@@ -609,7 +651,7 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
609
651
  dependencies,
610
652
  dynamicRouteAttr,
611
653
  uidl.outputOptions?.folderPath,
612
- buildProductTransformOptions(options)
654
+ transformOptions
613
655
  )
614
656
 
615
657
  if (result.success && result.chunk) {
@@ -641,17 +683,23 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
641
683
  }
642
684
  }
643
685
  }
644
- } else {
686
+ } else if (
645
687
  extractDataSourceIntoNextAPIFolder(
646
688
  dataSourceNode,
647
689
  dataSources,
648
690
  componentChunk,
649
691
  options.extractedResources,
650
- buildProductTransformOptions(options)
692
+ transformOptions
651
693
  )
694
+ ) {
695
+ wiredClientFetch = true
652
696
  }
653
697
  })
654
698
 
699
+ if (wiredClientFetch && transformOptions.localization) {
700
+ ensureRouterForClientFetches(componentChunk, dependencies)
701
+ }
702
+
655
703
  // After processing all data source nodes, wrap any element props containing dataSourceData
656
704
  // expressions in a DataProvider
657
705
  if (firstDataSourceInfo && componentChunk.content) {
@@ -1207,6 +1255,11 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
1207
1255
  return structure
1208
1256
  }
1209
1257
 
1258
+ const transformOptions = buildProductTransformOptions(options)
1259
+ // Whether a provider in this component fetches client-side — in a
1260
+ // localized project that fetch reads `router.locale`.
1261
+ let wiredClientFetch = false
1262
+
1210
1263
  // Track the first data source info for wrapping dataSourceData expressions
1211
1264
  let firstDataSourceInfo: DataSourceInfo | null = null
1212
1265
 
@@ -1264,15 +1317,23 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
1264
1317
  }
1265
1318
  }
1266
1319
 
1267
- extractDataSourceIntoNextAPIFolder(
1268
- dataSourceNode,
1269
- dataSources,
1270
- componentChunk,
1271
- options.extractedResources,
1272
- buildProductTransformOptions(options)
1273
- )
1320
+ if (
1321
+ extractDataSourceIntoNextAPIFolder(
1322
+ dataSourceNode,
1323
+ dataSources,
1324
+ componentChunk,
1325
+ options.extractedResources,
1326
+ transformOptions
1327
+ )
1328
+ ) {
1329
+ wiredClientFetch = true
1330
+ }
1274
1331
  })
1275
1332
 
1333
+ if (wiredClientFetch && transformOptions.localization) {
1334
+ ensureRouterForClientFetches(componentChunk, dependencies)
1335
+ }
1336
+
1276
1337
  // If no data source nodes were found but there are dataSources available,
1277
1338
  // use the first available dataSource to create firstDataSourceInfo
1278
1339
  // This handles the case where a component has element props with dataSourceData expressions
@@ -1355,7 +1416,7 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
1355
1416
  dataSource,
1356
1417
  firstDataSourceInfo.tableName,
1357
1418
  false,
1358
- buildProductTransformOptions(options)
1419
+ transformOptions
1359
1420
  )
1360
1421
  options.extractedResources[`utils/${fileName}`] = {
1361
1422
  fileName,
@@ -1399,8 +1460,15 @@ export * from './utils'
1399
1460
  export * from './array-mapper-pagination'
1400
1461
  export * from './pagination-plugin'
1401
1462
  export * from './count-fetchers'
1463
+ export * from './product-price-sort'
1464
+ export * from './product-filter-fields'
1402
1465
  // The product transform's build-time options (category taxonomy + storefront
1403
1466
  // tax rate) — every package that emits a data-source fetcher has to derive
1404
1467
  // them the same way.
1405
1468
  export { buildProductTransformOptions } from './transformations'
1406
1469
  export type { EcommerceProductTransformOptions, EntityTransformOptions } from './transformations'
1470
+ // How a fetch says which language it is for, and when a project needs it to —
1471
+ // shared with every other package that emits a data-source fetch.
1472
+ export { isLocalizedProject, resolveContentLocalization } from './content-localization'
1473
+ export type { ContentLocalization } from './content-localization'
1474
+ export { REQUEST_LOCALE_PARAM, buildServerLocaleParam } from './request-locale'
@@ -0,0 +1,74 @@
1
+ import * as types from '@babel/types'
2
+
3
+ /**
4
+ * Where a paginated/searchable list's rows sit inside the payload its data
5
+ * source returns, read off the repeater's `source` expression.
6
+ *
7
+ * A REST API or JavaScript source may answer `{ templates: [...], total }`, and
8
+ * the editor binds the list to `All_Templates_data_data?.templates || []`. The
9
+ * generated fetcher only searches, filters, sorts and slices a payload that IS
10
+ * an array, so without this every row came back on every page (the canvas
11
+ * slices on the client and looked right) and the count measured the wrapper as
12
+ * 0 rows, disabling "Next".
13
+ *
14
+ * Returns undefined when the list is bound to the payload itself, or when the
15
+ * source is anything other than a plain property chain off the data source's
16
+ * render prop — an expression this cannot read is left exactly as before.
17
+ */
18
+ const SEGMENT_RE =
19
+ /^(?:\?\.([A-Za-z_$][\w$]*)|\.([A-Za-z_$][\w$]*)|(?:\?\.)?\[\s*(?:"([^"]*)"|'([^']*)')\s*\])/
20
+
21
+ const PATH_AWARE_SOURCE_TYPES = new Set(['rest-api', 'javascript'])
22
+
23
+ export const extractItemsPath = (
24
+ source: unknown,
25
+ dataSourceIdentifier: string,
26
+ dataSourceType: string
27
+ ): string[] | undefined => {
28
+ if (typeof source !== 'string' || !dataSourceIdentifier) {
29
+ return undefined
30
+ }
31
+ if (!PATH_AWARE_SOURCE_TYPES.has(dataSourceType)) {
32
+ return undefined
33
+ }
34
+
35
+ let rest = source
36
+ .trim()
37
+ .replace(/\s*\|\|\s*\[\s*\]$/, '')
38
+ .trim()
39
+ if (!rest.startsWith(dataSourceIdentifier)) {
40
+ return undefined
41
+ }
42
+ rest = rest.slice(dataSourceIdentifier.length)
43
+
44
+ const path: string[] = []
45
+ while (rest.length > 0) {
46
+ const match = rest.match(SEGMENT_RE)
47
+ if (!match) {
48
+ return undefined
49
+ }
50
+ path.push(match[1] ?? match[2] ?? match[3] ?? match[4])
51
+ rest = rest.slice(match[0].length)
52
+ }
53
+
54
+ return path.length > 0 ? path : undefined
55
+ }
56
+
57
+ /**
58
+ * Push `<key>: '["templates"]'` onto a params object. Fetch calls send it as
59
+ * `itemsPath` (process the inner array, keep the wrapper the page reads back
60
+ * through); count calls send it as `collectionPath`, so the count handler
61
+ * measures the inner array instead of the wrapper.
62
+ */
63
+ export const appendItemsPathParam = (
64
+ paramsProps: types.ObjectProperty[],
65
+ itemsPath: string[] | undefined,
66
+ key: 'itemsPath' | 'collectionPath'
67
+ ): void => {
68
+ if (!itemsPath || itemsPath.length === 0) {
69
+ return
70
+ }
71
+ paramsProps.push(
72
+ types.objectProperty(types.identifier(key), types.stringLiteral(JSON.stringify(itemsPath)))
73
+ )
74
+ }
@@ -67,6 +67,19 @@ export interface LoadingStateVars {
67
67
  * stale rows back for the rest of the second request.
68
68
  */
69
69
  inFlightRefVar: string
70
+ /**
71
+ * Ref counting the requests STARTED, so a response can tell whether a newer
72
+ * request has overtaken it. `DataProvider` keeps whatever resolves last:
73
+ * two controls changed in quick succession (a "Clear all" emptying several
74
+ * filter states, one state write after another) start two requests, and
75
+ * when the older, slower one settles after the newer it painted the OLDER
76
+ * rows over the newer — a list showing two products for a URL that filters
77
+ * nothing. Each request notes its sequence number and, if overtaken, hands
78
+ * over the newest settled rows instead of its own.
79
+ */
80
+ fetchSeqRefVar: string
81
+ /** Ref holding the rows of the newest request that settled — what an overtaken one resolves with. */
82
+ latestDataRefVar: string
70
83
  }
71
84
 
72
85
  export function getLoadingStateVars(index: number): LoadingStateVars {
@@ -74,14 +87,18 @@ export function getLoadingStateVars(index: number): LoadingStateVars {
74
87
  isFetchingVar: `ds_${index}_isFetching`,
75
88
  setIsFetchingVar: `setDs_${index}_isFetching`,
76
89
  inFlightRefVar: `ds_${index}_fetchesInFlight`,
90
+ fetchSeqRefVar: `ds_${index}_fetchSeq`,
91
+ latestDataRefVar: `ds_${index}_latestData`,
77
92
  }
78
93
  }
79
94
 
80
95
  /**
81
96
  * `const ds_N_fetchesInFlight = useRef(0)`
97
+ * `const ds_N_fetchSeq = useRef(0)`
98
+ * `const ds_N_latestData = useRef(undefined)`
82
99
  * `const [ds_N_isFetching, setDs_N_isFetching] = useState(false)`
83
100
  *
84
- * Both are stable across renders (a ref object and a `useState` setter), which
101
+ * All are stable across renders (ref objects and a `useState` setter), which
85
102
  * is what lets the wrapped `fetchData` keep its empty `useCallback` dependency
86
103
  * array — a changing `fetchData` identity would retrigger the provider's fetch
87
104
  * effect on every render.
@@ -94,6 +111,18 @@ export function buildLoadingStateDeclarations(vars: LoadingStateVars): types.Sta
94
111
  types.callExpression(types.identifier('useRef'), [types.numericLiteral(0)])
95
112
  ),
96
113
  ]),
114
+ types.variableDeclaration('const', [
115
+ types.variableDeclarator(
116
+ types.identifier(vars.fetchSeqRefVar),
117
+ types.callExpression(types.identifier('useRef'), [types.numericLiteral(0)])
118
+ ),
119
+ ]),
120
+ types.variableDeclaration('const', [
121
+ types.variableDeclarator(
122
+ types.identifier(vars.latestDataRefVar),
123
+ types.callExpression(types.identifier('useRef'), [types.identifier('undefined')])
124
+ ),
125
+ ]),
97
126
  types.variableDeclaration('const', [
98
127
  types.variableDeclarator(
99
128
  types.arrayPattern([
@@ -183,10 +212,54 @@ function injectTrackingIntoCachedBody(body: types.BlockStatement, vars: LoadingS
183
212
 
184
213
  const tracked = buildTrackedFetchBody(networkChain.statement.argument as types.Expression, vars)
185
214
  body.body.splice(networkChain.index, 1, ...tracked.body)
215
+ markCacheHitAsNewest(body, vars)
186
216
 
187
217
  return true
188
218
  }
189
219
 
220
+ /**
221
+ * A cache hit answers synchronously with the rows of THIS request, so it is
222
+ * the newest settled result too: it advances the sequence and records its rows,
223
+ * or an older network request still in flight would resolve after it and paint
224
+ * over it. Adds the two statements at the head of the hit branch — before its
225
+ * `return Promise.resolve(__tqHit)` — and nothing else, so the flag the branch
226
+ * deliberately never raises stays untouched.
227
+ */
228
+ function markCacheHitAsNewest(body: types.BlockStatement, vars: LoadingStateVars): void {
229
+ const hit = body.body.find(
230
+ (statement): statement is types.IfStatement =>
231
+ statement.type === 'IfStatement' &&
232
+ statement.test.type === 'BinaryExpression' &&
233
+ statement.test.left.type === 'Identifier' &&
234
+ statement.test.left.name === CACHE_HIT_IDENTIFIER &&
235
+ statement.consequent.type === 'BlockStatement'
236
+ )
237
+ if (!hit || hit.consequent.type !== 'BlockStatement') {
238
+ return
239
+ }
240
+ hit.consequent.body.unshift(
241
+ types.expressionStatement(types.updateExpression('++', refCurrent(vars.fetchSeqRefVar), true)),
242
+ types.expressionStatement(
243
+ types.assignmentExpression(
244
+ '=',
245
+ refCurrent(vars.latestDataRefVar),
246
+ types.identifier(CACHE_HIT_IDENTIFIER)
247
+ )
248
+ )
249
+ )
250
+ }
251
+
252
+ /** The identifier the cache wiring binds a hit to — see `cache/ast.ts`. */
253
+ const CACHE_HIT_IDENTIFIER = '__tqHit'
254
+ /** The rows a tracked request resolved with, inside its own `then`. */
255
+ const TRACKED_ROWS_IDENTIFIER = '__tqRows'
256
+ /** This request's sequence number. */
257
+ const TRACKED_SEQ_IDENTIFIER = '__tqSeq'
258
+
259
+ function refCurrent(refVar: string): types.MemberExpression {
260
+ return types.memberExpression(types.identifier(refVar), types.identifier('current'))
261
+ }
262
+
190
263
  // tslint:disable-next-line:no-any
191
264
  function containsIdentifier(node: any, name: string): boolean {
192
265
  if (!node || typeof node !== 'object') {
@@ -257,9 +330,16 @@ function findMemoizedFetchDataArrow(
257
330
  * Turns `(params) => fetch(...).then(...)` into
258
331
  *
259
332
  * (params) => {
333
+ * const __tqSeq = ++ds_N_fetchSeq.current
260
334
  * ds_N_fetchesInFlight.current += 1
261
335
  * setDs_N_isFetching(true)
262
- * return fetch(...).then(...).finally(() => {
336
+ * return fetch(...).then(...).then((__tqRows) => {
337
+ * if (__tqSeq !== ds_N_fetchSeq.current) {
338
+ * return ds_N_latestData.current !== undefined ? ds_N_latestData.current : __tqRows
339
+ * }
340
+ * ds_N_latestData.current = __tqRows
341
+ * return __tqRows
342
+ * }).finally(() => {
263
343
  * ds_N_fetchesInFlight.current -= 1
264
344
  * if (ds_N_fetchesInFlight.current <= 0) {
265
345
  * ds_N_fetchesInFlight.current = 0
@@ -268,6 +348,12 @@ function findMemoizedFetchDataArrow(
268
348
  * })
269
349
  * }
270
350
  *
351
+ * The `then` is the stale guard (see `fetchSeqRefVar`): a request overtaken by
352
+ * a newer one resolves with the newest settled rows rather than its own, so
353
+ * the provider — which keeps whatever resolves LAST — never paints older rows
354
+ * over newer ones. An overtaken request whose successor has not settled yet
355
+ * resolves with its own rows; the successor then replaces them.
356
+ *
271
357
  * `finally` (rather than a `then` pair) keeps the flag honest when the request
272
358
  * rejects: the provider switches to its error status and the loading state must
273
359
  * not stay on screen. The counter is clamped at 0 so a late settle from a
@@ -317,7 +403,52 @@ function buildTrackedFetchBody(
317
403
  ])
318
404
  )
319
405
 
406
+ const latestData = refCurrent(vars.latestDataRefVar)
407
+ const staleGuard = types.arrowFunctionExpression(
408
+ [types.identifier(TRACKED_ROWS_IDENTIFIER)],
409
+ types.blockStatement([
410
+ types.ifStatement(
411
+ types.binaryExpression(
412
+ '!==',
413
+ types.identifier(TRACKED_SEQ_IDENTIFIER),
414
+ refCurrent(vars.fetchSeqRefVar)
415
+ ),
416
+ types.blockStatement([
417
+ types.returnStatement(
418
+ types.conditionalExpression(
419
+ types.binaryExpression(
420
+ '!==',
421
+ types.cloneNode(latestData, true),
422
+ types.identifier('undefined')
423
+ ),
424
+ types.cloneNode(latestData, true),
425
+ types.identifier(TRACKED_ROWS_IDENTIFIER)
426
+ )
427
+ ),
428
+ ])
429
+ ),
430
+ types.expressionStatement(
431
+ types.assignmentExpression(
432
+ '=',
433
+ types.cloneNode(latestData, true),
434
+ types.identifier(TRACKED_ROWS_IDENTIFIER)
435
+ )
436
+ ),
437
+ types.returnStatement(types.identifier(TRACKED_ROWS_IDENTIFIER)),
438
+ ])
439
+ )
440
+ const guardedFetch = types.callExpression(
441
+ types.memberExpression(fetchExpression, types.identifier('then')),
442
+ [staleGuard]
443
+ )
444
+
320
445
  return types.blockStatement([
446
+ types.variableDeclaration('const', [
447
+ types.variableDeclarator(
448
+ types.identifier(TRACKED_SEQ_IDENTIFIER),
449
+ types.updateExpression('++', refCurrent(vars.fetchSeqRefVar), true)
450
+ ),
451
+ ]),
321
452
  types.expressionStatement(
322
453
  types.assignmentExpression(
323
454
  '+=',
@@ -329,7 +460,7 @@ function buildTrackedFetchBody(
329
460
  types.callExpression(types.identifier(vars.setIsFetchingVar), [types.booleanLiteral(true)])
330
461
  ),
331
462
  types.returnStatement(
332
- types.callExpression(types.memberExpression(fetchExpression, types.identifier('finally')), [
463
+ types.callExpression(types.memberExpression(guardedFetch, types.identifier('finally')), [
333
464
  settleHandler,
334
465
  ])
335
466
  ),