rightsize 0.1.0

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 (216) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +255 -0
  4. package/dist/backend-docker/backend.d.ts +53 -0
  5. package/dist/backend-docker/backend.d.ts.map +1 -0
  6. package/dist/backend-docker/backend.js +336 -0
  7. package/dist/backend-docker/backend.js.map +1 -0
  8. package/dist/backend-docker/client.d.ts +82 -0
  9. package/dist/backend-docker/client.d.ts.map +1 -0
  10. package/dist/backend-docker/client.js +190 -0
  11. package/dist/backend-docker/client.js.map +1 -0
  12. package/dist/backend-docker/frames.d.ts +44 -0
  13. package/dist/backend-docker/frames.d.ts.map +1 -0
  14. package/dist/backend-docker/frames.js +105 -0
  15. package/dist/backend-docker/frames.js.map +1 -0
  16. package/dist/backend-docker/index.d.ts +5 -0
  17. package/dist/backend-docker/index.d.ts.map +1 -0
  18. package/dist/backend-docker/index.js +20 -0
  19. package/dist/backend-docker/index.js.map +1 -0
  20. package/dist/backend-docker/json.d.ts +13 -0
  21. package/dist/backend-docker/json.d.ts.map +1 -0
  22. package/dist/backend-docker/json.js +47 -0
  23. package/dist/backend-docker/json.js.map +1 -0
  24. package/dist/backend-docker/labels.d.ts +11 -0
  25. package/dist/backend-docker/labels.d.ts.map +1 -0
  26. package/dist/backend-docker/labels.js +13 -0
  27. package/dist/backend-docker/labels.js.map +1 -0
  28. package/dist/backend-docker/port-conflict.d.ts +9 -0
  29. package/dist/backend-docker/port-conflict.d.ts.map +1 -0
  30. package/dist/backend-docker/port-conflict.js +12 -0
  31. package/dist/backend-docker/port-conflict.js.map +1 -0
  32. package/dist/backend-docker/provider.d.ts +22 -0
  33. package/dist/backend-docker/provider.d.ts.map +1 -0
  34. package/dist/backend-docker/provider.js +36 -0
  35. package/dist/backend-docker/provider.js.map +1 -0
  36. package/dist/backend-msb/backend.d.ts +169 -0
  37. package/dist/backend-msb/backend.d.ts.map +1 -0
  38. package/dist/backend-msb/backend.js +664 -0
  39. package/dist/backend-msb/backend.js.map +1 -0
  40. package/dist/backend-msb/commands.d.ts +28 -0
  41. package/dist/backend-msb/commands.d.ts.map +1 -0
  42. package/dist/backend-msb/commands.js +67 -0
  43. package/dist/backend-msb/commands.js.map +1 -0
  44. package/dist/backend-msb/exec-tunnel.d.ts +33 -0
  45. package/dist/backend-msb/exec-tunnel.d.ts.map +1 -0
  46. package/dist/backend-msb/exec-tunnel.js +163 -0
  47. package/dist/backend-msb/exec-tunnel.js.map +1 -0
  48. package/dist/backend-msb/follow-replay.d.ts +14 -0
  49. package/dist/backend-msb/follow-replay.d.ts.map +1 -0
  50. package/dist/backend-msb/follow-replay.js +20 -0
  51. package/dist/backend-msb/follow-replay.js.map +1 -0
  52. package/dist/backend-msb/image-cache.d.ts +25 -0
  53. package/dist/backend-msb/image-cache.d.ts.map +1 -0
  54. package/dist/backend-msb/image-cache.js +27 -0
  55. package/dist/backend-msb/image-cache.js.map +1 -0
  56. package/dist/backend-msb/index.d.ts +6 -0
  57. package/dist/backend-msb/index.d.ts.map +1 -0
  58. package/dist/backend-msb/index.js +21 -0
  59. package/dist/backend-msb/index.js.map +1 -0
  60. package/dist/backend-msb/invoke.d.ts +24 -0
  61. package/dist/backend-msb/invoke.d.ts.map +1 -0
  62. package/dist/backend-msb/invoke.js +73 -0
  63. package/dist/backend-msb/invoke.js.map +1 -0
  64. package/dist/backend-msb/ls-json.d.ts +23 -0
  65. package/dist/backend-msb/ls-json.d.ts.map +1 -0
  66. package/dist/backend-msb/ls-json.js +113 -0
  67. package/dist/backend-msb/ls-json.js.map +1 -0
  68. package/dist/backend-msb/network-links.d.ts +7 -0
  69. package/dist/backend-msb/network-links.d.ts.map +1 -0
  70. package/dist/backend-msb/network-links.js +31 -0
  71. package/dist/backend-msb/network-links.js.map +1 -0
  72. package/dist/backend-msb/platform.d.ts +29 -0
  73. package/dist/backend-msb/platform.d.ts.map +1 -0
  74. package/dist/backend-msb/platform.js +128 -0
  75. package/dist/backend-msb/platform.js.map +1 -0
  76. package/dist/backend-msb/port-conflict.d.ts +10 -0
  77. package/dist/backend-msb/port-conflict.d.ts.map +1 -0
  78. package/dist/backend-msb/port-conflict.js +16 -0
  79. package/dist/backend-msb/port-conflict.js.map +1 -0
  80. package/dist/backend-msb/provider.d.ts +10 -0
  81. package/dist/backend-msb/provider.d.ts.map +1 -0
  82. package/dist/backend-msb/provider.js +40 -0
  83. package/dist/backend-msb/provider.js.map +1 -0
  84. package/dist/backend-msb/provisioner.d.ts +19 -0
  85. package/dist/backend-msb/provisioner.d.ts.map +1 -0
  86. package/dist/backend-msb/provisioner.js +332 -0
  87. package/dist/backend-msb/provisioner.js.map +1 -0
  88. package/dist/backend-msb/reaper.d.ts +10 -0
  89. package/dist/backend-msb/reaper.d.ts.map +1 -0
  90. package/dist/backend-msb/reaper.js +73 -0
  91. package/dist/backend-msb/reaper.js.map +1 -0
  92. package/dist/core/backend.d.ts +98 -0
  93. package/dist/core/backend.d.ts.map +1 -0
  94. package/dist/core/backend.js +2 -0
  95. package/dist/core/backend.js.map +1 -0
  96. package/dist/core/backends.d.ts +37 -0
  97. package/dist/core/backends.d.ts.map +1 -0
  98. package/dist/core/backends.js +127 -0
  99. package/dist/core/backends.js.map +1 -0
  100. package/dist/core/cleanup.d.ts +37 -0
  101. package/dist/core/cleanup.d.ts.map +1 -0
  102. package/dist/core/cleanup.js +86 -0
  103. package/dist/core/cleanup.js.map +1 -0
  104. package/dist/core/errors.d.ts +51 -0
  105. package/dist/core/errors.d.ts.map +1 -0
  106. package/dist/core/errors.js +66 -0
  107. package/dist/core/errors.js.map +1 -0
  108. package/dist/core/free-ports.d.ts +23 -0
  109. package/dist/core/free-ports.d.ts.map +1 -0
  110. package/dist/core/free-ports.js +63 -0
  111. package/dist/core/free-ports.js.map +1 -0
  112. package/dist/core/generic-container.d.ts +145 -0
  113. package/dist/core/generic-container.d.ts.map +1 -0
  114. package/dist/core/generic-container.js +385 -0
  115. package/dist/core/generic-container.js.map +1 -0
  116. package/dist/core/model.d.ts +60 -0
  117. package/dist/core/model.d.ts.map +1 -0
  118. package/dist/core/model.js +2 -0
  119. package/dist/core/model.js.map +1 -0
  120. package/dist/core/mountable-file.d.ts +25 -0
  121. package/dist/core/mountable-file.d.ts.map +1 -0
  122. package/dist/core/mountable-file.js +64 -0
  123. package/dist/core/mountable-file.js.map +1 -0
  124. package/dist/core/network.d.ts +58 -0
  125. package/dist/core/network.d.ts.map +1 -0
  126. package/dist/core/network.js +84 -0
  127. package/dist/core/network.js.map +1 -0
  128. package/dist/core/run-id.d.ts +18 -0
  129. package/dist/core/run-id.d.ts.map +1 -0
  130. package/dist/core/run-id.js +18 -0
  131. package/dist/core/run-id.js.map +1 -0
  132. package/dist/core/wait.d.ts +59 -0
  133. package/dist/core/wait.d.ts.map +1 -0
  134. package/dist/core/wait.js +239 -0
  135. package/dist/core/wait.js.map +1 -0
  136. package/dist/index.d.ts +26 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +22 -0
  139. package/dist/index.js.map +1 -0
  140. package/dist/modules/arango.d.ts +14 -0
  141. package/dist/modules/arango.d.ts.map +1 -0
  142. package/dist/modules/arango.js +37 -0
  143. package/dist/modules/arango.js.map +1 -0
  144. package/dist/modules/clickhouse.d.ts +37 -0
  145. package/dist/modules/clickhouse.d.ts.map +1 -0
  146. package/dist/modules/clickhouse.js +69 -0
  147. package/dist/modules/clickhouse.js.map +1 -0
  148. package/dist/modules/flink.d.ts +65 -0
  149. package/dist/modules/flink.d.ts.map +1 -0
  150. package/dist/modules/flink.js +125 -0
  151. package/dist/modules/flink.js.map +1 -0
  152. package/dist/modules/floci.d.ts +46 -0
  153. package/dist/modules/floci.d.ts.map +1 -0
  154. package/dist/modules/floci.js +62 -0
  155. package/dist/modules/floci.js.map +1 -0
  156. package/dist/modules/index.d.ts +31 -0
  157. package/dist/modules/index.d.ts.map +1 -0
  158. package/dist/modules/index.js +31 -0
  159. package/dist/modules/index.js.map +1 -0
  160. package/dist/modules/kafka.d.ts +11 -0
  161. package/dist/modules/kafka.d.ts.map +1 -0
  162. package/dist/modules/kafka.js +41 -0
  163. package/dist/modules/kafka.js.map +1 -0
  164. package/dist/modules/keycloak.d.ts +40 -0
  165. package/dist/modules/keycloak.d.ts.map +1 -0
  166. package/dist/modules/keycloak.js +67 -0
  167. package/dist/modules/keycloak.js.map +1 -0
  168. package/dist/modules/mariadb.d.ts +56 -0
  169. package/dist/modules/mariadb.d.ts.map +1 -0
  170. package/dist/modules/mariadb.js +89 -0
  171. package/dist/modules/mariadb.js.map +1 -0
  172. package/dist/modules/memcached.d.ts +24 -0
  173. package/dist/modules/memcached.d.ts.map +1 -0
  174. package/dist/modules/memcached.js +105 -0
  175. package/dist/modules/memcached.js.map +1 -0
  176. package/dist/modules/mongodb.d.ts +19 -0
  177. package/dist/modules/mongodb.d.ts.map +1 -0
  178. package/dist/modules/mongodb.js +69 -0
  179. package/dist/modules/mongodb.js.map +1 -0
  180. package/dist/modules/mysql.d.ts +61 -0
  181. package/dist/modules/mysql.d.ts.map +1 -0
  182. package/dist/modules/mysql.js +101 -0
  183. package/dist/modules/mysql.js.map +1 -0
  184. package/dist/modules/neo4j.d.ts +60 -0
  185. package/dist/modules/neo4j.d.ts.map +1 -0
  186. package/dist/modules/neo4j.js +83 -0
  187. package/dist/modules/neo4j.js.map +1 -0
  188. package/dist/modules/pinot.d.ts +30 -0
  189. package/dist/modules/pinot.d.ts.map +1 -0
  190. package/dist/modules/pinot.js +49 -0
  191. package/dist/modules/pinot.js.map +1 -0
  192. package/dist/modules/postgres.d.ts +29 -0
  193. package/dist/modules/postgres.d.ts.map +1 -0
  194. package/dist/modules/postgres.js +73 -0
  195. package/dist/modules/postgres.js.map +1 -0
  196. package/dist/modules/rabbitmq.d.ts +46 -0
  197. package/dist/modules/rabbitmq.d.ts.map +1 -0
  198. package/dist/modules/rabbitmq.js +68 -0
  199. package/dist/modules/rabbitmq.js.map +1 -0
  200. package/dist/modules/redis.d.ts +15 -0
  201. package/dist/modules/redis.d.ts.map +1 -0
  202. package/dist/modules/redis.js +24 -0
  203. package/dist/modules/redis.js.map +1 -0
  204. package/dist/modules/redpanda.d.ts +27 -0
  205. package/dist/modules/redpanda.d.ts.map +1 -0
  206. package/dist/modules/redpanda.js +58 -0
  207. package/dist/modules/redpanda.js.map +1 -0
  208. package/dist/modules/spring-cloud-config.d.ts +18 -0
  209. package/dist/modules/spring-cloud-config.d.ts.map +1 -0
  210. package/dist/modules/spring-cloud-config.js +28 -0
  211. package/dist/modules/spring-cloud-config.js.map +1 -0
  212. package/dist/modules/wiremock.d.ts +21 -0
  213. package/dist/modules/wiremock.d.ts.map +1 -0
  214. package/dist/modules/wiremock.js +32 -0
  215. package/dist/modules/wiremock.js.map +1 -0
  216. package/package.json +104 -0
