Skip to content

Commit 0b1b24e

Browse files
authored
Merge pull request #2 from alvis/fix/unexpected-node-update
Ensure that adding/updating a resource won't get others removed
2 parents 8c9d178 + c97d301 commit 0b1b24e

File tree

2 files changed

+317
-129
lines changed

2 files changed

+317
-129
lines changed

source/node.ts

Lines changed: 118 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ type NormalisedEntity<E extends FullEntity = FullEntity> = E extends any
3838
? Omit<E, 'parent'> & {
3939
parent: Link | null;
4040
children: Link[];
41-
digest: string;
4241
}
4342
: never;
4443

@@ -50,6 +49,7 @@ export class NodeManager {
5049
private createNodeId: NodePluginArgs['createNodeId'];
5150
private createContentDigest: NodePluginArgs['createContentDigest'];
5251
private cache: NodePluginArgs['cache'];
52+
private getNode: NodePluginArgs['getNode'];
5353
private reporter: NodePluginArgs['reporter'];
5454

5555
/**
@@ -63,6 +63,7 @@ export class NodeManager {
6363
cache,
6464
createContentDigest,
6565
createNodeId,
66+
getNode,
6667
reporter,
6768
} = args;
6869
/* eslint-enable */
@@ -73,6 +74,7 @@ export class NodeManager {
7374
this.touchNode = touchNode;
7475
this.createNodeId = createNodeId;
7576
this.createContentDigest = createContentDigest;
77+
this.getNode = getNode;
7678
this.reporter = reporter;
7779
}
7880

@@ -82,29 +84,28 @@ export class NodeManager {
8284
*/
8385
public async update(entities: FullEntity[]): Promise<void> {
8486
// get entries with relationship build-in
85-
const oldMap = new Map<string, NormalisedEntity>(
86-
(await this.cache.get('entityMap')) ?? [],
87+
const old = new Map<string, NodeInput>(
88+
(await this.cache.get('nodeGraph')) ?? [],
8789
);
88-
const newMap = computeEntityMap(entities, this.createContentDigest);
90+
const current = this.computeNodeGraph(entities);
91+
const { added, updated, removed, unchanged } = computeChanges(old, current);
8992

9093
// for the usage of createNode
9194
// see https://www.gatsbyjs.com/docs/reference/config-files/actions/#createNode
92-
await this.addNodes(this.findNewEntities(oldMap, newMap));
93-
this.updateNodes(this.findUpdatedEntities(oldMap, newMap));
94-
this.removeNodes(this.findRemovedEntities(oldMap, newMap));
95-
this.touchNodes([...newMap.values()]);
95+
await this.addNodes(added);
96+
await this.updateNodes(updated);
97+
this.removeNodes(removed);
98+
this.touchNodes(unchanged);
9699

97-
await this.cache.set('entityMap', [...newMap.entries()]);
100+
await this.cache.set('nodeGraph', [...current.entries()]);
98101
}
99102

100103
/**
101104
* add new nodes
102105
* @param added new nodes to be added
103106
*/
104-
private async addNodes(added: NormalisedEntity[]): Promise<void> {
105-
for (const entity of added) {
106-
const node = this.nodifyEntity(entity);
107-
107+
private async addNodes(added: NodeInput[]): Promise<void> {
108+
for (const node of added) {
108109
// DEBT: disable a false alarm from eslint as currently Gatsby is exporting an incorrect type
109110
// this should be removed when https://github.com/gatsbyjs/gatsby/pull/32522 is merged
110111
/* eslint-disable @typescript-eslint/await-thenable */
@@ -123,9 +124,14 @@ export class NodeManager {
123124
* update existing nodes
124125
* @param updated updated nodes
125126
*/
126-
private updateNodes(updated: NormalisedEntity[]): void {
127-
for (const entity of updated) {
128-
this.createNode(this.nodifyEntity(entity));
127+
private async updateNodes(updated: NodeInput[]): Promise<void> {
128+
for (const node of updated) {
129+
// DEBT: disable a false alarm from eslint as currently Gatsby is exporting an incorrect type
130+
// this should be removed when https://github.com/gatsbyjs/gatsby/pull/32522 is merged
131+
/* eslint-disable @typescript-eslint/await-thenable */
132+
// update the node
133+
await this.createNode(node);
134+
/* eslint-enable */
129135
}
130136

131137
// don't be noisy if there's nothing new happen
@@ -138,9 +144,9 @@ export class NodeManager {
138144
* remove old nodes
139145
* @param removed nodes to be removed
140146
*/
141-
private removeNodes(removed: NormalisedEntity[]): void {
142-
for (const entity of removed) {
143-
this.deleteNode(this.nodifyEntity(entity));
147+
private removeNodes(removed: NodeInput[]): void {
148+
for (const node of removed) {
149+
this.deleteNode(node);
144150
}
145151

146152
// don't be noisy if there's nothing new happen
@@ -150,22 +156,47 @@ export class NodeManager {
150156
}
151157

152158
/**
153-
* keep all current notion nodes alive
154-
* @param entities list of current notion entities
159+
* keep unchanged notion nodes alive
160+
* @param untouched list of current notion entities
155161
*/
156-
private touchNodes(entities: NormalisedEntity[]): void {
157-
for (const entity of entities) {
158-
const node = this.nodifyEntity(entity);
159-
this.touchNode({
160-
id: node.id,
161-
internal: {
162-
type: node.internal.type,
163-
contentDigest: node.internal.contentDigest,
164-
},
165-
});
162+
private touchNodes(untouched: NodeInput[]): void {
163+
for (const node of untouched) {
164+
// DEBT: disable a false alarm from eslint as currently Gatsby is exporting an incorrect type
165+
// this should be removed when https://github.com/gatsbyjs/gatsby/pull/32522 is merged
166+
/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
167+
if (this.getNode(node.id)) {
168+
// just make a light-touched operation if the node is still alive
169+
this.touchNode({
170+
id: node.id,
171+
internal: {
172+
type: node.internal.type,
173+
contentDigest: node.internal.contentDigest,
174+
},
175+
});
176+
} else {
177+
// recreate it again if somehow it's missing
178+
this.createNode(node);
179+
}
166180
}
167181

168-
this.reporter.info(`[${name}] processed ${entities.length} nodes`);
182+
this.reporter.info(`[${name}] keeping ${untouched.length} nodes`);
183+
}
184+
185+
/**
186+
* convert entities into gatsby node with full parent-child relationship
187+
* @param entities all sort of entities including database and page
188+
* @returns a map of gatsby nodes with parent and children linked
189+
*/
190+
private computeNodeGraph(entities: FullEntity[]): Map<string, NodeInput> {
191+
// first compute the graph with entities before converting to nodes
192+
const entityMap = computeEntityMap(entities);
193+
194+
return new Map<string, NodeInput>(
195+
[...entityMap.entries()].map(([id, entity]) => [
196+
id,
197+
this.nodifyEntity(entity),
198+
]),
199+
);
169200
}
170201

171202
/**
@@ -204,7 +235,7 @@ export class NodeManager {
204235
entity: NormalisedEntity,
205236
internal: Omit<NodeInput['internal'], 'contentDigest'> & { type: T },
206237
): ContentNode<T> {
207-
return {
238+
const basis = {
208239
id: this.createNodeId(`${entity.object}:${entity.id}`),
209240
ref: entity.id,
210241
createdTime: entity.created_time,
@@ -217,77 +248,20 @@ export class NodeManager {
217248
children: entity.children.map(({ object, id }) =>
218249
this.createNodeId(`${object}:${id}`),
219250
),
251+
};
252+
253+
const excludedKeys = ['parent', 'children', 'internal'];
254+
const contentDigest = this.createContentDigest(omit(basis, excludedKeys));
255+
256+
return {
257+
...basis,
220258
internal: {
221-
contentDigest: entity.digest,
259+
contentDigest,
222260
...internal,
223261
},
224262
};
225263
}
226264

227-
/**
228-
* find new entities
229-
* @param oldMap the old entity map generated from earlier data
230-
* @param newMap the new entity map computed from up-to-date data from Notion
231-
* @returns a list of new entities
232-
*/
233-
private findNewEntities(
234-
oldMap: Map<string, NormalisedEntity>,
235-
newMap: Map<string, NormalisedEntity>,
236-
): NormalisedEntity[] {
237-
const added: NormalisedEntity[] = [];
238-
for (const [id, newEntity] of newMap.entries()) {
239-
const oldEntity = oldMap.get(id);
240-
if (!oldEntity) {
241-
added.push(newEntity);
242-
}
243-
}
244-
245-
return added;
246-
}
247-
248-
/**
249-
* find removed entities
250-
* @param oldMap the old entity map generated from earlier data
251-
* @param newMap the new entity map computed from up-to-date data from Notion
252-
* @returns a list of removed entities
253-
*/
254-
private findRemovedEntities(
255-
oldMap: Map<string, NormalisedEntity>,
256-
newMap: Map<string, NormalisedEntity>,
257-
): NormalisedEntity[] {
258-
const removed: NormalisedEntity[] = [];
259-
260-
for (const [id, oldEntity] of oldMap.entries()) {
261-
if (!newMap.has(id)) {
262-
removed.push(oldEntity);
263-
}
264-
}
265-
266-
return removed;
267-
}
268-
269-
/**
270-
* find updated entities
271-
* @param oldMap the old entity map generated from earlier data
272-
* @param newMap the new entity map computed from up-to-date data from Notion
273-
* @returns a list of updated entities
274-
*/
275-
private findUpdatedEntities(
276-
oldMap: Map<string, NormalisedEntity>,
277-
newMap: Map<string, NormalisedEntity>,
278-
): NormalisedEntity[] {
279-
const updated: NormalisedEntity[] = [];
280-
281-
for (const [id, newEntity] of newMap.entries()) {
282-
const oldEntity = oldMap.get(id);
283-
if (oldEntity && oldEntity.digest !== newEntity.digest) {
284-
updated.push(newEntity);
285-
}
286-
}
287-
288-
return updated;
289-
}
290-
291265
/**
292266
* convert an entity to a NodeInput
293267
* @param entity the entity to be converted
@@ -306,15 +280,44 @@ export class NodeManager {
306280
}
307281
}
308282

283+
/**
284+
* compute changes between two node graphs
285+
* @param old the old graph
286+
* @param current the latest graph
287+
* @returns a map of nodes in different states
288+
*/
289+
export function computeChanges(
290+
old: Map<string, NodeInput>,
291+
current: Map<string, NodeInput>,
292+
): Record<'added' | 'updated' | 'removed' | 'unchanged', NodeInput[]> {
293+
const added = [...current.entries()].filter(([id]) => !old.has(id));
294+
const removed = [...old.entries()].filter(([id]) => !current.has(id));
295+
296+
const bothExists = [...current.entries()].filter(([id]) => old.has(id));
297+
const updated = bothExists.filter(
298+
([id, node]) =>
299+
old.get(id)!.internal.contentDigest !== node.internal.contentDigest,
300+
);
301+
const unchanged = bothExists.filter(
302+
([id, node]) =>
303+
old.get(id)!.internal.contentDigest === node.internal.contentDigest,
304+
);
305+
306+
return {
307+
added: added.map(([, node]) => node),
308+
updated: updated.map(([, node]) => node),
309+
removed: removed.map(([, node]) => node),
310+
unchanged: unchanged.map(([, node]) => node),
311+
};
312+
}
313+
309314
/**
310315
* attach parent-child relationship to gatsby node
311316
* @param entities all sort of entities including database and page
312-
* @param hashFn a hash function for generating the content digest
313317
* @returns a map of entities with parent and children linked
314318
*/
315319
export function computeEntityMap(
316320
entities: FullEntity[],
317-
hashFn: (content: string | FullEntity) => string,
318321
): Map<string, NormalisedEntity> {
319322
// create a new working set
320323
const map = new Map<string, NormalisedEntity>();
@@ -323,7 +326,6 @@ export function computeEntityMap(
323326
...entity,
324327
parent: normaliseParent(entity.parent),
325328
children: [],
326-
digest: hashFn(entity),
327329
});
328330
}
329331

@@ -366,3 +368,18 @@ export function normaliseParent(parent: FullEntity['parent']): Link | null {
366368
throw new TypeError(`unknown parent`);
367369
}
368370
}
371+
372+
/**
373+
* return an object with the specified keys omitted
374+
* @param record the record to be converted
375+
* @param keys a list of keys to be omitted
376+
* @returns an object with the specified keys omitted
377+
*/
378+
function omit(
379+
record: Record<string, unknown>,
380+
keys: string[],
381+
): Record<string, unknown> {
382+
return Object.fromEntries(
383+
Object.entries(record).filter(([key]) => !keys.includes(key)),
384+
);
385+
}

0 commit comments

Comments
 (0)