@helipod/test 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.
- package/README.md +25 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @helipod/test
|
|
2
|
+
|
|
3
|
+
An in-process test harness for Helipod apps: `createTestHelipod` boots a real
|
|
4
|
+
`EmbeddedRuntime` — the actual transactor, query engine, and reactive subscription manager — over
|
|
5
|
+
an in-memory SQLite database, so your query/mutation/action tests exercise real behavior instead of
|
|
6
|
+
a mocked `ctx.db`.
|
|
7
|
+
|
|
8
|
+
See **[`docs/enduser/testing.md`](../../docs/enduser/testing.md)** for the full guide (the 3-layer
|
|
9
|
+
testing model, usage, and the documented differences from Convex's testing tooling).
|
|
10
|
+
|
|
11
|
+
## Public API
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createTestHelipod, type TestHelipod, type CreateTestOptions, type TestSubscription } from "@helipod/test";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `createTestHelipod(opts: CreateTestOptions): Promise<TestHelipod>` — boots a fresh, isolated
|
|
18
|
+
backend (its own `:memory:` SQLite database and temp blob directory).
|
|
19
|
+
- `TestHelipod` — `query` / `mutation` / `action`, `run` (privileged, bypasses the public gate),
|
|
20
|
+
`withIdentity`, `fetch`, `subscribe`, `finishScheduledFunctions` / `advanceTimers`, `close`.
|
|
21
|
+
- `CreateTestOptions` — `{ modules, components?, schema?, now? }`.
|
|
22
|
+
- `TestSubscription<T>` — the value returned by `t.subscribe(...)`: `value()`, `onChange(cb)`,
|
|
23
|
+
`unsubscribe()`.
|
|
24
|
+
|
|
25
|
+
Always `await t.close()` — see the guide for isolation/cleanup guarantees.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { RegisteredFunction } from '@helipod/executor';
|
|
2
|
+
import { SchemaDefinition, Value } from '@helipod/values';
|
|
3
|
+
import { FunctionReference } from '@helipod/client';
|
|
4
|
+
import { DocStore } from '@helipod/docstore';
|
|
5
|
+
import { ComponentDefinition } from '@helipod/component';
|
|
6
|
+
|
|
7
|
+
interface FlattenedModules {
|
|
8
|
+
moduleMap: Record<string, RegisteredFunction>;
|
|
9
|
+
schemaModule: unknown | null;
|
|
10
|
+
httpModule: unknown | null;
|
|
11
|
+
}
|
|
12
|
+
declare function flattenModules(modules: Record<string, unknown>, functionsRoot?: string): Promise<FlattenedModules>;
|
|
13
|
+
|
|
14
|
+
interface TestSubscription<T> {
|
|
15
|
+
/** Latest computed value, or `undefined` until the first compute arrives. */
|
|
16
|
+
value(): T | undefined;
|
|
17
|
+
/** Registers a listener fired on every reactive re-run (and the first compute); returns a remover. */
|
|
18
|
+
onChange(cb: (v: T) => void): () => void;
|
|
19
|
+
/** Tears down the underlying query subscription. */
|
|
20
|
+
unsubscribe(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CreateTestOptions {
|
|
24
|
+
modules: Record<string, unknown>;
|
|
25
|
+
components?: ComponentDefinition[];
|
|
26
|
+
schema?: SchemaDefinition | "auto" | false;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
/**
|
|
29
|
+
* The document store to run against. Defaults to an in-memory SQLite store. Pass an alternative
|
|
30
|
+
* (e.g. a `PostgresDocStore`) to exercise the real engine against a different backend — its
|
|
31
|
+
* `setupSchema()` is called by the runtime and `close()` by the harness cleanup. The store must
|
|
32
|
+
* start empty; the harness owns its lifecycle from here.
|
|
33
|
+
*/
|
|
34
|
+
store?: DocStore;
|
|
35
|
+
/**
|
|
36
|
+
* The leading path segment `import.meta.glob`-sourced `modules` keys are prefixed with (e.g.
|
|
37
|
+
* `import.meta.glob("./helipod/**\/*.ts")` yields keys like `./helipod/messages.ts`) —
|
|
38
|
+
* stripped so a glob-sourced module registers under the same `messages:send`-style path an
|
|
39
|
+
* explicit `{ "messages.ts": messages }` map would. Defaults to the same value as
|
|
40
|
+
* `DEFAULT_FUNCTIONS_DIR` in `@helipod/cli`. Only needed if `modules` came from a glob AND the
|
|
41
|
+
* project's `helipod.config.ts` sets a non-default `functionsDir` — an explicit `{ "messages.ts":
|
|
42
|
+
* ... }` map (no path prefix to strip) is unaffected either way.
|
|
43
|
+
*/
|
|
44
|
+
functionsRoot?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Args = Record<string, Value>;
|
|
48
|
+
interface TestHelipod {
|
|
49
|
+
query<T = unknown>(ref: FunctionReference | string, args?: Args): Promise<T>;
|
|
50
|
+
mutation<T = unknown>(ref: FunctionReference | string, args?: Args): Promise<T>;
|
|
51
|
+
action<T = unknown>(ref: FunctionReference | string, args?: Args): Promise<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Runs `fn` with a full db-writer `ctx` (the engine's `MutationCtx`, typed `any` here to avoid
|
|
54
|
+
* leaking internal types) inside one real transaction — for test setup/assertions without
|
|
55
|
+
* having to define an app function. Backed by the `_test:_run` system mutation. Always
|
|
56
|
+
* privileged (no ambient identity), regardless of which view it's called on.
|
|
57
|
+
*/
|
|
58
|
+
run<T>(fn: (ctx: any) => Promise<T>): Promise<T>;
|
|
59
|
+
/**
|
|
60
|
+
* Routes `request` through the app's `http.ts` router, exactly as the real `helipod dev`/`serve`
|
|
61
|
+
* HTTP handler dispatches to an `httpAction` (see `packages/cli/src/http-handler.ts`). Returns a
|
|
62
|
+
* plain 404 `Response` for an unmatched method+path — it never throws for that case. This view's
|
|
63
|
+
* identity (set via `withIdentity`) is passed through as the httpAction's `ctx` identity, taking
|
|
64
|
+
* precedence over the request's own `Authorization` header if both are present.
|
|
65
|
+
*/
|
|
66
|
+
fetch(request: Request): Promise<Response>;
|
|
67
|
+
/**
|
|
68
|
+
* Subscribes to a reactive query over the REAL client -> sync protocol -> SubscriptionManager ->
|
|
69
|
+
* engine path (a loopback `HelipodClient`, shared across every `subscribe` call on this
|
|
70
|
+
* backend and every view of it — built lazily on first use, closed in `close()`). A committed
|
|
71
|
+
* write re-runs and re-pushes only when its write set intersects this query's recorded read
|
|
72
|
+
* set (surgical, range-based invalidation) — no polling.
|
|
73
|
+
*
|
|
74
|
+
* v1 limitation: always uses the base (no-identity) client, regardless of which view (see
|
|
75
|
+
* `withIdentity`) `subscribe` is called on — there is no per-identity subscription yet.
|
|
76
|
+
*/
|
|
77
|
+
subscribe<T = any>(ref: FunctionReference | string, args?: Args): TestSubscription<T>;
|
|
78
|
+
/**
|
|
79
|
+
* Returns a view of the SAME backend whose `query`/`mutation`/`action` calls carry `identity` as
|
|
80
|
+
* the ambient session token (reaching user code only through a context provider's
|
|
81
|
+
* `build({ identity })`, e.g. `components/auth`'s `ctx.auth` — there is no bare `ctx.identity`).
|
|
82
|
+
* `run`/`close` remain shared with the backend, not per-view.
|
|
83
|
+
*/
|
|
84
|
+
withIdentity(identity: string): TestHelipod;
|
|
85
|
+
/**
|
|
86
|
+
* Deterministically drives every currently- and eventually-due `ctx.scheduler.runAfter`/`runAt`
|
|
87
|
+
* job (including cascades) to completion by advancing the harness's virtual clock — no real
|
|
88
|
+
* timers/sleeps. A clean no-op if no `@helipod/scheduler` component was composed (via
|
|
89
|
+
* `opts.components`). Throws if `opts.now` was supplied to `createTestHelipod` (the harness
|
|
90
|
+
* then doesn't own the clock). See `./scheduler.ts`.
|
|
91
|
+
*/
|
|
92
|
+
finishScheduledFunctions(): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Advances the harness's virtual clock by `ms` and drives one scheduler-driver pass (a no-op
|
|
95
|
+
* pass if no scheduler is composed) — the one-shot counterpart to `finishScheduledFunctions`.
|
|
96
|
+
* Throws if `opts.now` was supplied to `createTestHelipod`. See `./scheduler.ts`.
|
|
97
|
+
*/
|
|
98
|
+
advanceTimers(ms: number): Promise<void>;
|
|
99
|
+
close(): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
declare function createTestHelipod(opts: CreateTestOptions): Promise<TestHelipod>;
|
|
102
|
+
|
|
103
|
+
export { type CreateTestOptions, type FlattenedModules, type TestHelipod, type TestSubscription, createTestHelipod, flattenModules };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// src/flatten.ts
|
|
2
|
+
var DEFAULT_FUNCTIONS_ROOT = "helipod";
|
|
3
|
+
function isRegisteredFunction(v) {
|
|
4
|
+
return typeof v === "object" && v !== null && typeof v.type === "string" && typeof v.handler === "function";
|
|
5
|
+
}
|
|
6
|
+
function escapeRegExp(s) {
|
|
7
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8
|
+
}
|
|
9
|
+
function normalizeModulePath(key, functionsRoot) {
|
|
10
|
+
const rootPattern = new RegExp(`^${escapeRegExp(functionsRoot)}/`);
|
|
11
|
+
return key.replace(/^(\.\.?\/)+/, "").replace(rootPattern, "").replace(/\.(ts|tsx|js|jsx|mts|cts|mjs|cjs)$/, "");
|
|
12
|
+
}
|
|
13
|
+
async function resolveModule(v) {
|
|
14
|
+
return typeof v === "function" ? await v() : v;
|
|
15
|
+
}
|
|
16
|
+
async function flattenModules(modules, functionsRoot = DEFAULT_FUNCTIONS_ROOT) {
|
|
17
|
+
const moduleMap = {};
|
|
18
|
+
let schemaModule = null;
|
|
19
|
+
let httpModule = null;
|
|
20
|
+
for (const [rawKey, rawVal] of Object.entries(modules)) {
|
|
21
|
+
const modPath = normalizeModulePath(rawKey, functionsRoot);
|
|
22
|
+
const mod = await resolveModule(rawVal);
|
|
23
|
+
const def = mod && typeof mod === "object" ? mod.default : void 0;
|
|
24
|
+
if (modPath === "schema") {
|
|
25
|
+
schemaModule = def ?? mod;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (modPath === "http") {
|
|
29
|
+
httpModule = def ?? mod;
|
|
30
|
+
}
|
|
31
|
+
for (const [exportName, exportVal] of Object.entries(mod ?? {})) {
|
|
32
|
+
if (isRegisteredFunction(exportVal)) moduleMap[`${modPath}:${exportName}`] = exportVal;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { moduleMap, schemaModule, httpModule };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/harness.ts
|
|
39
|
+
import { getFunctionPath as getFunctionPath2 } from "@helipod/client";
|
|
40
|
+
|
|
41
|
+
// src/compose.ts
|
|
42
|
+
import * as fs from "fs";
|
|
43
|
+
import * as os from "os";
|
|
44
|
+
import * as path from "path";
|
|
45
|
+
import { SqliteDocStore, NodeSqliteAdapter } from "@helipod/docstore-sqlite";
|
|
46
|
+
import { composeComponents } from "@helipod/component";
|
|
47
|
+
import { EmbeddedRuntime } from "@helipod/runtime-embedded";
|
|
48
|
+
import { defineSchema } from "@helipod/values";
|
|
49
|
+
import { mutation, matchRoute } from "@helipod/executor";
|
|
50
|
+
import {
|
|
51
|
+
STORAGE_TABLE,
|
|
52
|
+
STORAGE_TABLE_NUMBER,
|
|
53
|
+
storageTableDefinition,
|
|
54
|
+
storageModules,
|
|
55
|
+
storageContextProvider,
|
|
56
|
+
storageReaper
|
|
57
|
+
} from "@helipod/storage";
|
|
58
|
+
import { FsBlobStore } from "@helipod/blobstore-fs";
|
|
59
|
+
|
|
60
|
+
// src/reactivity.ts
|
|
61
|
+
import { HelipodClient, loopbackTransport, getFunctionPath } from "@helipod/client";
|
|
62
|
+
function createReactivity(runtime) {
|
|
63
|
+
let client = null;
|
|
64
|
+
function ensureClient() {
|
|
65
|
+
if (!client) client = new HelipodClient(loopbackTransport(runtime.connect()));
|
|
66
|
+
return client;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
subscribe(ref, args = {}) {
|
|
70
|
+
const path2 = getFunctionPath(ref);
|
|
71
|
+
let latest;
|
|
72
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
73
|
+
const unsubscribeClient = ensureClient().subscribe(path2, args, (v) => {
|
|
74
|
+
latest = v;
|
|
75
|
+
for (const listener of listeners) listener(latest);
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
value: () => latest,
|
|
79
|
+
onChange(cb) {
|
|
80
|
+
listeners.add(cb);
|
|
81
|
+
return () => listeners.delete(cb);
|
|
82
|
+
},
|
|
83
|
+
unsubscribe() {
|
|
84
|
+
unsubscribeClient();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
async close() {
|
|
89
|
+
if (client) {
|
|
90
|
+
client.close();
|
|
91
|
+
client = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/compose.ts
|
|
98
|
+
async function buildRuntime(opts) {
|
|
99
|
+
const flat = await flattenModules(opts.modules, opts.functionsRoot);
|
|
100
|
+
const schemaDef = (
|
|
101
|
+
// `opts.schema &&` already excludes `false` (falsy) as well as `undefined`, so an explicit
|
|
102
|
+
// `!== false` check afterward is both redundant and a TS error (no overlap once narrowed).
|
|
103
|
+
opts.schema && opts.schema !== "auto" ? opts.schema : flat.schemaModule ?? defineSchema({})
|
|
104
|
+
);
|
|
105
|
+
const schemaJson = schemaDef.export();
|
|
106
|
+
schemaJson.tables[STORAGE_TABLE] = storageTableDefinition.export();
|
|
107
|
+
const composed = composeComponents({ schemaJson, moduleMap: flat.moduleMap }, opts.components ?? [], {
|
|
108
|
+
[STORAGE_TABLE]: STORAGE_TABLE_NUMBER
|
|
109
|
+
});
|
|
110
|
+
const resolvedRoutes = [];
|
|
111
|
+
const router = flat.httpModule;
|
|
112
|
+
if (router?.routes) {
|
|
113
|
+
const pathByFn = /* @__PURE__ */ new Map();
|
|
114
|
+
for (const [path2, fn] of Object.entries(composed.moduleMap)) pathByFn.set(fn, path2);
|
|
115
|
+
for (const r of router.routes) {
|
|
116
|
+
const handlerPath = pathByFn.get(r.handler);
|
|
117
|
+
if (!handlerPath) {
|
|
118
|
+
const where = r.path ?? r.pathPrefix ?? "?";
|
|
119
|
+
throw new Error(
|
|
120
|
+
`http.route handler for "${where}" must be an exported httpAction (declare it as a named export of an app module)`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
resolvedRoutes.push({
|
|
124
|
+
method: r.method,
|
|
125
|
+
...r.path !== void 0 ? { path: r.path } : { pathPrefix: r.pathPrefix },
|
|
126
|
+
handlerPath
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "sb-test-"));
|
|
131
|
+
const blobStore = new FsBlobStore({ root: tempDir });
|
|
132
|
+
const ownsClock = opts.now === void 0;
|
|
133
|
+
let clockMs = 17e11;
|
|
134
|
+
const now = opts.now ?? (() => clockMs);
|
|
135
|
+
let currentRunFn = null;
|
|
136
|
+
let runResult = void 0;
|
|
137
|
+
const systemModules = {
|
|
138
|
+
"_test:_run": mutation(async (ctx) => {
|
|
139
|
+
if (!currentRunFn) throw new Error("_test:_run invoked with no callback set");
|
|
140
|
+
runResult = await currentRunFn(ctx);
|
|
141
|
+
return null;
|
|
142
|
+
})
|
|
143
|
+
};
|
|
144
|
+
const store = opts.store ?? new SqliteDocStore(new NodeSqliteAdapter());
|
|
145
|
+
let runtime;
|
|
146
|
+
try {
|
|
147
|
+
runtime = await EmbeddedRuntime.create({
|
|
148
|
+
store,
|
|
149
|
+
catalog: composed.catalog,
|
|
150
|
+
// `_storage:*` built-ins go in `modules` — the action-mode `ctx.storage` reaches them through
|
|
151
|
+
// the trusted `invoke`, and the reaper driver through `runFunction`, both of which resolve
|
|
152
|
+
// `modules` (not `systemModules`).
|
|
153
|
+
modules: { ...composed.moduleMap, ...storageModules },
|
|
154
|
+
systemModules,
|
|
155
|
+
componentNames: composed.componentNames,
|
|
156
|
+
contextProviders: [
|
|
157
|
+
storageContextProvider(blobStore, { signingKey: "helipod-test-signing-key" }),
|
|
158
|
+
...composed.contextProviders
|
|
159
|
+
],
|
|
160
|
+
policyRegistry: composed.policyRegistry,
|
|
161
|
+
policyProviders: composed.policyProviders,
|
|
162
|
+
relationRegistry: composed.relationRegistry,
|
|
163
|
+
bootSteps: composed.bootSteps,
|
|
164
|
+
drivers: [storageReaper(blobStore), ...composed.drivers],
|
|
165
|
+
tableNumbers: composed.tableNumbers,
|
|
166
|
+
now
|
|
167
|
+
});
|
|
168
|
+
} catch (err) {
|
|
169
|
+
await store.close();
|
|
170
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
const reactivity = createReactivity(runtime);
|
|
174
|
+
const cleanup = async () => {
|
|
175
|
+
await reactivity.close();
|
|
176
|
+
await runtime.stopDrivers();
|
|
177
|
+
await store.close();
|
|
178
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
179
|
+
};
|
|
180
|
+
const dispatchHttp = async (request, identity) => {
|
|
181
|
+
const url = new URL(request.url);
|
|
182
|
+
const match = matchRoute(resolvedRoutes, request.method, url.pathname);
|
|
183
|
+
if (!match) return new Response("Not Found", { status: 404 });
|
|
184
|
+
const auth = request.headers.get("authorization");
|
|
185
|
+
const headerIdentity = auth && auth.startsWith("Bearer ") ? auth.slice("Bearer ".length) : null;
|
|
186
|
+
return runtime.runHttpAction(match.handlerPath, request, { identity: identity ?? headerIdentity });
|
|
187
|
+
};
|
|
188
|
+
const schedulerDriver = composed.drivers.find((d) => d.name === "scheduler");
|
|
189
|
+
return {
|
|
190
|
+
runtime,
|
|
191
|
+
tableNumbers: composed.tableNumbers,
|
|
192
|
+
schemaJson,
|
|
193
|
+
cleanup,
|
|
194
|
+
setRunFn: (fn) => {
|
|
195
|
+
currentRunFn = fn;
|
|
196
|
+
},
|
|
197
|
+
takeRunResult: () => {
|
|
198
|
+
const r = runResult;
|
|
199
|
+
runResult = void 0;
|
|
200
|
+
return r;
|
|
201
|
+
},
|
|
202
|
+
dispatchHttp,
|
|
203
|
+
reactivity,
|
|
204
|
+
ownsClock,
|
|
205
|
+
advanceClock: (ms) => {
|
|
206
|
+
if (!ownsClock) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
"advanceClock/advanceTimers/finishScheduledFunctions require the harness-owned virtual clock \u2014 createTestHelipod() was given a custom `now`, so the harness has no clock to advance. Omit `opts.now` to use scheduler time control."
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
clockMs += ms;
|
|
212
|
+
},
|
|
213
|
+
getSchedulerDriver: () => schedulerDriver
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/scheduler.ts
|
|
218
|
+
var SCHEDULER_JOBS_TABLE = "scheduler/jobs";
|
|
219
|
+
var OUTSTANDING_STATES = /* @__PURE__ */ new Set(["pending", "inProgress"]);
|
|
220
|
+
var STEP_MS = 36e5;
|
|
221
|
+
var MAX_ITERATIONS = 100;
|
|
222
|
+
async function scanSchedulerJobs(built) {
|
|
223
|
+
built.setRunFn(async (ctx) => await ctx.db.query(SCHEDULER_JOBS_TABLE, "by_creation").collect());
|
|
224
|
+
try {
|
|
225
|
+
await built.runtime.runSystem("_test:_run", {});
|
|
226
|
+
return built.takeRunResult() ?? [];
|
|
227
|
+
} finally {
|
|
228
|
+
built.setRunFn(null);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function hasOutstandingJobs(built) {
|
|
232
|
+
const rows = await scanSchedulerJobs(built);
|
|
233
|
+
return rows.some((row) => OUTSTANDING_STATES.has(String(row.state)));
|
|
234
|
+
}
|
|
235
|
+
async function finishScheduledFunctions(built) {
|
|
236
|
+
const driver = built.getSchedulerDriver();
|
|
237
|
+
if (!driver) return;
|
|
238
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
239
|
+
if (!await hasOutstandingJobs(built)) return;
|
|
240
|
+
built.advanceClock(STEP_MS);
|
|
241
|
+
await driver.__tick();
|
|
242
|
+
}
|
|
243
|
+
if (!await hasOutstandingJobs(built)) return;
|
|
244
|
+
throw new Error(
|
|
245
|
+
`finishScheduledFunctions: scheduled jobs did not settle after ${MAX_ITERATIONS} iterations (advancing the virtual clock by ${STEP_MS}ms each time) \u2014 check for a cron or a runAfter chain that keeps rescheduling itself forever.`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
async function advanceTimers(built, ms) {
|
|
249
|
+
built.advanceClock(ms);
|
|
250
|
+
const driver = built.getSchedulerDriver();
|
|
251
|
+
if (driver) await driver.__tick();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/harness.ts
|
|
255
|
+
async function createTestHelipod(opts) {
|
|
256
|
+
const built = await buildRuntime(opts);
|
|
257
|
+
const { runtime } = built;
|
|
258
|
+
function makeView(identity) {
|
|
259
|
+
return {
|
|
260
|
+
async query(ref, args = {}) {
|
|
261
|
+
return (await runtime.run(getFunctionPath2(ref), args, { identity })).value;
|
|
262
|
+
},
|
|
263
|
+
async mutation(ref, args = {}) {
|
|
264
|
+
return (await runtime.run(getFunctionPath2(ref), args, { identity })).value;
|
|
265
|
+
},
|
|
266
|
+
async action(ref, args = {}) {
|
|
267
|
+
return (await runtime.runAction(getFunctionPath2(ref), args, { identity })).value;
|
|
268
|
+
},
|
|
269
|
+
async run(fn) {
|
|
270
|
+
built.setRunFn(fn);
|
|
271
|
+
try {
|
|
272
|
+
await runtime.runSystem("_test:_run", {});
|
|
273
|
+
return built.takeRunResult();
|
|
274
|
+
} finally {
|
|
275
|
+
built.setRunFn(null);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
async fetch(request) {
|
|
279
|
+
return built.dispatchHttp(request, identity);
|
|
280
|
+
},
|
|
281
|
+
subscribe(ref, args = {}) {
|
|
282
|
+
return built.reactivity.subscribe(ref, args);
|
|
283
|
+
},
|
|
284
|
+
withIdentity(id) {
|
|
285
|
+
return makeView(id);
|
|
286
|
+
},
|
|
287
|
+
async finishScheduledFunctions() {
|
|
288
|
+
await finishScheduledFunctions(built);
|
|
289
|
+
},
|
|
290
|
+
async advanceTimers(ms) {
|
|
291
|
+
await advanceTimers(built, ms);
|
|
292
|
+
},
|
|
293
|
+
async close() {
|
|
294
|
+
await built.cleanup();
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return makeView(null);
|
|
299
|
+
}
|
|
300
|
+
export {
|
|
301
|
+
createTestHelipod,
|
|
302
|
+
flattenModules
|
|
303
|
+
};
|
|
304
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/flatten.ts","../src/harness.ts","../src/compose.ts","../src/reactivity.ts","../src/scheduler.ts"],"sourcesContent":["import type { RegisteredFunction } from \"@helipod/executor\";\n\n// Mirrors `DEFAULT_FUNCTIONS_DIR` from `@helipod/cli` (`packages/cli/src/functions-dir.ts`).\n// `packages/test` doesn't depend on `@helipod/cli`, so the value is duplicated rather than\n// imported — same pattern as `packages/vite/src/index.ts`'s own `DEFAULT_FUNCTIONS_DIR` mirror.\nconst DEFAULT_FUNCTIONS_ROOT = \"helipod\";\n\nfunction isRegisteredFunction(v: unknown): v is RegisteredFunction {\n return typeof v === \"object\" && v !== null && typeof (v as { type?: unknown }).type === \"string\"\n && typeof (v as { handler?: unknown }).handler === \"function\";\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n// `import.meta.glob(\"./helipod/**/*.ts\")` keys come back prefixed with the glob's own leading\n// path segments (`./helipod/messages.ts`, `../helipod/messages.ts`, etc.) — not just an\n// extension to strip. Normalize to the same function-path root the codegen `api`/string refs use\n// (relative to the app's functions root), so a glob-sourced module registers under the exact same\n// path an explicit `{ \"messages.ts\": messages }` map would. `functionsRoot` is the ONE leading path\n// segment to strip after the `./`/`../` prefix — it defaults to `DEFAULT_FUNCTIONS_ROOT` but a\n// caller on a non-default `functionsDir` (via `helipod.config.ts`) must pass its actual value;\n// there is no implicit fallback to any other name (including the legacy `convex/`), by design.\nfunction normalizeModulePath(key: string, functionsRoot: string): string {\n const rootPattern = new RegExp(`^${escapeRegExp(functionsRoot)}/`);\n return key\n .replace(/^(\\.\\.?\\/)+/, \"\") // strip leading ./ ../ ../../\n .replace(rootPattern, \"\") // strip the configured functions root, if present\n .replace(/\\.(ts|tsx|js|jsx|mts|cts|mjs|cjs)$/, \"\"); // strip extension\n}\n\nasync function resolveModule(v: unknown): Promise<unknown> {\n return typeof v === \"function\" ? await (v as () => Promise<unknown>)() : v;\n}\n\nexport interface FlattenedModules {\n moduleMap: Record<string, RegisteredFunction>;\n schemaModule: unknown | null;\n httpModule: unknown | null;\n}\n\nexport async function flattenModules(\n modules: Record<string, unknown>,\n functionsRoot: string = DEFAULT_FUNCTIONS_ROOT,\n): Promise<FlattenedModules> {\n const moduleMap: Record<string, RegisteredFunction> = {};\n let schemaModule: unknown | null = null;\n let httpModule: unknown | null = null;\n for (const [rawKey, rawVal] of Object.entries(modules)) {\n const modPath = normalizeModulePath(rawKey, functionsRoot);\n const mod = (await resolveModule(rawVal)) as Record<string, unknown>;\n const def = mod && typeof mod === \"object\" ? (mod as { default?: unknown }).default : undefined;\n if (modPath === \"schema\") { schemaModule = def ?? mod; continue; }\n // http.ts's default export is the `HttpRouter` itself (captured as `httpModule` for route\n // resolution), but its NAMED exports are the httpAction `RegisteredFunction`s the router's\n // routes point to — those still need to land in `moduleMap` as `http:<name>` so `dispatchHttp`\n // can resolve a route's handler VALUE back to a dispatchable path (no `continue` here, unlike\n // `schema` above: the loop below must still run for http.ts).\n if (modPath === \"http\") { httpModule = def ?? mod; }\n for (const [exportName, exportVal] of Object.entries(mod ?? {})) {\n if (isRegisteredFunction(exportVal)) moduleMap[`${modPath}:${exportName}`] = exportVal;\n }\n }\n return { moduleMap, schemaModule, httpModule };\n}\n","import type { Value } from \"@helipod/values\";\nimport { getFunctionPath, type FunctionReference } from \"@helipod/client\";\nimport { buildRuntime, type CreateTestOptions, type BuiltRuntime } from \"./compose\";\nimport type { TestSubscription } from \"./reactivity\";\nimport { finishScheduledFunctions, advanceTimers } from \"./scheduler\";\n\ntype Args = Record<string, Value>;\n\nexport interface TestHelipod {\n query<T = unknown>(ref: FunctionReference | string, args?: Args): Promise<T>;\n mutation<T = unknown>(ref: FunctionReference | string, args?: Args): Promise<T>;\n action<T = unknown>(ref: FunctionReference | string, args?: Args): Promise<T>;\n /**\n * Runs `fn` with a full db-writer `ctx` (the engine's `MutationCtx`, typed `any` here to avoid\n * leaking internal types) inside one real transaction — for test setup/assertions without\n * having to define an app function. Backed by the `_test:_run` system mutation. Always\n * privileged (no ambient identity), regardless of which view it's called on.\n */\n run<T>(fn: (ctx: any) => Promise<T>): Promise<T>;\n /**\n * Routes `request` through the app's `http.ts` router, exactly as the real `helipod dev`/`serve`\n * HTTP handler dispatches to an `httpAction` (see `packages/cli/src/http-handler.ts`). Returns a\n * plain 404 `Response` for an unmatched method+path — it never throws for that case. This view's\n * identity (set via `withIdentity`) is passed through as the httpAction's `ctx` identity, taking\n * precedence over the request's own `Authorization` header if both are present.\n */\n fetch(request: Request): Promise<Response>;\n /**\n * Subscribes to a reactive query over the REAL client -> sync protocol -> SubscriptionManager ->\n * engine path (a loopback `HelipodClient`, shared across every `subscribe` call on this\n * backend and every view of it — built lazily on first use, closed in `close()`). A committed\n * write re-runs and re-pushes only when its write set intersects this query's recorded read\n * set (surgical, range-based invalidation) — no polling.\n *\n * v1 limitation: always uses the base (no-identity) client, regardless of which view (see\n * `withIdentity`) `subscribe` is called on — there is no per-identity subscription yet.\n */\n subscribe<T = any>(ref: FunctionReference | string, args?: Args): TestSubscription<T>;\n /**\n * Returns a view of the SAME backend whose `query`/`mutation`/`action` calls carry `identity` as\n * the ambient session token (reaching user code only through a context provider's\n * `build({ identity })`, e.g. `components/auth`'s `ctx.auth` — there is no bare `ctx.identity`).\n * `run`/`close` remain shared with the backend, not per-view.\n */\n withIdentity(identity: string): TestHelipod;\n /**\n * Deterministically drives every currently- and eventually-due `ctx.scheduler.runAfter`/`runAt`\n * job (including cascades) to completion by advancing the harness's virtual clock — no real\n * timers/sleeps. A clean no-op if no `@helipod/scheduler` component was composed (via\n * `opts.components`). Throws if `opts.now` was supplied to `createTestHelipod` (the harness\n * then doesn't own the clock). See `./scheduler.ts`.\n */\n finishScheduledFunctions(): Promise<void>;\n /**\n * Advances the harness's virtual clock by `ms` and drives one scheduler-driver pass (a no-op\n * pass if no scheduler is composed) — the one-shot counterpart to `finishScheduledFunctions`.\n * Throws if `opts.now` was supplied to `createTestHelipod`. See `./scheduler.ts`.\n */\n advanceTimers(ms: number): Promise<void>;\n close(): Promise<void>;\n}\n\nexport async function createTestHelipod(opts: CreateTestOptions): Promise<TestHelipod> {\n const built: BuiltRuntime = await buildRuntime(opts);\n const { runtime } = built;\n\n function makeView(identity: string | null): TestHelipod {\n return {\n async query(ref, args = {}) {\n return (await runtime.run(getFunctionPath(ref), args as never, { identity })).value as never;\n },\n async mutation(ref, args = {}) {\n return (await runtime.run(getFunctionPath(ref), args as never, { identity })).value as never;\n },\n async action(ref, args = {}) {\n return (await runtime.runAction(getFunctionPath(ref), args as never, { identity })).value as never;\n },\n async run(fn) {\n built.setRunFn(fn as (ctx: unknown) => Promise<unknown>);\n try {\n await runtime.runSystem(\"_test:_run\", {});\n return built.takeRunResult() as never;\n } finally {\n built.setRunFn(null);\n }\n },\n async fetch(request) {\n return built.dispatchHttp(request, identity);\n },\n subscribe(ref, args = {}) {\n return built.reactivity.subscribe(ref, args);\n },\n withIdentity(id) {\n return makeView(id);\n },\n async finishScheduledFunctions() {\n await finishScheduledFunctions(built);\n },\n async advanceTimers(ms) {\n await advanceTimers(built, ms);\n },\n async close() {\n await built.cleanup();\n },\n };\n }\n\n return makeView(null);\n}\n","import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { DocStore } from \"@helipod/docstore\";\nimport { SqliteDocStore, NodeSqliteAdapter } from \"@helipod/docstore-sqlite\";\nimport { composeComponents, type ComponentDefinition } from \"@helipod/component\";\nimport { EmbeddedRuntime } from \"@helipod/runtime-embedded\";\nimport type { SchedulerDriver } from \"@helipod/scheduler\";\nimport { defineSchema, type SchemaDefinition, type SchemaDefinitionJSON } from \"@helipod/values\";\nimport { mutation, matchRoute, type RegisteredFunction, type RouteEntry } from \"@helipod/executor\";\nimport {\n STORAGE_TABLE,\n STORAGE_TABLE_NUMBER,\n storageTableDefinition,\n storageModules,\n storageContextProvider,\n storageReaper,\n} from \"@helipod/storage\";\nimport { FsBlobStore } from \"@helipod/blobstore-fs\";\nimport { flattenModules } from \"./flatten\";\nimport { createReactivity, type Reactivity } from \"./reactivity\";\n\nexport interface CreateTestOptions {\n modules: Record<string, unknown>;\n components?: ComponentDefinition[];\n schema?: SchemaDefinition | \"auto\" | false;\n now?: () => number;\n /**\n * The document store to run against. Defaults to an in-memory SQLite store. Pass an alternative\n * (e.g. a `PostgresDocStore`) to exercise the real engine against a different backend — its\n * `setupSchema()` is called by the runtime and `close()` by the harness cleanup. The store must\n * start empty; the harness owns its lifecycle from here.\n */\n store?: DocStore;\n /**\n * The leading path segment `import.meta.glob`-sourced `modules` keys are prefixed with (e.g.\n * `import.meta.glob(\"./helipod/**\\/*.ts\")` yields keys like `./helipod/messages.ts`) —\n * stripped so a glob-sourced module registers under the same `messages:send`-style path an\n * explicit `{ \"messages.ts\": messages }` map would. Defaults to the same value as\n * `DEFAULT_FUNCTIONS_DIR` in `@helipod/cli`. Only needed if `modules` came from a glob AND the\n * project's `helipod.config.ts` sets a non-default `functionsDir` — an explicit `{ \"messages.ts\":\n * ... }` map (no path prefix to strip) is unaffected either way.\n */\n functionsRoot?: string;\n}\n\n/** A single `http.ts` route, with its handler resolved from a `RegisteredFunction` value to the\n * `path:name` function path `runtime.runHttpAction` looks up — mirrors `packages/cli/src/project.ts`'s\n * `ResolvedRoute`. */\nexport interface ResolvedRoute {\n method: string;\n path?: string;\n pathPrefix?: string;\n handlerPath: string;\n}\n\nexport interface BuiltRuntime {\n runtime: EmbeddedRuntime;\n tableNumbers: Record<string, number>;\n schemaJson: SchemaDefinitionJSON;\n cleanup: () => Promise<void>;\n /** Sets the callback `_test:_run`'s handler invokes with a full db-writer `ctx`. */\n setRunFn: (fn: ((ctx: unknown) => Promise<unknown>) | null) => void;\n /** Reads back (and clears) the value the last `setRunFn` callback returned. */\n takeRunResult: () => unknown;\n /**\n * Routes a raw `Request` through the app's `http.ts` router (if any), the same way the real\n * `helipod dev`/`serve` HTTP handler dispatches to an `httpAction` — see\n * `packages/cli/src/http-handler.ts`. Returns a plain `Response(\"Not Found\", { status: 404 })`\n * for an unmatched method+path, never throws for that case. `identity` (if non-null) wins over\n * the request's own `Authorization` header, which is used as a fallback and Bearer-stripped\n * (`Bearer abc123` -> `abc123`, non-Bearer -> null) — exact parity with the real engine's\n * httpAction identity passthrough (there, identity is ALWAYS derived from the header, since there\n * is no session concept at the raw-HTTP layer).\n */\n dispatchHttp: (request: Request, identity: string | null) => Promise<Response>;\n /**\n * The harness-shared reactive-subscription surface (`t.subscribe`), built lazily on first use\n * over a real loopback connection to `runtime` — see `./reactivity.ts`. Its `HelipodClient`\n * is closed in `cleanup` BEFORE `runtime.stopDrivers()`, so no loopback session outlives the\n * runtime it's connected to.\n */\n reactivity: Reactivity;\n /**\n * True when this harness owns the virtual clock — i.e. `opts.now` was NOT supplied, so `now()`\n * is backed by the mutable `clockMs` this module owns. When `opts.now` WAS supplied, the caller\n * owns time and `advanceClock` throws (mutating `clockMs` would have no effect on `now()`,\n * which reads the caller's own function instead).\n */\n ownsClock: boolean;\n /**\n * Advances the harness-owned virtual clock by `ms` (relative to its current value). Backs\n * `t.advanceTimers`/`t.finishScheduledFunctions` (see `./scheduler.ts`). Throws if `ownsClock`\n * is false.\n */\n advanceClock: (ms: number) => void;\n /**\n * Finds `@helipod/scheduler`'s driver among the composed drivers (by `name === \"scheduler\"`),\n * if `defineScheduler()` was included in `opts.components` — `undefined` otherwise. Used by\n * `./scheduler.ts` to drive `__tick()`.\n */\n getSchedulerDriver: () => SchedulerDriver | undefined;\n}\n\nexport async function buildRuntime(opts: CreateTestOptions): Promise<BuiltRuntime> {\n const flat = await flattenModules(opts.modules, opts.functionsRoot);\n // Resolve the schema: explicit option wins; else the schema.ts module; else empty.\n const schemaDef: SchemaDefinition =\n // `opts.schema &&` already excludes `false` (falsy) as well as `undefined`, so an explicit\n // `!== false` check afterward is both redundant and a TS error (no overlap once narrowed).\n opts.schema && opts.schema !== \"auto\"\n ? opts.schema\n : (flat.schemaModule as SchemaDefinition | null) ?? defineSchema({});\n const schemaJson = schemaDef.export();\n\n // File storage is an ALWAYS-ON core feature (not opt-in) — inject its reserved `_storage` system\n // table into the schema BEFORE composing, mirroring `packages/cli`'s `loadProject` (see\n // `packages/cli/src/project.ts`), so the catalog/tableNumbers include it and `v.id(\"_storage\")`\n // validates. Its table number is pinned via `existingTableNumbers` below so ids encode/decode\n // consistently.\n schemaJson.tables[STORAGE_TABLE] = storageTableDefinition.export();\n\n const composed = composeComponents({ schemaJson, moduleMap: flat.moduleMap }, opts.components ?? [], {\n [STORAGE_TABLE]: STORAGE_TABLE_NUMBER,\n });\n\n // Extract + resolve the `http.ts` router (if any): its default export is an `HttpRouter` whose\n // route handlers are `RegisteredFunction` VALUES — resolve each to its `path:name` function path\n // by identity over `composed.moduleMap` (the same objects the router references), for\n // `runtime.runHttpAction` to look up — mirrors `packages/cli/src/project.ts`'s route resolution.\n const resolvedRoutes: ResolvedRoute[] = [];\n const router = flat.httpModule as { routes?: RouteEntry[] } | null;\n if (router?.routes) {\n const pathByFn = new Map<RegisteredFunction, string>();\n for (const [path, fn] of Object.entries(composed.moduleMap)) pathByFn.set(fn, path);\n for (const r of router.routes) {\n const handlerPath = pathByFn.get(r.handler);\n if (!handlerPath) {\n const where = r.path ?? r.pathPrefix ?? \"?\";\n throw new Error(\n `http.route handler for \"${where}\" must be an exported httpAction (declare it as a named export of an app module)`,\n );\n }\n resolvedRoutes.push({\n method: r.method,\n ...(r.path !== undefined ? { path: r.path } : { pathPrefix: r.pathPrefix }),\n handlerPath,\n });\n }\n }\n\n // Per-instance temp dir for the FS blob backend — removed in `cleanup`.\n const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), \"sb-test-\"));\n const blobStore = new FsBlobStore({ root: tempDir });\n\n // The harness-owned virtual clock — only used when `opts.now` isn't supplied. `advanceClock`\n // (below) mutates `clockMs`; `now` reads it fresh on every call, so `EmbeddedRuntime` (and\n // everything downstream — the scheduler's `_peekDue`/`_claim`/`_complete`, `ctx.now()` in app\n // code, etc.) always sees the harness's current virtual time with no real timers involved.\n const ownsClock = opts.now === undefined;\n let clockMs = 1_700_000_000_000;\n const now = opts.now ?? (() => clockMs);\n\n // `_test:_run` — the mechanism behind `t.run(fn)`: a system mutation whose handler invokes\n // whatever callback `t.run` most recently parked in `currentRunFn`, giving test code a full\n // db-writer `ctx` inside a real transaction without having to define an app function for it.\n let currentRunFn: ((ctx: unknown) => Promise<unknown>) | null = null;\n let runResult: unknown = undefined;\n const systemModules = {\n \"_test:_run\": mutation(async (ctx: unknown) => {\n if (!currentRunFn) throw new Error(\"_test:_run invoked with no callback set\");\n runResult = await currentRunFn(ctx);\n return null;\n }),\n };\n\n // `create` can throw before `cleanup` is returned (a driver's `start()`, a boot step, or schema\n // setup failing) — in that case nothing would ever remove the temp dir. Remove it on failure so a\n // failed `createTestHelipod` never orphans a `sb-test-*` dir in the OS temp dir, then rethrow.\n // The store is captured (not constructed inline) so both the success path's `cleanup` and this\n // catch block can close its underlying SQLite handle — otherwise an in-memory db handle (and, on\n // `create`-throws, the process-visible file descriptor it holds) leaks for the life of the process.\n const store = opts.store ?? new SqliteDocStore(new NodeSqliteAdapter());\n let runtime: EmbeddedRuntime;\n try {\n runtime = await EmbeddedRuntime.create({\n store,\n catalog: composed.catalog,\n // `_storage:*` built-ins go in `modules` — the action-mode `ctx.storage` reaches them through\n // the trusted `invoke`, and the reaper driver through `runFunction`, both of which resolve\n // `modules` (not `systemModules`).\n modules: { ...composed.moduleMap, ...storageModules },\n systemModules,\n componentNames: composed.componentNames,\n contextProviders: [\n storageContextProvider(blobStore, { signingKey: \"helipod-test-signing-key\" }),\n ...composed.contextProviders,\n ],\n policyRegistry: composed.policyRegistry,\n policyProviders: composed.policyProviders,\n relationRegistry: composed.relationRegistry,\n bootSteps: composed.bootSteps,\n drivers: [storageReaper(blobStore), ...composed.drivers],\n tableNumbers: composed.tableNumbers,\n now,\n });\n } catch (err) {\n await store.close();\n fs.rmSync(tempDir, { recursive: true, force: true });\n throw err;\n }\n\n const reactivity = createReactivity(runtime);\n const cleanup = async () => {\n // Close the shared loopback client BEFORE stopping drivers, so its connection tears down\n // against a still-live runtime rather than racing driver shutdown. The docstore itself is\n // closed AFTER drivers stop (so nothing still-running can touch it mid-close) and before the\n // temp dir backing file storage is removed.\n await reactivity.close();\n await runtime.stopDrivers();\n await store.close();\n fs.rmSync(tempDir, { recursive: true, force: true });\n };\n // See `packages/cli/src/http-handler.ts`'s \"User httpAction routes\" block — this is the same\n // match-then-dispatch, minus the wire (de)serialization a real HTTP server needs since `t.fetch`\n // already deals in `Request`/`Response` objects directly.\n const dispatchHttp = async (request: Request, identity: string | null): Promise<Response> => {\n const url = new URL(request.url);\n const match = matchRoute(resolvedRoutes, request.method, url.pathname);\n if (!match) return new Response(\"Not Found\", { status: 404 });\n // Header-fallback identity mirrors the real engine EXACTLY (see\n // `packages/cli/src/http-handler.ts`): the raw `Authorization` header is Bearer-stripped —\n // `Bearer abc123` -> `abc123`, anything else -> null — so a bare `t.fetch(req)`'s httpAction\n // sees the same `ctx.identity` it would in production. The view's `withIdentity` token (already\n // a bare string) still wins when present.\n const auth = request.headers.get(\"authorization\");\n const headerIdentity = auth && auth.startsWith(\"Bearer \") ? auth.slice(\"Bearer \".length) : null;\n return runtime.runHttpAction(match.handlerPath, request, { identity: identity ?? headerIdentity });\n };\n // Mirrors `components/scheduler/test/helpers.ts`'s lookup — the scheduler's own driver, if\n // `defineScheduler()` was passed in `opts.components`.\n const schedulerDriver = composed.drivers.find((d) => d.name === \"scheduler\") as SchedulerDriver | undefined;\n return {\n runtime,\n tableNumbers: composed.tableNumbers,\n schemaJson,\n cleanup,\n setRunFn: (fn) => { currentRunFn = fn; },\n takeRunResult: () => { const r = runResult; runResult = undefined; return r; },\n dispatchHttp,\n reactivity,\n ownsClock,\n advanceClock: (ms) => {\n if (!ownsClock) {\n throw new Error(\n \"advanceClock/advanceTimers/finishScheduledFunctions require the harness-owned virtual clock — \" +\n \"createTestHelipod() was given a custom `now`, so the harness has no clock to advance. \" +\n \"Omit `opts.now` to use scheduler time control.\",\n );\n }\n clockMs += ms;\n },\n getSchedulerDriver: () => schedulerDriver,\n };\n}\n","import { HelipodClient, loopbackTransport, getFunctionPath, type FunctionReference } from \"@helipod/client\";\nimport type { EmbeddedRuntime } from \"@helipod/runtime-embedded\";\nimport type { Value } from \"@helipod/values\";\n\nexport interface TestSubscription<T> {\n /** Latest computed value, or `undefined` until the first compute arrives. */\n value(): T | undefined;\n /** Registers a listener fired on every reactive re-run (and the first compute); returns a remover. */\n onChange(cb: (v: T) => void): () => void;\n /** Tears down the underlying query subscription. */\n unsubscribe(): void;\n}\n\n/**\n * Lazily-built, harness-shared reactive subscription surface. Exercises the REAL\n * client -> sync protocol -> SubscriptionManager -> engine invalidation path over an in-process\n * loopback connection (`runtime.connect()` + `loopbackTransport`) — the same wiring\n * `examples/chat/test/chat.test.ts` uses. ONE `HelipodClient` is shared across every\n * `t.subscribe(...)` call from a given harness instance; it's built on first use and closed via\n * `close()` (called from `BuiltRuntime.cleanup` before `stopDrivers`).\n */\nexport interface Reactivity {\n // Defaults to `any` (not `unknown`) so an unannotated `t.subscribe(...)` call still lets test\n // code chain straight off `.value()` (e.g. `sub.value()?.length`) without an explicit type\n // argument — matching the ergonomics of `t.query`/`t.mutation`'s own `T = unknown` defaults\n // would force every caller to annotate just to read a property off the result.\n subscribe<T = any>(ref: FunctionReference | string, args?: Record<string, Value>): TestSubscription<T>;\n close(): Promise<void>;\n}\n\nexport function createReactivity(runtime: EmbeddedRuntime): Reactivity {\n let client: HelipodClient | null = null;\n\n function ensureClient(): HelipodClient {\n if (!client) client = new HelipodClient(loopbackTransport(runtime.connect()));\n return client;\n }\n\n return {\n subscribe<T>(ref: FunctionReference | string, args: Record<string, Value> = {}): TestSubscription<T> {\n const path = getFunctionPath(ref);\n let latest: T | undefined;\n const listeners = new Set<(v: T) => void>();\n\n const unsubscribeClient = ensureClient().subscribe(path, args, (v) => {\n latest = v as T;\n for (const listener of listeners) listener(latest as T);\n });\n\n return {\n value: () => latest,\n onChange(cb) {\n listeners.add(cb);\n return () => listeners.delete(cb);\n },\n unsubscribe() {\n unsubscribeClient();\n },\n };\n },\n async close() {\n if (client) {\n client.close();\n client = null;\n }\n },\n };\n}\n","import type { BuiltRuntime } from \"./compose\";\n\n/** Fully-qualified name of `@helipod/scheduler`'s jobs table, as it lands in the composed\n * catalog (namespaced `scheduler/*`) — see `components/scheduler/src/schema.ts`. Privileged scans\n * (below) address it directly, mirroring `components/scheduler/test/helpers.ts`'s `_system:scan`. */\nconst SCHEDULER_JOBS_TABLE = \"scheduler/jobs\";\n\n/** `jobs.state` values that still represent outstanding work — see `components/scheduler/src/\n * schema.ts`'s `state` union. `\"inProgress\"` should never actually be observed here in practice:\n * by the time `driver.__tick()`'s promise resolves, every claimed job has already been completed\n * (terminal `success`/`failed`, or back to `pending` for a retry) — see `../../../components/\n * scheduler/src/driver.ts`'s `runPass`. It's included anyway as a defensive belt-and-suspenders\n * check, not load-bearing for correctness. */\nconst OUTSTANDING_STATES = new Set([\"pending\", \"inProgress\"]);\n\n/** How far the virtual clock jumps each iteration of `finishScheduledFunctions`'s loop — large\n * enough to clear any realistic `runAfter`/`runAt`/cron delay in a handful of iterations. */\nconst STEP_MS = 3_600_000; // 1 hour\n\n/** Bound on `finishScheduledFunctions`'s loop — a recurring cron (or a runAfter chain that keeps\n * rescheduling itself forever) would otherwise spin this forever; this is the safety valve. */\nconst MAX_ITERATIONS = 100;\n\n/**\n * Privileged raw scan of `@helipod/scheduler`'s `jobs` table, reusing the SAME `_test:_run`\n * plumbing `t.run()` is built on (see `./compose.ts`) rather than registering a bespoke system\n * module — `_test:_run` already runs with a full, privileged (namespace-bypassing) db-writer\n * `ctx`, so a plain `ctx.db.query(fullyQualifiedName, \"by_creation\").collect()` resolves the\n * component-internal table directly, exactly like `components/scheduler/test/helpers.ts`'s\n * `_system:scan`.\n */\nasync function scanSchedulerJobs(built: BuiltRuntime): Promise<Array<{ state?: unknown }>> {\n built.setRunFn(async (ctx: any) => await ctx.db.query(SCHEDULER_JOBS_TABLE, \"by_creation\").collect());\n try {\n await built.runtime.runSystem(\"_test:_run\", {});\n return (built.takeRunResult() as Array<{ state?: unknown }>) ?? [];\n } finally {\n built.setRunFn(null);\n }\n}\n\nasync function hasOutstandingJobs(built: BuiltRuntime): Promise<boolean> {\n const rows = await scanSchedulerJobs(built);\n return rows.some((row) => OUTSTANDING_STATES.has(String(row.state)));\n}\n\n/**\n * Drives every currently- and eventually-due scheduled job (`ctx.scheduler.runAfter`/`runAt`,\n * including cascades — a job that itself schedules another) to completion, without real timers:\n * repeatedly advances the harness's virtual clock by `STEP_MS` and awaits one `driver.__tick()`\n * (which — see `components/scheduler/src/driver.ts`'s reactive `pendingWake` coalescing — already\n * fully drains everything due AT the clock's current value, including 0-delay cascades, before its\n * promise resolves), stopping as soon as a scan of the scheduler's `jobs` table shows nothing left\n * in `\"pending\"`/`\"inProgress\"`.\n *\n * A clean no-op if `@helipod/scheduler` wasn't composed (no `defineScheduler()` in\n * `opts.components`) — there's nothing to drive.\n *\n * Bounded at `MAX_ITERATIONS` — a recurring cron (or a chain that keeps rescheduling itself\n * indefinitely) can never fully settle, so an unbounded loop here would hang forever instead of\n * failing loudly; this throws a clear error instead.\n */\nexport async function finishScheduledFunctions(built: BuiltRuntime): Promise<void> {\n const driver = built.getSchedulerDriver();\n if (!driver) return; // no scheduler composed — nothing scheduled, nothing to drive.\n\n for (let i = 0; i < MAX_ITERATIONS; i++) {\n if (!(await hasOutstandingJobs(built))) return;\n built.advanceClock(STEP_MS);\n await driver.__tick();\n }\n if (!(await hasOutstandingJobs(built))) return;\n throw new Error(\n `finishScheduledFunctions: scheduled jobs did not settle after ${MAX_ITERATIONS} iterations ` +\n `(advancing the virtual clock by ${STEP_MS}ms each time) — check for a cron or a runAfter chain ` +\n \"that keeps rescheduling itself forever.\",\n );\n}\n\n/**\n * Advances the harness's virtual clock by `ms`, then drives one `driver.__tick()` pass if\n * `@helipod/scheduler` is composed (a no-op tick otherwise — there's no driver to drive).\n * Unlike `finishScheduledFunctions`, this does exactly one pass: it will NOT itself drain a job\n * scheduled further out than `ms`, mirroring a real fake-timer `advanceTimersByTime`.\n *\n * Always advances the clock (throwing via `advanceClock` if `opts.now` was supplied and the\n * harness doesn't own it), regardless of whether a scheduler is composed — advancing time is a\n * general harness primitive, not scheduler-specific.\n */\nexport async function advanceTimers(built: BuiltRuntime, ms: number): Promise<void> {\n built.advanceClock(ms);\n const driver = built.getSchedulerDriver();\n if (driver) await driver.__tick();\n}\n"],"mappings":";AAKA,IAAM,yBAAyB;AAE/B,SAAS,qBAAqB,GAAqC;AACjE,SAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,OAAQ,EAAyB,SAAS,YACnF,OAAQ,EAA4B,YAAY;AACvD;AAEA,SAAS,aAAa,GAAmB;AACvC,SAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;AAUA,SAAS,oBAAoB,KAAa,eAA+B;AACvE,QAAM,cAAc,IAAI,OAAO,IAAI,aAAa,aAAa,CAAC,GAAG;AACjE,SAAO,IACJ,QAAQ,eAAe,EAAE,EACzB,QAAQ,aAAa,EAAE,EACvB,QAAQ,sCAAsC,EAAE;AACrD;AAEA,eAAe,cAAc,GAA8B;AACzD,SAAO,OAAO,MAAM,aAAa,MAAO,EAA6B,IAAI;AAC3E;AAQA,eAAsB,eACpB,SACA,gBAAwB,wBACG;AAC3B,QAAM,YAAgD,CAAC;AACvD,MAAI,eAA+B;AACnC,MAAI,aAA6B;AACjC,aAAW,CAAC,QAAQ,MAAM,KAAK,OAAO,QAAQ,OAAO,GAAG;AACtD,UAAM,UAAU,oBAAoB,QAAQ,aAAa;AACzD,UAAM,MAAO,MAAM,cAAc,MAAM;AACvC,UAAM,MAAM,OAAO,OAAO,QAAQ,WAAY,IAA8B,UAAU;AACtF,QAAI,YAAY,UAAU;AAAE,qBAAe,OAAO;AAAK;AAAA,IAAU;AAMjE,QAAI,YAAY,QAAQ;AAAE,mBAAa,OAAO;AAAA,IAAK;AACnD,eAAW,CAAC,YAAY,SAAS,KAAK,OAAO,QAAQ,OAAO,CAAC,CAAC,GAAG;AAC/D,UAAI,qBAAqB,SAAS,EAAG,WAAU,GAAG,OAAO,IAAI,UAAU,EAAE,IAAI;AAAA,IAC/E;AAAA,EACF;AACA,SAAO,EAAE,WAAW,cAAc,WAAW;AAC/C;;;AChEA,SAAS,mBAAAA,wBAA+C;;;ACDxD,YAAY,QAAQ;AACpB,YAAY,QAAQ;AACpB,YAAY,UAAU;AAEtB,SAAS,gBAAgB,yBAAyB;AAClD,SAAS,yBAAmD;AAC5D,SAAS,uBAAuB;AAEhC,SAAS,oBAAsE;AAC/E,SAAS,UAAU,kBAA4D;AAC/E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,mBAAmB;;;AClB5B,SAAS,eAAe,mBAAmB,uBAA+C;AA8BnF,SAAS,iBAAiB,SAAsC;AACrE,MAAI,SAA+B;AAEnC,WAAS,eAA8B;AACrC,QAAI,CAAC,OAAQ,UAAS,IAAI,cAAc,kBAAkB,QAAQ,QAAQ,CAAC,CAAC;AAC5E,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,UAAa,KAAiC,OAA8B,CAAC,GAAwB;AACnG,YAAMC,QAAO,gBAAgB,GAAG;AAChC,UAAI;AACJ,YAAM,YAAY,oBAAI,IAAoB;AAE1C,YAAM,oBAAoB,aAAa,EAAE,UAAUA,OAAM,MAAM,CAAC,MAAM;AACpE,iBAAS;AACT,mBAAW,YAAY,UAAW,UAAS,MAAW;AAAA,MACxD,CAAC;AAED,aAAO;AAAA,QACL,OAAO,MAAM;AAAA,QACb,SAAS,IAAI;AACX,oBAAU,IAAI,EAAE;AAChB,iBAAO,MAAM,UAAU,OAAO,EAAE;AAAA,QAClC;AAAA,QACA,cAAc;AACZ,4BAAkB;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA,IACA,MAAM,QAAQ;AACZ,UAAI,QAAQ;AACV,eAAO,MAAM;AACb,iBAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;;;ADqCA,eAAsB,aAAa,MAAgD;AACjF,QAAM,OAAO,MAAM,eAAe,KAAK,SAAS,KAAK,aAAa;AAElE,QAAM;AAAA;AAAA;AAAA,IAGJ,KAAK,UAAU,KAAK,WAAW,SAC3B,KAAK,SACJ,KAAK,gBAA4C,aAAa,CAAC,CAAC;AAAA;AACvE,QAAM,aAAa,UAAU,OAAO;AAOpC,aAAW,OAAO,aAAa,IAAI,uBAAuB,OAAO;AAEjE,QAAM,WAAW,kBAAkB,EAAE,YAAY,WAAW,KAAK,UAAU,GAAG,KAAK,cAAc,CAAC,GAAG;AAAA,IACnG,CAAC,aAAa,GAAG;AAAA,EACnB,CAAC;AAMD,QAAM,iBAAkC,CAAC;AACzC,QAAM,SAAS,KAAK;AACpB,MAAI,QAAQ,QAAQ;AAClB,UAAM,WAAW,oBAAI,IAAgC;AACrD,eAAW,CAACC,OAAM,EAAE,KAAK,OAAO,QAAQ,SAAS,SAAS,EAAG,UAAS,IAAI,IAAIA,KAAI;AAClF,eAAW,KAAK,OAAO,QAAQ;AAC7B,YAAM,cAAc,SAAS,IAAI,EAAE,OAAO;AAC1C,UAAI,CAAC,aAAa;AAChB,cAAM,QAAQ,EAAE,QAAQ,EAAE,cAAc;AACxC,cAAM,IAAI;AAAA,UACR,2BAA2B,KAAK;AAAA,QAClC;AAAA,MACF;AACA,qBAAe,KAAK;AAAA,QAClB,QAAQ,EAAE;AAAA,QACV,GAAI,EAAE,SAAS,SAAY,EAAE,MAAM,EAAE,KAAK,IAAI,EAAE,YAAY,EAAE,WAAW;AAAA,QACzE;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAGA,QAAM,UAAa,eAAiB,UAAQ,UAAO,GAAG,UAAU,CAAC;AACjE,QAAM,YAAY,IAAI,YAAY,EAAE,MAAM,QAAQ,CAAC;AAMnD,QAAM,YAAY,KAAK,QAAQ;AAC/B,MAAI,UAAU;AACd,QAAM,MAAM,KAAK,QAAQ,MAAM;AAK/B,MAAI,eAA4D;AAChE,MAAI,YAAqB;AACzB,QAAM,gBAAgB;AAAA,IACpB,cAAc,SAAS,OAAO,QAAiB;AAC7C,UAAI,CAAC,aAAc,OAAM,IAAI,MAAM,yCAAyC;AAC5E,kBAAY,MAAM,aAAa,GAAG;AAClC,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAQA,QAAM,QAAQ,KAAK,SAAS,IAAI,eAAe,IAAI,kBAAkB,CAAC;AACtE,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,gBAAgB,OAAO;AAAA,MACrC;AAAA,MACA,SAAS,SAAS;AAAA;AAAA;AAAA;AAAA,MAIlB,SAAS,EAAE,GAAG,SAAS,WAAW,GAAG,eAAe;AAAA,MACpD;AAAA,MACA,gBAAgB,SAAS;AAAA,MACzB,kBAAkB;AAAA,QAChB,uBAAuB,WAAW,EAAE,YAAY,2BAA2B,CAAC;AAAA,QAC5E,GAAG,SAAS;AAAA,MACd;AAAA,MACA,gBAAgB,SAAS;AAAA,MACzB,iBAAiB,SAAS;AAAA,MAC1B,kBAAkB,SAAS;AAAA,MAC3B,WAAW,SAAS;AAAA,MACpB,SAAS,CAAC,cAAc,SAAS,GAAG,GAAG,SAAS,OAAO;AAAA,MACvD,cAAc,SAAS;AAAA,MACvB;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,UAAM,MAAM,MAAM;AAClB,IAAG,UAAO,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACnD,UAAM;AAAA,EACR;AAEA,QAAM,aAAa,iBAAiB,OAAO;AAC3C,QAAM,UAAU,YAAY;AAK1B,UAAM,WAAW,MAAM;AACvB,UAAM,QAAQ,YAAY;AAC1B,UAAM,MAAM,MAAM;AAClB,IAAG,UAAO,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACrD;AAIA,QAAM,eAAe,OAAO,SAAkB,aAA+C;AAC3F,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,UAAM,QAAQ,WAAW,gBAAgB,QAAQ,QAAQ,IAAI,QAAQ;AACrE,QAAI,CAAC,MAAO,QAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;AAM5D,UAAM,OAAO,QAAQ,QAAQ,IAAI,eAAe;AAChD,UAAM,iBAAiB,QAAQ,KAAK,WAAW,SAAS,IAAI,KAAK,MAAM,UAAU,MAAM,IAAI;AAC3F,WAAO,QAAQ,cAAc,MAAM,aAAa,SAAS,EAAE,UAAU,YAAY,eAAe,CAAC;AAAA,EACnG;AAGA,QAAM,kBAAkB,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,WAAW;AAC3E,SAAO;AAAA,IACL;AAAA,IACA,cAAc,SAAS;AAAA,IACvB;AAAA,IACA;AAAA,IACA,UAAU,CAAC,OAAO;AAAE,qBAAe;AAAA,IAAI;AAAA,IACvC,eAAe,MAAM;AAAE,YAAM,IAAI;AAAW,kBAAY;AAAW,aAAO;AAAA,IAAG;AAAA,IAC7E;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,CAAC,OAAO;AACpB,UAAI,CAAC,WAAW;AACd,cAAM,IAAI;AAAA,UACR;AAAA,QAGF;AAAA,MACF;AACA,iBAAW;AAAA,IACb;AAAA,IACA,oBAAoB,MAAM;AAAA,EAC5B;AACF;;;AEnQA,IAAM,uBAAuB;AAQ7B,IAAM,qBAAqB,oBAAI,IAAI,CAAC,WAAW,YAAY,CAAC;AAI5D,IAAM,UAAU;AAIhB,IAAM,iBAAiB;AAUvB,eAAe,kBAAkB,OAA0D;AACzF,QAAM,SAAS,OAAO,QAAa,MAAM,IAAI,GAAG,MAAM,sBAAsB,aAAa,EAAE,QAAQ,CAAC;AACpG,MAAI;AACF,UAAM,MAAM,QAAQ,UAAU,cAAc,CAAC,CAAC;AAC9C,WAAQ,MAAM,cAAc,KAAoC,CAAC;AAAA,EACnE,UAAE;AACA,UAAM,SAAS,IAAI;AAAA,EACrB;AACF;AAEA,eAAe,mBAAmB,OAAuC;AACvE,QAAM,OAAO,MAAM,kBAAkB,KAAK;AAC1C,SAAO,KAAK,KAAK,CAAC,QAAQ,mBAAmB,IAAI,OAAO,IAAI,KAAK,CAAC,CAAC;AACrE;AAkBA,eAAsB,yBAAyB,OAAoC;AACjF,QAAM,SAAS,MAAM,mBAAmB;AACxC,MAAI,CAAC,OAAQ;AAEb,WAAS,IAAI,GAAG,IAAI,gBAAgB,KAAK;AACvC,QAAI,CAAE,MAAM,mBAAmB,KAAK,EAAI;AACxC,UAAM,aAAa,OAAO;AAC1B,UAAM,OAAO,OAAO;AAAA,EACtB;AACA,MAAI,CAAE,MAAM,mBAAmB,KAAK,EAAI;AACxC,QAAM,IAAI;AAAA,IACR,iEAAiE,cAAc,+CAC1C,OAAO;AAAA,EAE9C;AACF;AAYA,eAAsB,cAAc,OAAqB,IAA2B;AAClF,QAAM,aAAa,EAAE;AACrB,QAAM,SAAS,MAAM,mBAAmB;AACxC,MAAI,OAAQ,OAAM,OAAO,OAAO;AAClC;;;AH/BA,eAAsB,kBAAkB,MAA+C;AACrF,QAAM,QAAsB,MAAM,aAAa,IAAI;AACnD,QAAM,EAAE,QAAQ,IAAI;AAEpB,WAAS,SAAS,UAAsC;AACtD,WAAO;AAAA,MACL,MAAM,MAAM,KAAK,OAAO,CAAC,GAAG;AAC1B,gBAAQ,MAAM,QAAQ,IAAIC,iBAAgB,GAAG,GAAG,MAAe,EAAE,SAAS,CAAC,GAAG;AAAA,MAChF;AAAA,MACA,MAAM,SAAS,KAAK,OAAO,CAAC,GAAG;AAC7B,gBAAQ,MAAM,QAAQ,IAAIA,iBAAgB,GAAG,GAAG,MAAe,EAAE,SAAS,CAAC,GAAG;AAAA,MAChF;AAAA,MACA,MAAM,OAAO,KAAK,OAAO,CAAC,GAAG;AAC3B,gBAAQ,MAAM,QAAQ,UAAUA,iBAAgB,GAAG,GAAG,MAAe,EAAE,SAAS,CAAC,GAAG;AAAA,MACtF;AAAA,MACA,MAAM,IAAI,IAAI;AACZ,cAAM,SAAS,EAAwC;AACvD,YAAI;AACF,gBAAM,QAAQ,UAAU,cAAc,CAAC,CAAC;AACxC,iBAAO,MAAM,cAAc;AAAA,QAC7B,UAAE;AACA,gBAAM,SAAS,IAAI;AAAA,QACrB;AAAA,MACF;AAAA,MACA,MAAM,MAAM,SAAS;AACnB,eAAO,MAAM,aAAa,SAAS,QAAQ;AAAA,MAC7C;AAAA,MACA,UAAU,KAAK,OAAO,CAAC,GAAG;AACxB,eAAO,MAAM,WAAW,UAAU,KAAK,IAAI;AAAA,MAC7C;AAAA,MACA,aAAa,IAAI;AACf,eAAO,SAAS,EAAE;AAAA,MACpB;AAAA,MACA,MAAM,2BAA2B;AAC/B,cAAM,yBAAyB,KAAK;AAAA,MACtC;AAAA,MACA,MAAM,cAAc,IAAI;AACtB,cAAM,cAAc,OAAO,EAAE;AAAA,MAC/B;AAAA,MACA,MAAM,QAAQ;AACZ,cAAM,MAAM,QAAQ;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,SAAS,IAAI;AACtB;","names":["getFunctionPath","path","path","getFunctionPath"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@helipod/test",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"clean": "rm -rf dist .turbo"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@helipod/blobstore-fs": "0.1.0",
|
|
27
|
+
"@helipod/client": "0.1.0",
|
|
28
|
+
"@helipod/component": "0.1.0",
|
|
29
|
+
"@helipod/docstore": "0.1.0",
|
|
30
|
+
"@helipod/docstore-sqlite": "0.1.0",
|
|
31
|
+
"@helipod/executor": "0.1.0",
|
|
32
|
+
"@helipod/id-codec": "0.1.0",
|
|
33
|
+
"@helipod/runtime-embedded": "0.1.0",
|
|
34
|
+
"@helipod/scheduler": "0.1.0",
|
|
35
|
+
"@helipod/storage": "0.1.0",
|
|
36
|
+
"@helipod/sync": "0.1.0",
|
|
37
|
+
"@helipod/values": "0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.10.5",
|
|
41
|
+
"tsup": "^8.3.5",
|
|
42
|
+
"typescript": "^5.7.2",
|
|
43
|
+
"vitest": "^2.1.8"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/helipod-sh/helipod.git",
|
|
51
|
+
"directory": "packages/test"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/helipod-sh/helipod"
|
|
54
|
+
}
|