@@ -0,0 +1,127 @@
1
+ // The ServiceLoader analog: an explicit process-global registry. Each
2
+ // backend subpath (`rightsize/backend-msb`, `rightsize/backend-docker`)
3
+ // calls registerBackend at import time — the `sideEffects` entry in
4
+ // package.json exists so bundlers keep that call.
5
+ const registered = [];
6
+ /** Adds a provider to the process-global registry. Called once, at import time, by each backend subpath's `index.ts`. */
7
+ export function registerBackend(provider) {
8
+ registered.push(provider);
9
+ }
10
+ /** Test seam: clears the process-global registry. Never call from library code. */
11
+ export function _resetRegistryForTests() {
12
+ registered.length = 0;
13
+ }
14
+ /**
15
+ * Test seam: snapshot of the currently registered providers, so an impure
16
+ * suite that wipes the registry can put the real ones back when it finishes —
17
+ * under a single-process test runner, whatever it leaves behind is what every
18
+ * later-loaded test file sees.
19
+ */
20
+ export function _providersSnapshotForTests() {
21
+ return [...registered];
22
+ }
23
+ function knownNames(providers) {
24
+ return providers.map((p) => p.name).join(", ");
25
+ }
26
+ /**
27
+ * Pure resolution: no I/O, no module-global registry access, so it is fully
28
+ * unit-testable with fake providers. `Backends.active()` is the impure
29
+ * caller that supplies the real registry and `process.env`.
30
+ */
31
+ export function resolve(providers, requested) {
32
+ if (providers.length === 0) {
33
+ throw new Error("No backend providers are registered. Import 'rightsize/backend-msb' or 'rightsize/backend-docker' to register one.");
34
+ }
35
+ if (requested !== undefined) {
36
+ const wanted = requested.toLowerCase();
37
+ const match = providers.find((p) => p.name.toLowerCase() === wanted);
38
+ if (match === undefined) {
39
+ throw new Error(`Unknown backend '${requested}'. Known backends: ${knownNames(providers)}.`);
40
+ }
41
+ if (!match.isSupported()) {
42
+ throw new Error(`Backend '${match.name}' was requested but is not supported here: ${match.unsupportedReason()}`);
43
+ }
44
+ return match.create();
45
+ }
46
+ const supported = providers.filter((p) => p.isSupported());
47
+ if (supported.length === 0) {
48
+ const reasons = providers.map((p) => `${p.name}: ${p.unsupportedReason()}`).join("; ");
49
+ throw new Error(`No supported backend found. ${reasons}`);
50
+ }
51
+ const winner = supported.reduce((best, candidate) => (candidate.priority > best.priority ? candidate : best));
52
+ return winner.create();
53
+ }
54
+ let activeBackend;
55
+ let exitHookInstalled = false;
56
+ /**
57
+ * Resolves once per process (memoized) against the real registry and
58
+ * `process.env.RIGHTSIZE_BACKEND`, then registers the best-effort exit-path
59
+ * cleanup (see `src/core/cleanup.ts`) the first time a backend is created.
60
+ */
61
+ function active() {
62
+ if (activeBackend === undefined) {
63
+ activeBackend = resolve(registered, process.env["RIGHTSIZE_BACKEND"]);
64
+ if (!exitHookInstalled) {
65
+ exitHookInstalled = true;
66
+ let closeStarted = false;
67
+ // "beforeExit" fires while the event loop is still alive (unlike
68
+ // "exit", which runs synchronously and cannot await), so it's the one
69
+ // hook where the backend's real async close() can actually run. It is
70
+ // NOT a true shutdown hook, though: it fires at every full event-loop
71
+ // drain, not only at process exit. With the once-guard below, close()
72
+ // therefore runs at the FIRST complete drain — in a process that goes
73
+ // fully idle between containers, that can be before its last container
74
+ // was booted. Accepted tradeoff: a completely drained loop has no
75
+ // pending timers, sockets, or awaits, so nothing in the process is
76
+ // still waiting on those containers, and both backends stay usable
77
+ // after close() (stateless per-request clients) for any later boot.
78
+ // This is best-effort and NOT the primary safety net: a process that
79
+ // dies via SIGKILL, or exits through the synchronous "exit" path
80
+ // before beforeExit ever fires, is instead covered by the per-
81
+ // container cleanupSync registered in cleanup.ts and by the orphan
82
+ // reaper each backend runs at construction.
83
+ //
84
+ // "beforeExit" re-fires every time the event loop would otherwise go
85
+ // idle. close() is async — it awaits HTTP/subprocess calls — so
86
+ // invoking it unconditionally here schedules new event-loop work on
87
+ // every firing, which makes the NEXT idle point trigger this same
88
+ // handler again: an infinite close()-then-beforeExit cycle that never
89
+ // lets the process exit (observed as a `node --test` run hanging
90
+ // indefinitely after every assertion had already passed). The
91
+ // closeStarted guard makes this a once-per-process call — the single
92
+ // shutdown pass a process-lifetime cleanup hook is meant to be.
93
+ process.on("beforeExit", () => {
94
+ if (closeStarted) {
95
+ return;
96
+ }
97
+ closeStarted = true;
98
+ // Best-effort: a backend whose close() rejects — or, as a defensive
99
+ // fake in a test can do, throws synchronously before ever producing
100
+ // a Promise to attach .catch() to — must not crash the process on
101
+ // its way out. There is no caller left to report the failure to.
102
+ try {
103
+ activeBackend?.close().catch(() => { });
104
+ }
105
+ catch {
106
+ // Swallowed for the same reason as the .catch() above.
107
+ }
108
+ });
109
+ }
110
+ }
111
+ return activeBackend;
112
+ }
113
+ /** Test seam: forces the next `Backends.active()` call to re-resolve. */
114
+ function _resetActiveForTests() {
115
+ activeBackend = undefined;
116
+ exitHookInstalled = false;
117
+ }
118
+ /** The library's single entry point for obtaining the active `SandboxBackend`. */
119
+ export const Backends = {
120
+ /** Pure resolution against an explicit provider list — see `resolve` above. */
121
+ resolve,
122
+ /** The memoized, per-process active backend — resolved from whatever's registered plus `RIGHTSIZE_BACKEND`. */
123
+ active,
124
+ /** Test seam: forces the next `active()` call to re-resolve. Never call from library code. */
125
+ _resetActiveForTests,
126
+ };
127
+ //# sourceMappingURL=backends.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backends.js","sourceRoot":"","sources":["../../src/core/backends.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Best-effort exit-path cleanup — the JVM-shutdown-hook / Rust-cleanup-
3
+ * thread analog for a process that dies (or is asked to die) before its
4
+ * `await using` scopes unwind normally.
5
+ *
6
+ * Node's `process.on("exit", ...)` handler runs SYNCHRONOUSLY and cannot
7
+ * `await` — by the time it fires, the event loop is already being torn
8
+ * down, so a backend's real async `stop()`/`remove()` cannot run there.
9
+ * Each backend therefore registers a synchronous, blocking teardown
10
+ * function per live container (`child_process.spawnSync` for msb, a
11
+ * blocking unix-socket DELETE for docker) instead of its normal async
12
+ * path. This module only owns the REGISTRY and the process hooks; it has
13
+ * no opinion on how any one backend tears a container down.
14
+ *
15
+ * This is a last-resort backstop, not the primary cleanup path — the
16
+ * primary path is `GenericContainer.stop()` / `[Symbol.asyncDispose]`.
17
+ * SIGKILL bypasses even this (no handler runs at all); the orphan reaper
18
+ * each backend runs at startup (`sweepOrphans`) is the backstop for that
19
+ * case, sweeping up `rz-<other-runid>-*` leftovers from a crashed prior
20
+ * run.
21
+ */
22
+ export type SyncCleanup = () => void;
23
+ /** Registers a synchronous teardown for a live container, keyed by handle id. */
24
+ export declare function registerSyncCleanup(handleId: string, cleanupFn: SyncCleanup): void;
25
+ /** Unregisters a container's teardown once it has been stopped/removed normally. */
26
+ export declare function unregisterSyncCleanup(handleId: string): void;
27
+ /** Test seam: clears registered cleanups without running them. */
28
+ export declare function _resetForTests(): void;
29
+ /**
30
+ * Test seam: runs every registered cleanup exactly the way the real "exit"
31
+ * handler does (best-effort, swallowing failures, then clearing the
32
+ * registry) without actually terminating the test process. This is how a
33
+ * unit test proves the exit-path teardown is wired to something real,
34
+ * rather than asserting on `registered`'s internal size.
35
+ */
36
+ export declare function _runAllForTests(): void;
37
+ //# sourceMappingURL=cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../src/core/cleanup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AA+CrC,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,GAAG,IAAI,CAGlF;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED,kEAAkE;AAClE,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Best-effort exit-path cleanup — the JVM-shutdown-hook / Rust-cleanup-
3
+ * thread analog for a process that dies (or is asked to die) before its
4
+ * `await using` scopes unwind normally.
5
+ *
6
+ * Node's `process.on("exit", ...)` handler runs SYNCHRONOUSLY and cannot
7
+ * `await` — by the time it fires, the event loop is already being torn
8
+ * down, so a backend's real async `stop()`/`remove()` cannot run there.
9
+ * Each backend therefore registers a synchronous, blocking teardown
10
+ * function per live container (`child_process.spawnSync` for msb, a
11
+ * blocking unix-socket DELETE for docker) instead of its normal async
12
+ * path. This module only owns the REGISTRY and the process hooks; it has
13
+ * no opinion on how any one backend tears a container down.
14
+ *
15
+ * This is a last-resort backstop, not the primary cleanup path — the
16
+ * primary path is `GenericContainer.stop()` / `[Symbol.asyncDispose]`.
17
+ * SIGKILL bypasses even this (no handler runs at all); the orphan reaper
18
+ * each backend runs at startup (`sweepOrphans`) is the backstop for that
19
+ * case, sweeping up `rz-<other-runid>-*` leftovers from a crashed prior
20
+ * run.
21
+ */
22
+ const registered = new Map();
23
+ let hooksInstalled = false;
24
+ function runAll() {
25
+ for (const cleanupFn of registered.values()) {
26
+ try {
27
+ cleanupFn();
28
+ }
29
+ catch {
30
+ // Best-effort: a failure to clean up one container must not block
31
+ // cleanup of the others, and the process is exiting regardless.
32
+ }
33
+ }
34
+ registered.clear();
35
+ }
36
+ function installHooksOnce() {
37
+ if (hooksInstalled) {
38
+ return;
39
+ }
40
+ hooksInstalled = true;
41
+ process.on("exit", runAll);
42
+ // SIGINT/SIGTERM: run the same synchronous cleanup, then re-raise the
43
+ // signal so the process still exits the way it would have without this
44
+ // handler (correct exit code, no swallowed Ctrl-C).
45
+ //
46
+ // On Windows this pair is best-effort, not the real safety net: Node
47
+ // delivers a synthetic SIGINT for Ctrl+C (and SIGBREAK for Ctrl+Break,
48
+ // which is not registered here) but there is no real SIGTERM delivery from
49
+ // another process the way POSIX has it — Windows has no signal-based IPC
50
+ // for that. The interactive Ctrl+C path still fires this handler as
51
+ // expected; a `taskkill`-style external termination does not. The `"exit"`
52
+ // handler above is therefore the one Windows-portable guarantee — it fires
53
+ // on every normal process exit regardless of platform or what triggered
54
+ // it — while this signal-handler loop is a POSIX nicety layered on top.
55
+ for (const signal of ["SIGINT", "SIGTERM"]) {
56
+ process.on(signal, () => {
57
+ runAll();
58
+ process.removeAllListeners(signal);
59
+ process.kill(process.pid, signal);
60
+ });
61
+ }
62
+ }
63
+ /** Registers a synchronous teardown for a live container, keyed by handle id. */
64
+ export function registerSyncCleanup(handleId, cleanupFn) {
65
+ installHooksOnce();
66
+ registered.set(handleId, cleanupFn);
67
+ }
68
+ /** Unregisters a container's teardown once it has been stopped/removed normally. */
69
+ export function unregisterSyncCleanup(handleId) {
70
+ registered.delete(handleId);
71
+ }
72
+ /** Test seam: clears registered cleanups without running them. */
73
+ export function _resetForTests() {
74
+ registered.clear();
75
+ }
76
+ /**
77
+ * Test seam: runs every registered cleanup exactly the way the real "exit"
78
+ * handler does (best-effort, swallowing failures, then clearing the
79
+ * registry) without actually terminating the test process. This is how a
80
+ * unit test proves the exit-path teardown is wired to something real,
81
+ * rather than asserting on `registered`'s internal size.
82
+ */
83
+ export function _runAllForTests() {
84
+ runAll();
85
+ }
86
+ //# sourceMappingURL=cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/core/cleanup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;AAClD,IAAI,cAAc,GAAG,KAAK,CAAC;AAE3B,SAAS,MAAM;IACb,KAAK,MAAM,SAAS,IAAI,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;QAC5C,IAAI,CAAC;YACH,SAAS,EAAE,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,kEAAkE;YAClE,gEAAgE;QAClE,CAAC;IACH,CAAC;IACD,UAAU,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC;AAED,SAAS,gBAAgB;IACvB,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO;IACT,CAAC;IACD,cAAc,GAAG,IAAI,CAAC;IAEtB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE3B,sEAAsE;IACtE,uEAAuE;IACvE,oDAAoD;IACpD,EAAE;IACF,qEAAqE;IACrE,uEAAuE;IACvE,2EAA2E;IAC3E,yEAAyE;IACzE,oEAAoE;IACpE,2EAA2E;IAC3E,2EAA2E;IAC3E,wEAAwE;IACxE,wEAAwE;IACxE,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;QACpD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YACtB,MAAM,EAAE,CAAC;YACT,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;YACnC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,mBAAmB,CAAC,QAAgB,EAAE,SAAsB;IAC1E,gBAAgB,EAAE,CAAC;IACnB,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AACtC,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC9B,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,cAAc;IAC5B,UAAU,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,EAAE,CAAC;AACX,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `feature` stays a noun phrase ("network links", "read-only mount
3
+ * enforcement") and advice lives in `remedy`; the two are composed with an
4
+ * em-dash so every backend renders the same sentence shape.
5
+ *
6
+ * @param feature - A noun phrase naming the unsupported capability.
7
+ * @param backend - The active backend's name (e.g. `"microsandbox"`).
8
+ * @param remedy - Optional actionable advice, appended after an em-dash.
9
+ */
10
+ export declare class UnsupportedByBackendError extends Error {
11
+ /** A noun phrase naming the unsupported capability. */
12
+ readonly feature: string;
13
+ /** The active backend's name (e.g. `"microsandbox"`). */
14
+ readonly backend: string;
15
+ /** Optional actionable advice, appended after an em-dash in the rendered message. */
16
+ readonly remedy?: string | undefined;
17
+ constructor(
18
+ /** A noun phrase naming the unsupported capability. */
19
+ feature: string,
20
+ /** The active backend's name (e.g. `"microsandbox"`). */
21
+ backend: string,
22
+ /** Optional actionable advice, appended after an em-dash in the rendered message. */
23
+ remedy?: string | undefined);
24
+ }
25
+ /**
26
+ * Thrown when a backend's `start()` fails because a chosen host port is
27
+ * already bound by something else. `GenericContainer`'s start loop classifies
28
+ * this (typed-first, message-substring fallback) and retries with fresh
29
+ * ports rather than surfacing it directly — it only escapes after every
30
+ * retry attempt is exhausted.
31
+ */
32
+ export declare class PortBindConflictError extends Error {
33
+ /** The underlying error or daemon response this classification was derived from, if any. */
34
+ readonly cause?: unknown | undefined;
35
+ constructor(message: string,
36
+ /** The underlying error or daemon response this classification was derived from, if any. */
37
+ cause?: unknown | undefined);
38
+ }
39
+ /** Thrown when a wait strategy never observes readiness before its deadline. */
40
+ export declare class ContainerLaunchError extends Error {
41
+ constructor(message: string);
42
+ }
43
+ /** Thrown on subprocess/daemon failures; message carries full stderr/body. */
44
+ export declare class BackendError extends Error {
45
+ constructor(message: string);
46
+ }
47
+ /** Thrown when the msb toolchain provisioner can't get a runnable `msb` binary in place (download, checksum, or lock failure). */
48
+ export declare class ProvisionError extends Error {
49
+ constructor(message: string);
50
+ }
51
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAEhD,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM;IACxB,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM;IACxB,qFAAqF;IACrF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;;IALxB,uDAAuD;IAC9C,OAAO,EAAE,MAAM;IACxB,yDAAyD;IAChD,OAAO,EAAE,MAAM;IACxB,qFAAqF;IAC5E,MAAM,CAAC,EAAE,MAAM,YAAA;CAK3B;AAED;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAG5C,4FAA4F;IAC5F,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO;gBAFxB,OAAO,EAAE,MAAM;IACf,4FAA4F;IACnF,KAAK,CAAC,EAAE,OAAO,YAAA;CAK3B;AAED,gFAAgF;AAChF,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED,8EAA8E;AAC9E,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED,kIAAkI;AAClI,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `feature` stays a noun phrase ("network links", "read-only mount
3
+ * enforcement") and advice lives in `remedy`; the two are composed with an
4
+ * em-dash so every backend renders the same sentence shape.
5
+ *
6
+ * @param feature - A noun phrase naming the unsupported capability.
7
+ * @param backend - The active backend's name (e.g. `"microsandbox"`).
8
+ * @param remedy - Optional actionable advice, appended after an em-dash.
9
+ */
10
+ export class UnsupportedByBackendError extends Error {
11
+ feature;
12
+ backend;
13
+ remedy;
14
+ constructor(
15
+ /** A noun phrase naming the unsupported capability. */
16
+ feature,
17
+ /** The active backend's name (e.g. `"microsandbox"`). */
18
+ backend,
19
+ /** Optional actionable advice, appended after an em-dash in the rendered message. */
20
+ remedy) {
21
+ super(`Feature '${feature}' is not supported by the '${backend}' backend${remedy ? ` — ${remedy}` : ""}`);
22
+ this.feature = feature;
23
+ this.backend = backend;
24
+ this.remedy = remedy;
25
+ this.name = "UnsupportedByBackendError";
26
+ }
27
+ }
28
+ /**
29
+ * Thrown when a backend's `start()` fails because a chosen host port is
30
+ * already bound by something else. `GenericContainer`'s start loop classifies
31
+ * this (typed-first, message-substring fallback) and retries with fresh
32
+ * ports rather than surfacing it directly — it only escapes after every
33
+ * retry attempt is exhausted.
34
+ */
35
+ export class PortBindConflictError extends Error {
36
+ cause;
37
+ constructor(message,
38
+ /** The underlying error or daemon response this classification was derived from, if any. */
39
+ cause) {
40
+ super(message, cause !== undefined ? { cause } : undefined);
41
+ this.cause = cause;
42
+ this.name = "PortBindConflictError";
43
+ }
44
+ }
45
+ /** Thrown when a wait strategy never observes readiness before its deadline. */
46
+ export class ContainerLaunchError extends Error {
47
+ constructor(message) {
48
+ super(message);
49
+ this.name = "ContainerLaunchError";
50
+ }
51
+ }
52
+ /** Thrown on subprocess/daemon failures; message carries full stderr/body. */
53
+ export class BackendError extends Error {
54
+ constructor(message) {
55
+ super(message);
56
+ this.name = "BackendError";
57
+ }
58
+ }
59
+ /** Thrown when the msb toolchain provisioner can't get a runnable `msb` binary in place (download, checksum, or lock failure). */
60
+ export class ProvisionError extends Error {
61
+ constructor(message) {
62
+ super(message);
63
+ this.name = "ProvisionError";
64
+ }
65
+ }
66
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAGvC;IAEA;IAEA;IANX;IACE,uDAAuD;IAC9C,OAAe;IACxB,yDAAyD;IAChD,OAAe;IACxB,qFAAqF;IAC5E,MAAe;QAExB,KAAK,CAAC,YAAY,OAAO,8BAA8B,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QANjG,YAAO,GAAP,OAAO,CAAQ;QAEf,YAAO,GAAP,OAAO,CAAQ;QAEf,WAAM,GAAN,MAAM,CAAS;QAGxB,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC1C,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAInC;IAHX,YACE,OAAe;IACf,4FAA4F;IACnF,KAAe;QAExB,KAAK,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAFnD,UAAK,GAAL,KAAK,CAAU;QAGxB,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,gFAAgF;AAChF,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED,8EAA8E;AAC9E,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED,kIAAkI;AAClI,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Allocates a host port this process has not already handed out. Binding
3
+ * `127.0.0.1:0` and immediately closing the socket leaves a window where
4
+ * another process (or another allocate() racing on ITS OWN OS-level choice)
5
+ * could rebind the same port before the caller's container process starts —
6
+ * `GenericContainer`'s port-retry loop is the mitigation for that race, not
7
+ * this function. What this function guarantees is in-process uniqueness:
8
+ * two calls here never return the same port while either is still issued.
9
+ */
10
+ export declare function allocate(): Promise<number>;
11
+ /** Releases a port back to the pool. Releasing a port never issued by this process is a harmless no-op. */
12
+ export declare function release(port: number): void;
13
+ /** Test-only observability seam: the ports currently considered issued. */
14
+ export declare function issuedView(): ReadonlySet<number>;
15
+ export declare const FreePorts: {
16
+ /** Allocates a host port this process has not already handed out — see `allocate` above. */
17
+ allocate: typeof allocate;
18
+ /** Releases a port back to the pool — see `release` above. */
19
+ release: typeof release;
20
+ /** Test-only observability seam — see `issuedView` above. */
21
+ issuedView: typeof issuedView;
22
+ };
23
+ //# sourceMappingURL=free-ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"free-ports.d.ts","sourceRoot":"","sources":["../../src/core/free-ports.ts"],"names":[],"mappings":"AAgCA;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAShD;AAED,2GAA2G;AAC3G,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,IAAI,WAAW,CAAC,MAAM,CAAC,CAEhD;AAED,eAAO,MAAM,SAAS;IACpB,4FAA4F;;IAE5F,8DAA8D;;IAE9D,6DAA6D;;CAE9D,CAAC"}
@@ -0,0 +1,63 @@
1
+ import * as net from "node:net";
2
+ const MAX_ALLOCATE_ATTEMPTS = 100;
3
+ // Binds are loopback-only (127.0.0.1), not wildcard — the same conservative
4
+ // choice used everywhere else a host port is touched (publishing, wait
5
+ // probes). A deliberate divergence from binding 0.0.0.0, not an oversight.
6
+ const BIND_HOST = "127.0.0.1";
7
+ function bindEphemeralPort() {
8
+ return new Promise((resolvePort, rejectPort) => {
9
+ const server = net.createServer();
10
+ server.once("error", (err) => {
11
+ rejectPort(err);
12
+ });
13
+ server.listen(0, BIND_HOST, () => {
14
+ const address = server.address();
15
+ if (address === null || typeof address === "string") {
16
+ server.close();
17
+ rejectPort(new Error("failed to read an ephemeral port from the bound server"));
18
+ return;
19
+ }
20
+ const port = address.port;
21
+ server.close(() => {
22
+ resolvePort(port);
23
+ });
24
+ });
25
+ });
26
+ }
27
+ const issued = new Set();
28
+ /**
29
+ * Allocates a host port this process has not already handed out. Binding
30
+ * `127.0.0.1:0` and immediately closing the socket leaves a window where
31
+ * another process (or another allocate() racing on ITS OWN OS-level choice)
32
+ * could rebind the same port before the caller's container process starts —
33
+ * `GenericContainer`'s port-retry loop is the mitigation for that race, not
34
+ * this function. What this function guarantees is in-process uniqueness:
35
+ * two calls here never return the same port while either is still issued.
36
+ */
37
+ export async function allocate() {
38
+ for (let attempt = 0; attempt < MAX_ALLOCATE_ATTEMPTS; attempt++) {
39
+ const port = await bindEphemeralPort();
40
+ if (!issued.has(port)) {
41
+ issued.add(port);
42
+ return port;
43
+ }
44
+ }
45
+ throw new Error(`could not allocate a unique free port after ${MAX_ALLOCATE_ATTEMPTS} attempts`);
46
+ }
47
+ /** Releases a port back to the pool. Releasing a port never issued by this process is a harmless no-op. */
48
+ export function release(port) {
49
+ issued.delete(port);
50
+ }
51
+ /** Test-only observability seam: the ports currently considered issued. */
52
+ export function issuedView() {
53
+ return new Set(issued);
54
+ }
55
+ export const FreePorts = {
56
+ /** Allocates a host port this process has not already handed out — see `allocate` above. */
57
+ allocate,
58
+ /** Releases a port back to the pool — see `release` above. */
59
+ release,
60
+ /** Test-only observability seam — see `issuedView` above. */
61
+ issuedView,
62
+ };
63
+ //# sourceMappingURL=free-ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"free-ports.js","sourceRoot":"","sources":["../../src/core/free-ports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAEhC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAElC,4EAA4E;AAC5E,uEAAuE;AACvE,2EAA2E;AAC3E,MAAM,SAAS,GAAG,WAAW,CAAC;AAE9B,SAAS,iBAAiB;IACxB,OAAO,IAAI,OAAO,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,EAAE;QAC7C,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YAC3B,UAAU,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE;YAC/B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;YACjC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBACpD,MAAM,CAAC,KAAK,EAAE,CAAC;gBACf,UAAU,CAAC,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC,CAAC;gBAChF,OAAO;YACT,CAAC;YACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;YAC1B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE;gBAChB,WAAW,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;AAEjC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,qBAAqB,EAAE,OAAO,EAAE,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,MAAM,iBAAiB,EAAE,CAAC;QACvC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,+CAA+C,qBAAqB,WAAW,CAAC,CAAC;AACnG,CAAC;AAED,2GAA2G;AAC3G,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,4FAA4F;IAC5F,QAAQ;IACR,8DAA8D;IAC9D,OAAO;IACP,6DAA6D;IAC7D,UAAU;CACX,CAAC"}
@@ -0,0 +1,145 @@
1
+ import { Network } from "./network.js";
2
+ import type { NetworkMember } from "./network.js";
3
+ import type { WaitStrategy } from "./wait.js";
4
+ import type { SandboxBackend, FollowHandle } from "./backend.js";
5
+ import type { ContainerSpec, ExecResult } from "./model.js";
6
+ import type { MountableFile } from "./mountable-file.js";
7
+ /**
8
+ * The builder, launcher, and lifecycle guard for a single container — the
9
+ * one class every module (`RedisContainer`, `PostgresContainer`, …)
10
+ * subclasses, and the type you reach for directly for any image without a
11
+ * dedicated module.
12
+ *
13
+ * The instance itself is the guard: after `start()` it implements
14
+ * `Symbol.asyncDispose`, so `await using c = await new GenericContainer(img).start()`
15
+ * tears the container down at scope exit — the direct analog of the Kotlin
16
+ * `AutoCloseable` and Rust's RAII guard, expressed with TC39 explicit
17
+ * resource management. Prefer `await using` in tests; use explicit
18
+ * `start()`/`stop()` where a `using` scope can't span a framework hook
19
+ * (Jest/Vitest `beforeAll`/`afterAll`, `node:test`'s `before`/`after`).
20
+ *
21
+ * Builder methods (`withEnv`, `withExposedPorts`, …) mutate and return
22
+ * `this` for chaining; none of them perform I/O. All I/O happens in
23
+ * `start()`, `stop()`, `exec()`, `logs()`, and `followOutput()`.
24
+ */
25
+ export declare class GenericContainer implements AsyncDisposable, NetworkMember {
26
+ private readonly image;
27
+ private envPairs;
28
+ private exposedPorts;
29
+ private command;
30
+ private network;
31
+ private aliasNames;
32
+ private mounts;
33
+ private waitStrategy;
34
+ private memoryLimitMb;
35
+ private backendOverride;
36
+ private handle;
37
+ private backend;
38
+ private mappedPorts;
39
+ private running;
40
+ /** Builds against `image` (e.g. `"redis:8.6-alpine"`); no I/O happens until `start()`. */
41
+ constructor(image: string);
42
+ /** Convenience: `new GenericContainer(image).start()`. */
43
+ static start(image: string): Promise<GenericContainer>;
44
+ /** Sets an environment variable visible to the workload. Last-write-wins if called again with the same key; insertion order is otherwise preserved. */
45
+ withEnv(key: string, value: string): this;
46
+ /** Publishes these guest ports to pre-allocated host ports (see `FreePorts`); read back with `getMappedPort`. */
47
+ withExposedPorts(...ports: number[]): this;
48
+ /** Overrides the image's default ENTRYPOINT/CMD. Omit this call entirely to run the image unmodified. */
49
+ withCommand(...cmd: string[]): this;
50
+ /** Joins a `Network`, making this container reachable from (and to) its running siblings by alias. */
51
+ withNetwork(net: Network): this;
52
+ /** Names this container answers to on its network — resolved via `Network.resolve(alias, port)` by other members. */
53
+ withNetworkAliases(...names: string[]): this;
54
+ /** Copies a host file into the guest before boot, at `guestPath`. Read-only is enforced on docker; advisory only on msb (current microsandbox releases do not enforce guest-side read-only mounts). */
55
+ withCopyFileToContainer(file: MountableFile, guestPath: string): this;
56
+ /** Sets the readiness check `start()` waits on before returning. Defaults to `Wait.forListeningPort()`. */
57
+ waitingFor(strategy: WaitStrategy): this;
58
+ /** Raises the container's memory ceiling (in MB) above the backend default — required by several JVM-heavy modules; see each module's own doc for its pin. */
59
+ withMemoryLimit(megabytes: number): this;
60
+ /** Test/advanced seam: pin the backend instead of resolving `Backends.active()`. */
61
+ withBackend(backend: SandboxBackend): this;
62
+ /** Module hook: override to adjust the spec once mapped host ports are known (e.g. advertised listeners). */
63
+ protected customizeSpec(spec: ContainerSpec, _mapped: (guest: number) => number): ContainerSpec;
64
+ /** Module hook: runs after the container is ready (e.g. `rs.initiate` for a Mongo replica set). */
65
+ protected containerIsStarted(): Promise<void>;
66
+ /**
67
+ * Module hook: runs the instant `start()` has resolved a backend — before
68
+ * `ensureNetwork`, port allocation, or the container is created/booted at
69
+ * all. The one hook that lets a module reject a backend-incompatible
70
+ * request (e.g. `FlinkContainer.withTaskManager()` on msb) without paying
71
+ * for a boot first. `containerIsStarted()` (above) fires much later, after
72
+ * this container is already fully up — fine for post-readiness setup, but
73
+ * too late to fail fast on a capability the backend never had.
74
+ */
75
+ protected containerIsStarting(_backend: SandboxBackend): Promise<void>;
76
+ /**
77
+ * Module hook: the backend this container actually started against (only
78
+ * valid once `start()` has resolved it — i.e. from within
79
+ * `containerIsStarted()` or later). Lets a module's post-start hook branch
80
+ * on backend capability, e.g. `FlinkContainer.withTaskManager()` throwing a
81
+ * typed `UnsupportedByBackendError` on msb instead of attempting a
82
+ * network-link topology the exec-tunnel emulation can't carry.
83
+ */
84
+ protected currentBackend(): SandboxBackend;
85
+ private resolveBackend;
86
+ private buildSpec;
87
+ private allocatePorts;
88
+ private releasePorts;
89
+ /**
90
+ * Resolves a backend, allocates host ports, boots the container, installs
91
+ * any network links, and waits for readiness — in that order. On ANY
92
+ * failure partway through (a bad wait strategy, a network-link rejection,
93
+ * a port conflict exhausting its retries), everything already allocated
94
+ * or started is torn down to completion BEFORE this rejects: a
95
+ * half-started container never leaks, and there is no detached
96
+ * fire-and-forget cleanup running after `start()` has already returned
97
+ * control to the caller.
98
+ *
99
+ * A host-port-bind conflict specifically retries (up to 5 attempts) with
100
+ * a fresh port before giving up — pre-allocated ports can still lose an
101
+ * allocate-then-bind race to an unrelated process.
102
+ */
103
+ start(): Promise<this>;
104
+ /**
105
+ * Stops and removes the backend-native resource, releases its host ports,
106
+ * and clears the port map. Idempotent — stopping a container that never
107
+ * started, or stopping twice, is a harmless no-op the second time. Errors
108
+ * from the backend's own stop/remove calls are swallowed (best-effort
109
+ * teardown); this method itself never throws.
110
+ */
111
+ stop(): Promise<void>;
112
+ /** `= stop()`. What `await using c = await new GenericContainer(img).start()` calls at scope exit; never throws. */
113
+ [Symbol.asyncDispose](): Promise<void>;
114
+ /** Whether `start()` has completed and `stop()` has not yet run. */
115
+ get isRunning(): boolean;
116
+ /** Always `"127.0.0.1"` — every published port is bound loopback-only, on both backends. */
117
+ get host(): string;
118
+ /** Names this container answers to on its network (set via `withNetworkAliases`). Part of the `NetworkMember` contract `Network` uses to compute links. */
119
+ get aliases(): ReadonlyArray<string>;
120
+ /** Every guest port this container published (set via `withExposedPorts`). Part of the `NetworkMember` contract `Network` uses to compute links. */
121
+ get exposedGuestPorts(): ReadonlyArray<number>;
122
+ /**
123
+ * The host port bound to `guestPort`. Throws distinct messages for the
124
+ * two ways this can be wrong: not running at all ("call start() first"),
125
+ * versus running but never exposed via `withExposedPorts` ("call
126
+ * withExposedPorts(N)").
127
+ */
128
+ getMappedPort(guestPort: number): number;
129
+ /** Alias for `getMappedPort`, satisfying the `NetworkMember`/`WaitTarget` shape (which name this method `mappedPort`). */
130
+ mappedPort(guestPort: number): number;
131
+ private requireHandle;
132
+ /** Runs a one-shot command inside the running container and waits for it to exit. Throws if the container is not running. */
133
+ exec(...cmd: string[]): Promise<ExecResult>;
134
+ /** Fetches the workload's logs so far (a bounded tail), as a single string. Throws if the container is not running. */
135
+ logs(): Promise<string>;
136
+ /**
137
+ * Streams log lines to `consumer` as they're produced, in order, with no
138
+ * duplicates — including the final unterminated fragment after the
139
+ * workload exits, delivered exactly once. Call `close()` on the returned
140
+ * handle to stop delivery without flushing anything further.
141
+ */
142
+ followOutput(consumer: (line: string) => void): Promise<FollowHandle>;
143
+ private asWaitTarget;
144
+ }
145
+ //# sourceMappingURL=generic-container.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generic-container.d.ts","sourceRoot":"","sources":["../../src/core/generic-container.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,WAAW,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAiB,YAAY,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAa,UAAU,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAyCzD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,gBAAiB,YAAW,eAAe,EAAE,aAAa;IACrE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,eAAe,CAA6B;IAEpD,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,OAAO,CAAS;IAExB,0FAA0F;gBAC9E,KAAK,EAAE,MAAM;IAIzB,0DAA0D;WAC7C,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAI5D,uJAAuJ;IACvJ,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IASzC,iHAAiH;IACjH,gBAAgB,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAK1C,yGAAyG;IACzG,WAAW,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI;IAKnC,sGAAsG;IACtG,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAK/B,qHAAqH;IACrH,kBAAkB,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAK5C,uMAAuM;IACvM,uBAAuB,CAAC,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKrE,2GAA2G;IAC3G,UAAU,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAKxC,8JAA8J;IAC9J,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKxC,oFAAoF;IACpF,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAK1C,6GAA6G;IAC7G,SAAS,CAAC,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,aAAa;IAI/F,mGAAmG;IACnG,SAAS,CAAC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7C;;;;;;;;OAQG;IACH,SAAS,CAAC,mBAAmB,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE;;;;;;;OAOG;IACH,SAAS,CAAC,cAAc,IAAI,cAAc;IAI1C,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,SAAS;YA2BH,aAAa;IAQ3B,OAAO,CAAC,YAAY;IAMpB;;;;;;;;;;;;;OAaG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgF5B;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B,oHAAoH;IAC9G,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5C,oEAAoE;IACpE,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,4FAA4F;IAC5F,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,2JAA2J;IAC3J,IAAI,OAAO,IAAI,aAAa,CAAC,MAAM,CAAC,CAEnC;IAED,oJAAoJ;IACpJ,IAAI,iBAAiB,IAAI,aAAa,CAAC,MAAM,CAAC,CAE7C;IAED;;;;;OAKG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAWxC,0HAA0H;IAC1H,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAIrC,OAAO,CAAC,aAAa;IAOrB,6HAA6H;IACvH,IAAI,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAKjD,uHAAuH;IACjH,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAK7B;;;;;OAKG;IACG,YAAY,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC;IAK3E,OAAO,CAAC,YAAY;CASrB"}