@langchain/langgraph 1.4.7 → 1.4.9

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 CHANGED
@@ -1,9 +1,9 @@
1
1
  <div align="center">
2
2
  <a href="https://www.langchain.com/langgraph">
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-dark.svg">
5
- <source media="(prefers-color-scheme: light)" srcset=".github/images/logo-light.svg">
6
- <img alt="LangGraph Logo" src=".github/images/logo-dark.svg" width="50%">
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/langchain-ai/langgraphjs/HEAD/.github/images/logo-dark.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/langchain-ai/langgraphjs/HEAD/.github/images/logo-light.svg">
6
+ <img alt="LangGraph Logo" src="https://raw.githubusercontent.com/langchain-ai/langgraphjs/HEAD/.github/images/logo-dark.svg" width="50%">
7
7
  </picture>
8
8
  </a>
9
9
  </div>
@@ -2,6 +2,15 @@ const require_errors = require("../errors.cjs");
2
2
  const require_base = require("./base.cjs");
3
3
  //#region src/channels/topic.ts
4
4
  /**
5
+ * Detect the `[seen, values]` Topic checkpoint shape.
6
+ *
7
+ * Used for `unique: true` topics (current) and pre-flat JS checkpoints
8
+ * (legacy). Flat Python-parity checkpoints are a single values list.
9
+ */
10
+ function isUniqueTopicCheckpoint(checkpoint) {
11
+ return Array.isArray(checkpoint) && checkpoint.length === 2 && Array.isArray(checkpoint[0]) && Array.isArray(checkpoint[1]);
12
+ }
13
+ /**
5
14
  * A configurable PubSub Topic.
6
15
  */
7
16
  var Topic = class Topic extends require_base.BaseChannel {
@@ -22,10 +31,14 @@ var Topic = class Topic extends require_base.BaseChannel {
22
31
  unique: this.unique,
23
32
  accumulate: this.accumulate
24
33
  });
25
- if (typeof checkpoint !== "undefined") {
34
+ if (typeof checkpoint === "undefined") return empty;
35
+ if (isUniqueTopicCheckpoint(checkpoint)) {
26
36
  empty.seen = new Set(checkpoint[0]);
27
- empty.values = checkpoint[1];
37
+ empty.values = [...checkpoint[1]];
38
+ return empty;
28
39
  }
40
+ empty.values = [...checkpoint];
41
+ if (this.unique) empty.seen = new Set(empty.values);
29
42
  return empty;
30
43
  }
