@rocicorp/zero 1.12.0-canary.12 → 1.12.0-canary.13

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 (211) hide show
  1. package/out/replicache/src/btree/diff.d.ts.map +1 -1
  2. package/out/replicache/src/btree/diff.js +1 -2
  3. package/out/replicache/src/btree/diff.js.map +1 -1
  4. package/out/replicache/src/btree/read.d.ts +31 -1
  5. package/out/replicache/src/btree/read.d.ts.map +1 -1
  6. package/out/replicache/src/btree/read.js +74 -19
  7. package/out/replicache/src/btree/read.js.map +1 -1
  8. package/out/replicache/src/btree/splice.d.ts.map +1 -1
  9. package/out/replicache/src/btree/splice.js +2 -1
  10. package/out/replicache/src/btree/splice.js.map +1 -1
  11. package/out/replicache/src/btree/write.d.ts.map +1 -1
  12. package/out/replicache/src/btree/write.js +2 -1
  13. package/out/replicache/src/btree/write.js.map +1 -1
  14. package/out/replicache/src/persist/collect-idb-databases.d.ts +1 -0
  15. package/out/replicache/src/persist/collect-idb-databases.d.ts.map +1 -1
  16. package/out/replicache/src/persist/collect-idb-databases.js +13 -5
  17. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  18. package/out/replicache/src/scan-iterator.js +1 -1
  19. package/out/replicache/src/sync/request-id.js +2 -2
  20. package/out/replicache/src/sync/request-id.js.map +1 -1
  21. package/out/shared/src/bigint-json.d.ts.map +1 -1
  22. package/out/shared/src/bigint-json.js +1 -0
  23. package/out/shared/src/bigint-json.js.map +1 -1
  24. package/out/shared/src/btree-set.d.ts +21 -0
  25. package/out/shared/src/btree-set.d.ts.map +1 -1
  26. package/out/shared/src/btree-set.js +54 -0
  27. package/out/shared/src/btree-set.js.map +1 -1
  28. package/out/shared/src/headers.js +20 -0
  29. package/out/shared/src/headers.js.map +1 -0
  30. package/out/shared/src/natural-runs.d.ts +191 -0
  31. package/out/shared/src/natural-runs.d.ts.map +1 -0
  32. package/out/shared/src/natural-runs.js +337 -0
  33. package/out/shared/src/natural-runs.js.map +1 -0
  34. package/out/shared/src/random-uint64.d.ts.map +1 -1
  35. package/out/shared/src/random-uint64.js +2 -2
  36. package/out/shared/src/random-uint64.js.map +1 -1
  37. package/out/shared/src/random-values.d.ts +19 -1
  38. package/out/shared/src/random-values.d.ts.map +1 -1
  39. package/out/shared/src/random-values.js +38 -4
  40. package/out/shared/src/random-values.js.map +1 -1
  41. package/out/shared/src/tuple-map.d.ts +22 -0
  42. package/out/shared/src/tuple-map.d.ts.map +1 -0
  43. package/out/shared/src/tuple-map.js +48 -0
  44. package/out/shared/src/tuple-map.js.map +1 -0
  45. package/out/zero/package.js +1 -1
  46. package/out/zero/package.js.map +1 -1
  47. package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
  48. package/out/zero-cache/src/auth/auth.js +5 -0
  49. package/out/zero-cache/src/auth/auth.js.map +1 -1
  50. package/out/zero-cache/src/config/zero-config.d.ts +21 -0
  51. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  52. package/out/zero-cache/src/config/zero-config.js +47 -9
  53. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  54. package/out/zero-cache/src/custom-queries/transform-query.js +2 -2
  55. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  56. package/out/zero-cache/src/db/pg-to-lite.d.ts.map +1 -1
  57. package/out/zero-cache/src/db/pg-to-lite.js +3 -2
  58. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  59. package/out/zero-cache/src/db/pg-type-parser.js +55 -2
  60. package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
  61. package/out/zero-cache/src/server/change-streamer.js +5 -2
  62. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  63. package/out/zero-cache/src/server/inspector-delegate.js +6 -29
  64. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  65. package/out/zero-cache/src/server/main.js +5 -0
  66. package/out/zero-cache/src/server/main.js.map +1 -1
  67. package/out/zero-cache/src/server/replicator.js +3 -2
  68. package/out/zero-cache/src/server/replicator.js.map +1 -1
  69. package/out/zero-cache/src/server/syncer.js +5 -4
  70. package/out/zero-cache/src/server/syncer.js.map +1 -1
  71. package/out/zero-cache/src/server/worker-dispatcher.d.ts.map +1 -1
  72. package/out/zero-cache/src/server/worker-dispatcher.js +2 -1
  73. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  74. package/out/zero-cache/src/services/admin-auth.d.ts +22 -0
  75. package/out/zero-cache/src/services/admin-auth.d.ts.map +1 -0
  76. package/out/zero-cache/src/services/admin-auth.js +42 -0
  77. package/out/zero-cache/src/services/admin-auth.js.map +1 -0
  78. package/out/zero-cache/src/services/auth-attempt-counter.d.ts +33 -0
  79. package/out/zero-cache/src/services/auth-attempt-counter.d.ts.map +1 -0
  80. package/out/zero-cache/src/services/auth-attempt-counter.js +61 -0
  81. package/out/zero-cache/src/services/auth-attempt-counter.js.map +1 -0
  82. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
  83. package/out/zero-cache/src/services/change-source/pg/change-source.js +110 -13
  84. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  85. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +13 -7
  86. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
  87. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +38 -5
  88. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  89. package/out/zero-cache/src/services/change-source/pg/schema/init.js +10 -1
  90. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  91. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +4 -0
  92. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  93. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +10 -3
  94. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  95. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +23 -2
  96. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  97. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +2 -2
  98. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  99. package/out/zero-cache/src/services/change-streamer/change-streamer.js +10 -3
  100. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  101. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  102. package/out/zero-cache/src/services/heapz.d.ts.map +1 -1
  103. package/out/zero-cache/src/services/heapz.js +4 -5
  104. package/out/zero-cache/src/services/heapz.js.map +1 -1
  105. package/out/zero-cache/src/services/mutagen/error.d.ts +5 -0
  106. package/out/zero-cache/src/services/mutagen/error.d.ts.map +1 -1
  107. package/out/zero-cache/src/services/mutagen/error.js +7 -1
  108. package/out/zero-cache/src/services/mutagen/error.js.map +1 -1
  109. package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
  110. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  111. package/out/zero-cache/src/services/mutagen/pusher.js +1 -1
  112. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  113. package/out/zero-cache/src/services/plannerz.d.ts.map +1 -1
  114. package/out/zero-cache/src/services/plannerz.js +3 -7
  115. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  116. package/out/zero-cache/src/services/profz.d.ts.map +1 -1
  117. package/out/zero-cache/src/services/profz.js +4 -13
  118. package/out/zero-cache/src/services/profz.js.map +1 -1
  119. package/out/zero-cache/src/services/replicator/change-processor.js +29 -2
  120. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  121. package/out/zero-cache/src/services/replicator/incremental-sync.js +3 -3
  122. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  123. package/out/zero-cache/src/services/statz.d.ts.map +1 -1
  124. package/out/zero-cache/src/services/statz.js +2 -7
  125. package/out/zero-cache/src/services/statz.js.map +1 -1
  126. package/out/zero-cache/src/services/view-syncer/client-handler.js +22 -1
  127. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  128. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +40 -2
  129. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  130. package/out/zero-cache/src/services/view-syncer/cvr-store.js +54 -17
  131. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  132. package/out/zero-cache/src/services/view-syncer/cvr.js +11 -0
  133. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  134. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js +107 -0
  135. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js.map +1 -0
  136. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +11 -6
  137. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  138. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +2 -1
  139. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  140. package/out/zero-cache/src/services/view-syncer/row-record-cache.js +16 -14
  141. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  142. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +4 -1
  143. package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
  144. package/out/zero-cache/src/services/view-syncer/schema/init.js +3 -0
  145. package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
  146. package/out/zero-cache/src/services/view-syncer/view-syncer.js +89 -51
  147. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  148. package/out/zero-cache/src/types/client-group-log-id.d.ts +9 -0
  149. package/out/zero-cache/src/types/client-group-log-id.d.ts.map +1 -0
  150. package/out/zero-cache/src/types/client-group-log-id.js +16 -0
  151. package/out/zero-cache/src/types/client-group-log-id.js.map +1 -0
  152. package/out/zero-cache/src/types/lite.d.ts.map +1 -1
  153. package/out/zero-cache/src/types/lite.js +1 -0
  154. package/out/zero-cache/src/types/lite.js.map +1 -1
  155. package/out/zero-cache/src/types/pg.d.ts +13 -2
  156. package/out/zero-cache/src/types/pg.d.ts.map +1 -1
  157. package/out/zero-cache/src/types/pg.js +2 -2
  158. package/out/zero-cache/src/types/pg.js.map +1 -1
  159. package/out/zero-cache/src/types/poke-chunk.d.ts +25 -4
  160. package/out/zero-cache/src/types/poke-chunk.d.ts.map +1 -1
  161. package/out/zero-cache/src/types/poke-chunk.js +46 -5
  162. package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
  163. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  164. package/out/zero-cache/src/workers/connection.js +10 -3
  165. package/out/zero-cache/src/workers/connection.js.map +1 -1
  166. package/out/zero-cache/src/workers/rehome.d.ts +10 -0
  167. package/out/zero-cache/src/workers/rehome.d.ts.map +1 -1
  168. package/out/zero-cache/src/workers/rehome.js +4 -2
  169. package/out/zero-cache/src/workers/rehome.js.map +1 -1
  170. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +3 -2
  171. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  172. package/out/zero-cache/src/workers/syncer.js +15 -15
  173. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  174. package/out/zero-client/src/client/inspector/inspector.d.ts +4 -0
  175. package/out/zero-client/src/client/inspector/inspector.d.ts.map +1 -1
  176. package/out/zero-client/src/client/inspector/inspector.js +4 -0
  177. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  178. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts +10 -2
  179. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
  180. package/out/zero-client/src/client/inspector/lazy-inspector.js +38 -31
  181. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  182. package/out/zero-client/src/client/inspector/query.d.ts +2 -2
  183. package/out/zero-client/src/client/inspector/query.d.ts.map +1 -1
  184. package/out/zero-client/src/client/inspector/query.js +4 -4
  185. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  186. package/out/zero-client/src/client/ivm-branch.d.ts +3 -0
  187. package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
  188. package/out/zero-client/src/client/ivm-branch.js +10 -5
  189. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  190. package/out/zero-client/src/client/reconnect-backoff.d.ts +31 -0
  191. package/out/zero-client/src/client/reconnect-backoff.d.ts.map +1 -0
  192. package/out/zero-client/src/client/reconnect-backoff.js +44 -0
  193. package/out/zero-client/src/client/reconnect-backoff.js.map +1 -0
  194. package/out/zero-client/src/client/version.js +1 -1
  195. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  196. package/out/zero-client/src/client/zero.js +7 -0
  197. package/out/zero-client/src/client/zero.js.map +1 -1
  198. package/out/zero-client/src/util/nanoid.js +2 -2
  199. package/out/zero-client/src/util/nanoid.js.map +1 -1
  200. package/out/zero-protocol/src/up.d.ts +42 -42
  201. package/out/zero-server/src/process-mutations.d.ts +10 -0
  202. package/out/zero-server/src/process-mutations.d.ts.map +1 -1
  203. package/out/zero-server/src/process-mutations.js +39 -6
  204. package/out/zero-server/src/process-mutations.js.map +1 -1
  205. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  206. package/out/zql/src/ivm/memory-source.js +89 -6
  207. package/out/zql/src/ivm/memory-source.js.map +1 -1
  208. package/out/zql/src/planner/planner-connection.d.ts.map +1 -1
  209. package/out/zql/src/planner/planner-connection.js +37 -3
  210. package/out/zql/src/planner/planner-connection.js.map +1 -1
  211. package/package.json +1 -1
