next-evolve 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +55 -1
  2. package/dist/_virtual/_rolldown/runtime.js +12 -0
  3. package/dist/aggregate/attribution.d.ts +40 -0
  4. package/dist/aggregate/attribution.js +115 -0
  5. package/dist/aggregate/run.d.ts +21 -0
  6. package/dist/aggregate/run.js +139 -0
  7. package/dist/cli/aggregate.d.ts +9 -0
  8. package/dist/cli/aggregate.js +104 -0
  9. package/dist/cli/args.d.ts +12 -0
  10. package/dist/cli/args.js +23 -0
  11. package/dist/cli/cloud.d.ts +41 -0
  12. package/dist/cli/cloud.js +74 -0
  13. package/dist/cli/connect.d.ts +8 -0
  14. package/dist/cli/connect.js +46 -0
  15. package/dist/cli/detect.d.ts +26 -0
  16. package/dist/cli/detect.js +97 -0
  17. package/dist/cli/doctor.d.ts +34 -0
  18. package/dist/cli/doctor.js +230 -0
  19. package/dist/cli/env-file.d.ts +4 -0
  20. package/dist/cli/env-file.js +15 -0
  21. package/dist/cli/files.d.ts +25 -0
  22. package/dist/cli/files.js +173 -0
  23. package/dist/cli/index.js +73 -32
  24. package/dist/cli/init.d.ts +26 -0
  25. package/dist/cli/init.js +240 -0
  26. package/dist/cli/load-app.d.ts +19 -0
  27. package/dist/cli/load-app.js +67 -0
  28. package/dist/cli/migrate.d.ts +7 -0
  29. package/dist/cli/migrate.js +30 -0
  30. package/dist/cli/pages.d.ts +17 -0
  31. package/dist/cli/pages.js +304 -0
  32. package/dist/cli/plan.d.ts +27 -0
  33. package/dist/cli/plan.js +129 -0
  34. package/dist/cli/pull.d.ts +13 -0
  35. package/dist/cli/pull.js +62 -0
  36. package/dist/cli/push.d.ts +14 -0
  37. package/dist/cli/push.js +33 -0
  38. package/dist/cloud/manifest.d.ts +2 -0
  39. package/dist/cloud/manifest.js +6 -2
  40. package/dist/core/assign.d.ts +13 -1
  41. package/dist/core/assign.js +25 -3
  42. package/dist/db/writer.d.ts +2 -0
  43. package/dist/db/writer.js +1 -1
  44. package/dist/instrumentation/index.d.ts +3 -2
  45. package/dist/instrumentation/index.js +17 -2
  46. package/dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/typescript.d.ts +11169 -0
  47. package/dist/proxy/index.d.ts +2 -2
  48. package/dist/proxy/index.js +1 -1
  49. package/dist/proxy/proxy.d.ts +7 -1
  50. package/dist/proxy/proxy.js +23 -6
  51. package/dist/react/render-bundle.d.ts +10 -4
  52. package/dist/react/render-bundle.js +34 -5
  53. package/dist/server/aggregate-route.d.ts +16 -0
  54. package/dist/server/aggregate-route.js +68 -0
  55. package/dist/server/collect.d.ts +16 -0
  56. package/dist/server/collect.js +34 -8
  57. package/dist/server/handler.d.ts +3 -2
  58. package/dist/server/handler.js +37 -4
  59. package/dist/server/sync-route.d.ts +28 -0
  60. package/dist/server/sync-route.js +100 -0
  61. package/dist/store/disk.d.ts +6 -0
  62. package/dist/store/disk.js +23 -0
  63. package/dist/store/fetch.d.ts +4 -0
  64. package/dist/store/fetch.js +36 -1
  65. package/dist/store/global-config.d.ts +39 -0
  66. package/dist/store/global-config.js +122 -0
  67. package/dist/store/memory.d.ts +11 -2
  68. package/dist/store/memory.js +6 -2
  69. package/dist/store/runtime.d.ts +14 -0
  70. package/dist/store/runtime.js +39 -7
  71. package/package.json +2 -1
@@ -15,6 +15,28 @@ function weightedPick(variants, r) {
15
15
  return last;
16
16
  }