31
44
  update(values) {
@@ -51,8 +64,20 @@ var Topic = class Topic extends require_base.BaseChannel {
51
64
  if (this.values.length === 0) throw new require_errors.EmptyChannelError();
52
65
  return this.values;
53
66
  }
67
+ /**
68
+ * Checkpoint Topic state.
69
+ *
70
+ * Default / Host path (`unique: false`, including `__pregel_tasks`): flat
71
+ * values list, matching Python Topic. Previously this always returned
72
+ * `[seen, values]`, so empty TASKS became `[[], []]` and Host's Python
73
+ * checkpointer rejected it.
74
+ *
75
+ * When `unique: true`, keep `[seen, values]` so dedupe history that outlives
76
+ * the current buffer still survives resume.
77
+ */
54
78
  checkpoint() {
55
- return [[...this.seen], this.values];
79
+ if (this.unique) return [[...this.seen], [...this.values]];
80
+ return [...this.values];
56
81
  }
57
82
  isAvailable() {
58
83
  return this.values.length !== 0;
@@ -1 +1 @@
1
- {"version":3,"file":"topic.cjs","names":["BaseChannel","EmptyChannelError"],"sources":["../../src/channels/topic.ts"],"sourcesContent":["import { EmptyChannelError } from \"../errors.js\";\nimport { BaseChannel } from \"./base.js\";\n\n/**\n * A configurable PubSub Topic.\n */\nexport class Topic<Value> extends BaseChannel<\n Array<Value>,\n Value | Value[],\n [Value[], Value[]]\n> {\n lc_graph_name = \"Topic\";\n\n unique = false;\n\n accumulate = false;\n\n seen: Set<Value>;\n\n values: Value[];\n\n constructor(fields?: {\n /**\n * Whether to only add unique values to the topic. If `true`, only unique values (using reference equality) will be added to the topic.\n */\n unique?: boolean;\n /**\n * Whether to accumulate values across steps. If `false`, the channel will be emptied after each step.\n */\n accumulate?: boolean;\n }) {\n super();\n\n this.unique = fields?.unique ?? this.unique;\n this.accumulate = fields?.accumulate ?? this.accumulate;\n // State\n this.seen = new Set<Value>();\n this.values = [];\n }\n\n public fromCheckpoint(checkpoint?: [Value[], Value[]]) {\n const empty = new Topic<Value>({\n unique: this.unique,\n accumulate: this.accumulate,\n });\n if (typeof checkpoint !== \"undefined\") {\n empty.seen = new Set(checkpoint[0]);\n // eslint-disable-next-line prefer-destructuring\n empty.values = checkpoint[1];\n }\n return empty as this;\n }\n\n public update(values: Array<Value | Value[]>): boolean {\n let updated = false;\n if (!this.accumulate) {\n updated = this.values.length > 0;\n this.values = [];\n }\n const flatValues = values.flat() as Value[];\n if (flatValues.length > 0) {\n if (this.unique) {\n for (const value of flatValues) {\n if (!this.seen.has(value)) {\n updated = true;\n this.seen.add(value);\n this.values.push(value);\n }\n }\n } else {\n updated = true;\n this.values.push(...flatValues);\n }\n }\n return updated;\n }\n\n public get(): Array<Value> {\n if (this.values.length === 0) {\n throw new EmptyChannelError();\n }\n return this.values;\n }\n\n public checkpoint(): [Value[], Value[]] {\n return [[...this.seen], this.values];\n }\n\n isAvailable(): boolean {\n return this.values.length !== 0;\n }\n}\n"],"mappings":";;;;;;AAMA,IAAa,QAAb,MAAa,cAAqBA,aAAAA,YAIhC;CACA,gBAAgB;CAEhB,SAAS;CAET,aAAa;CAEb;CAEA;CAEA,YAAY,QAST;AACD,SAAO;AAEP,OAAK,SAAS,QAAQ,UAAU,KAAK;AACrC,OAAK,aAAa,QAAQ,cAAc,KAAK;AAE7C,OAAK,uBAAO,IAAI,KAAY;AAC5B,OAAK,SAAS,EAAE;;CAGlB,eAAsB,YAAiC;EACrD,MAAM,QAAQ,IAAI,MAAa;GAC7B,QAAQ,KAAK;GACb,YAAY,KAAK;GAClB,CAAC;AACF,MAAI,OAAO,eAAe,aAAa;AACrC,SAAM,OAAO,IAAI,IAAI,WAAW,GAAG;AAEnC,SAAM,SAAS,WAAW;;AAE5B,SAAO;;CAGT,OAAc,QAAyC;EACrD,IAAI,UAAU;AACd,MAAI,CAAC,KAAK,YAAY;AACpB,aAAU,KAAK,OAAO,SAAS;AAC/B,QAAK,SAAS,EAAE;;EAElB,MAAM,aAAa,OAAO,MAAM;AAChC,MAAI,WAAW,SAAS,EACtB,KAAI,KAAK;QACF,MAAM,SAAS,WAClB,KAAI,CAAC,KAAK,KAAK,IAAI,MAAM,EAAE;AACzB,cAAU;AACV,SAAK,KAAK,IAAI,MAAM;AACpB,SAAK,OAAO,KAAK,MAAM;;SAGtB;AACL,aAAU;AACV,QAAK,OAAO,KAAK,GAAG,WAAW;;AAGnC,SAAO;;CAGT,MAA2B;AACzB,MAAI,KAAK,OAAO,WAAW,EACzB,OAAM,IAAIC,eAAAA,mBAAmB;AAE/B,SAAO,KAAK;;CAGd,aAAwC;AACtC,SAAO,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,KAAK,OAAO;;CAGtC,cAAuB;AACrB,SAAO,KAAK,OAAO,WAAW"}
1
+ {"version":3,"file":"topic.cjs","names":["BaseChannel","EmptyChannelError"],"sources":["../../src/channels/topic.ts"],"sourcesContent":["import { EmptyChannelError } from \"../errors.js\";\nimport { BaseChannel } from \"./base.js\";\n\n/** JS Topic checkpoint when `unique` is set: `[seen, values]`. */\ntype UniqueTopicCheckpoint<Value> = [Value[], Value[]];\n\n/**\n * Detect the `[seen, values]` Topic checkpoint shape.\n *\n * Used for `unique: true` topics (current) and pre-flat JS checkpoints\n * (legacy). Flat Python-parity checkpoints are a single values list.\n */\nfunction isUniqueTopicCheckpoint<Value>(\n checkpoint: unknown\n): checkpoint is UniqueTopicCheckpoint<Value> {\n return (\n Array.isArray(checkpoint) &&\n checkpoint.length === 2 &&\n Array.isArray(checkpoint[0]) &&\n Array.isArray(checkpoint[1])\n );\n}\n\n/**\n * A configurable PubSub Topic.\n */\nexport class Topic<Value> extends BaseChannel<\n Array<Value>,\n Value | Value[],\n Value[] | UniqueTopicCheckpoint<Value>\n> {\n lc_graph_name = \"Topic\";\n\n unique = false;\n\n accumulate = false;\n\n seen: Set<Value>;\n\n values: Value[];\n\n constructor(fields?: {\n /**\n * Whether to only add unique values to the topic. If `true`, only unique values (using reference equality) will be added to the topic.\n */\n unique?: boolean;\n /**\n * Whether to accumulate values across steps. If `false`, the channel will be emptied after each step.\n */\n accumulate?: boolean;\n }) {\n super();\n\n this.unique = fields?.unique ?? this.unique;\n this.accumulate = fields?.accumulate ?? this.accumulate;\n // State\n this.seen = new Set<Value>();\n this.values = [];\n }\n\n public fromCheckpoint(checkpoint?: Value[] | UniqueTopicCheckpoint<Value>) {\n const empty = new Topic<Value>({\n unique: this.unique,\n accumulate: this.accumulate,\n });\n if (typeof checkpoint === \"undefined\") {\n return empty as this;\n }\n // `unique: true` (and legacy pre-flat) checkpoints are `[seen, values]`.\n if (isUniqueTopicCheckpoint<Value>(checkpoint)) {\n empty.seen = new Set(checkpoint[0]);\n empty.values = [...checkpoint[1]];\n return empty as this;\n }\n empty.values = [...checkpoint];\n // Flat checkpoints have no historical `seen`. Seed from the current\n // buffer so `unique: true` still de-dupes values present after restore.\n if (this.unique) {\n empty.seen = new Set(empty.values);\n }\n return empty as this;\n }\n\n public update(values: Array<Value | Value[]>): boolean {\n let updated = false;\n if (!this.accumulate) {\n updated = this.values.length > 0;\n this.values = [];\n }\n const flatValues = values.flat() as Value[];\n if (flatValues.length > 0) {\n if (this.unique) {\n for (const value of flatValues) {\n if (!this.seen.has(value)) {\n updated = true;\n this.seen.add(value);\n this.values.push(value);\n }\n }\n } else {\n updated = true;\n this.values.push(...flatValues);\n }\n }\n return updated;\n }\n\n public get(): Array<Value> {\n if (this.values.length === 0) {\n throw new EmptyChannelError();\n }\n return this.values;\n }\n\n /**\n * Checkpoint Topic state.\n *\n * Default / Host path (`unique: false`, including `__pregel_tasks`): flat\n * values list, matching Python Topic. Previously this always returned\n * `[seen, values]`, so empty TASKS became `[[], []]` and Host's Python\n * checkpointer rejected it.\n *\n * When `unique: true`, keep `[seen, values]` so dedupe history that outlives\n * the current buffer still survives resume.\n */\n public checkpoint(): Value[] | UniqueTopicCheckpoint<Value> {\n if (this.unique) {\n return [[...this.seen], [...this.values]];\n }\n return [...this.values];\n }\n\n isAvailable(): boolean {\n return this.values.length !== 0;\n }\n}\n"],"mappings":";;;;;;;;;AAYA,SAAS,wBACP,YAC4C;AAC5C,QACE,MAAM,QAAQ,WAAW,IACzB,WAAW,WAAW,KACtB,MAAM,QAAQ,WAAW,GAAG,IAC5B,MAAM,QAAQ,WAAW,GAAG;;;;;AAOhC,IAAa,QAAb,MAAa,cAAqBA,aAAAA,YAIhC;CACA,gBAAgB;CAEhB,SAAS;CAET,aAAa;CAEb;CAEA;CAEA,YAAY,QAST;AACD,SAAO;AAEP,OAAK,SAAS,QAAQ,UAAU,KAAK;AACrC,OAAK,aAAa,QAAQ,cAAc,KAAK;AAE7C,OAAK,uBAAO,IAAI,KAAY;AAC5B,OAAK,SAAS,EAAE;;CAGlB,eAAsB,YAAqD;EACzE,MAAM,QAAQ,IAAI,MAAa;GAC7B,QAAQ,KAAK;GACb,YAAY,KAAK;GAClB,CAAC;AACF,MAAI,OAAO,eAAe,YACxB,QAAO;AAGT,MAAI,wBAA+B,WAAW,EAAE;AAC9C,SAAM,OAAO,IAAI,IAAI,WAAW,GAAG;AACnC,SAAM,SAAS,CAAC,GAAG,WAAW,GAAG;AACjC,UAAO;;AAET,QAAM,SAAS,CAAC,GAAG,WAAW;AAG9B,MAAI,KAAK,OACP,OAAM,OAAO,IAAI,IAAI,MAAM,OAAO;AAEpC,SAAO;;CAGT,OAAc,QAAyC;EACrD,IAAI,UAAU;AACd,MAAI,CAAC,KAAK,YAAY;AACpB,aAAU,KAAK,OAAO,SAAS;AAC/B,QAAK,SAAS,EAAE;;EAElB,MAAM,aAAa,OAAO,MAAM;AAChC,MAAI,WAAW,SAAS,EACtB,KAAI,KAAK;QACF,MAAM,SAAS,WAClB,KAAI,CAAC,KAAK,KAAK,IAAI,MAAM,EAAE;AACzB,cAAU;AACV,SAAK,KAAK,IAAI,MAAM;AACpB,SAAK,OAAO,KAAK,MAAM;;SAGtB;AACL,aAAU;AACV,QAAK,OAAO,KAAK,GAAG,WAAW;;AAGnC,SAAO;;CAGT,MAA2B;AACzB,MAAI,KAAK,OAAO,WAAW,EACzB,OAAM,IAAIC,eAAAA,mBAAmB;AAE/B,SAAO,KAAK;;;;;;;;;;;;;CAcd,aAA4D;AAC1D,MAAI,KAAK,OACP,QAAO,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC,GAAG,KAAK,OAAO,CAAC;AAE3C,SAAO,CAAC,GAAG,KAAK,OAAO;;CAGzB,cAAuB;AACrB,SAAO,KAAK,OAAO,WAAW"}
@@ -1,10 +1,12 @@
1
1
  import { BaseChannel } from "./base.cjs";
2
2
 
3
3
  //#region src/channels/topic.d.ts
4
+ /** JS Topic checkpoint when `unique` is set: `[seen, values]`. */
5
+ type UniqueTopicCheckpoint<Value> = [Value[], Value[]];
4
6
  /**
5
7
  * A configurable PubSub Topic.
6
8
  */
7
- declare class Topic<Value> extends BaseChannel<Array<Value>, Value | Value[], [Value[], Value[]]> {
9
+ declare class Topic<Value> extends BaseChannel<Array<Value>, Value | Value[], Value[] | UniqueTopicCheckpoint<Value>> {
8
10
  lc_graph_name: string;
9
11
  unique: boolean;
10
12
  accumulate: boolean;
@@ -20,10 +22,21 @@ declare class Topic<Value> extends BaseChannel<Array<Value>, Value | Value[], [V
20
22
  */
21
23
  accumulate?: boolean;
22
24
  });
23
- fromCheckpoint(checkpoint?: [Value[], Value[]]): this;
25
+ fromCheckpoint(checkpoint?: Value[] | UniqueTopicCheckpoint<Value>): this;
24
26
  update(values: Array<Value | Value[]>): boolean;
25
27
  get(): Array<Value>;
26
- checkpoint(): [Value[], Value[]];
28
+ /**
29
+ * Checkpoint Topic state.
30
+ *
31
+ * Default / Host path (`unique: false`, including `__pregel_tasks`): flat
32
+ * values list, matching Python Topic. Previously this always returned
33
+ * `[seen, values]`, so empty TASKS became `[[], []]` and Host's Python
34
+ * checkpointer rejected it.
35
+ *
36
+ * When `unique: true`, keep `[seen, values]` so dedupe history that outlives
37
+ * the current buffer still survives resume.
38
+ */
39
+ checkpoint(): Value[] | UniqueTopicCheckpoint<Value>;
27
40
  isAvailable(): boolean;
28
41
  }
29
42
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"topic.d.cts","names":[],"sources":["../../src/channels/topic.ts"],"mappings":";;;;;AAMA;cAAa,KAAA,gBAAqB,WAAA,CAChC,KAAA,CAAM,KAAA,GACN,KAAA,GAAQ,KAAA,KACP,KAAA,IAAS,KAAA;EAEV,aAAA;EAEA,MAAA;EAEA,UAAA;EAEA,IAAA,EAAM,GAAA,CAAI,KAAA;EAEV,MAAA,EAAQ,KAAA;EAER,WAAA,CAAY,MAAA;IAFJ;;;IAMN,MAAA;IA4BkC;;;IAxBlC,UAAA;EAAA;EAWK,cAAA,CAAe,UAAA,IAAc,KAAA,IAAS,KAAA;EAatC,MAAA,CAAO,MAAA,EAAQ,KAAA,CAAM,KAAA,GAAQ,KAAA;EAwB7B,GAAA,CAAA,GAAO,KAAA,CAAM,KAAA;EAOb,UAAA,CAAA,IAAe,KAAA,IAAS,KAAA;EAI/B,WAAA,CAAA;AAAA"}
1
+ {"version":3,"file":"topic.d.cts","names":[],"sources":["../../src/channels/topic.ts"],"mappings":";;;;KAIK,qBAAA,WAAgC,KAAA,IAAS,KAAA;AAHN;;;AAAA,cAyB3B,KAAA,gBAAqB,WAAA,CAChC,KAAA,CAAM,KAAA,GACN,KAAA,GAAQ,KAAA,IACR,KAAA,KAAU,qBAAA,CAAsB,KAAA;EAEhC,aAAA;EAEA,MAAA;EAEA,UAAA;EAEA,IAAA,EAAM,GAAA,CAAI,KAAA;EAEV,MAAA,EAAQ,KAAA;EAER,WAAA,CAAY,MAAA;IAfI;;;IAmBd,MAAA;IAjBF;;;IAqBE,UAAA;EAAA;EAWK,cAAA,CAAe,UAAA,GAAa,KAAA,KAAU,qBAAA,CAAsB,KAAA;EAuB5D,MAAA,CAAO,MAAA,EAAQ,KAAA,CAAM,KAAA,GAAQ,KAAA;EAwB7B,GAAA,CAAA,GAAO,KAAA,CAAM,KAAA;EA/Ce;;;;;;;;;;;EAiE5B,UAAA,CAAA,GAAc,KAAA,KAAU,qBAAA,CAAsB,KAAA;EAOrD,WAAA,CAAA;AAAA"}
@@ -1,10 +1,12 @@
1
1
  import { BaseChannel } from "./base.js";
2
2
 
3
3
  //#region src/channels/topic.d.ts
4
+ /** JS Topic checkpoint when `unique` is set: `[seen, values]`. */
5
+ type UniqueTopicCheckpoint<Value> = [Value[], Value[]];
4
6
  /**
5
7
  * A configurable PubSub Topic.
6
8
  */
7
- declare class Topic<Value> extends BaseChannel<Array<Value>, Value | Value[], [Value[], Value[]]> {
9
+ declare class Topic<Value> extends BaseChannel<Array<Value>, Value | Value[], Value[] | UniqueTopicCheckpoint<Value>> {
8
10
  lc_graph_name: string;
9
11
  unique: boolean;
10
12
  accumulate: boolean;
@@ -20,10 +22,21 @@ declare class Topic<Value> extends BaseChannel<Array<Value>, Value | Value[], [V
20
22
  */
21
23
  accumulate?: boolean;
22
24
  });
23
- fromCheckpoint(checkpoint?: [Value[], Value[]]): this;
25
+ fromCheckpoint(checkpoint?: Value[] | UniqueTopicCheckpoint<Value>): this;
24
26
  update(values: Array<Value | Value[]>): boolean;
25
27
  get(): Array<Value>;
26
- checkpoint(): [Value[], Value[]];
28
+ /**
29
+ * Checkpoint Topic state.
30
+ *
31
+ * Default / Host path (`unique: false`, including `__pregel_tasks`): flat
32
+ * values list, matching Python Topic. Previously this always returned
33
+ * `[seen, values]`, so empty TASKS became `[[], []]` and Host's Python
34
+ * checkpointer rejected it.
35
+ *
36
+ * When `unique: true`, keep `[seen, values]` so dedupe history that outlives
37
+ * the current buffer still survives resume.
38
+ */
39
+ checkpoint(): Value[] | UniqueTopicCheckpoint<Value>;
27
40
  isAvailable(): boolean;
28
41
  }
29
42
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"topic.d.ts","names":[],"sources":["../../src/channels/topic.ts"],"mappings":";;;;;AAMA;cAAa,KAAA,gBAAqB,WAAA,CAChC,KAAA,CAAM,KAAA,GACN,KAAA,GAAQ,KAAA,KACP,KAAA,IAAS,KAAA;EAEV,aAAA;EAEA,MAAA;EAEA,UAAA;EAEA,IAAA,EAAM,GAAA,CAAI,KAAA;EAEV,MAAA,EAAQ,KAAA;EAER,WAAA,CAAY,MAAA;IAFJ;;;IAMN,MAAA;IA4BkC;;;IAxBlC,UAAA;EAAA;EAWK,cAAA,CAAe,UAAA,IAAc,KAAA,IAAS,KAAA;EAatC,MAAA,CAAO,MAAA,EAAQ,KAAA,CAAM,KAAA,GAAQ,KAAA;EAwB7B,GAAA,CAAA,GAAO,KAAA,CAAM,KAAA;EAOb,UAAA,CAAA,IAAe,KAAA,IAAS,KAAA;EAI/B,WAAA,CAAA;AAAA"}
1
+ {"version":3,"file":"topic.d.ts","names":[],"sources":["../../src/channels/topic.ts"],"mappings":";;;;KAIK,qBAAA,WAAgC,KAAA,IAAS,KAAA;AAHN;;;AAAA,cAyB3B,KAAA,gBAAqB,WAAA,CAChC,KAAA,CAAM,KAAA,GACN,KAAA,GAAQ,KAAA,IACR,KAAA,KAAU,qBAAA,CAAsB,KAAA;EAEhC,aAAA;EAEA,MAAA;EAEA,UAAA;EAEA,IAAA,EAAM,GAAA,CAAI,KAAA;EAEV,MAAA,EAAQ,KAAA;EAER,WAAA,CAAY,MAAA;IAfI;;;IAmBd,MAAA;IAjBF;;;IAqBE,UAAA;EAAA;EAWK,cAAA,CAAe,UAAA,GAAa,KAAA,KAAU,qBAAA,CAAsB,KAAA;EAuB5D,MAAA,CAAO,MAAA,EAAQ,KAAA,CAAM,KAAA,GAAQ,KAAA;EAwB7B,GAAA,CAAA,GAAO,KAAA,CAAM,KAAA;EA/Ce;;;;;;;;;;;EAiE5B,UAAA,CAAA,GAAc,KAAA,KAAU,qBAAA,CAAsB,KAAA;EAOrD,WAAA,CAAA;AAAA"}
@@ -2,6 +2,15 @@ import { EmptyChannelError } from "../errors.js";
2
2
  import { BaseChannel } from "./base.js";
