Skip to content

Commit

Permalink
Implement TypeScript types via JSDoc comments.
Browse files Browse the repository at this point in the history
  • Loading branch information
DevHawkNov committed Jan 14, 2022
1 parent 63130d7 commit 047ed7f
Show file tree
Hide file tree
Showing 80 changed files with 1,924 additions and 1,773 deletions.
8 changes: 8 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"extends": ["env"],
"settings": {
"jsdoc": {
"mode": "typescript"
},
"polyfills": [
"AbortController",
"CustomEvent",
Expand All @@ -10,5 +13,10 @@
"FormData",
"performance"
]
},
"rules": {
"jsdoc/require-description": "off",
"jsdoc/require-returns": "off",
"jsdoc/valid-types": "off"
}
}
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"typescript.disableAutomaticTypeAcquisition": true,
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
85 changes: 43 additions & 42 deletions Cache.mjs
Original file line number Diff line number Diff line change
@@ -1,69 +1,70 @@
// @ts-check

/**
* Cache store.
* @kind class
* @name Cache
* @param {object} [store={}] Initial [cache store]{@link Cache#store}. Useful for hydrating cache data from a server side render prior to the initial client side render.
* @example <caption>How to import.</caption>
* ```js
* import Cache from "graphql-react/Cache.mjs";
* ```
* @example <caption>Construct a new instance.</caption>
* ```js
* const cache = new Cache();
* ```
* @see {@link CacheEventMap `CacheEventMap`} for a map of possible events.
*/
export default class Cache extends EventTarget {
/**
* @param {CacheStore} [store] Initial {@link Cache.store cache store} record.
* Defaults to `{}`. Useful for hydrating cache data from a server side
* render prior to the initial client side render.
*/
constructor(store = {}) {
super();

if (typeof store !== "object" || !store || Array.isArray(store))
throw new TypeError("Constructor argument 1 `store` must be an object.");

/**
* Store of cache [keys]{@link CacheKey} and [values]{@link CacheValue}.
* @kind member
* @name Cache#store
* @type {object}
* Store of cache {@link CacheKey keys} and associated
* {@link CacheValue values}.
* @type {CacheStore}
*/
this.store = store;
}
}

/**
* Signals that a [cache store]{@link Cache#store} entry was set. The event name
* starts with the [cache key]{@link CacheKey} of the set entry, followed by
* `/set`.
* @kind event
* @name Cache#event:set
* @type {CustomEvent}
* @prop {object} detail Event detail.
* @prop {CacheValue} detail.cacheValue Cache value that was set.
* Map of possible {@linkcode Cache} events. Note that the keys don’t match the
* dispatched event names that dynamically contain the associated
* {@link CacheKey cache key}.
* @typedef {object} CacheEventMap
* @prop {CustomEvent<CacheEventSetDetail>} set Signals that a
* {@link Cache.store cache store} entry was set. The event name starts with
* the {@link CacheKey cache key} of the set entry, followed by `/set`.
* @prop {CustomEvent} stale Signals that a {@link Cache.store cache store}
* entry is now stale (often due to a mutation) and should probably be
* reloaded. The event name starts with the
* {@link CacheKey cache key} of the stale entry, followed by `/stale`.
* @prop {CustomEvent} prune Signals that a {@link Cache.store cache store}
* entry will be deleted unless the event is canceled via
* `event.preventDefault()`. The event name starts with the
* {@link CacheKey cache key} of the entry being pruned, followed by `/prune`.
* @prop {CustomEvent} delete Signals that a {@link Cache.store cache store}
* entry was deleted. The event name starts with the
* {@link CacheKey cache key} of the deleted entry, followed by `/delete`.
*/

/**
* @typedef {object} CacheEventSetDetail
* @prop {CacheValue} cacheValue The set {@link CacheValue cache value}.
*/

/**
* Signals that a [cache store]{@link Cache#store} entry is now stale (often due
* to a mutation) and should probably be reloaded. The event name starts with
* the [cache key]{@link CacheKey} of the stale entry, followed by `/stale`.
* @kind event
* @name Cache#event:stale
* @type {CustomEvent}
* A unique key to access a {@link CacheValue cache value}.
* @typedef {string} CacheKey
*/