@@ -0,0 +1,191 @@
1
+ /**
2
+ * A stable sort for input that already arrives in a handful of ordered runs,
3
+ * for the engines whose built-in sort cannot exploit that.
4
+ *
5
+ * `Array.prototype.sort` is TimSort on V8, so already-sorted input costs N-1
6
+ * comparisons. **On Hermes it is not adaptive**, and the difference is not
7
+ * small. Sorting 100,000 objects through a two-column comparator:
8
+ *
9
+ * ```text
10
+ * Hermes 0.12 V8
11
+ * sorted 93.6ms / 1,483,630 0.8ms / 99,999
12
+ * reversed 168.6ms / 2,571,203 0.8ms / 99,999
13
+ * shuffled 134.8ms / 1,770,233 17.0ms / 1,529,373
14
+ * ```
15
+ *
16
+ * Hermes does fifteen times the necessary comparisons on sorted input, and
17
+ * reversed input is its *worst* case -- slower than random data, where an O(N)
18
+ * reverse would do. That matters because ordered input is the common case here
19
+ * and not a lucky one: rows come out of a source in primary-key order, and
20
+ * secondary sort keys are usually timestamps or sequence numbers that move with
21
+ * insertion order, so a secondary index is often built over rows that are
22
+ * already sorted, exactly reversed, or a few ordered batches concatenated.
23
+ *
24
+ * This merges the natural runs instead, which is O(N) on one run and
25
+ * O(N log R) on R of them. Measured against the same inputs:
26
+ *
27
+ * ```text
28
+ * Hermes native -> runs V8 native -> runs
29
+ * sorted 93.2ms -> 5.4ms (17x) 1.0ms -> 0.4ms
30
+ * reversed 166.6ms -> 6.6ms (25x) 1.0ms -> 0.4ms
31
+ * 20 runs 147.2ms -> 9.4ms (16x) 1.0ms -> 0.8ms
32
+ * ```
33
+ *
34
+ * ## Why it gives up rather than always merging
35
+ *
36
+ * A full TimSort would handle near-sorted input too, but without galloping it
37
+ * loses badly to V8's native sort there (8.0ms against 2.8ms on 100,000
38
+ * near-sorted rows, 1.26M comparisons against 268k), and galloping is a large
39
+ * amount of code to carry. So this returns `undefined` once the input has more
40
+ * runs than merging can pay for, and the caller falls back to whatever it would
41
+ * have done -- which keeps V8's galloping and costs Hermes nothing it was not
42
+ * already paying.
43
+ *
44
+ * ## Where the bound sits, and why it scales
45
+ *
46
+ * Merging R runs costs about `n * log2(R)` comparisons; a non-adaptive sort
47
+ * costs about `n * log2(n)` whatever the input looks like. So merging is worth
48
+ * it exactly while `log2(R)` is the smaller of the two, and `R <= sqrt(n)` is
49
+ * the region where it is at most half -- a bound that needs no fitted constant
50
+ * and tightens on its own as `n` grows. That is what `maxRuns` defaults to,
51
+ * floored at 32 so nothing below 1,024 elements changes.
52
+ *
53
+ * Read the other way round, that bound is a statement about the *input* rather
54
+ * than a tolerance for a number of runs: `R <= sqrt(n)` is the same as saying
55
+ * the average run is at least `sqrt(n)` long. Which is the useful form, because
56
+ * what makes merging pay is runs long enough to amortise a merge pass over, and
57
+ * that does not change when a data set grows. A fixed run count does: the
58
+ * bound was 32 until a table with 50 of something turned up, which is the
59
+ * failure mode this shape avoids.
60
+ *
61
+ * The floor used to be the whole bound, and a fixed 32 was too low to catch the
62
+ * shape this was written for. A secondary index built over a junction table
63
+ * whose primary key leads with the *other* column arrives as one ordered run
64
+ * per distinct value of that column: 50 runs of 1,200 rows over 60,000, for a
65
+ * table with 50 of them. Merging that is O(n log 50), but at `maxRuns` 32 it
66
+ * declined and Hermes fell through to the full non-adaptive sort this function
67
+ * exists to avoid.
68
+ *
69
+ * The bound is deliberately well inside the crossover, and comparisons are not
70
+ * the only cost: merging R runs is `ceil(log2(R))` passes, each writing every
71
+ * element, which an in-place sort does not do. So it was measured past the
72
+ * bound rather than up to it. Sorting 60,000 rows through a three-column
73
+ * comparator on an Android emulator, in a release build, medians of 3 repeats.
74
+ * The engine is Static Hermes 250829098.0.14 -- read out of the APK's
75
+ * `libhermesvm.so` rather than taken from the build config, because what React
76
+ * Native ships is not the engine `sdks/.hermesversion` pins. Hence
77
+ * `libhermesvm.so` and not `libhermes.so`: a different engine, not a later
78
+ * build of the one in the 0.12 table above.
79
+ *
80
+ * Which does not make that table stale, and it is worth being exact about what
81
+ * carries over. Its *comparison counts* reproduce on the shipped engine to the
82
+ * digit -- 1,483,630 sorted and 2,571,203 reversed at 100,000, and 741 at
83
+ * n = 128 -- so the two engines sort by the same algorithm, and the premise all
84
+ * of this rests on holds where it matters: `nativeSortIsAdaptive()` is false on
85
+ * the VM that actually runs, not merely on an old CLI. Its *times* do not carry
86
+ * over, in either direction: those were a standalone CLI on a development host,
87
+ * these are an emulator, and the shipped VM is about 2.3x faster than that CLI
88
+ * on the same host. Compare the counts, never the milliseconds.
89
+ *
90
+ * ```text
91
+ * runs native merge
92
+ * 2 517.9ms 23.0ms
93
+ * 50 218.3ms 82.0ms
94
+ * 100 221.6ms 90.3ms
95
+ * 244 202.8ms 101.4ms <- the bound at this size
96
+ * 400 210.7ms 110.9ms
97
+ * 1,000 199.4ms 121.5ms
98
+ * shuffled 221.0ms 207.3ms
99
+ * ```
100
+ *
101
+ * The extra passes do show up -- merging costs five times more at 1,000 runs
102
+ * than at 2 -- but on this engine it still wins everywhere measured, and only
103
+ * on shuffled input does the margin close to nothing. At the bound it is 2x,
104
+ * which is the margin the bound is chosen to keep rather than the point where
105
+ * merging stops paying; a higher one would capture more, and no workload here
106
+ * calls for it. Note also that two ordered runs are the native sort's *worst*
107
+ * case (517.9ms, worse than shuffled), which is the asymmetry this whole
108
+ * function exists to exploit.
109
+ *
110
+ * On V8, which never reaches this path, merging holds level to about 1,000 runs
111
+ * (23.1ms against 24.1ms) and loses past 5,000 and on shuffled input (78.6ms
112
+ * against 43.0ms). That bounds how far merging scales on an adaptive engine; it
113
+ * does not justify the bound, since no caller gets here on one.
114
+ *
115
+ * Declining is not free, and what bounds it is runs, not comparisons: counting
116
+ * stops once a `maxRuns + 1`th run is known to exist, before walking that one,
117
+ * but every run before it is walked to its end. Measured over 100,000 elements
118
+ * with `maxRuns` at 32, against the comparisons the native sort then does:
119
+ *
120
+ * ```text
121
+ * shuffled 76 comparisons 0.005%
122
+ * 32 long runs, then more 80,000 comparisons 15%
123
+ * ```
124
+ *
125
+ * Raising the bound raises what a decline can cost, and the shape that pays is
126
+ * an input whose run count lands just past it. Over 60,000 elements in equal
127
+ * ordered runs, comparisons spent before declining:
128
+ *
129
+ * ```text
130
+ * runs at bound 32 at bound 244
131
+ * 245 7,840 59,780
132
+ * 400 4,800 36,600
133
+ * 1,000 1,920 14,640
134
+ * ```
135
+ *
136
+ * So a 245-run input now scans essentially the whole array where it used to
137
+ * stop after an eighth of it. What does *not* change is the ceiling: the worst
138
+ * case is `maxRuns` long runs followed by anything at all, and that walks
139
+ * almost every element at either bound -- 60,000 comparisons at 32 against
140
+ * 59,780 at 244. The old bound never bounded the scan below `n`; it only made
141
+ * fewer shapes reach it. Against the non-adaptive sort a decline falls into,
142
+ * which costs about 14.8 comparisons per element on the engine that gets here,
143
+ * a full scan is around 7% on top. (Measured as a fraction of V8's adaptive
144
+ * sort it looks like 17-19%, but nothing reaches this path on V8.)
145
+ *
146
+ * A scan budget cannot be separated from the merge budget without giving up
147
+ * merges that would have won: `R` is only known to exceed the budget once
148
+ * budget-many runs have been seen, and merging `R` runs has to scan all `n`
149
+ * elements regardless. Any limit tight enough to cut a decline short of `n`
150
+ * also rejects inputs just inside the bound, which is the case this change
151
+ * exists to serve.
152
+ *
153
+ * One interaction is worth naming because this bound makes it bigger. The
154
+ * caller in `MemorySource` tries this before its specialised numeric-column
155
+ * pass, so an ordering that the numeric pass could have handled may now pay a
156
+ * scan of up to `n` before taking it anyway. Bounded, and not a shape seen in
157
+ * the ten index builds of a cold pass -- the nine that merge are under 32 runs
158
+ * and the one that declines is string-keyed -- but the chain order is worth
159
+ * revisiting on its own measurement rather than inside a bound change.
160
+ *
161
+ * Of the two rows in the first table, shuffled is the shape that declines in
162
+ * practice, and it gives up almost at once because its runs are two or three
163
+ * elements long. The other is the honest worst case, where the scan walks most
164
+ * of the array before handing it back. Nothing in the
165
+ * workloads this was built for looks like that, but it is what the bound
166
+ * permits, and a caller sorting adversarial input should know it.
167
+ *
168
+ * ## Stability
169
+ *
170
+ * Stable, and that is load-bearing rather than incidental: callers rely on
171
+ * equal elements keeping their input order. Two rules preserve it. Descending
172
+ * runs are detected **strictly**, so equal neighbours are never part of a run
173
+ * that gets reversed; and the merge takes from the earlier run when its head
174
+ * compares equal. The counting pass also does not mutate, so an array handed
175
+ * back on the `undefined` path is untouched -- reversing runs first and then
176
+ * falling back would hand the caller's sort an array whose equal elements had
177
+ * already been reordered, which is how a stable sort quietly stops being one.
178
+ */
179
+ export declare function nativeSortIsAdaptive(): boolean;
180
+ /**
181
+ * Sorts `array` in place by merging its natural runs, and returns it.
182
+ *
183
+ * Returns `undefined`, leaving `array` **unmodified**, when it holds more than
184
+ * `maxRuns` runs -- the caller should then sort it however it otherwise would.
185
+ * `maxRuns` defaults to `sqrt(array.length)`, floored at 32; see "Where the
186
+ * bound sits" above.
187
+ *
188
+ * `compare` must be a consistent comparator; results are stable.
189
+ */
190
+ export declare function sortByNaturalRuns<T>(array: T[], compare: (a: T, b: T) => number, maxRuns?: number): T[] | undefined;
191
+ //# sourceMappingURL=natural-runs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"natural-runs.d.ts","sourceRoot":"","sources":["../../../../shared/src/natural-runs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiLG;AAgDH,wBAAgB,oBAAoB,IAAI,OAAO,CAc9C;AAWD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,EAC/B,OAAO,SAA2B,GACjC,CAAC,EAAE,GAAG,SAAS,CAoJjB"}
@@ -0,0 +1,337 @@
1
+ //#region ../shared/src/natural-runs.ts
2
+ /**
3
+ * A stable sort for input that already arrives in a handful of ordered runs,
4
+ * for the engines whose built-in sort cannot exploit that.
5
+ *
6
+ * `Array.prototype.sort` is TimSort on V8, so already-sorted input costs N-1
7
+ * comparisons. **On Hermes it is not adaptive**, and the difference is not
8
+ * small. Sorting 100,000 objects through a two-column comparator:
9
+ *
10
+ * ```text
11
+ * Hermes 0.12 V8
12
+ * sorted 93.6ms / 1,483,630 0.8ms / 99,999
13
+ * reversed 168.6ms / 2,571,203 0.8ms / 99,999
14
+ * shuffled 134.8ms / 1,770,233 17.0ms / 1,529,373
15
+ * ```
16
+ *
17
+ * Hermes does fifteen times the necessary comparisons on sorted input, and
18
+ * reversed input is its *worst* case -- slower than random data, where an O(N)
19
+ * reverse would do. That matters because ordered input is the common case here
20
+ * and not a lucky one: rows come out of a source in primary-key order, and
21
+ * secondary sort keys are usually timestamps or sequence numbers that move with
22
+ * insertion order, so a secondary index is often built over rows that are
23
+ * already sorted, exactly reversed, or a few ordered batches concatenated.
24
+ *
25
+ * This merges the natural runs instead, which is O(N) on one run and
26
+ * O(N log R) on R of them. Measured against the same inputs:
27
+ *
28
+ * ```text
29
+ * Hermes native -> runs V8 native -> runs
30
+ * sorted 93.2ms -> 5.4ms (17x) 1.0ms -> 0.4ms
31
+ * reversed 166.6ms -> 6.6ms (25x) 1.0ms -> 0.4ms
32
+ * 20 runs 147.2ms -> 9.4ms (16x) 1.0ms -> 0.8ms
33
+ * ```
34
+ *
35
+ * ## Why it gives up rather than always merging
36
+ *
37
+ * A full TimSort would handle near-sorted input too, but without galloping it
38
+ * loses badly to V8's native sort there (8.0ms against 2.8ms on 100,000
39
+ * near-sorted rows, 1.26M comparisons against 268k), and galloping is a large
40
+ * amount of code to carry. So this returns `undefined` once the input has more
41
+ * runs than merging can pay for, and the caller falls back to whatever it would
42
+ * have done -- which keeps V8's galloping and costs Hermes nothing it was not
43
+ * already paying.
44
+ *
45
+ * ## Where the bound sits, and why it scales
46
+ *
47
+ * Merging R runs costs about `n * log2(R)` comparisons; a non-adaptive sort
48
+ * costs about `n * log2(n)` whatever the input looks like. So merging is worth
49
+ * it exactly while `log2(R)` is the smaller of the two, and `R <= sqrt(n)` is
50
+ * the region where it is at most half -- a bound that needs no fitted constant
51
+ * and tightens on its own as `n` grows. That is what `maxRuns` defaults to,
52
+ * floored at 32 so nothing below 1,024 elements changes.
53
+ *
54
+ * Read the other way round, that bound is a statement about the *input* rather
55
+ * than a tolerance for a number of runs: `R <= sqrt(n)` is the same as saying
56
+ * the average run is at least `sqrt(n)` long. Which is the useful form, because
57
+ * what makes merging pay is runs long enough to amortise a merge pass over, and
58
+ * that does not change when a data set grows. A fixed run count does: the
59
+ * bound was 32 until a table with 50 of something turned up, which is the
60
+ * failure mode this shape avoids.
61
+ *
62
+ * The floor used to be the whole bound, and a fixed 32 was too low to catch the
63
+ * shape this was written for. A secondary index built over a junction table
64
+ * whose primary key leads with the *other* column arrives as one ordered run
65
+ * per distinct value of that column: 50 runs of 1,200 rows over 60,000, for a
66
+ * table with 50 of them. Merging that is O(n log 50), but at `maxRuns` 32 it
67
+ * declined and Hermes fell through to the full non-adaptive sort this function
68
+ * exists to avoid.
69
+ *
70
+ * The bound is deliberately well inside the crossover, and comparisons are not
71
+ * the only cost: merging R runs is `ceil(log2(R))` passes, each writing every
72
+ * element, which an in-place sort does not do. So it was measured past the
73
+ * bound rather than up to it. Sorting 60,000 rows through a three-column
74
+ * comparator on an Android emulator, in a release build, medians of 3 repeats.
75
+ * The engine is Static Hermes 250829098.0.14 -- read out of the APK's
76
+ * `libhermesvm.so` rather than taken from the build config, because what React
77
+ * Native ships is not the engine `sdks/.hermesversion` pins. Hence
78
+ * `libhermesvm.so` and not `libhermes.so`: a different engine, not a later
79
+ * build of the one in the 0.12 table above.
80
+ *
81
+ * Which does not make that table stale, and it is worth being exact about what
82
+ * carries over. Its *comparison counts* reproduce on the shipped engine to the
83
+ * digit -- 1,483,630 sorted and 2,571,203 reversed at 100,000, and 741 at
84
+ * n = 128 -- so the two engines sort by the same algorithm, and the premise all
85
+ * of this rests on holds where it matters: `nativeSortIsAdaptive()` is false on
86
+ * the VM that actually runs, not merely on an old CLI. Its *times* do not carry
87
+ * over, in either direction: those were a standalone CLI on a development host,
88
+ * these are an emulator, and the shipped VM is about 2.3x faster than that CLI
89
+ * on the same host. Compare the counts, never the milliseconds.
90
+ *
91
+ * ```text
92
+ * runs native merge
93
+ * 2 517.9ms 23.0ms
94
+ * 50 218.3ms 82.0ms
95
+ * 100 221.6ms 90.3ms
96
+ * 244 202.8ms 101.4ms <- the bound at this size
97
+ * 400 210.7ms 110.9ms
98
+ * 1,000 199.4ms 121.5ms
99
+ * shuffled 221.0ms 207.3ms
100
+ * ```
101
+ *
102
+ * The extra passes do show up -- merging costs five times more at 1,000 runs
103
+ * than at 2 -- but on this engine it still wins everywhere measured, and only
104
+ * on shuffled input does the margin close to nothing. At the bound it is 2x,
105
+ * which is the margin the bound is chosen to keep rather than the point where
106
+ * merging stops paying; a higher one would capture more, and no workload here
107
+ * calls for it. Note also that two ordered runs are the native sort's *worst*
108
+ * case (517.9ms, worse than shuffled), which is the asymmetry this whole
109
+ * function exists to exploit.
110
+ *
111
+ * On V8, which never reaches this path, merging holds level to about 1,000 runs
112
+ * (23.1ms against 24.1ms) and loses past 5,000 and on shuffled input (78.6ms
113
+ * against 43.0ms). That bounds how far merging scales on an adaptive engine; it
114
+ * does not justify the bound, since no caller gets here on one.
115
+ *
116
+ * Declining is not free, and what bounds it is runs, not comparisons: counting
117
+ * stops once a `maxRuns + 1`th run is known to exist, before walking that one,
118
+ * but every run before it is walked to its end. Measured over 100,000 elements
119
+ * with `maxRuns` at 32, against the comparisons the native sort then does:
120
+ *
121
+ * ```text
122
+ * shuffled 76 comparisons 0.005%
123
+ * 32 long runs, then more 80,000 comparisons 15%
124
+ * ```
125
+ *
126
+ * Raising the bound raises what a decline can cost, and the shape that pays is
127
+ * an input whose run count lands just past it. Over 60,000 elements in equal
128
+ * ordered runs, comparisons spent before declining:
129
+ *
130
+ * ```text
131
+ * runs at bound 32 at bound 244
132
+ * 245 7,840 59,780
133
+ * 400 4,800 36,600
134
+ * 1,000 1,920 14,640
135
+ * ```
136
+ *
137
+ * So a 245-run input now scans essentially the whole array where it used to
138
+ * stop after an eighth of it. What does *not* change is the ceiling: the worst
139
+ * case is `maxRuns` long runs followed by anything at all, and that walks
140
+ * almost every element at either bound -- 60,000 comparisons at 32 against
141
+ * 59,780 at 244. The old bound never bounded the scan below `n`; it only made
142
+ * fewer shapes reach it. Against the non-adaptive sort a decline falls into,
143
+ * which costs about 14.8 comparisons per element on the engine that gets here,
144
+ * a full scan is around 7% on top. (Measured as a fraction of V8's adaptive
145
+ * sort it looks like 17-19%, but nothing reaches this path on V8.)
146
+ *
147
+ * A scan budget cannot be separated from the merge budget without giving up
148
+ * merges that would have won: `R` is only known to exceed the budget once
149
+ * budget-many runs have been seen, and merging `R` runs has to scan all `n`
150
+ * elements regardless. Any limit tight enough to cut a decline short of `n`
151
+ * also rejects inputs just inside the bound, which is the case this change
152
+ * exists to serve.
153
+ *
154
+ * One interaction is worth naming because this bound makes it bigger. The
155
+ * caller in `MemorySource` tries this before its specialised numeric-column
156
+ * pass, so an ordering that the numeric pass could have handled may now pay a
157
+ * scan of up to `n` before taking it anyway. Bounded, and not a shape seen in
158
+ * the ten index builds of a cold pass -- the nine that merge are under 32 runs
159
+ * and the one that declines is string-keyed -- but the chain order is worth
160
+ * revisiting on its own measurement rather than inside a bound change.
161
+ *
162
+ * Of the two rows in the first table, shuffled is the shape that declines in
163
+ * practice, and it gives up almost at once because its runs are two or three
164
+ * elements long. The other is the honest worst case, where the scan walks most
165
+ * of the array before handing it back. Nothing in the
166
+ * workloads this was built for looks like that, but it is what the bound
167
+ * permits, and a caller sorting adversarial input should know it.
168
+ *
169
+ * ## Stability
170
+ *
171
+ * Stable, and that is load-bearing rather than incidental: callers rely on
172
+ * equal elements keeping their input order. Two rules preserve it. Descending
173
+ * runs are detected **strictly**, so equal neighbours are never part of a run
174
+ * that gets reversed; and the merge takes from the earlier run when its head
175
+ * compares equal. The counting pass also does not mutate, so an array handed
176
+ * back on the `undefined` path is untouched -- reversing runs first and then
177
+ * falling back would hand the caller's sort an array whose equal elements had
178
+ * already been reordered, which is how a stable sort quietly stops being one.
179
+ */
180
+ /**
181
+ * Whether this engine's `Array.prototype.sort` already exploits ordered input,
182
+ * measured rather than guessed from the user agent.
183
+ *
184
+ * Sorting an already-sorted array of `PROBE_SIZE` elements takes exactly
185
+ * `n - 1` comparisons on an adaptive sort and several times that otherwise.
186
+ * Measured at `n = 128`: V8 does 127, Hermes does 741. The threshold below sits
187
+ * between them with a wide margin at every size either engine was tried at.
188
+ *
189
+ * This exists so `sortByNaturalRuns` can be skipped where it cannot win. On V8
190
+ * the native sort is already optimal on ordered input, so run detection only
191
+ * adds a JS pass over data a faster native pass would have walked anyway --
192
+ * measured at a 6% regression on cold-start hydration when applied there
193
+ * unconditionally.
194
+ *
195
+ * The probe runs once, costs a few hundred comparisons, and is deliberately not
196
+ * consulted by `sortByNaturalRuns` itself: callers decide, so the sort stays a
197
+ * pure function that can be tested on whichever engine the tests run on.
198
+ *
199
+ * ## Why this is measured at runtime rather than compiled away
200
+ *
201
+ * The obvious alternative is a build-time flag, so that only the React Native
202
+ * build carries this file at all -- 1,064 bytes minified, 590 gzipped. It was
203
+ * considered and rejected, because "React Native" is a proxy for "Hermes" and
204
+ * is wrong in both directions: React Native can run on JavaScriptCore, as it
205
+ * did by default for years, and Hermes is embedded outside React Native. A
206
+ * build keyed on the former would run the natural-run sort on an engine whose
207
+ * own sort is already adaptive, and doing that is not merely wasted work: it
208
+ * measured a 6.4% regression on cold-start hydration. The probe itself is not
209
+ * what costs anything -- a few hundred comparisons, once: 127 where the native
210
+ * sort is adaptive, 741 on Hermes, which is the one that pays most and the one
211
+ * that gets the 17x back.
212
+ *
213
+ * Measured, sorting 128 already-sorted elements: V8 and JavaScriptCore each do
214
+ * exactly `n - 1` comparisons, Hermes 741. SpiderMonkey was not measured, and
215
+ * that is the argument rather than a hole in it. A hand-maintained list of
216
+ * engines would have to be right about SpiderMonkey, about whatever ships next,
217
+ * and about every embedding of each -- and would be wrong silently. The probe
218
+ * is right about all of them without being told: if SpiderMonkey turns out not
219
+ * to be adaptive, Firefox gets the faster path automatically, and if it is, it
220
+ * never pays for one it does not need.
221
+ */
222
+ var PROBE_SIZE = 128;
223
+ var adaptive;
224
+ function nativeSortIsAdaptive() {
225
+ if (adaptive === void 0) {
226
+ const probe = [];
227
+ for (let i = 0; i < PROBE_SIZE; i++) probe.push(i);
228
+ let comparisons = 0;
229
+ probe.sort((a, b) => {
230
+ comparisons++;
231
+ return a - b;
232
+ });
233
+ adaptive = comparisons < 256;
234
+ }
235
+ return adaptive;
236
+ }
237
+ /**
238
+ * The default run budget for an array of `n` elements: `sqrt(n)`, floored at
239
+ * 32. Integral, because the scan compares its run count against it for
240
+ * equality.
241
+ */
242
+ function maxRunsFor(n) {
243
+ return Math.max(32, Math.floor(Math.sqrt(n)));
244
+ }
245
+ /**
246
+ * Sorts `array` in place by merging its natural runs, and returns it.
247
+ *
248
+ * Returns `undefined`, leaving `array` **unmodified**, when it holds more than
249
+ * `maxRuns` runs -- the caller should then sort it however it otherwise would.
250
+ * `maxRuns` defaults to `sqrt(array.length)`, floored at 32; see "Where the
251
+ * bound sits" above.
252
+ *
253
+ * `compare` must be a consistent comparator; results are stable.
254
+ */
255
+ function sortByNaturalRuns(array, compare, maxRuns = maxRunsFor(array.length)) {
256
+ const n = array.length;
257
+ if (n < 2) return array;
258
+ const starts = [];
259
+ const descending = [];
260
+ let i = 0;
261
+ while (i < n) {
262
+ if (starts.length >= maxRuns) return;
263
+ const start = i;
264
+ let isDescending = false;
265
+ if (i + 1 < n) {
266
+ const first = compare(array[i + 1], array[i]);
267
+ if (first !== first) return;
268
+ isDescending = first < 0;
269
+ i += 2;
270
+ if (isDescending) while (i < n) {
271
+ const c = compare(array[i], array[i - 1]);
272
+ if (c !== c) return;
273
+ if (c >= 0) break;
274
+ i++;
275
+ }
276
+ else while (i < n) {
277
+ const c = compare(array[i], array[i - 1]);
278
+ if (c !== c) return;
279
+ if (c < 0) break;
280
+ i++;
281
+ }
282
+ } else i++;
283
+ starts.push(start);
284
+ descending.push(isDescending);
285
+ }
286
+ starts.push(n);
287
+ let runCount = descending.length;
288
+ if (runCount === 1) {
289
+ if (descending[0]) reverse(array, 0, n - 1);
290
+ return array;
291
+ }
292
+ let src = array.slice();
293
+ let dst = new Array(n);
294
+ let bounds = starts;
295
+ for (let r = 0; r < runCount; r++) if (descending[r]) reverse(src, starts[r], starts[r + 1] - 1);
296
+ while (runCount > 1) {
297
+ const nextBounds = [0];
298
+ let write = 0;
299
+ let merged = 0;
300
+ for (let p = 0; p < runCount; p += 2) {
301
+ let a = bounds[p];
302
+ const endA = bounds[p + 1];
303
+ if (p + 1 >= runCount) while (a < endA) dst[write++] = src[a++];
304
+ else {
305
+ let b = bounds[p + 1];
306
+ const endB = bounds[p + 2];
307
+ while (a < endA && b < endB) {
308
+ const c = compare(src[b], src[a]);
309
+ if (c !== c) return;
310
+ dst[write++] = c < 0 ? src[b++] : src[a++];
311
+ }
312
+ while (a < endA) dst[write++] = src[a++];
313
+ while (b < endB) dst[write++] = src[b++];
314
+ }
315
+ merged++;
316
+ nextBounds.push(write);
317
+ }
318
+ const swap = src;
319
+ src = dst;
320
+ dst = swap;
321
+ bounds = nextBounds;
322
+ runCount = merged;
323
+ }
324
+ for (let c = 0; c < n; c++) array[c] = src[c];
325
+ return array;
326
+ }
327
+ function reverse(array, lo, hi) {
328
+ while (lo < hi) {
329
+ const t = array[lo];
330
+ array[lo++] = array[hi];
331
+ array[hi--] = t;
332
+ }
333
+ }
334
+ //#endregion
335
+ export { nativeSortIsAdaptive, sortByNaturalRuns };
336
+
337
+ //# sourceMappingURL=natural-runs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"natural-runs.js","names":[],"sources":["../../../../shared/src/natural-runs.ts"],"sourcesContent":["/**\n * A stable sort for input that already arrives in a handful of ordered runs,\n * for the engines whose built-in sort cannot exploit that.\n *\n * `Array.prototype.sort` is TimSort on V8, so already-sorted input costs N-1\n * comparisons. **On Hermes it is not adaptive**, and the difference is not\n * small. Sorting 100,000 objects through a two-column comparator:\n *\n * ```text\n * Hermes 0.12 V8\n * sorted 93.6ms / 1,483,630 0.8ms / 99,999\n * reversed 168.6ms / 2,571,203 0.8ms / 99,999\n * shuffled 134.8ms / 1,770,233 17.0ms / 1,529,373\n * ```\n *\n * Hermes does fifteen times the necessary comparisons on sorted input, and\n * reversed input is its *worst* case -- slower than random data, where an O(N)\n * reverse would do. That matters because ordered input is the common case here\n * and not a lucky one: rows come out of a source in primary-key order, and\n * secondary sort keys are usually timestamps or sequence numbers that move with\n * insertion order, so a secondary index is often built over rows that are\n * already sorted, exactly reversed, or a few ordered batches concatenated.\n *\n * This merges the natural runs instead, which is O(N) on one run and\n * O(N log R) on R of them. Measured against the same inputs:\n *\n * ```text\n * Hermes native -> runs V8 native -> runs\n * sorted 93.2ms -> 5.4ms (17x) 1.0ms -> 0.4ms\n * reversed 166.6ms -> 6.6ms (25x) 1.0ms -> 0.4ms\n * 20 runs 147.2ms -> 9.4ms (16x) 1.0ms -> 0.8ms\n * ```\n *\n * ## Why it gives up rather than always merging\n *\n * A full TimSort would handle near-sorted input too, but without galloping it\n * loses badly to V8's native sort there (8.0ms against 2.8ms on 100,000\n * near-sorted rows, 1.26M comparisons against 268k), and galloping is a large\n * amount of code to carry. So this returns `undefined` once the input has more\n * runs than merging can pay for, and the caller falls back to whatever it would\n * have done -- which keeps V8's galloping and costs Hermes nothing it was not\n * already paying.\n *\n * ## Where the bound sits, and why it scales\n *\n * Merging R runs costs about `n * log2(R)` comparisons; a non-adaptive sort\n * costs about `n * log2(n)` whatever the input looks like. So merging is worth\n * it exactly while `log2(R)` is the smaller of the two, and `R <= sqrt(n)` is\n * the region where it is at most half -- a bound that needs no fitted constant\n * and tightens on its own as `n` grows. That is what `maxRuns` defaults to,\n * floored at 32 so nothing below 1,024 elements changes.\n *\n * Read the other way round, that bound is a statement about the *input* rather\n * than a tolerance for a number of runs: `R <= sqrt(n)` is the same as saying\n * the average run is at least `sqrt(n)` long. Which is the useful form, because\n * what makes merging pay is runs long enough to amortise a merge pass over, and\n * that does not change when a data set grows. A fixed run count does: the\n * bound was 32 until a table with 50 of something turned up, which is the\n * failure mode this shape avoids.\n *\n * The floor used to be the whole bound, and a fixed 32 was too low to catch the\n * shape this was written for. A secondary index built over a junction table\n * whose primary key leads with the *other* column arrives as one ordered run\n * per distinct value of that column: 50 runs of 1,200 rows over 60,000, for a\n * table with 50 of them. Merging that is O(n log 50), but at `maxRuns` 32 it\n * declined and Hermes fell through to the full non-adaptive sort this function\n * exists to avoid.\n *\n * The bound is deliberately well inside the crossover, and comparisons are not\n * the only cost: merging R runs is `ceil(log2(R))` passes, each writing every\n * element, which an in-place sort does not do. So it was measured past the\n * bound rather than up to it. Sorting 60,000 rows through a three-column\n * comparator on an Android emulator, in a release build, medians of 3 repeats.\n * The engine is Static Hermes 250829098.0.14 -- read out of the APK's\n * `libhermesvm.so` rather than taken from the build config, because what React\n * Native ships is not the engine `sdks/.hermesversion` pins. Hence\n * `libhermesvm.so` and not `libhermes.so`: a different engine, not a later\n * build of the one in the 0.12 table above.\n *\n * Which does not make that table stale, and it is worth being exact about what\n * carries over. Its *comparison counts* reproduce on the shipped engine to the\n * digit -- 1,483,630 sorted and 2,571,203 reversed at 100,000, and 741 at\n * n = 128 -- so the two engines sort by the same algorithm, and the premise all\n * of this rests on holds where it matters: `nativeSortIsAdaptive()` is false on\n * the VM that actually runs, not merely on an old CLI. Its *times* do not carry\n * over, in either direction: those were a standalone CLI on a development host,\n * these are an emulator, and the shipped VM is about 2.3x faster than that CLI\n * on the same host. Compare the counts, never the milliseconds.\n *\n * ```text\n * runs native merge\n * 2 517.9ms 23.0ms\n * 50 218.3ms 82.0ms\n * 100 221.6ms 90.3ms\n * 244 202.8ms 101.4ms <- the bound at this size\n * 400 210.7ms 110.9ms\n * 1,000 199.4ms 121.5ms\n * shuffled 221.0ms 207.3ms\n * ```\n *\n * The extra passes do show up -- merging costs five times more at 1,000 runs\n * than at 2 -- but on this engine it still wins everywhere measured, and only\n * on shuffled input does the margin close to nothing. At the bound it is 2x,\n * which is the margin the bound is chosen to keep rather than the point where\n * merging stops paying; a higher one would capture more, and no workload here\n * calls for it. Note also that two ordered runs are the native sort's *worst*\n * case (517.9ms, worse than shuffled), which is the asymmetry this whole\n * function exists to exploit.\n *\n * On V8, which never reaches this path, merging holds level to about 1,000 runs\n * (23.1ms against 24.1ms) and loses past 5,000 and on shuffled input (78.6ms\n * against 43.0ms). That bounds how far merging scales on an adaptive engine; it\n * does not justify the bound, since no caller gets here on one.\n *\n * Declining is not free, and what bounds it is runs, not comparisons: counting\n * stops once a `maxRuns + 1`th run is known to exist, before walking that one,\n * but every run before it is walked to its end. Measured over 100,000 elements\n * with `maxRuns` at 32, against the comparisons the native sort then does:\n *\n * ```text\n * shuffled 76 comparisons 0.005%\n * 32 long runs, then more 80,000 comparisons 15%\n * ```\n *\n * Raising the bound raises what a decline can cost, and the shape that pays is\n * an input whose run count lands just past it. Over 60,000 elements in equal\n * ordered runs, comparisons spent before declining:\n *\n * ```text\n * runs at bound 32 at bound 244\n * 245 7,840 59,780\n * 400 4,800 36,600\n * 1,000 1,920 14,640\n * ```\n *\n * So a 245-run input now scans essentially the whole array where it used to\n * stop after an eighth of it. What does *not* change is the ceiling: the worst\n * case is `maxRuns` long runs followed by anything at all, and that walks\n * almost every element at either bound -- 60,000 comparisons at 32 against\n * 59,780 at 244. The old bound never bounded the scan below `n`; it only made\n * fewer shapes reach it. Against the non-adaptive sort a decline falls into,\n * which costs about 14.8 comparisons per element on the engine that gets here,\n * a full scan is around 7% on top. (Measured as a fraction of V8's adaptive\n * sort it looks like 17-19%, but nothing reaches this path on V8.)\n *\n * A scan budget cannot be separated from the merge budget without giving up\n * merges that would have won: `R` is only known to exceed the budget once\n * budget-many runs have been seen, and merging `R` runs has to scan all `n`\n * elements regardless. Any limit tight enough to cut a decline short of `n`\n * also rejects inputs just inside the bound, which is the case this change\n * exists to serve.\n *\n * One interaction is worth naming because this bound makes it bigger. The\n * caller in `MemorySource` tries this before its specialised numeric-column\n * pass, so an ordering that the numeric pass could have handled may now pay a\n * scan of up to `n` before taking it anyway. Bounded, and not a shape seen in\n * the ten index builds of a cold pass -- the nine that merge are under 32 runs\n * and the one that declines is string-keyed -- but the chain order is worth\n * revisiting on its own measurement rather than inside a bound change.\n *\n * Of the two rows in the first table, shuffled is the shape that declines in\n * practice, and it gives up almost at once because its runs are two or three\n * elements long. The other is the honest worst case, where the scan walks most\n * of the array before handing it back. Nothing in the\n * workloads this was built for looks like that, but it is what the bound\n * permits, and a caller sorting adversarial input should know it.\n *\n * ## Stability\n *\n * Stable, and that is load-bearing rather than incidental: callers rely on\n * equal elements keeping their input order. Two rules preserve it. Descending\n * runs are detected **strictly**, so equal neighbours are never part of a run\n * that gets reversed; and the merge takes from the earlier run when its head\n * compares equal. The counting pass also does not mutate, so an array handed\n * back on the `undefined` path is untouched -- reversing runs first and then\n * falling back would hand the caller's sort an array whose equal elements had\n * already been reordered, which is how a stable sort quietly stops being one.\n */\n\n/**\n * Whether this engine's `Array.prototype.sort` already exploits ordered input,\n * measured rather than guessed from the user agent.\n *\n * Sorting an already-sorted array of `PROBE_SIZE` elements takes exactly\n * `n - 1` comparisons on an adaptive sort and several times that otherwise.\n * Measured at `n = 128`: V8 does 127, Hermes does 741. The threshold below sits\n * between them with a wide margin at every size either engine was tried at.\n *\n * This exists so `sortByNaturalRuns` can be skipped where it cannot win. On V8\n * the native sort is already optimal on ordered input, so run detection only\n * adds a JS pass over data a faster native pass would have walked anyway --\n * measured at a 6% regression on cold-start hydration when applied there\n * unconditionally.\n *\n * The probe runs once, costs a few hundred comparisons, and is deliberately not\n * consulted by `sortByNaturalRuns` itself: callers decide, so the sort stays a\n * pure function that can be tested on whichever engine the tests run on.\n *\n * ## Why this is measured at runtime rather than compiled away\n *\n * The obvious alternative is a build-time flag, so that only the React Native\n * build carries this file at all -- 1,064 bytes minified, 590 gzipped. It was\n * considered and rejected, because \"React Native\" is a proxy for \"Hermes\" and\n * is wrong in both directions: React Native can run on JavaScriptCore, as it\n * did by default for years, and Hermes is embedded outside React Native. A\n * build keyed on the former would run the natural-run sort on an engine whose\n * own sort is already adaptive, and doing that is not merely wasted work: it\n * measured a 6.4% regression on cold-start hydration. The probe itself is not\n * what costs anything -- a few hundred comparisons, once: 127 where the native\n * sort is adaptive, 741 on Hermes, which is the one that pays most and the one\n * that gets the 17x back.\n *\n * Measured, sorting 128 already-sorted elements: V8 and JavaScriptCore each do\n * exactly `n - 1` comparisons, Hermes 741. SpiderMonkey was not measured, and\n * that is the argument rather than a hole in it. A hand-maintained list of\n * engines would have to be right about SpiderMonkey, about whatever ships next,\n * and about every embedding of each -- and would be wrong silently. The probe\n * is right about all of them without being told: if SpiderMonkey turns out not\n * to be adaptive, Firefox gets the faster path automatically, and if it is, it\n * never pays for one it does not need.\n */\nconst PROBE_SIZE = 128;\n\nlet adaptive: boolean | undefined;\n\nexport function nativeSortIsAdaptive(): boolean {\n if (adaptive === undefined) {\n const probe: number[] = [];\n for (let i = 0; i < PROBE_SIZE; i++) {\n probe.push(i);\n }\n let comparisons = 0;\n probe.sort((a, b) => {\n comparisons++;\n return a - b;\n });\n adaptive = comparisons < PROBE_SIZE * 2;\n }\n return adaptive;\n}\n\n/**\n * The default run budget for an array of `n` elements: `sqrt(n)`, floored at\n * 32. Integral, because the scan compares its run count against it for\n * equality.\n */\nfunction maxRunsFor(n: number): number {\n return Math.max(32, Math.floor(Math.sqrt(n)));\n}\n\n/**\n * Sorts `array` in place by merging its natural runs, and returns it.\n *\n * Returns `undefined`, leaving `array` **unmodified**, when it holds more than\n * `maxRuns` runs -- the caller should then sort it however it otherwise would.\n * `maxRuns` defaults to `sqrt(array.length)`, floored at 32; see \"Where the\n * bound sits\" above.\n *\n * `compare` must be a consistent comparator; results are stable.\n */\nexport function sortByNaturalRuns<T>(\n array: T[],\n compare: (a: T, b: T) => number,\n maxRuns = maxRunsFor(array.length),\n): T[] | undefined {\n const n = array.length;\n if (n < 2) {\n return array;\n }\n\n // Pass one, which must not mutate: where each run starts, and whether it is\n // descending and so needs reversing before it can be merged.\n const starts: number[] = [];\n const descending: boolean[] = [];\n let i = 0;\n while (i < n) {\n // Stop before walking another run once `maxRuns` are already in hand and\n // input is left: this call cannot succeed now, and the next run could be\n // the whole remainder of the array.\n if (starts.length >= maxRuns) {\n return undefined;\n }\n const start = i;\n let isDescending = false;\n if (i + 1 < n) {\n const first = compare(array[i + 1], array[i]);\n // `c !== c` is a NaN test without a call. A comparator that returns NaN\n // is not an ordering -- it is not even antisymmetric -- so runs and\n // merges are meaningless and, worse, would silently disagree with the\n // caller's own sort: `compareValues` yields NaN for a NaN sort key, and\n // for the values `[1, NaN, 0]` a native sort leaves them alone while\n // merging produces `[0, 1, NaN]`. Decline and let the caller's sort\n // decide, so whatever it does today it keeps doing.\n //\n // This catches a NaN only where this pass happens to compare the key\n // holding it. A multi-column comparator short-circuits on the first\n // differing column, so a NaN in a later column stays invisible here and\n // first surfaces during the merge, which compares rows this pass never\n // put side by side. The merge checks too, and the merge path works on a\n // copy so that it can still decline when it does.\n if (first !== first) {\n return undefined;\n }\n isDescending = first < 0;\n i += 2;\n if (isDescending) {\n // Strictly descending: a run containing equal neighbours must not be\n // reversed, or their relative order would flip.\n while (i < n) {\n const c = compare(array[i], array[i - 1]);\n if (c !== c) {\n return undefined;\n }\n if (c >= 0) {\n break;\n }\n i++;\n }\n } else {\n while (i < n) {\n const c = compare(array[i], array[i - 1]);\n if (c !== c) {\n return undefined;\n }\n if (c < 0) {\n break;\n }\n i++;\n }\n }\n } else {\n i++;\n }\n starts.push(start);\n descending.push(isDescending);\n }\n starts.push(n);\n\n let runCount = descending.length;\n if (runCount === 1) {\n // One run means no merge, so no comparison beyond the ones already made:\n // nothing further can surface a NaN, and there is nothing to undo. This is\n // also the case worth the most -- already-sorted and exactly-reversed input\n // -- so it stays free of the copy the merge path needs.\n if (descending[0]) {\n reverse(array, 0, n - 1);\n }\n return array;\n }\n\n // From here the merge compares rows the counting pass never put side by side,\n // which is where a NaN in a later ordering column can first appear. Declining\n // at that point has to leave `array` exactly as it was, so the work happens on\n // a copy and is written back only once it has succeeded.\n let src = array.slice();\n // Pre-sized on purpose: it is filled by index below, and `Array.from` would\n // allocate and then fill it twice.\n // oxlint-disable-next-line unicorn/no-new-array\n let dst = new Array<T>(n);\n let bounds = starts;\n\n for (let r = 0; r < runCount; r++) {\n if (descending[r]) {\n reverse(src, starts[r], starts[r + 1] - 1);\n }\n }\n while (runCount > 1) {\n const nextBounds: number[] = [0];\n let write = 0;\n let merged = 0;\n for (let p = 0; p < runCount; p += 2) {\n let a = bounds[p];\n const endA = bounds[p + 1];\n if (p + 1 >= runCount) {\n while (a < endA) {\n dst[write++] = src[a++];\n }\n } else {\n let b = bounds[p + 1];\n const endB = bounds[p + 2];\n while (a < endA && b < endB) {\n const c = compare(src[b], src[a]);\n if (c !== c) {\n // A NaN the counting pass could not see. `array` has not been\n // written, so handing it back untouched is still possible.\n return undefined;\n }\n // `< 0` and not `<= 0`: on a tie the earlier run wins, which is what\n // keeps this stable.\n dst[write++] = c < 0 ? src[b++] : src[a++];\n }\n while (a < endA) {\n dst[write++] = src[a++];\n }\n while (b < endB) {\n dst[write++] = src[b++];\n }\n }\n merged++;\n nextBounds.push(write);\n }\n const swap = src;\n src = dst;\n dst = swap;\n bounds = nextBounds;\n runCount = merged;\n }\n\n for (let c = 0; c < n; c++) {\n array[c] = src[c];\n }\n return array;\n}\n\nfunction reverse<T>(array: T[], lo: number, hi: number): void {\n while (lo < hi) {\n const t = array[lo];\n array[lo++] = array[hi];\n array[hi--] = t;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6NA,IAAM,aAAa;AAEnB,IAAI;AAEJ,SAAgB,uBAAgC;CAC9C,IAAI,aAAa,KAAA,GAAW;EAC1B,MAAM,QAAkB,CAAC;EACzB,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAC9B,MAAM,KAAK,CAAC;EAEd,IAAI,cAAc;EAClB,MAAM,MAAM,GAAG,MAAM;GACnB;GACA,OAAO,IAAI;EACb,CAAC;EACD,WAAW,cAAc;CAC3B;CACA,OAAO;AACT;;;;;;AAOA,SAAS,WAAW,GAAmB;CACrC,OAAO,KAAK,IAAI,IAAI,KAAK,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC;AAC9C;;;;;;;;;;;AAYA,SAAgB,kBACd,OACA,SACA,UAAU,WAAW,MAAM,MAAM,GAChB;CACjB,MAAM,IAAI,MAAM;CAChB,IAAI,IAAI,GACN,OAAO;CAKT,MAAM,SAAmB,CAAC;CAC1B,MAAM,aAAwB,CAAC;CAC/B,IAAI,IAAI;CACR,OAAO,IAAI,GAAG;EAIZ,IAAI,OAAO,UAAU,SACnB;EAEF,MAAM,QAAQ;EACd,IAAI,eAAe;EACnB,IAAI,IAAI,IAAI,GAAG;GACb,MAAM,QAAQ,QAAQ,MAAM,IAAI,IAAI,MAAM,EAAE;GAe5C,IAAI,UAAU,OACZ;GAEF,eAAe,QAAQ;GACvB,KAAK;GACL,IAAI,cAGF,OAAO,IAAI,GAAG;IACZ,MAAM,IAAI,QAAQ,MAAM,IAAI,MAAM,IAAI,EAAE;IACxC,IAAI,MAAM,GACR;IAEF,IAAI,KAAK,GACP;IAEF;GACF;QAEA,OAAO,IAAI,GAAG;IACZ,MAAM,IAAI,QAAQ,MAAM,IAAI,MAAM,IAAI,EAAE;IACxC,IAAI,MAAM,GACR;IAEF,IAAI,IAAI,GACN;IAEF;GACF;EAEJ,OACE;EAEF,OAAO,KAAK,KAAK;EACjB,WAAW,KAAK,YAAY;CAC9B;CACA,OAAO,KAAK,CAAC;CAEb,IAAI,WAAW,WAAW;CAC1B,IAAI,aAAa,GAAG;EAKlB,IAAI,WAAW,IACb,QAAQ,OAAO,GAAG,IAAI,CAAC;EAEzB,OAAO;CACT;CAMA,IAAI,MAAM,MAAM,MAAM;CAItB,IAAI,MAAM,IAAI,MAAS,CAAC;CACxB,IAAI,SAAS;CAEb,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAC5B,IAAI,WAAW,IACb,QAAQ,KAAK,OAAO,IAAI,OAAO,IAAI,KAAK,CAAC;CAG7C,OAAO,WAAW,GAAG;EACnB,MAAM,aAAuB,CAAC,CAAC;EAC/B,IAAI,QAAQ;EACZ,IAAI,SAAS;EACb,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAAK,GAAG;GACpC,IAAI,IAAI,OAAO;GACf,MAAM,OAAO,OAAO,IAAI;GACxB,IAAI,IAAI,KAAK,UACX,OAAO,IAAI,MACT,IAAI,WAAW,IAAI;QAEhB;IACL,IAAI,IAAI,OAAO,IAAI;IACnB,MAAM,OAAO,OAAO,IAAI;IACxB,OAAO,IAAI,QAAQ,IAAI,MAAM;KAC3B,MAAM,IAAI,QAAQ,IAAI,IAAI,IAAI,EAAE;KAChC,IAAI,MAAM,GAGR;KAIF,IAAI,WAAW,IAAI,IAAI,IAAI,OAAO,IAAI;IACxC;IACA,OAAO,IAAI,MACT,IAAI,WAAW,IAAI;IAErB,OAAO,IAAI,MACT,IAAI,WAAW,IAAI;GAEvB;GACA;GACA,WAAW,KAAK,KAAK;EACvB;EACA,MAAM,OAAO;EACb,MAAM;EACN,MAAM;EACN,SAAS;EACT,WAAW;CACb;CAEA,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,MAAM,KAAK,IAAI;CAEjB,OAAO;AACT;AAEA,SAAS,QAAW,OAAY,IAAY,IAAkB;CAC5D,OAAO,KAAK,IAAI;EACd,MAAM,IAAI,MAAM;EAChB,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ;CAChB;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"random-uint64.d.ts","sourceRoot":"","sources":["../../../../shared/src/random-uint64.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,IAAI,MAAM,CAOrC"}
1
+ {"version":3,"file":"random-uint64.d.ts","sourceRoot":"","sources":["../../../../shared/src/random-uint64.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,IAAI,MAAM,CAKrC"}
@@ -1,7 +1,7 @@
1
+ import { getRandomValues } from "./random-values.js";
1
2
  //#region ../shared/src/random-uint64.ts
2
3
  function randomUint64() {
3
- const high = Math.floor(Math.random() * 4294967295);
4
- const low = Math.floor(Math.random() * 4294967295);
4
+ const [high, low] = getRandomValues(/* @__PURE__ */ new Uint32Array(2));
5
5
  return BigInt(high) << 32n | BigInt(low);
6
6
  }
7
7
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"random-uint64.js","names":[],"sources":["../../../../shared/src/random-uint64.ts"],"sourcesContent":["export function randomUint64(): bigint {\n // Generate two random 32-bit unsigned integers using Math.random()\n const high = Math.floor(Math.random() * 0xffffffff); // High 32 bits\n const low = Math.floor(Math.random() * 0xffffffff); // Low 32 bits\n\n // Combine the high and low parts to form a 64-bit unsigned integer\n return (BigInt(high) << 32n) | BigInt(low);\n}\n"],"mappings":";AAAA,SAAgB,eAAuB;CAErC,MAAM,OAAO,KAAK,MAAM,KAAK,OAAO,IAAI,UAAU;CAClD,MAAM,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,UAAU;CAGjD,OAAQ,OAAO,IAAI,KAAK,MAAO,OAAO,GAAG;AAC3C"}
1
+ {"version":3,"file":"random-uint64.js","names":[],"sources":["../../../../shared/src/random-uint64.ts"],"sourcesContent":["import {getRandomValues} from './random-values.ts';\n\nexport function randomUint64(): bigint {\n // Two random 32-bit words rather than a BigUint64Array, which Hermes did\n // not have as of September 2024. The result is the same either way.\n const [high, low] = getRandomValues(new Uint32Array(2));\n return (BigInt(high) << 32n) | BigInt(low);\n}\n"],"mappings":";;AAEA,SAAgB,eAAuB;CAGrC,MAAM,CAAC,MAAM,OAAO,gCAAgB,IAAI,YAAY,CAAC,CAAC;CACtD,OAAQ,OAAO,IAAI,KAAK,MAAO,OAAO,GAAG;AAC3C"}
@@ -1,3 +1,21 @@
1
- export declare function getNonCryptoRandomValues(array: Uint8Array): Uint8Array<ArrayBufferLike>;
1
+ type RandomArray = Uint8Array<ArrayBuffer> | Uint32Array<ArrayBuffer>;
2
+ /**
3
+ * Fills `array` with random values. Uses `crypto.getRandomValues` when the
4
+ * runtime provides it, then the native `ExpoCrypto` module when running in an
5
+ * Expo app, and falls back to `Math.random` otherwise, logging a warning the
6
+ * first time the fallback is used.
7
+ *
8
+ * React Native's Hermes engine has no WebCrypto. Expo installs its native
9
+ * modules on `globalThis.expo.modules`, and `ExpoCrypto` is there in Expo Go
10
+ * and in any app that has `expo-crypto` installed, even if the app never
11
+ * imports it. Other React Native apps should import a polyfill such as
12
+ * `react-native-get-random-values` before creating IDs.
13
+ *
14
+ * The globals are looked up on every call rather than at module load: some
15
+ * runtimes (e.g. Cloudflare Workers) only allow generating random values
16
+ * while handling a request.
17
+ */
18
+ export declare function getRandomValues<T extends RandomArray>(array: T): T;
2
19
  export declare function randomCharacters(length: number): string;
20
+ export {};
3
21
  //# sourceMappingURL=random-values.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"random-values.d.ts","sourceRoot":"","sources":["../../../../shared/src/random-values.ts"],"names":[],"mappings":"AAAA,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,+BAWzD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,UAW9C"}
1
+ {"version":3,"file":"random-values.d.ts","sourceRoot":"","sources":["../../../../shared/src/random-values.ts"],"names":[],"mappings":"AAEA,KAAK,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;AAgBtE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAuBlE;AAgBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,UAW9C"}