houdini 2.0.7 → 2.0.8

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/build/cmd/init.js CHANGED
@@ -472,12 +472,12 @@ async function packageJSON(targetPath, frameworkInfo) {
472
472
  }
473
473
  packageJSON2.devDependencies = {
474
474
  ...packageJSON2.devDependencies,
475
- houdini: "^2.0.7"
475
+ houdini: "^2.0.8"
476
476
  };
477
477
  if (frameworkInfo.framework === "svelte" || frameworkInfo.framework === "kit") {
478
478
  packageJSON2.devDependencies = {
479
479
  ...packageJSON2.devDependencies,
480
- "houdini-svelte": "^3.0.3"
480
+ "houdini-svelte": "^3.0.4"
481
481
  };
482
482
  } else {
483
483
  throw new Error(`Unmanaged framework: "${JSON.stringify(frameworkInfo)}"`);
@@ -196,8 +196,16 @@ async function codegen_setup(config, mode, db, db_file) {
196
196
  };
197
197
  spec_results[name] = spec;
198
198
  _db.run(
199
- `INSERT OR IGNORE INTO plugins (name, hooks, port, plugin_order, include_runtime, include_static_runtime, config_module, client_plugins)
200
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
199
+ `INSERT INTO plugins (name, hooks, port, plugin_order, include_runtime, include_static_runtime, config_module, client_plugins)
200
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
201
+ ON CONFLICT(name) DO UPDATE SET
202
+ hooks = excluded.hooks,
203
+ port = excluded.port,
204
+ plugin_order = excluded.plugin_order,
205
+ include_runtime = excluded.include_runtime,
206
+ include_static_runtime = excluded.include_static_runtime,
207
+ config_module = excluded.config_module,
208
+ client_plugins = excluded.client_plugins`,
201
209
  [
202
210
  spec.name,
203
211
  JSON.stringify([...spec.hooks]),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "houdini",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "The disappearing GraphQL clients",
5
5
  "keywords": [
6
6
  "typescript",
@@ -45,6 +45,7 @@ export declare class Cache {
45
45
  when?: {};
46
46
  }): void;
47
47
  refresh(id: string | string[]): void;
48
+ refreshAll(session?: App.Session | null): void;
48
49
  markRecordStale(id: string, options: {
49
50
  field?: string;
50
51
  when?: {};
@@ -153,6 +153,22 @@ class Cache {
153
153
  }
154
154
  }
155
155
  }
156
+ // ask every active query document to refetch itself.
157
+ // this is used when global request context changes (for example, the
158
+ // SvelteKit session) and any query might resolve differently even if its
159
+ // cached records did not change. non-query artifacts are skipped because
160
+ // only queries can be refetched through this cache notification path.
161
+ refreshAll(session) {
162
+ const message = session === void 0 ? { kind: "refetch" } : { kind: "refetch", session };
163
+ for (const [spec] of this._internal_unstable.subscriptions.all({
164
+ includeMaskedParents: true
165
+ })) {
166
+ if (spec.kind !== ArtifactKind.Query) {
167
+ continue;
168
+ }
169
+ spec.onMessage(message);
170
+ }
171
+ }
156
172
  markRecordStale(id, options) {
157
173
  if (options.field) {
158
174
  const key = computeKey({ field: options.field, args: options.when ?? {} });
@@ -49,6 +49,9 @@ export declare class InMemorySubscriptions {
49
49
  getAll(id: string, { includeMaskedParents }?: {
50
50
  includeMaskedParents?: boolean;
51
51
  }): FieldSelection[];
52
+ all({ includeMaskedParents }?: {
53
+ includeMaskedParents?: boolean;
54
+ }): FieldSelection[];
52
55
  remove(id: string, selection: SubscriptionSelection, targets: SubscriptionSpec[], variables: {}, visited?: string[], masked?: boolean): void;
53
56
  reset(): Readonly<{
54
57
  rootType: string;
@@ -209,6 +209,29 @@ class InMemorySubscriptions {
209
209
  (fieldSub) => includeMaskedParents ? fieldSub.selections.concat(fieldSub.maskedParentSelections) : fieldSub.selections
210
210
  );
211
211
  }
212
+ all({ includeMaskedParents = false } = {}) {
213
+ const result = [];
214
+ const notified = /* @__PURE__ */ new Set();
215
+ for (const fields of this.subscribers.values()) {
216
+ for (const fieldSub of fields.values()) {
217
+ for (const selection of fieldSub.selections) {
218
+ if (!notified.has(selection[0].onMessage)) {
219
+ notified.add(selection[0].onMessage);
220
+ result.push(selection);
221
+ }
222
+ }
223
+ if (includeMaskedParents) {
224
+ for (const selection of fieldSub.maskedParentSelections) {
225
+ if (!notified.has(selection[0].onMessage)) {
226
+ notified.add(selection[0].onMessage);
227
+ result.push(selection);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ return result;
234
+ }
212
235
  remove(id, selection, targets, variables, visited = [], masked = false) {
213
236
  visited.push(id);
214
237
  const linkedIDs = [];
@@ -267,6 +267,7 @@ export type CacheMessage<_Data = any> = {
267
267
  data: _Data;
268
268
  } | {
269
269
  kind: 'refetch';
270
+ session?: App.Session | null;
270
271
  };
271
272
  export type SubscriptionSpec = Readonly<{
272
273
  rootType: string;
@@ -1,5 +1,4 @@
1
1
  import type { HmrContext, Plugin as VitePlugin } from 'vite';
2
- import { type CompilerProxy } from '../lib/index.js';
3
2
  import type { VitePluginContext } from './index.js';
4
3
  /**
5
4
  * Houdini Vite HMR Plugin
@@ -16,7 +15,6 @@ import type { VitePluginContext } from './index.js';
16
15
  * The plugin uses the same dependency resolution logic for both scenarios, walking both up and down
17
16
  * the dependency graph to ensure all related documents are properly regenerated.
18
17
  */
19
- export declare let compiler: CompilerProxy;
20
18
  export declare function document_hmr(ctx: VitePluginContext): VitePlugin;
21
19
  type BatchCallback = (filesWithContent: Record<string, string>, deletedFiles: string[], batchId: string) => void | Promise<void>;
22
20
  export declare function createDebounceHmr(debounceMs?: number): {
package/build/vite/hmr.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { readFileSync, writeFileSync } from "fs";
2
2
  import { codegen_setup, get_config, path, run_pipeline } from "../lib/index.js";
3
- let compiler;
3
+ import { dispose_active_session, register_session } from "./session.js";
4
4
  function document_hmr(ctx) {
5
5
  const debounceHmr = createDebounceHmr(50);
6
+ let compiler;
6
7
  let config;
7
8
  const ownWrites = /* @__PURE__ */ new Map();
8
9
  let generatedDir;
@@ -62,17 +63,29 @@ function document_hmr(ctx) {
62
63
  // this is called when the dev server starts
63
64
  async configureServer(server) {
64
65
  config = await get_config();
65
- compiler = await codegen_setup(config, "dev", ctx.db, ctx.db_file);
66
+ await dispose_active_session(ctx.db_file);
67
+ const ownedCompiler = await codegen_setup(config, "dev", ctx.db, ctx.db_file);
68
+ compiler = ownedCompiler;
69
+ ctx.compiler = ownedCompiler;
66
70
  generatedDir = path.join(
67
71
  server.config.root,
68
72
  ctx.config.config_file.runtimeDir ?? ".houdini"
69
73
  );
70
74
  rootPrefix = server.config.root.endsWith("/") ? server.config.root : `${server.config.root}/`;
75
+ const dispose_session = register_session(ctx.db_file, async () => {
76
+ if (compiler === ownedCompiler) {
77
+ compiler = void 0;
78
+ }
79
+ if (ctx.compiler === ownedCompiler) {
80
+ ctx.compiler = void 0;
81
+ }
82
+ await ownedCompiler.close();
83
+ });
71
84
  server.httpServer?.once("close", () => {
72
- compiler.close();
85
+ dispose_session();
73
86
  });
74
87
  try {
75
- await compiler.run_pipeline({
88
+ await ownedCompiler.run_pipeline({
76
89
  // the pipeline through schema is run as part of codegen_setup
77
90
  after: "Schema"
78
91
  });
@@ -140,8 +153,9 @@ function document_hmr(ctx) {
140
153
  return opts.modules.filter((mod) => mod.type === "js");
141
154
  }
142
155
  async function batchCallback(files, deletedFiles, task_id) {
143
- if (!compiler) return;
144
- await compiler.pipeline_lock(async () => {
156
+ const batchCompiler = compiler;
157
+ if (!batchCompiler) return;
158
+ await batchCompiler.pipeline_lock(async () => {
145
159
  if (deletedFiles.length > 0) {
146
160
  const delClause = deletedFiles.map(() => "filepath = ?").join(" OR ");
147
161
  ctx.db.run(`DELETE FROM raw_documents WHERE ${delClause}`, deletedFiles);
@@ -196,7 +210,7 @@ function document_hmr(ctx) {
196
210
  `);
197
211
  });