17
17
  /**
18
+ * Deterministic draws for one visitor: the same `evolve_id` and experiment give
19
+ * the same assignment on every request, so concurrent first requests (a link
20
+ * prefetch and the click that follows) can't assign different variants.
21
+ * FNV-1a with a murmur3 finalizer; uniform enough for traffic splits, not crypto.
22
+ */
23
+ function visitorDraw(anonId) {
24
+ return (key) => {
25
+ let h = 2166136261;
26
+ const s = `${anonId}|${key}`;
27
+ for (let i = 0; i < s.length; i++) {
28
+ h ^= s.charCodeAt(i);
29
+ h = Math.imul(h, 16777619);
30
+ }
31
+ h ^= h >>> 16;
32
+ h = Math.imul(h, 2246822507);
33
+ h ^= h >>> 13;
34
+ h = Math.imul(h, 3266489909);
35
+ h ^= h >>> 16;
36
+ return (h >>> 0) / 4294967296;
37
+ };
38
+ }
39
+ /**
18
40
  * Assigns a visitor to the experiments on `path` (spec §6.3). Pure: no I/O.
19
41
  * Sticky: an active assignment is kept. Completed experiments render their
20
42
  * winner and aren't counted. Cookie entries for experiments no longer in the
@@ -56,7 +78,7 @@ function assign(bundle, path, cookie, rand) {
56
78
  continue;
57
79
  }
58
80
  changed = true;
59
- if (rand() >= e.traffic) {
81
+ if (rand(`${exp}:t`) >= e.traffic) {
60
82
  assigned.set(exp, "x");
61
83
  pairs.push({
62
84
  exp,
@@ -65,7 +87,7 @@ function assign(bundle, path, cookie, rand) {
65
87
  });
66
88
  continue;
67
89
  }
68
- const v = weightedPick(e.variants, rand());
90
+ const v = weightedPick(e.variants, rand(`${exp}:v`));
69
91
  assigned.set(exp, v);
70
92
  pairs.push({
71
93
  exp,
@@ -79,4 +101,4 @@ function assign(bundle, path, cookie, rand) {
79
101
  changed
80
102
  };
81
103
  }
82
- export { assign, weightedPick };
104
+ export { assign, visitorDraw, weightedPick };
@@ -5,6 +5,8 @@ export type ExposureRow = {
5
5
  anon_id: string;
6
6
  variant_id: string;
7
7
  path: string;
8
+ /** When the beacon arrived (same clock as event `ts`), not when the batch is written. */
9
+ first_at: Date;
8
10
  };
9
11
  export type EventRow = {
10
12
  ts: Date;
package/dist/db/writer.js CHANGED
@@ -46,7 +46,7 @@ function createWriter(opts) {
46
46
  }
47
47
  }
48
48
  async function write(sql, batch) {
49
- for (const rows of chunks(batch.exposures)) await sql`insert into evolve.exposures ${sql(rows, "experiment_id", "anon_id", "variant_id", "path")}
49
+ for (const rows of chunks(batch.exposures)) await sql`insert into evolve.exposures ${sql(rows, "experiment_id", "anon_id", "variant_id", "path", "first_at")}
50
50
  on conflict do nothing`;
51
51
  for (const rows of chunks(batch.events)) await sql`insert into evolve.events ${sql(rows.map((r) => ({
52
52
  ...r,
@@ -15,8 +15,9 @@ export type RegisterEvolveOptions = {
15
15
  * }
16
16
  *
17
17
  * Registers the goals for this process and pushes the manifest to the cloud
18
- * (goals, routes, versions, app URL; spec §8.4), once per process. Never
19
- * throws. Background polling and the disk cache arrive with T7.4.
18
+ * (goals, routes, versions, app URL; spec §8.4), once per process. On
19
+ * long-running Node it also polls the bundle in the background (spec §7.5),
20
+ * so the disk cache stays fresh between requests. Never throws.
20
21
  */
21
22
  export declare function registerEvolve(opts: RegisterEvolveOptions): Promise<void>;
22
23
  /** Test helper. */
@@ -1,8 +1,11 @@
1
1
  import { buildManifest, detectAppUrl } from "../cloud/manifest.js";
2
2
  import { pushToCloud } from "../cloud/push.js";
3
+ import { resolveConfig } from "../config/config.js";
3
4
  import { setRuntime } from "../server/runtime.js";
5
+ import { getBundleStore, isLongRunningNode } from "../store/runtime.js";
4
6
  // src/instrumentation/index.ts
5
7
  let registered = false;
8
+ let poll = null;
6
9
  /**
7
10
  * Call from `instrumentation.ts` `register()`:
8
11
  *
@@ -14,8 +17,9 @@ let registered = false;
14
17
  * }
15
18
  *
16
19
  * Registers the goals for this process and pushes the manifest to the cloud
17
- * (goals, routes, versions, app URL; spec §8.4), once per process. Never
18
- * throws. Background polling and the disk cache arrive with T7.4.
20
+ * (goals, routes, versions, app URL; spec §8.4), once per process. On
21
+ * long-running Node it also polls the bundle in the background (spec §7.5),
22
+ * so the disk cache stays fresh between requests. Never throws.
19
23
  */
20
24
  async function registerEvolve(opts) {
21
25
  if (registered) return;
@@ -25,6 +29,15 @@ async function registerEvolve(opts) {
25
29
  config: opts.config,
26
30
  goals: opts.goals
27
31
  });
32
+ if (isLongRunningNode()) {
33
+ const cfg = resolveConfig(opts.config);
34
+ const store = getBundleStore(cfg);
35
+ if (store) {
36
+ store.refresh();
37
+ poll = setInterval(() => void store.refresh(), cfg.pollSeconds * 1e3);
38
+ poll.unref();
39
+ }
40
+ }
28
41
  const manifest = buildManifest(opts.config, opts.goals, { appUrl: detectAppUrl() });
29
42
  const res = await pushToCloud("/v1/manifest", manifest);
30
43
  if (!res.ok && res.status !== "no_key") console.error(`[evolve] manifest push failed (${res.status})`);
@@ -35,5 +48,7 @@ async function registerEvolve(opts) {
35
48
  /** Test helper. */
36
49
  function resetRegisterEvolve() {
37
50
  registered = false;
51
+ if (poll) clearInterval(poll);
52
+ poll = null;
38
53
  }
39
54
  export { registerEvolve, resetRegisterEvolve };