Skip to content

Commit 7a889b3

Browse files
committed
feat: make preview possible in development mode
1 parent 93d65bb commit 7a889b3

File tree

6 files changed

+245
-9
lines changed

6 files changed

+245
-9
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Tired of uploading a markdown file to your GitHub for every new blog post? Havin
2929
- ⌨️ Title and their properties in plain text accessible via the front matter
3030
- 🔮 All raw properties accessible via GraphQL
3131
- 🍻 Support for `remark` and `mdx`
32+
- 👀 Near real-time preview in development mode
3233

3334
# Quick Start
3435

@@ -213,9 +214,38 @@ interface PluginConfig {
213214
databases?: string[];
214215
/** UUID of pages to be sourced, default to be `[]` i.e. none */
215216
pages?: string[];
217+
/** the number of api calls per seconds allowed for preview, 0 to disable preview default to be 2.5 */
218+
previewCallRate?: number;
219+
/** TTL settings for each API call types, default to cache database metadata and blocks */
220+
previewTTL?: {
221+
/** the number of seconds in which a database metadata will be cached, default to be 0 i.e. permanent */
222+
databaseMeta?: number;
223+
/** the number of seconds in which a metadata of a database's entries will be cached, default to be 0.5 */
224+
databaseEntries?: number;
225+
/** the number of seconds in which a page metadata will be cached, default to be 0.5 */
226+
pageMeta?: number;
227+
/** the number of seconds in which a page content will be cached, default to be 0 i.e. permanent */
228+
pageContent?: number;
229+
};
216230
}
217231
```
218232

233+
# Preview Mode
234+
235+
This plugin ships with a preview mode by default and it is enabled.
236+
Start your development server and type on your Notion page to see the content get updated on the Gatsby website.
237+
238+
Under the hood, this plugin automatically pulls the page metadata from Notion regularly and checks for any updates using the `last_edited_time` property.
239+
When a change is detected, this plugin will reload the content automatically.
240+
241+
**NOTE** To adjust the frequency of update, you can specify the maximum allowed number of API calls.
242+
The higher the more frequently it checks for updates.
243+
The actual frequency will be computed automatically according to your needs but be mindful of current limits for Notion API which is 3 requests per second at time of publishing.
244+
245+
**NOTE** Unlike other integrations with preview, such as `gatsby-source-sanity`, this plugin can't sync any content from your Notion document that wasn't saved.
246+
Notion has autosaving, but it is delayed so you might not see an immediate change in preview.
247+
Don't worry though, because it’s only a matter of time before you see the change.
248+
219249
# Known Limitations
220250

221251
As this plugin relies on the the official Notion API which is still in beta, we share the same limitations as the API.
@@ -260,6 +290,7 @@ You just need to embed them using the normal markdown syntax as part of your par
260290

261291
3. What can I do if I don't want to permanently delete a post but just hide it for awhile?
262292
You can create a page property (for example, a publish double checkbox) and use this information in your page creation process.
293+
If you're in the development mode with preview enabled, you should be able to see the removal in near real-time.
263294

264295
# About
265296

source/gatsby-node.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import { version as gatsbyVersion } from 'gatsby/package.json';
1717

1818
import { name } from '#.';
19-
import { normaliseConfig, sync } from '#plugin';
19+
import { computePreviewUpdateInterval, normaliseConfig, sync } from '#plugin';
2020

2121
import type { GatsbyNode } from 'gatsby';
2222

@@ -31,6 +31,14 @@ export const pluginOptionsSchema: NonNullable<
3131
version: joi.string().optional(),
3232
databases: joi.array().items(joi.string()).optional(),
3333
pages: joi.array().items(joi.string()).optional(),
34+
ttl: joi
35+
.object({
36+
databaseMeta: joi.number().optional(),
37+
databaseEntries: joi.number().optional(),
38+
pageMeta: joi.number().optional(),
39+
pageContent: joi.number().optional(),
40+
})
41+
.optional(),
3442
});
3543
};
3644

@@ -47,6 +55,26 @@ export const onPreBootstrap: NonNullable<GatsbyNode['onPreBootstrap']> = async (
4755
}
4856
};
4957

58+
export const onCreateDevServer: NonNullable<GatsbyNode['onCreateDevServer']> =
59+
async (args, partialConfig) => {
60+
const pluginConfig = normaliseConfig(partialConfig);
61+
const { previewCallRate } = pluginConfig;
62+
63+
const previewUpdateInterval = computePreviewUpdateInterval(pluginConfig);
64+
if (previewCallRate && previewUpdateInterval) {
65+
const scheduleUpdate = (): NodeJS.Timeout =>
66+
setTimeout(async () => {
67+
// sync entities from notion
68+
await sync(args, pluginConfig);
69+
70+
// schedule the next update
71+
scheduleUpdate();
72+
}, previewUpdateInterval);
73+
74+
scheduleUpdate();
75+
}
76+
};
77+
5078
export const sourceNodes: NonNullable<GatsbyNode['sourceNodes']> = async (
5179
args,
5280
partialConfig,

source/node.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ export class NodeManager {
103103
this.createNode(this.nodifyEntity(entity));
104104
}
105105

106-
this.reporter.info(`[${name}] added ${added.length} nodes`);
106+
// don't be noisy if there's nothing new happen
107+
if (added.length > 0) {
108+
this.reporter.info(`[${name}] added ${added.length} nodes`);
109+
}
107110
}
108111

109112
/**
@@ -115,7 +118,10 @@ export class NodeManager {
115118
this.createNode(this.nodifyEntity(entity));
116119
}
117120

118-
this.reporter.info(`[${name}] updated ${updated.length} nodes`);
121+
// don't be noisy if there's nothing new happen
122+
if (updated.length > 0) {
123+
this.reporter.info(`[${name}] updated ${updated.length} nodes`);
124+
}
119125
}
120126

121127
/**
@@ -127,7 +133,10 @@ export class NodeManager {
127133
this.deleteNode(this.nodifyEntity(entity));
128134
}
129135

130-
this.reporter.info(`[${name}] removed ${removed.length} nodes`);
136+
// don't be noisy if there's nothing new happen
137+
if (removed.length > 0) {
138+
this.reporter.info(`[${name}] removed ${removed.length} nodes`);
139+
}
131140
}
132141

133142
/**

source/plugin.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
* -------------------------------------------------------------------------
1414
*/
1515

16-
import { Notion } from '#client';
16+
import { DEFAULT_TTL, Notion } from '#client';
1717
import { NodeManager } from '#node';
1818

1919
import type { NodePluginArgs, PluginOptions } from 'gatsby';
2020

21-
import type { NotionOptions } from '#client';
21+
import type { NotionOptions, NotionTTL } from '#client';
2222
import type { FullDatabase, FullPage } from '#types';
2323

2424
/** options for the source plugin */
@@ -27,11 +27,47 @@ export interface PluginConfig extends PluginOptions, NotionOptions {
2727
databases?: string[];
2828
/** id of pages to be sourced, default to be all shared pages */
2929
pages?: string[];
30+
/** the number of api calls per seconds allowed for preview, 0 to disable preview default 2.5 */
31+
previewCallRate?: number;
32+
/** TTL settings for each API call types, default to cache database metadata and blocks */
33+
previewTTL?: Partial<NotionTTL>;
3034
}
3135

3236
interface FullPluginConfig extends PluginConfig {
3337
databases: string[];
3438
pages: string[];
39+
previewCallRate: number;
40+
previewTTL: NotionTTL;
41+
}
42+
43+
const ONE_SECOND = 1000;
44+
45+
const DEFAULT_PREVIEW_API_RATE = 2.5;
46+
47+
/**
48+
* compute the update interval for the preview mode
49+
* @param pluginConfig the normalised plugin config
50+
* @returns the number of milliseconds needed between each sync
51+
*/
52+
export function computePreviewUpdateInterval(
53+
pluginConfig: FullPluginConfig,
54+
): number | null {
55+
const { previewCallRate, databases, pages, previewTTL } = pluginConfig;
56+
57+
// it's minimum because if a page get edited, it will consume more
58+
const minAPICallsNeededPerSync =
59+
databases.length *
60+
// get page title and properties etc.
61+
((previewTTL.databaseEntries !== 0 ? 1 : 0) +
62+
// it will take 1 more if we want to keep database title and properties etc. up-to-date as well
63+
(previewTTL.databaseMeta !== 0 ? 1 : 0)) +
64+
pages.length *
65+
// get page title and properties etc.
66+
(previewTTL.pageMeta !== 0 ? 1 : 0);
67+
68+
const interval = (ONE_SECOND * minAPICallsNeededPerSync) / previewCallRate;
69+
70+
return interval > 0 ? interval : null;
3571
}
3672

3773
/**
@@ -42,6 +78,8 @@ interface FullPluginConfig extends PluginConfig {
4278
export function normaliseConfig(
4379
config: Partial<PluginConfig>,
4480
): FullPluginConfig {
81+
const { previewCallRate = DEFAULT_PREVIEW_API_RATE } = config;
82+
4583
const databases = [
4684
...(config.databases ?? []),
4785
...(process.env['GATSBY_NOTION_DATABASES']?.split(/, +/) ?? []),
@@ -58,7 +96,16 @@ export function normaliseConfig(
5896
(id) => !!id,
5997
);
6098

61-
return { ...config, databases, pages, plugins: [] };
99+
const previewTTL = { ...DEFAULT_TTL, ...config.previewTTL };
100+
101+
return {
102+
...config,
103+
databases,
104+
pages,
105+
previewCallRate,
106+
previewTTL,
107+
plugins: [],
108+
};
62109
}
63110

64111
/**

spec/gatsby-node.spec.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
import gatsbyPackage from 'gatsby/package.json';
1717
import joi from 'joi';
1818

19-
import { pluginOptionsSchema, sourceNodes } from '#gatsby-node';
19+
import {
20+
onCreateDevServer,
21+
pluginOptionsSchema,
22+
sourceNodes,
23+
} from '#gatsby-node';
2024
import { sync } from '#plugin';
2125

2226
jest.mock('gatsby/package.json', () => {
@@ -73,6 +77,57 @@ describe('fn:onPreBootstrap', () => {
7377
it('fail with future gatsby after v3', testVersion('4.0.0', false));
7478
});
7579

80+
describe('fn:onCreateDevServer', () => {
81+
beforeEach(() => jest.clearAllMocks());
82+
beforeEach(() => jest.useFakeTimers());
83+
afterAll(() => jest.useRealTimers());
84+
85+
it('disable preview mode if the API rate is 0', async () => {
86+
await onCreateDevServer(
87+
{} as any,
88+
{
89+
previewCallRate: 0,
90+
databases: ['database'],
91+
pages: ['page'],
92+
plugins: [],
93+
},
94+
jest.fn(),
95+
);
96+
97+
jest.advanceTimersToNextTimer();
98+
99+
// not calling because it's disabled
100+
expect(sync).toBeCalledTimes(0);
101+
});
102+
103+
it('disable preview mode if no database or page is given', async () => {
104+
await onCreateDevServer({} as any, { plugins: [] }, jest.fn());
105+
106+
jest.advanceTimersToNextTimer();
107+
108+
// not calling because it's disabled
109+
expect(sync).toBeCalledTimes(0);
110+
});
111+
112+
it('continuously sync data with Notion', async () => {
113+
jest.clearAllMocks();
114+
await onCreateDevServer(
115+
{} as any,
116+
{ databases: ['database_continuous'], pages: ['page'], plugins: [] },
117+
jest.fn(),
118+
);
119+
120+
jest.advanceTimersToNextTimer();
121+
await Promise.resolve();
122+
expect(sync).toBeCalledTimes(1);
123+
124+
// sync again after a certain time
125+
jest.advanceTimersToNextTimer();
126+
await Promise.resolve();
127+
expect(sync).toBeCalledTimes(2);
128+
});
129+
});
130+
76131
describe('fn:sourceNodes', () => {
77132
beforeEach(() => jest.clearAllMocks());
78133
beforeEach(() => jest.useFakeTimers());

spec/plugin.spec.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
*/
1515

1616
import { Notion } from '#client';
17-
import { getDatabases, getPages, normaliseConfig, sync } from '#plugin';
17+
import {
18+
computePreviewUpdateInterval,
19+
getDatabases,
20+
getPages,
21+
normaliseConfig,
22+
sync,
23+
} from '#plugin';
1824
import { mockDatabase, mockPage } from './mock';
1925

2026
const mockUpdate = jest.fn();
@@ -27,6 +33,66 @@ jest.mock('#node', () => ({
2733

2834
const client = new Notion({ token: 'token' });
2935

36+
describe('fn:computeUpdateInterval', () => {
37+
it('compute the interval based on the total number of databases and pages', () => {
38+
expect(
39+
computePreviewUpdateInterval({
40+
databases: ['database1', 'database2'],
41+
pages: ['page1', 'page2'],
42+
// 2 for database page query, 2 for page query
43+
previewCallRate: 4,
44+
// cache database title, but not pages' properties
45+
previewTTL: {
46+
databaseMeta: 0,
47+
databaseEntries: 1,
48+
pageMeta: 1,
49+
pageContent: 0,
50+
},
51+
plugins: [],
52+
}),
53+
).toEqual(1000);
54+
});
55+
56+
it('increase the call demand if database title and properties etc. needed to be synchronised as well', () => {
57+
expect(
58+
computePreviewUpdateInterval({
59+
databases: ['database1', 'database2'],
60+
pages: ['page1', 'page2'],
61+
// 2 for database meta, 2 for database page query, 2 for page query
62+
previewCallRate: 6,
63+
// cache database title, but not pages' properties
64+
previewTTL: {
65+
databaseMeta: 1,
66+
databaseEntries: 1,
67+
pageMeta: 1,
68+
// NOTE pageContent doesn't make a difference here because it will be reloaded if the `last_edited_time` has changed
69+
pageContent: 1,
70+
},
71+
plugins: [],
72+
}),
73+
).toEqual(1000);
74+
});
75+
76+
it('return null if everything is cached', () => {
77+
expect(
78+
computePreviewUpdateInterval({
79+
databases: ['database1', 'database2'],
80+
pages: ['page1', 'page2'],
81+
// 2 for database meta, 2 for database page query, 2 for page query
82+
previewCallRate: 100,
83+
// cache database title, but not pages' properties
84+
previewTTL: {
85+
databaseMeta: 0,
86+
databaseEntries: 0,
87+
pageMeta: 0,
88+
pageContent: 0,
89+
},
90+
plugins: [],
91+
}),
92+
).toEqual(null);
93+
});
94+
});
95+
3096
describe('fn:normaliseConfig', () => {
3197
const env = { ...process.env };
3298
afterEach(() => (process.env = { ...env }));

0 commit comments

Comments
 (0)