198
212
  try {
199
- const results2 = await run_pipeline(compiler.trigger_hook, {
213
+ const results2 = await run_pipeline(batchCompiler.trigger_hook, {
200
214
  after: "AfterExtract"
201
215
  });
202
216
  const updated_modules2 = [
@@ -240,7 +254,7 @@ function document_hmr(ctx) {
240
254
  `);
241
255
  });
242
256
  try {
243
- await compiler.trigger_hook("ExtractDocuments", {
257
+ await batchCompiler.trigger_hook("ExtractDocuments", {
244
258
  payload: { filepaths }
245
259
  });
246
260
  } catch (err) {
@@ -273,7 +287,7 @@ function document_hmr(ctx) {
273
287
  if (changes === 0 && savedDocs.length === 0) {
274
288
  if (!filepaths.some(isManifestFile)) return;
275
289
  }
276
- await compiler.trigger_hook("AfterExtract", { task_id });
290
+ await batchCompiler.trigger_hook("AfterExtract", { task_id });
277
291
  ctx.db.run(
278
292
  `
279
293
  WITH RECURSIVE
@@ -314,7 +328,7 @@ function document_hmr(ctx) {
314
328
  );
315
329
  let results;
316
330
  try {
317
- results = await run_pipeline(compiler.trigger_hook, {
331
+ results = await run_pipeline(batchCompiler.trigger_hook, {
318
332
  task_id,
319
333
  after: "AfterExtract"
320
334
  });
@@ -437,7 +451,6 @@ function createDebounceHmr(debounceMs = 50) {
437
451
  };
438
452
  }
439
453
  export {
440
- compiler,
441
454
  createDebounceHmr,
442
455
  document_hmr
443
456
  };
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { codegen_setup, init_db } from "../lib/codegen.js";
3
3
  import * as fs from "../lib/fs.js";
4
+ import { dispose_active_session } from "./session.js";
4
5
  let compiler;
5
6
  let alreadyBuilt = false;
6
7
  function houdini(ctx) {
@@ -19,6 +20,7 @@ function houdini(ctx) {
19
20
  return;
20
21
  }
21
22
  if (!ctx.db) {
23
+ await dispose_active_session(ctx.db_file);
22
24
  const [db] = await init_db(ctx.config, false);
23
25
  ctx.db = db;
24
26
  }
@@ -1,6 +1,6 @@
1
1
  import type { Db } from '../lib/db.js';
2
2
  import type { PluginOption } from 'vite';
3
- import { type Adapter, type ConfigFile, type Config } from '../lib/index.js';
3
+ import { type Adapter, type CompilerProxy, type ConfigFile, type Config } from '../lib/index.js';
4
4
  export type PluginConfig = {
5
5
  configPath?: string;
6
6
  adapter?: Adapter;
@@ -9,5 +9,6 @@ export type VitePluginContext = PluginConfig & {
9
9
  db: Db;
10
10
  db_file: string;
11
11
  config: Config;
12
+ compiler?: CompilerProxy;
12
13
  };
13
14
  export default function (opts?: PluginConfig): Promise<Array<PluginOption>>;
@@ -2,7 +2,9 @@ import * as fs from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { get_config } from "../lib/index.js";
5
+ import {
6
+ get_config
7
+ } from "../lib/index.js";
6
8
  import { db_path } from "../router/conventions.js";
7
9
  import { document_hmr } from "./hmr.js";
8
10
  import { houdini } from "./houdini.js";
@@ -1,5 +1,5 @@
1
1
  import type { PluginOption } from 'vite';
2
2
  import type { VitePluginContext } from './index.js';
3
- export declare function refresh_on_schema(_ctx: VitePluginContext): PluginOption;
3
+ export declare function refresh_on_schema(ctx: VitePluginContext): PluginOption;
4
4
  export declare function poll_remote_schema(_ctx: VitePluginContext): PluginOption;
5
- export declare function watch_local_schema(_ctx: VitePluginContext): PluginOption;
5
+ export declare function watch_local_schema(ctx: VitePluginContext): PluginOption;
@@ -3,9 +3,8 @@ import * as graphql from "graphql";
3
3
  import { fs, get_config, run_pipeline } from "../lib/index.js";
4
4
  import { pull_schema } from "../lib/schema.js";
5
5
  import { sleep } from "../lib/sleep.js";
6
- import { compiler } from "./hmr.js";
7
6
  const ownSchemaWrites = /* @__PURE__ */ new Set();
8
- function refresh_on_schema(_ctx) {
7
+ function refresh_on_schema(ctx) {
9
8
  return {
10
9
  name: "houdini-refresh-on-schema",
11
10
  async handleHotUpdate({ file, server }) {
@@ -17,6 +16,7 @@ function refresh_on_schema(_ctx) {
17
16
  ownSchemaWrites.delete(file);
18
17
  return;
19
18
  }
19
+ const compiler = ctx.compiler;
20
20
  if (!compiler) {
21
21
  return;
22
22
  }
@@ -89,7 +89,7 @@ function poll_remote_schema(_ctx) {
89
89
  }
90
90
  };
91
91
  }
92
- function watch_local_schema(_ctx) {
92
+ function watch_local_schema(ctx) {
93
93
  return {
94
94
  name: "houdini-watch-local-schema",
95
95
  // Write the serialized schema at startup so houdini-core's Schema hook sees
@@ -120,6 +120,7 @@ function watch_local_schema(_ctx) {
120
120
  ownSchemaWrites.add(write_target);
121
121
  await serialize_and_write(schema, write_target);
122
122
  setTimeout(() => ownSchemaWrites.delete(write_target), 2e3);
123
+ const compiler = ctx.compiler;
123
124
  if (!compiler) return;
124
125
  try {
125
126
  await compiler.pipeline_lock(
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Coordinates the handoff between houdini dev sessions within one process.
3
+ *
4
+ * A vite restart creates the replacement server (and with it a fresh houdini
5
+ * session: plugin context, database connection, compiler, and Go plugin
6
+ * processes) *before* closing the old server. Without coordination the two
7
+ * sessions overlap on the shared orchestration database: the new session wipes
8
+ * the file and spawns its plugins while the old session's processes still hold
9
+ * it, so plugin registrations collide and the surviving session can end up
10
+ * pointing at plugin processes that no longer exist.
11
+ *
12
+ * This registry is deliberately module-level: its whole purpose is to pass
13
+ * ownership from one plugin-instance generation to the next, which no
14
+ * per-instance state can do. Each dev session registers a dispose callback
15
+ * keyed by its database file; the next session for that file (or the owning
16
+ * server's close event, whichever comes first) runs it exactly once.
17
+ */
18
+ /**
19
+ * Register the active session for a database file. Returns a disposer bound to
20
+ * this registration: it runs the teardown at most once, and removes the
21
+ * registration only if it is still the active one (a replacement session may
22
+ * have already taken the slot).
23
+ */
24
+ export declare function register_session(db_file: string, dispose: () => Promise<void>): () => Promise<void>;
25
+ /**
26
+ * Dispose whatever session currently owns the database file (no-op when none).
27
+ * A new session calls this before it recreates the database and spawns its own
28
+ * plugin processes.
29
+ */
30
+ export declare function dispose_active_session(db_file: string): Promise<void>;
@@ -0,0 +1,29 @@
1
+ const sessions = /* @__PURE__ */ new Map();
2
+ function register_session(db_file, dispose) {
3
+ let running = null;
4
+ const entry = {
5
+ dispose: () => {
6
+ running ??= dispose();
7
+ return running;
8
+ }
9
+ };
10
+ sessions.set(db_file, entry);
11
+ return () => {
12
+ if (sessions.get(db_file) === entry) {
13
+ sessions.delete(db_file);
14
+ }
15
+ return entry.dispose();
16
+ };
17
+ }
18
+ async function dispose_active_session(db_file) {
19
+ const entry = sessions.get(db_file);
20
+ if (!entry) {
21
+ return;
22
+ }
23
+ sessions.delete(db_file);
24
+ await entry.dispose();
25
+ }
26
+ export {
27
+ dispose_active_session,
28
+ register_session
29
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "houdini",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "The disappearing GraphQL clients",
5
5
  "keywords": [
6
6
  "typescript",
@@ -52,7 +52,7 @@
52
52
  "recast": "^0.23.11",
53
53
  "sql.js": "^1.14.1",
54
54
  "ws": "^8.21.0",
55
- "houdini-core": "^2.0.5"
55
+ "houdini-core": "^2.0.6"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "graphql": ">=16",