/**
* Signals that a [cache store]{@link Cache#store} entry will be deleted unless
* the event is canceled via `event.preventDefault()`. The event name starts
* with the [cache key]{@link CacheKey} of the entry being pruned, followed by
* `/prune`.
* @kind event
* @name Cache#event:prune
* @type {CustomEvent}
* A {@link Cache.store cache store} value. If server side rendering, it should
* be JSON serializable for client hydration. It should contain information
* about any errors that occurred during loading so they can be rendered, and if
* server side rendering, be hydrated on the client.
* @typedef {unknown} CacheValue
*/

/**
* Signals that a [cache store]{@link Cache#store} entry was deleted. The event
* name starts with the [cache key]{@link CacheKey} of the deleted entry,
* followed by `/delete`.
* @kind event
* @name Cache#event:delete
* @type {CustomEvent}
* Cache store record.
* @typedef {Record<CacheKey, CacheValue>} CacheStore
*/
26 changes: 17 additions & 9 deletions Cache.test.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
// @ts-check

import { deepStrictEqual, strictEqual, throws } from "assert";
import Cache from "./Cache.mjs";
import assertBundleSize from "./test/assertBundleSize.mjs";
import assertInstanceOf from "./test/assertInstanceOf.mjs";

/**
* Adds `Cache` tests.
* @param {import("test-director").default} tests Test director.
*/
export default (tests) => {
tests.add("`Cache` bundle size.", async () => {
await assertBundleSize(new URL("./Cache.mjs", import.meta.url), 200);
});

tests.add("`Cache` constructor argument 1 `store`, not an object.", () => {
throws(() => {
new Cache(null);
new Cache(
// @ts-expect-error Testing invalid.
null
);
}, new TypeError("Constructor argument 1 `store` must be an object."));
});

Expand All @@ -32,25 +42,23 @@ export default (tests) => {
tests.add("`Cache` events.", () => {
const cache = new Cache();

strictEqual(cache instanceof EventTarget, true);
assertInstanceOf(cache, EventTarget);

let listenedEvent;
/** @type {Event | null} */
let listenedEvent = null;

/** @type {EventListener} */
const listener = (event) => {
listenedEvent = event;
};

const eventName = "a";
const eventDetail = 1;
const event = new CustomEvent(eventName, {
detail: eventDetail,
});
const event = new CustomEvent(eventName);

cache.addEventListener(eventName, listener);
cache.dispatchEvent(event);

deepStrictEqual(listenedEvent, event);
strictEqual(listenedEvent.detail, eventDetail);
strictEqual(listenedEvent, event);

listenedEvent = null;

Expand Down
21 changes: 10 additions & 11 deletions CacheContext.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
// @ts-check

import React from "react";

/** @typedef {import("./Cache.mjs").default} Cache */

/**
* React context for a [`Cache`]{@link Cache} instance.
* @kind member
* @name CacheContext
* @type {object}
* @prop {Function} Provider [React context provider component](https://reactjs.org/docs/context.html#contextprovider).
* @prop {Function} Consumer [React context consumer component](https://reactjs.org/docs/context.html#contextconsumer).
* @example <caption>How to import.</caption>
* ```js
* import CacheContext from "graphql-react/CacheContext.mjs";
* ```
* [React context](https://reactjs.org/docs/context.html) for a
* {@linkcode Cache} instance.
* @type {React.Context<Cache | undefined>}
*/
const CacheContext = React.createContext();
const CacheContext = React.createContext(
/** @type {Cache | undefined} */ (undefined)
);

CacheContext.displayName = "CacheContext";

Expand Down
27 changes: 21 additions & 6 deletions CacheContext.test.mjs
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
// @ts-check

import { strictEqual } from "assert";
import React from "react";
import ReactTestRenderer from "react-test-renderer";
import ReactDOMServer from "react-dom/server.js";
import Cache from "./Cache.mjs";
import CacheContext from "./CacheContext.mjs";
import assertBundleSize from "./test/assertBundleSize.mjs";

/**
* Adds `CacheContext` tests.
* @param {import("test-director").default} tests Test director.
*/
export default (tests) => {
tests.add("`CacheContext` bundle size.", async () => {
await assertBundleSize(new URL("./CacheContext.mjs", import.meta.url), 120);
});

tests.add("`CacheContext` used as a React context.", () => {
const TestComponent = () => React.useContext(CacheContext);
const contextValue = "a";
const testRenderer = ReactTestRenderer.create(
let contextValue;

/** Test component. */
function TestComponent() {
contextValue = React.useContext(CacheContext);
return null;
}

const value = new Cache();

ReactDOMServer.renderToStaticMarkup(
React.createElement(
CacheContext.Provider,
{ value: contextValue },
{ value },
React.createElement(TestComponent)
)
);

strictEqual(testRenderer.toJSON(), contextValue);
strictEqual(contextValue, value);
});
};
16 changes: 6 additions & 10 deletions HYDRATION_TIME_MS.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
// @ts-check

/** @typedef {import("./useAutoLoad.mjs").default} useAutoLoad */

/**
* Number of milliseconds after the first client render that’s considered the
* hydration time; during which the
* [`useAutoLoad`]{@link useAutoLoad} React hook won’t load if the
* cache entry is already populated.
* @kind constant
* @name HYDRATION_TIME_MS
* @type {number}
* @example <caption>How to import.</caption>
* ```js
* import HYDRATION_TIME_MS from "graphql-react/HYDRATION_TIME_MS.mjs";
* ```
* hydration time; during which the {@linkcode useAutoLoad} React hook won’t
* load if the cache entry is already populated.
*/
export default 1000;
6 changes: 6 additions & 0 deletions HYDRATION_TIME_MS.test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// @ts-check

import { strictEqual } from "assert";
import HYDRATION_TIME_MS from "./HYDRATION_TIME_MS.mjs";
import assertBundleSize from "./test/assertBundleSize.mjs";

/**
* Adds `HYDRATION_TIME_MS` tests.
* @param {import("test-director").default} tests Test director.
*/
export default (tests) => {
tests.add("`HYDRATION_TIME_MS` bundle size.", async () => {
await assertBundleSize(
Expand Down
19 changes: 8 additions & 11 deletions HydrationTimeStampContext.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
// @ts-check

import React from "react";

/**
* React context for the client side hydration [time stamp]{@link HighResTimeStamp}.
* @kind member
* @name HydrationTimeStampContext
* @type {object}
* @prop {Function} Provider [React context provider component](https://reactjs.org/docs/context.html#contextprovider).
* @prop {Function} Consumer [React context consumer component](https://reactjs.org/docs/context.html#contextconsumer).
* @example <caption>How to import.</caption>
* ```js
* import HydrationTimeStampContext from "graphql-react/HydrationTimeStampContext.mjs";
* ```
* [React context](https://reactjs.org/docs/context.html) for the client side
* hydration {@link DOMHighResTimeStamp time stamp}.
* @type {React.Context<DOMHighResTimeStamp | undefined>}
*/
const HydrationTimeStampContext = React.createContext();
const HydrationTimeStampContext = React.createContext(
/** @type {DOMHighResTimeStamp | undefined} */ (undefined)
);

HydrationTimeStampContext.displayName = "HydrationTimeStampContext";

Expand Down
26 changes: 20 additions & 6 deletions HydrationTimeStampContext.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// @ts-check

import { strictEqual } from "assert";
import React from "react";
import ReactTestRenderer from "react-test-renderer";
import ReactDOMServer from "react-dom/server.js";
import HydrationTimeStampContext from "./HydrationTimeStampContext.mjs";
import assertBundleSize from "./test/assertBundleSize.mjs";

/**
* Adds `HydrationTimeStampContext` tests.
* @param {import("test-director").default} tests Test director.
*/
export default (tests) => {
tests.add("`HydrationTimeStampContext` bundle size.", async () => {
await assertBundleSize(
Expand All @@ -13,16 +19,24 @@ export default (tests) => {
});

tests.add("`HydrationTimeStampContext` used as a React context.", () => {
const TestComponent = () => React.useContext(HydrationTimeStampContext);
const contextValue = "a";
const testRenderer = ReactTestRenderer.create(
let contextValue;

/** Test component. */
function TestComponent() {
contextValue = React.useContext(HydrationTimeStampContext);
return null;
}

const value = 1;

ReactDOMServer.renderToStaticMarkup(
React.createElement(
HydrationTimeStampContext.Provider,
{ value: contextValue },
{ value },
React.createElement(TestComponent)
)
);

strictEqual(testRenderer.toJSON(), contextValue);
strictEqual(contextValue, value);
});
};
Loading

0 comments on commit 047ed7f

Please sign in to comment.