3
3
  //#region src/channels/topic.ts
4
4
  /**
5
+ * Detect the `[seen, values]` Topic checkpoint shape.
6
+ *
7
+ * Used for `unique: true` topics (current) and pre-flat JS checkpoints
8
+ * (legacy). Flat Python-parity checkpoints are a single values list.
9
+ */
10
+ function isUniqueTopicCheckpoint(checkpoint) {
11
+ return Array.isArray(checkpoint) && checkpoint.length === 2 && Array.isArray(checkpoint[0]) && Array.isArray(checkpoint[1]);
12
+ }
13
+ /**
5
14
  * A configurable PubSub Topic.
6
15
  */
7
16
  var Topic = class Topic extends BaseChannel {
@@ -22,10 +31,14 @@ var Topic = class Topic extends BaseChannel {
22
31
  unique: this.unique,
23
32
  accumulate: this.accumulate
24
33
  });
25
- if (typeof checkpoint !== "undefined") {
34
+ if (typeof checkpoint === "undefined") return empty;
35
+ if (isUniqueTopicCheckpoint(checkpoint)) {
26
36
  empty.seen = new Set(checkpoint[0]);
27
- empty.values = checkpoint[1];
37
+ empty.values = [...checkpoint[1]];
38
+ return empty;
28
39
  }
40
+ empty.values = [...checkpoint];
41
+ if (this.unique) empty.seen = new Set(empty.values);
29
42
  return empty;
30
43
  }
31
44
  update(values) {
@@ -51,8 +64,20 @@ var Topic = class Topic extends BaseChannel {
51
64
  if (this.values.length === 0) throw new EmptyChannelError();
52
65
  return this.values;
53
66
  }
67
+ /**
68
+ * Checkpoint Topic state.
69
+ *
70
+ * Default / Host path (`unique: false`, including `__pregel_tasks`): flat
71
+ * values list, matching Python Topic. Previously this always returned
72
+ * `[seen, values]`, so empty TASKS became `[[], []]` and Host's Python
73
+ * checkpointer rejected it.
74
+ *
75
+ * When `unique: true`, keep `[seen, values]` so dedupe history that outlives
76
+ * the current buffer still survives resume.
77
+ */
54
78
  checkpoint() {
55
- return [[...this.seen], this.values];
79
+ if (this.unique) return [[...this.seen], [...this.values]];
80
+ return [...this.values];
56
81
  }
57
82
  isAvailable() {
58
83
  return this.values.length !== 0;
@@ -1 +1 @@
1
- {"version":3,"file":"topic.js","names":[],"sources":["../../src/channels/topic.ts"],"sourcesContent":["import { EmptyChannelError } from \"../errors.js\";\nimport { BaseChannel } from \"./base.js\";\n\n/**\n * A configurable PubSub Topic.\n */\nexport class Topic<Value> extends BaseChannel<\n Array<Value>,\n Value | Value[],\n [Value[], Value[]]\n> {\n lc_graph_name = \"Topic\";\n\n unique = false;\n\n accumulate = false;\n\n seen: Set<Value>;\n\n values: Value[];\n\n constructor(fields?: {\n /**\n * Whether to only add unique values to the topic. If `true`, only unique values (using reference equality) will be added to the topic.\n */\n unique?: boolean;\n /**\n * Whether to accumulate values across steps. If `false`, the channel will be emptied after each step.\n */\n accumulate?: boolean;\n }) {\n super();\n\n this.unique = fields?.unique ?? this.unique;\n this.accumulate = fields?.accumulate ?? this.accumulate;\n // State\n this.seen = new Set<Value>();\n this.values = [];\n }\n\n public fromCheckpoint(checkpoint?: [Value[], Value[]]) {\n const empty = new Topic<Value>({\n unique: this.unique,\n accumulate: this.accumulate,\n });\n if (typeof checkpoint !== \"undefined\") {\n empty.seen = new Set(checkpoint[0]);\n // eslint-disable-next-line prefer-destructuring\n empty.values = checkpoint[1];\n }\n return empty as this;\n }\n\n public update(values: Array<Value | Value[]>): boolean {\n let updated = false;\n if (!this.accumulate) {\n updated = this.values.length > 0;\n this.values = [];\n }\n const flatValues = values.flat() as Value[];\n if (flatValues.length > 0) {\n if (this.unique) {\n for (const value of flatValues) {\n if (!this.seen.has(value)) {\n updated = true;\n this.seen.add(value);\n this.values.push(value);\n }\n }\n } else {\n updated = true;\n this.values.push(...flatValues);\n }\n }\n return updated;\n }\n\n public get(): Array<Value> {\n if (this.values.length === 0) {\n throw new EmptyChannelError();\n }\n return this.values;\n }\n\n public checkpoint(): [Value[], Value[]] {\n return [[...this.seen], this.values];\n }\n\n isAvailable(): boolean {\n return this.values.length !== 0;\n }\n}\n"],"mappings":";;;;;;AAMA,IAAa,QAAb,MAAa,cAAqB,YAIhC;CACA,gBAAgB;CAEhB,SAAS;CAET,aAAa;CAEb;CAEA;CAEA,YAAY,QAST;AACD,SAAO;AAEP,OAAK,SAAS,QAAQ,UAAU,KAAK;AACrC,OAAK,aAAa,QAAQ,cAAc,KAAK;AAE7C,OAAK,uBAAO,IAAI,KAAY;AAC5B,OAAK,SAAS,EAAE;;CAGlB,eAAsB,YAAiC;EACrD,MAAM,QAAQ,IAAI,MAAa;GAC7B,QAAQ,KAAK;GACb,YAAY,KAAK;GAClB,CAAC;AACF,MAAI,OAAO,eAAe,aAAa;AACrC,SAAM,OAAO,IAAI,IAAI,WAAW,GAAG;AAEnC,SAAM,SAAS,WAAW;;AAE5B,SAAO;;CAGT,OAAc,QAAyC;EACrD,IAAI,UAAU;AACd,MAAI,CAAC,KAAK,YAAY;AACpB,aAAU,KAAK,OAAO,SAAS;AAC/B,QAAK,SAAS,EAAE;;EAElB,MAAM,aAAa,OAAO,MAAM;AAChC,MAAI,WAAW,SAAS,EACtB,KAAI,KAAK;QACF,MAAM,SAAS,WAClB,KAAI,CAAC,KAAK,KAAK,IAAI,MAAM,EAAE;AACzB,cAAU;AACV,SAAK,KAAK,IAAI,MAAM;AACpB,SAAK,OAAO,KAAK,MAAM;;SAGtB;AACL,aAAU;AACV,QAAK,OAAO,KAAK,GAAG,WAAW;;AAGnC,SAAO;;CAGT,MAA2B;AACzB,MAAI,KAAK,OAAO,WAAW,EACzB,OAAM,IAAI,mBAAmB;AAE/B,SAAO,KAAK;;CAGd,aAAwC;AACtC,SAAO,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,KAAK,OAAO;;CAGtC,cAAuB;AACrB,SAAO,KAAK,OAAO,WAAW"}
1
+ {"version":3,"file":"topic.js","names":[],"sources":["../../src/channels/topic.ts"],"sourcesContent":["import { EmptyChannelError } from \"../errors.js\";\nimport { BaseChannel } from \"./base.js\";\n\n/** JS Topic checkpoint when `unique` is set: `[seen, values]`. */\ntype UniqueTopicCheckpoint<Value> = [Value[], Value[]];\n\n/**\n * Detect the `[seen, values]` Topic checkpoint shape.\n *\n * Used for `unique: true` topics (current) and pre-flat JS checkpoints\n * (legacy). Flat Python-parity checkpoints are a single values list.\n */\nfunction isUniqueTopicCheckpoint<Value>(\n checkpoint: unknown\n): checkpoint is UniqueTopicCheckpoint<Value> {\n return (\n Array.isArray(checkpoint) &&\n checkpoint.length === 2 &&\n Array.isArray(checkpoint[0]) &&\n Array.isArray(checkpoint[1])\n );\n}\n\n/**\n * A configurable PubSub Topic.\n */\nexport class Topic<Value> extends BaseChannel<\n Array<Value>,\n Value | Value[],\n Value[] | UniqueTopicCheckpoint<Value>\n> {\n lc_graph_name = \"Topic\";\n\n unique = false;\n\n accumulate = false;\n\n seen: Set<Value>;\n\n values: Value[];\n\n constructor(fields?: {\n /**\n * Whether to only add unique values to the topic. If `true`, only unique values (using reference equality) will be added to the topic.\n */\n unique?: boolean;\n /**\n * Whether to accumulate values across steps. If `false`, the channel will be emptied after each step.\n */\n accumulate?: boolean;\n }) {\n super();\n\n this.unique = fields?.unique ?? this.unique;\n this.accumulate = fields?.accumulate ?? this.accumulate;\n // State\n this.seen = new Set<Value>();\n this.values = [];\n }\n\n public fromCheckpoint(checkpoint?: Value[] | UniqueTopicCheckpoint<Value>) {\n const empty = new Topic<Value>({\n unique: this.unique,\n accumulate: this.accumulate,\n });\n if (typeof checkpoint === \"undefined\") {\n return empty as this;\n }\n // `unique: true` (and legacy pre-flat) checkpoints are `[seen, values]`.\n if (isUniqueTopicCheckpoint<Value>(checkpoint)) {\n empty.seen = new Set(checkpoint[0]);\n empty.values = [...checkpoint[1]];\n return empty as this;\n }\n empty.values = [...checkpoint];\n // Flat checkpoints have no historical `seen`. Seed from the current\n // buffer so `unique: true` still de-dupes values present after restore.\n if (this.unique) {\n empty.seen = new Set(empty.values);\n }\n return empty as this;\n }\n\n public update(values: Array<Value | Value[]>): boolean {\n let updated = false;\n if (!this.accumulate) {\n updated = this.values.length > 0;\n this.values = [];\n }\n const flatValues = values.flat() as Value[];\n if (flatValues.length > 0) {\n if (this.unique) {\n for (const value of flatValues) {\n if (!this.seen.has(value)) {\n updated = true;\n this.seen.add(value);\n this.values.push(value);\n }\n }\n } else {\n updated = true;\n this.values.push(...flatValues);\n }\n }\n return updated;\n }\n\n public get(): Array<Value> {\n if (this.values.length === 0) {\n throw new EmptyChannelError();\n }\n return this.values;\n }\n\n /**\n * Checkpoint Topic state.\n *\n * Default / Host path (`unique: false`, including `__pregel_tasks`): flat\n * values list, matching Python Topic. Previously this always returned\n * `[seen, values]`, so empty TASKS became `[[], []]` and Host's Python\n * checkpointer rejected it.\n *\n * When `unique: true`, keep `[seen, values]` so dedupe history that outlives\n * the current buffer still survives resume.\n */\n public checkpoint(): Value[] | UniqueTopicCheckpoint<Value> {\n if (this.unique) {\n return [[...this.seen], [...this.values]];\n }\n return [...this.values];\n }\n\n isAvailable(): boolean {\n return this.values.length !== 0;\n }\n}\n"],"mappings":";;;;;;;;;AAYA,SAAS,wBACP,YAC4C;AAC5C,QACE,MAAM,QAAQ,WAAW,IACzB,WAAW,WAAW,KACtB,MAAM,QAAQ,WAAW,GAAG,IAC5B,MAAM,QAAQ,WAAW,GAAG;;;;;AAOhC,IAAa,QAAb,MAAa,cAAqB,YAIhC;CACA,gBAAgB;CAEhB,SAAS;CAET,aAAa;CAEb;CAEA;CAEA,YAAY,QAST;AACD,SAAO;AAEP,OAAK,SAAS,QAAQ,UAAU,KAAK;AACrC,OAAK,aAAa,QAAQ,cAAc,KAAK;AAE7C,OAAK,uBAAO,IAAI,KAAY;AAC5B,OAAK,SAAS,EAAE;;CAGlB,eAAsB,YAAqD;EACzE,MAAM,QAAQ,IAAI,MAAa;GAC7B,QAAQ,KAAK;GACb,YAAY,KAAK;GAClB,CAAC;AACF,MAAI,OAAO,eAAe,YACxB,QAAO;AAGT,MAAI,wBAA+B,WAAW,EAAE;AAC9C,SAAM,OAAO,IAAI,IAAI,WAAW,GAAG;AACnC,SAAM,SAAS,CAAC,GAAG,WAAW,GAAG;AACjC,UAAO;;AAET,QAAM,SAAS,CAAC,GAAG,WAAW;AAG9B,MAAI,KAAK,OACP,OAAM,OAAO,IAAI,IAAI,MAAM,OAAO;AAEpC,SAAO;;CAGT,OAAc,QAAyC;EACrD,IAAI,UAAU;AACd,MAAI,CAAC,KAAK,YAAY;AACpB,aAAU,KAAK,OAAO,SAAS;AAC/B,QAAK,SAAS,EAAE;;EAElB,MAAM,aAAa,OAAO,MAAM;AAChC,MAAI,WAAW,SAAS,EACtB,KAAI,KAAK;QACF,MAAM,SAAS,WAClB,KAAI,CAAC,KAAK,KAAK,IAAI,MAAM,EAAE;AACzB,cAAU;AACV,SAAK,KAAK,IAAI,MAAM;AACpB,SAAK,OAAO,KAAK,MAAM;;SAGtB;AACL,aAAU;AACV,QAAK,OAAO,KAAK,GAAG,WAAW;;AAGnC,SAAO;;CAGT,MAA2B;AACzB,MAAI,KAAK,OAAO,WAAW,EACzB,OAAM,IAAI,mBAAmB;AAE/B,SAAO,KAAK;;;;;;;;;;;;;CAcd,aAA4D;AAC1D,MAAI,KAAK,OACP,QAAO,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC,GAAG,KAAK,OAAO,CAAC;AAE3C,SAAO,CAAC,GAAG,KAAK,OAAO;;CAGzB,cAAuB;AACrB,SAAO,KAAK,OAAO,WAAW"}
@@ -170,6 +170,16 @@ var PregelLoop = class PregelLoop {
170
170
  });
171
171
  this.checkpointerPromises.add(tracked);
172
172
  }
173
+ /**
174
+ * Wait for persistence work scheduled by this run up to this point.
175
+ *
176
+ * Taking a snapshot keeps the barrier scoped to the completed superstep:
177
+ * persistence scheduled later cannot extend it, while failures from the
178
+ * captured work are propagated at the superstep boundary.
179
+ */
180
+ async _awaitCheckpointerPromises() {
181
+ await Promise.all([...this.checkpointerPromises]);
182
+ }
173
183
  store;
174
184
  cache;
175
185
  manager;
@@ -462,6 +472,7 @@ var PregelLoop = class PregelLoop {
462
472
  }
463
473
  this.checkpointPendingWrites = [];
464
474
  await this._putCheckpoint({ source: "loop" });
475
+ if (this.durability === "sync") await this._awaitCheckpointerPromises();
465
476
  this._emitValuesWithCheckpointMeta(valuesOutput);
466
477
  if (require_algo.shouldInterrupt(this.checkpoint, this.interruptAfter, finishTaskList)) {
467
478
  this.status = "interrupt_after";