From 3a01b68b44527d0d6a2ddbc3d47b49563e7eebdc Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Thu, 11 May 2023 11:18:55 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20First=20iteration=20on=20race=20?= =?UTF-8?q?conditions=20tutorial=20(#3820)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patches/react-npm-17.0.2-99ba37d931.patch | 15 + package.json | 3 + website/.gitattributes | 1 + .../detect-race-conditions/Playgrounds.jsx | 619 ++++++++++++++++++ .../Playgrounds.module.css | 28 + .../detect-race-conditions/_category_.json | 8 + .../detect-race-conditions/introduction.md | 46 ++ .../multiple-batches-of-calls.mdx | 117 ++++ .../one-step-closer-to-real-usages.mdx | 89 +++ .../the-missing-part.mdx | 70 ++ .../detect-race-conditions/wrapping-up.mdx | 156 +++++ .../your-first-race-condition-test.mdx | 116 ++++ website/package.json | 2 + .../tutorials/autocomplete-bug-screenshot.png | 3 + .../static/img/tutorials/autocomplete-bug.gif | 3 + .../tutorials/autocomplete-race-explained.png | 3 + yarn.lock | 471 ++++++++++++- 17 files changed, 1747 insertions(+), 3 deletions(-) create mode 100644 .yarn/patches/react-npm-17.0.2-99ba37d931.patch create mode 100644 website/docs/tutorials/detect-race-conditions/Playgrounds.jsx create mode 100644 website/docs/tutorials/detect-race-conditions/Playgrounds.module.css create mode 100644 website/docs/tutorials/detect-race-conditions/_category_.json create mode 100644 website/docs/tutorials/detect-race-conditions/introduction.md create mode 100644 website/docs/tutorials/detect-race-conditions/multiple-batches-of-calls.mdx create mode 100644 website/docs/tutorials/detect-race-conditions/one-step-closer-to-real-usages.mdx create mode 100644 website/docs/tutorials/detect-race-conditions/the-missing-part.mdx create mode 100644 website/docs/tutorials/detect-race-conditions/wrapping-up.mdx create mode 100644 website/docs/tutorials/detect-race-conditions/your-first-race-condition-test.mdx create mode 100644 website/static/img/tutorials/autocomplete-bug-screenshot.png create mode 100644 website/static/img/tutorials/autocomplete-bug.gif create mode 100644 website/static/img/tutorials/autocomplete-race-explained.png diff --git a/.yarn/patches/react-npm-17.0.2-99ba37d931.patch b/.yarn/patches/react-npm-17.0.2-99ba37d931.patch new file mode 100644 index 00000000..8996b52c --- /dev/null +++ b/.yarn/patches/react-npm-17.0.2-99ba37d931.patch @@ -0,0 +1,15 @@ +diff --git a/package.json b/package.json +index 936478a095255c0d16426733f95482e7a6c8f34e..6e46b1d21fd87f997aab73f1b5642312f790ff53 100644 +--- a/package.json ++++ b/package.json +@@ -35,5 +35,10 @@ + "transform": [ + "loose-envify" + ] ++ }, ++ "exports": { ++ ".": "./index.js", ++ "./jsx-runtime": "./jsx-runtime.js", ++ "./jsx-runtime.js": "./jsx-runtime.js" + } + } diff --git a/package.json b/package.json index 4bcf3adc..bd57772e 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,8 @@ "eslint-config-prettier": "^8.8.0", "prettier": "2.8.8", "publint": "^0.1.11" + }, + "resolutions": { + "react@^17.0.2": "patch:react@npm%3A17.0.2#./.yarn/patches/react-npm-17.0.2-99ba37d931.patch" } } diff --git a/website/.gitattributes b/website/.gitattributes index 24a8e879..4a9d8e83 100644 --- a/website/.gitattributes +++ b/website/.gitattributes @@ -1 +1,2 @@ *.png filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text diff --git a/website/docs/tutorials/detect-race-conditions/Playgrounds.jsx b/website/docs/tutorials/detect-race-conditions/Playgrounds.jsx new file mode 100644 index 00000000..b84eaf1c --- /dev/null +++ b/website/docs/tutorials/detect-race-conditions/Playgrounds.jsx @@ -0,0 +1,619 @@ +import React, { useState } from 'react'; +import { + SandpackProvider, + SandpackLayout, + SandpackCodeEditor, + SandpackFileExplorer, + SandpackTests, + UnstyledOpenInCodeSandboxButton, +} from '@codesandbox/sandpack-react'; +import { atomDark } from '@codesandbox/sandpack-themes'; +import styles from './Playgrounds.module.css'; + +// Does not queue at all +const queueCodeV0 = `export function queue(fun) { + return fun; +}`; + +// Only queue after last request +const queueCodeV1 = `export function queue(fun) { + let lastQuery = Promise.resolve(); + return (...args) => { + const currentQuery = fun(...args); + const returnedQuery = lastQuery.then(() => currentQuery); + lastQuery = currentQuery; + return returnedQuery; + }; +}`; + +// May still not queue enough due to early cleaning +const queueCodeV2 = `export function queue(fun) { + let pastQueries = []; + return (...args) => { + const currentQuery = fun(...args); + const returnedQuery = Promise.all(pastQueries) + .finally(() => (pastQueries = [])) + .then(() => currentQuery); + pastQueries.push(currentQuery); + return returnedQuery; + }; +}`; + +const queueCodeV3 = `export function queue(fun) { + let pastQueries = []; + return (...args) => { + const currentQuery = fun(...args); + const knownPastQueries = pastQueries; + const returnedQuery = Promise.all(pastQueries) + .finally(() => { + if (knownPastQueries === pastQueries) + pastQueries = []; + }) + .then(() => currentQuery); + pastQueries = [...pastQueries, currentQuery]; + return returnedQuery; + }; +}`; + +const queueCodeV4 = `export function queue(fun) { + let queryId = 0; + let lastQuery = null; + return (...args) => { + const selfQueryId = ++queryId; + if (lastQuery === null) { + lastQuery = fun(...args); + } else { + lastQuery = lastQuery + .then( + () => fun(...args), + () => fun(...args), + ); + } + lastQuery + .finally(() => { + if (queryId === selfQueryId) + lastQuery = null; + }); + return lastQuery; + }; +}`; + +const queueCodeV5 = `export function queue(fun) { + let pending = false; + let onDone = []; + function runNext() { + if (onDone.length === 0) { + pending = false; + return; + } + onDone.shift()(); + } + return (...args) => { + if (!pending) { + pending = true; + const p = fun(...args); + p.then(runNext, runNext); + return p; + } + return new Promise((resolve, reject) => { + onDone.push(() => { + const p = fun(...args); + p.then(runNext, runNext); + p.then(resolve, reject); + }); + }); + }; +}`; + +const queueUnitSpecCode = `import {queue} from './queue.js'; + +test('should resolve in call order', async () => { + // Arrange + const seenAnswers = []; + const call = jest.fn() + .mockImplementation(v => Promise.resolve(v)); + + // Act + const queued = queue(call); + await Promise.all([ + queued(1).then(v => (seenAnswers.push(v))), + queued(2).then(v => (seenAnswers.push(v))), + ]); + + // Assert + expect(seenAnswers).toEqual([1, 2]); +})`; + +const queueBasicPBTSpecCode = `import {queue} from './queue.js'; +import fc from 'fast-check'; + +test('should resolve in call order', async () => { + await fc.assert(fc.asyncProperty(fc.scheduler(), async (s) => { + // Arrange + const pendingQueries = []; + const seenAnswers = []; + const call = jest.fn() + .mockImplementation(v => Promise.resolve(v)); + + // Act + const queued = queue(s.scheduleFunction(call)); + pendingQueries.push(queued(1).then(v => (seenAnswers.push(v)))); + pendingQueries.push(queued(2).then(v => (seenAnswers.push(v)))); + await s.waitFor(Promise.all(pendingQueries)); + + // Assert + expect(seenAnswers).toEqual([1, 2]); + })) +})`; + +const queueBasicPBTWaitAllSpecCode = `import {queue} from './queue.js'; +import fc from 'fast-check'; + +test('should resolve in call order', async () => { + await fc.assert(fc.asyncProperty(fc.scheduler(), async (s) => { + // Arrange + const seenAnswers = []; + const call = jest.fn() + .mockImplementation(v => Promise.resolve(v)); + + // Act + const queued = queue(s.scheduleFunction(call)); + queued(1).then(v => (seenAnswers.push(v))); + queued(2).then(v => (seenAnswers.push(v))); + await s.waitAll(); + + // Assert + expect(seenAnswers).toEqual([1, 2]); + })) +})`; + +const queueMoreThan2CallsPBTSpecCode = `import {queue} from './queue.js'; +import fc from 'fast-check'; + +test('should resolve in call order', async () => { + await fc.assert(fc.asyncProperty(fc.scheduler(), fc.integer({min: 1, max: 10}), async (s, numCalls) => { + // Arrange + const pendingQueries = []; + const seenAnswers = []; + const expectedAnswers = []; + const call = jest.fn() + .mockImplementation(v => Promise.resolve(v)); + + // Act + const queued = queue(s.scheduleFunction(call)); + for (let id = 0 ; id !== numCalls ; ++id) { + expectedAnswers.push(id); + pendingQueries.push(queued(id).then(v => (seenAnswers.push(v)))); + } + await s.waitFor(Promise.all(pendingQueries)); + + // Assert + expect(seenAnswers).toEqual(expectedAnswers); + })) +})`; + +const queueBatchesAlternativePBTSpecCode = `import {queue} from './queue.js'; +import fc from 'fast-check'; + +test('should resolve in call order', async () => { + await fc.assert(fc.asyncProperty(fc.scheduler(), fc.integer({min: 1, max: 10}), async (s, numCalls) => { + // Arrange + const pendingQueries = []; + const seenAnswers = []; + const expectedAnswers = []; + const call = jest.fn() + .mockImplementation(v => Promise.resolve(v)); + + // Act + const queued = queue(s.scheduleFunction(call)); + for (let id = 0 ; id !== numCalls ; ++id) { + pendingQueries.push( + s.schedule(Promise.resolve(\`Fire the call for \${id}\`)) + .then(() => { + expectedAnswers.push(id); + return queued(id); + }) + .then(v => (seenAnswers.push(v))) + ); + } + await s.waitFor(Promise.all(pendingQueries)); + + // Assert + expect(seenAnswers).toEqual(expectedAnswers); + })) +})`; + +const queueFromBatchesPBTSpecCode = `import {queue} from './queue.js'; +import fc from 'fast-check'; + +test('should resolve in call order', async () => { + await fc.assert(fc.asyncProperty(fc.scheduler(), fc.array(fc.integer({min: 1, max: 10}), {minLength: 1}), async (s, batches) => { + // Arrange + const pendingQueries = []; + const seenAnswers = []; + const expectedAnswers = []; + const call = jest.fn() + .mockImplementation(v => Promise.resolve(v)); + + // Act + const queued = queue(s.scheduleFunction(call)); + let lastId = 0; + const { task } = s.scheduleSequence(batches.map((batch, index) => { + return { + label: \`Fire batch #\${index + 1} (\${batch} calls)\`, + builder: async () => { + for (let id = 0 ; id !== batch ; ++id, ++lastId) { + expectedAnswers.push(lastId); + pendingQueries.push(queued(lastId).then(v => (seenAnswers.push(v)))); + } + }, + } + })); + await s.waitFor(task); + await s.waitFor(Promise.all(pendingQueries)); + + // Assert + expect(seenAnswers).toEqual(expectedAnswers); + })) +})`; + +const missingPartPBTSpecCode = `import {queue} from './queue.js'; +import fc from 'fast-check'; + +test('should resolve in call order', async () => { + await fc.assert(fc.asyncProperty(fc.scheduler(), fc.array(fc.integer({min: 1, max: 10}), {minLength: 1}), async (s, batches) => { + // Arrange + const pendingQueries = []; + const seenAnswers = []; + const expectedAnswers = []; + const call = jest.fn() + .mockImplementation(v => Promise.resolve(v)); + const scheduledCall = s.scheduleFunction(call); + let concurrentQueriesDetected = false; + let queryPending = false; + const monitoredScheduledCall = (...args) => { + concurrentQueriesDetected ||= queryPending; + queryPending = true; + return scheduledCall(...args).finally(() => (queryPending = false)); + }; + + // Act + const queued = queue(monitoredScheduledCall); + let lastId = 0; + const { task } = s.scheduleSequence(batches.map((batch, index) => { + return { + label: \`Fire batch #\${index + 1} (\${batch} calls)\`, + builder: async () => { + for (let id = 0 ; id !== batch ; ++id, ++lastId) { + expectedAnswers.push(lastId); + pendingQueries.push(queued(lastId).then(v => (seenAnswers.push(v)))); + } + }, + } + })); + await s.waitFor(task); + await s.waitFor(Promise.all(pendingQueries)); + + // Assert + expect(seenAnswers).toEqual(expectedAnswers); + expect(concurrentQueriesDetected).toBe(false); + })) +})`; + +const extendedBackToWaitAllPBTSpecCode = `import {queue} from './queue.js'; +import fc from 'fast-check'; + +test('should resolve in call order', async () => { + await fc.assert(fc.asyncProperty(fc.scheduler(), fc.array(fc.integer({min: 1, max: 10}), {minLength: 1}), async (s, batches) => { + // Arrange + const seenAnswers = []; + const expectedAnswers = []; + const call = jest.fn() + .mockImplementation(v => Promise.resolve(v)); + const scheduledCall = s.scheduleFunction(call); + let concurrentQueriesDetected = false; + let queryPending = false; + const monitoredScheduledCall = (...args) => { + concurrentQueriesDetected ||= queryPending; + queryPending = true; + return scheduledCall(...args).finally(() => (queryPending = false)); + }; + + // Act + const queued = queue(monitoredScheduledCall); + let lastId = 0; + s.scheduleSequence(batches.map((batch, index) => { + return { + label: \`Fire batch #\${index + 1} (\${batch} calls)\`, + builder: async () => { + for (let id = 0 ; id !== batch ; ++id, ++lastId) { + expectedAnswers.push(lastId); + queued(lastId).then(v => (seenAnswers.push(v))); + } + }, + } + })); + await s.waitAll(); + + // Assert + expect(seenAnswers).toEqual(expectedAnswers); + expect(concurrentQueriesDetected).toBe(false); + })) +})`; + +const extendedWithExceptionsPBTSpecCode = `import {queue} from './queue.js'; +import fc from "fast-check"; + +test("should resolve in call order", async () => { + await fc.assert( + fc.asyncProperty( + fc.scheduler(), + fc.array(fc.integer({ min: 1, max: 10 }), { minLength: 1 }), + fc.func(fc.boolean()), + async (s, batches, isFailure) => { + // Arrange + const seenAnswers = []; + const expectedAnswers = []; + const call = jest + .fn() + .mockImplementation((v) => + isFailure(v) ? Promise.reject(v) : Promise.resolve(v) + ); + const scheduledCall = s.scheduleFunction(call); + let concurrentQueriesDetected = false; + let queryPending = false; + const monitoredScheduledCall = (...args) => { + concurrentQueriesDetected ||= queryPending; + queryPending = true; + return scheduledCall(...args).finally(() => (queryPending = false)); + }; + + // Act + const queued = queue(monitoredScheduledCall); + let lastId = 0; + s.scheduleSequence( + batches.map((batch, index) => { + return { + label: \`Fire batch #\${index + 1} (\${batch} calls)\`, + builder: async () => { + for (let id = 0; id !== batch; ++id, ++lastId) { + expectedAnswers.push( + isFailure(lastId) + ? \`failure:\${lastId}\` + : \`success:\${lastId}\` + ); + queued(lastId).then( + (v) => seenAnswers.push(\`success:\${v}\`), + (v) => seenAnswers.push(\`failure:\${v}\`) + ); + } + } + }; + }) + ); + await s.waitAll(); + + // Assert + expect(seenAnswers).toEqual(expectedAnswers); + expect(concurrentQueriesDetected).toBe(false); + } + ) + ); +})`; + +function SetupPlayground(props) { + const { startSpecCode, anwserSpecCode, fileContent, fileName, fileExtension } = props; + const [specFile, setSpecFile] = useState({ key: 0, content: startSpecCode }); + + return ( + + + + + + {specFile.content !== anwserSpecCode && ( + + )} + + } + verbose + style={{ height: 500 }} + /> + +
+ Open in CodeSandbox +
+
+ ); +} + +export function YourFirstRace() { + return ( + + ); +} + +export function OneStepCloserToRealUsages() { + return ( + + ); +} + +export function MultipleBatchesOfCalls() { + return ( + + ); +} + +export function MissingPart() { + return ( + + ); +} + +function codeWithComments(code, comments) { + return `${comments.map((line) => `// ${line}`).join('\n')}\n\n${code}`; +} +function pastImplementationSnippet(code, partName) { + return codeWithComments(code, ['Implementation used within the part:', '> ' + partName]); +} +function pastTestSnippet(code, partName, variationName) { + return codeWithComments(code, [ + 'Test suggested for the part:', + '> ' + partName, + ...(variationName !== undefined ? [variationName] : []), + ]); +} + +export function WrapUpPlaygroundQueue() { + const [reset, setReset] = useState(0); + const queueImplementations = { + 'queue.v0.js': pastImplementationSnippet(queueCodeV0, 'Your first race condition test'), + 'queue.v1.js': pastImplementationSnippet(queueCodeV1, 'One step close to real usages'), + 'queue.v2.js': pastImplementationSnippet(queueCodeV2, 'Multiple batches of calls'), + 'queue.v3.js': pastImplementationSnippet(queueCodeV3, 'The missing part'), + 'queue.v4.js': codeWithComments(queueCodeV4, ['Pass all the tests of the tutorial']), + 'queue.v5.js': codeWithComments(queueCodeV5, ['Pass all the tests']), + }; + const defaultQueueImplementation = 'queue.v5.js'; + const queueTests = { + 'queue.p0.spec.js': queueUnitSpecCode, + 'queue.p1.spec.js': pastTestSnippet(queueBasicPBTSpecCode, 'Your first race condition test'), + 'queue.p1.v2.spec.js': pastTestSnippet( + queueBasicPBTWaitAllSpecCode, + 'Your first race condition test', + 'With waitAll' + ), + 'queue.p2.spec.js': pastTestSnippet(queueMoreThan2CallsPBTSpecCode, 'One step close to real usages'), + 'queue.p3.spec.js': pastTestSnippet(queueFromBatchesPBTSpecCode, 'Multiple batches of calls'), + 'queue.p3.v2.spec.js': pastTestSnippet( + queueBatchesAlternativePBTSpecCode, + 'Multiple batches of calls', + 'With delayed calls (no batches)' + ), + 'queue.p4.spec.js': pastTestSnippet(missingPartPBTSpecCode, 'The missing part'), + 'queue.pnext.v1.spec.js': codeWithComments(extendedBackToWaitAllPBTSpecCode, [ + 'Switch back to waitAll in queue.p4.spec', + ]), + 'queue.pnext.v2.spec.js': codeWithComments(extendedWithExceptionsPBTSpecCode, ['Also cover error cases']), + }; + const defaultQueueTest = 'queue.p4.spec.js'; + return ( + { + return [ + `src/${fileName}`, + { + code: fileContent, + readOnly: false, + active: false, + hidden: true, + }, + ]; + }) + ), + ...Object.fromEntries( + Object.entries(queueTests).map(([fileName, fileContent]) => { + return [ + `tests/${fileName}`, + { + code: fileContent.replace('./queue.js', './../src/queue.js'), + readOnly: false, + active: fileName === defaultQueueTest, + hidden: fileName !== defaultQueueTest, + }, + ]; + }) + ), + 'src/queue.js': { + code: `export {queue} from './${defaultQueueImplementation}'`, + readOnly: false, + active: false, + hidden: false, + }, + 'package.json': { + code: JSON.stringify({ main: `src/queue.js` }), + readOnly: true, + active: false, + hidden: true, + }, + }} + customSetup={{ + entry: `src/queue.js`, + dependencies: { + 'fast-check': 'latest', + }, + }} + > + + + + setReset((r) => r + 1)}>Reset snippets} + verbose + style={{ height: 500 }} + /> + +
+ Open in CodeSandbox +
+
+ ); +} diff --git a/website/docs/tutorials/detect-race-conditions/Playgrounds.module.css b/website/docs/tutorials/detect-race-conditions/Playgrounds.module.css new file mode 100644 index 00000000..4b83d66c --- /dev/null +++ b/website/docs/tutorials/detect-race-conditions/Playgrounds.module.css @@ -0,0 +1,28 @@ +.openInCodeSandbox { + color: #282c34; + border-radius: 0 0 0.5em 0.5em; + margin-bottom: 1em; +} +.openInCodeSandbox:hover { + background: linear-gradient(135deg, #282c3477, #ffffff00 60%, #ffffff00 90%, #282c3477); +} +.openInCodeSandbox:hover button, +.openInCodeSandbox:hover a, +.openInCodeSandbox:hover a:focus, +.openInCodeSandbox:hover a:visited { + color: #ffffff; +} + +.openInCodeSandbox button, +.openInCodeSandbox a, +.openInCodeSandbox a:focus, +.openInCodeSandbox a:hover, +.openInCodeSandbox a:visited { + color: #282c34; + text-decoration: none; + + /* Reset button style (button is the fallback for huge snippets) */ + background-color: unset; + border: none; + cursor: pointer; +} diff --git a/website/docs/tutorials/detect-race-conditions/_category_.json b/website/docs/tutorials/detect-race-conditions/_category_.json new file mode 100644 index 00000000..52fd7133 --- /dev/null +++ b/website/docs/tutorials/detect-race-conditions/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Detect Race Conditions", + "position": 2, + "link": { + "type": "generated-index", + "description": "Learn how to detect race conditions in your code through clear and instructive examples" + } +} diff --git a/website/docs/tutorials/detect-race-conditions/introduction.md b/website/docs/tutorials/detect-race-conditions/introduction.md new file mode 100644 index 00000000..75f88297 --- /dev/null +++ b/website/docs/tutorials/detect-race-conditions/introduction.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 1 +slug: /tutorials/detect-race-conditions/introduction/ +description: What's the plan for this tutorial? +--- + +# Introduction + +## Tutorial structure + +This tutorial aims to equip you with techniques to write tests that can detect race conditions. To achieve this, it will cover specific algorithms and introduce helpful concepts and tools related to fast-check and its race condition detection mechanisms. By the end of this tutorial, you will be able to apply these techniques and tools to your own tests. + +Throughout this tutorial, the examples have been crafted to ensure that tests pass initially on the code being tested. Your objective will be to make the tests fail by implementing the suggested changes. Each section will introduce new concepts and provide the necessary information to apply these learnings to code beyond the scope of this tutorial. Each page will come with its own puzzle. + +## Definition of a race condition + +:::info Let's align our understanding of the concept +While this section is fully optional, it has the benefit to make sure that we all align on the definition of race condition throughout this tutorial. +::: + +Taking the Wikipedia definition: _"A race condition […] is the condition […] where the system's substantive behavior is dependent on the sequence or timing of other uncontrollable events."_ (source: https://en.wikipedia.org/wiki/Race_condition). This definition highlights two main reasons why race conditions are prevalent in JavaScript: + +- sequence of events, +- events being out-of-control. + +Consider a front-end application where a user types into a text field. User will focus and emit several keypress events. In addition, if the text field is connected to a backend API, it will receive updates over time. As none of these events are under our control, we may face race conditions. Indeed, we cannot predict when the user will type or what they will type, nor when our APIs will respond. + +JavaScript, being event-based by nature, is prone to race conditions when asynchronous operations or events are used. Despite the language being single-threaded, it does not prevent the occurrence of race conditions. + +## Race condition explained through an example + +To help grasp the concept of a race condition, let's look at a real-world example involving an autocomplete field. As previously discussed, the unpredicable events occuring in the autocomplete field can trigger a race condition. As an example, in the animated image below, we can see that while the user is typing, outdated suggestions appear and disappear in a flickering manner. It makes it difficult for the user to select any option before the input stabilizes. The suggestions seem to appear out of order, causing confusion and frustration for the user. + +![Dancing autocomplete field](@site/static/img/tutorials/autocomplete-bug.gif) + +Even worse, if we wait long enough, the autocomplete field may stabilize itself but on a past result. This means that the suggestions being displayed do not match the current query, leading to confusion and a poor user experience. + +![Inconsistent results in autocomplete field](@site/static/img/tutorials/autocomplete-bug-screenshot.png) + +To zoom in on what happened, we can summarize the issue with the help of this small diagram: + +![Autocomplete race explained](@site/static/img/tutorials/autocomplete-race-explained.png) + +In other words, the issue occurred as the user performed two searches subsequently: one for 'London' followed immediately by one for 'Paris'. However, the response for 'Paris' (the second request) came back before the response for 'London' (the first request), and our component did not handle it properly. This delay in responses is expected from an API perspective. There could be multiple reasons for this behavior, such as queries not being routed to the same servers with one server being heavily utilized and resulting in longer response times. + +As we have seen in this simple example, race conditions are easy to create, as they only require two concurrent events, and can cause significant problems from a user's perspective. It is worth noting that the example we took for this section was only a visual glitch, but race conditions can have much more critical impacts than just a wrong display. diff --git a/website/docs/tutorials/detect-race-conditions/multiple-batches-of-calls.mdx b/website/docs/tutorials/detect-race-conditions/multiple-batches-of-calls.mdx new file mode 100644 index 00000000..ca2e0fda --- /dev/null +++ b/website/docs/tutorials/detect-race-conditions/multiple-batches-of-calls.mdx @@ -0,0 +1,117 @@ +--- +sidebar_position: 4 +slug: /tutorials/detect-race-conditions/multiple-batches-of-calls/ +description: Third iteration, not only one batch firing all queries but multiple batches +--- + +import { MultipleBatchesOfCalls } from './Playgrounds'; + +# Multiple batches of calls + +## Zoom on previous test + +### The choice of integer + +In the previous part, we suggested to run the test against an arbitrary number of calls to `call`. The option we recommend and implemented is based on `integer` arbitrary. We use it to give us the number of calls we should do. + +```js +const queued = queue(s.scheduleFunction(call)); +for (let id = 0; id !== numCalls; ++id) { + expectedAnswers.push(id); + pendingQueries.push(queued(id).then((v) => seenAnswers.push(v))); +} +await s.waitFor(Promise.all(pendingQueries)); +``` + +We based our choice on the fact that the `queue` helper is designed to accept any input, regardless of its value. Thus, there was no particular reason to generate values for the inputs themselves, as they are never consumed by the logic of `queue`. Using integers from 0 onwards allows for simpler debugging, as opposed to arbitrary inputs like 123 or 45. + +### The array version + +Here is how we could have written the array alternative: + +```js +// ids being the result of fc.array(fc.nat(), {minLength: 1}) +const queued = queue(s.scheduleFunction(call)); +for (const id of ids) { + expectedAnswers.push(id); + pendingQueries.push(queued(id).then((v) => seenAnswers.push(v))); +} +await s.waitFor(Promise.all(pendingQueries)); +``` + +## Towards next test + +Our current test doesn't fully capture all possible issues that could arise. In fact, the previous implementation sent all requests at the same time in a synchronous way, without firing some, waiting a bit, and then firing others. + +In the next iteration, we aim to declare and run multiple batches of calls: firing them in order will simplify our expectations. + +To run things in an ordered way in fast-check, we need to use what we call scheduled sequences. Scheduled sequences can be declared by using the helper [`scheduleSequence`](/docs/advanced/race-conditions/#schedulesequence). When running scheduled tasks, fast-check interleaves parts coming from sequences in-between and ensures that items in a sequence are run and waited for in order. This means that an item in the sequence will never start before the one before it has stopped. To declare and use a sequence, you can follow the example below: + +```js +const { task } = s.scheduleSequence([ + async () => { + // 1st item: + // Runnning something for the 1st item. + }, + async () => { + // 2nd item: + // Runnning something for the 2nd item. + // Will never start before the end of `await firstItem()`. + // Will have to be scheduled by the runner to run, in other words, it may start + // very long after the 1st item. + }, +]); + +// The sequence also provides a `task` that can be awaited in order to know when all items +// of the sequence have been fully executed. It also provides other values such as done or +// faulty if you want to know bugs that may have occurred during the sechduling of it. +``` + +:::info Non-batched alternative? +We will discuss about a non-batched alternative in the next page. The batch option we suggest here has the benefit to make you use the [`scheduleSequence`](/docs/advanced/race-conditions/#schedulesequence) helper coming with fast-check. +::: + +## Your turn! + + + +:::info What to expect? +Your test should help us to detect a bug in our current implementation of `queue`. +::: + +
+ +Hint #1 + + +Previous test can be considered as a single batch. + +```js +for (let id = 0; id !== numCalls; ++id) { + expectedAnswers.push(id); + pendingQueries.push(queued(id).then((v) => seenAnswers.push(v))); +} +``` + +
+ +
+ +Hint #2 + + +In order to achieve our goal of running multiple batches of calls in an ordered way, we need to generate multiple values of `numCalls`. Instead of generating a single batch with `numCalls` calls using the following code: + +```js +fc.integer({ min: 1, max: 10 }); +``` + +we can generate multiple batches by using the following code: + +```js +fc.array(fc.integer({ min: 1, max: 10 }), { minLength: 1 }); +``` + +This will allow us to generate multiple batches of calls, each containing a random number of calls. + +
diff --git a/website/docs/tutorials/detect-race-conditions/one-step-closer-to-real-usages.mdx b/website/docs/tutorials/detect-race-conditions/one-step-closer-to-real-usages.mdx new file mode 100644 index 00000000..75b4f141 --- /dev/null +++ b/website/docs/tutorials/detect-race-conditions/one-step-closer-to-real-usages.mdx @@ -0,0 +1,89 @@ +--- +sidebar_position: 3 +slug: /tutorials/detect-race-conditions/one-step-closer-to-real-usages/ +description: Second iteration, make the test closer and detect even more race conditions +--- + +import { OneStepCloserToRealUsages } from './Playgrounds'; + +# One step closer to real usages + +## Zoom on previous test + +### What to schedule? + +One approach to solve the problem we discussed earlier is to follow a common pattern that we recommend when integrating a scheduler into existing tests. This involves replacing the original occurrences of the asynchronous API with scheduled versions of it. In our case, we replaced the line of code: + +```js +const queued = queue(call); +``` + +with: + +```js +const queued = queue(s.scheduleFunction(call)); +``` + +### What to wait? + +Once we've replaced raw asynchronous API calls with scheduled versions, the next step is to wait for them to complete. There are several ways to achieve this, but the recommended approach is to use `waitFor` to wait for all queries to resolve: + +```js +const queued = queue(s.scheduleFunction(call)); +pendingQueries.push(queued(1).then((v) => seenAnswers.push(v))); +pendingQueries.push(queued(2).then((v) => seenAnswers.push(v))); +await s.waitFor(Promise.all(pendingQueries)); +``` + +An alternative options would have been to use `waitAll` instead of `waitFor` but it comes with a precise requirements being that promise had to be imperatively scehduled by the time we reach it. In other words, if our code delays a little bit the call to `call` it will not wait anything. With the current implementation of `queue` it works, but with next versions of it it might not be the case anymore as queued calls might start queuing themselves behind already started and running promises. + +As the fact that `call` has to be fired synchronously when issuing a call on a clean context is not a requirement for our current function, we can relax the constraint in our test to make evolving this implementation easier. + +A `waitAll` version of the code above would be: + +Alternatively, we could use `waitAll` instead of `waitFor`, but this approach requires promises to be scheduled by the time we reach `waitAll`. If our code introduces any delays, `waitAll` may not wait for anything. While the current implementation of `queue` works with `waitAll`, future versions may not, as queued calls might start queuing behind already running promises meaning that promises might be scheduled after calling `waitAll`. Therefore, we can relax the requirement that `call` get fired synchronously in our test, to make the implementation easier to evolve. + +While not as good as the `waitFor` implementation, here's the `waitAll` version of the code: + +```js +const queued = queue(s.scheduleFunction(call)); +queued(1).then((v) => seenAnswers.push(v)); +queued(2).then((v) => seenAnswers.push(v)); +await s.waitAll(); +``` + +## Towards next test + +The current implementation of our test only involves running two calls, but there may be potential issues that arise when more calls are made. To capture these scenarios, we will update the test to run an arbitrary number of calls. This will allow us to detect race conditions on a wider range of scenarios, including those with 3, 4, or even more calls. + +## Your turn! + + + +:::info What to expect? +Your test should help us to detect a bug in our current implementation of `queue`. +::: + +
+ +Hint #1 + + +`fc.scheduler` alone will not be enough! You'll have to generate another entry to be able to properly control the number of calls to `call` function. + +
+ +
+ +Hint #2 + + +Some possible options for generating additional test scenarios include: + +- Using `fc.integer({min: 1, max: 10})` to generate a variable `numCalls` that determines how many times to call `call`. It's important to set a maximal value via `max` to prevent an excessively high number of calls. +- Using `fc.array(fc.nat(), {minLength: 1})` to generate the list of calls to issue against `call`. +- Using `fc.func(fc.boolean())` to generate a function that determines when to stop issuing calls. + +And there are plenty others… + +
diff --git a/website/docs/tutorials/detect-race-conditions/the-missing-part.mdx b/website/docs/tutorials/detect-race-conditions/the-missing-part.mdx new file mode 100644 index 00000000..871e1852 --- /dev/null +++ b/website/docs/tutorials/detect-race-conditions/the-missing-part.mdx @@ -0,0 +1,70 @@ +--- +sidebar_position: 5 +slug: /tutorials/detect-race-conditions/the-missing-part/ +description: Fourth iteration, check all the points of the specification +--- + +import { MissingPart } from './Playgrounds'; + +# The missing part + +## Zoom on previous test + +As mentioned earlier, the decision to use sequences was largely driven by the desire to cover most of the scheduler's APIs in this tutorial. However, there are alternative ways of achieving our goals. + +One of the issues we wanted to address was the need to trigger queries asynchronously. Although this functionality is not yet built into fast-check, we could use the technique outlined in [scheduling a function call](/docs/advanced/race-conditions/#scheduling-a-function-call). If we were to take this approach, we would update: + +```js +for (let id = 0; id !== numCalls; ++id) { + expectedAnswers.push(id); + pendingQueries.push(queued(id).then((v) => seenAnswers.push(v))); +} +``` + +into: + +```js +for (let id = 0; id !== numCalls; ++id) { + pendingQueries.push( + s + .schedule(Promise.resolve(`Fire the call for ${id}`)) + .then(() => { + expectedAnswers.push(id); + return queued(id); + }) + .then((v) => seenAnswers.push(v)) + ); +} +``` + +:::info Comparison +Contrary to the batch approach, the ordering of ids will not be ensured. For that reason, we decided to include it in the reports by scheduling a resolved promise with a value featuring this id. +::: + +## Towards next test + +Our tests may be incomplete because we are not taking into account all aspects of the specification: + +> Its purpose is to wrap an asynchronous function and queue subsequent calls to it in two ways: +> +> - Promises returned by the function will resolve in order, with the first call resolving before the second one, the second one resolving before the third one, and so on. +> - Concurrent calls are not allowed, meaning that a call will always wait for the previously started one to finish before being fired. + +Although we thoroughly tested the first point, we may have overlooked the second point in the specification. Therefore, in the final section of this tutorial, we will focus on validating the second requirement. + +## Your turn! + + + +:::info What to expect? +Your test should help us to detect a bug in our current implementation of `queue`. +::: + +
+ +Hint #1 + + +One potential solution is to wrap the scheduled API in an additional layer that sets a flag to `true` when the API is called, directly invokes the scheduled API, and then resets the flag to `false` when the API resolves. If the flag is already set to `true` when the wrapper layer is invoked, it should cause the test to fail. + +
diff --git a/website/docs/tutorials/detect-race-conditions/wrapping-up.mdx b/website/docs/tutorials/detect-race-conditions/wrapping-up.mdx new file mode 100644 index 00000000..cc73ba3c --- /dev/null +++ b/website/docs/tutorials/detect-race-conditions/wrapping-up.mdx @@ -0,0 +1,156 @@ +--- +sidebar_position: 6 +slug: /tutorials/detect-race-conditions/wrapping-up/ +description: Key learnings and summary of all our steps +--- + +import { WrapUpPlaygroundQueue } from './Playgrounds'; + +# Wrapping up + +:::tip Wanna play? +Want to directly try out the final result? Skip ahead to the [Have fun!](#have-fun) section to play with the code snippets we've created. +::: + +## Zoom on previous test + +Congratulations! You have learned how to detect race conditions using fast-check library. We explored the concept of race conditions, discussed their potential dangers, and demonstrated various techniques to identify them. By leveraging the powerful features of fast-check, such as property-based testing and shrinking, you now have a robust tool at your disposal to uncover and fix race conditions in your code. Remember to apply these techniques in your projects to ensure the reliability and stability of your software. + +Throughout this tutorial, we gradually added race condition detection and expanded its coverage. The final iteration brings us close to fully addressing all possible edge cases of a `queue`. + +One important aspect of the last added test is that it covers a specification point we had overlooked in previous iterations. The main change involved ensuring that we never get called twice simultaneously but always get queued. We accomplished this by replacing: + +```js +//... +const scheduledCall = s.scheduleFunction(call); +const queued = queue(scheduledCall); +//... +expect(concurrentQueriesDetected).toBe(false); +//... +``` + +with: + +```js +//... +const scheduledCall = s.scheduleFunction(call); +let concurrentQueriesDetected = false; +let queryPending = false; +const monitoredScheduledCall = (...args) => { + concurrentQueriesDetected ||= queryPending; + queryPending = true; + return scheduledCall(...args).finally(() => (queryPending = false)); +}; +const queued = queue(monitoredScheduledCall); +//... +expect(concurrentQueriesDetected).toBe(false); +//... +``` + +The above change ensures that we can detect whenever `scheduledCall` is called before the previous calls to it have resolved. + +## Towards next test + +Although we have covered the majority of the `queue` algorithm, there are always subtle aspects that we may want to address. In this section, we will provide you with some ideas to ensure that your implementation of `queue` is perfect. All the suggested changes have been implemented in the [CodeSandbox playground below](#have-fun), allowing you to see how they can be achieved. The tests associated with this section have been named `*.pnext.v*` and are stacked on top of each other, with the final test incorporating all the suggestions described in this section. + +### Synchronous calls + +While we previously rejected the approach in the [first part](/docs/tutorials/detect-race-conditions/your-first-race-condition-test/) of the tutorial, we could have considered that calls are expected to be fired synchronously. To achieve this, we can rely on `waitAll` and eliminate any code responsible to wait for the batch to be executed or for promises to resolve. + +Here is what we mean by not firing calls synchronously: this snippet does not execute calls in a synchronous manner. Instead, each call is queued and executed after the previous one has resolved: + +```js +let previous = Promise.resolve(); +function fireCall(call) { + previous = previous.then(() => call()); +} +``` + +To demonstrate this behavior, you can run the following snippet locally: + +```js +console.log('before fireCall'); +fireCall(async () => console.log('call')); +console.log('after fireCall'); + +// Results: +// > before fireCall +// > after fireCall +// > call +``` + +Let's explore different iterations attempting to enhance this snippet. Here's a naive attempt that addresses the issue for the first call, but it is still incomplete: + +```js +let previous = undefined; +function fireCall(call) { + if (previous === undefined) { + previous = call(); + } else { + previous = previous.then(() => call()); + } +} +``` + +While the above solution improves the situation for the first call, it doesn't handle subsequent calls properly. The issue on second call is highlighted by the following snippet: + +```js +function runOne() { + return new Promise((resolve) => { + console.log('before fireCall'); + fireCall(async () => { + console.log('call'); + resolve(); + }); + console.log('after fireCall'); + }); +} +await runOne(); +await runOne(); + +// Results: +// > before fireCall +// > call +// > after fireCall +// > before fireCall +// > after fireCall +// > call +``` + +Here is a more advanced but still not perfect implementation of `fireCall`: + +```js +let callId = 0; +let previous = undefined; +function fireCall(call) { + const currentCallId = ++callId; + const next = previous === undefined ? call() : previous.then(() => call()); + previous = next.then(() => { + if (callId === currentCallId) { + previous = undefined; + } + }); +} +``` + +This last iteration, implemented in `src/queue.v4.js`, represents the most advanced solution we will show in that section. However, if you examine the CodeSandbox playground](#have-fun), you'll notice that even this implementation misses some cases and can be fixed. + +### Support exceptions + +When working with asynchronous code, it is common to encounter situations where code can potentially throw errors. As this scenario may occur in production code, it is essential to test our helper against such cases as well. + +To enhance our existing tests with this capability, we can modify our mock `call` implementation to simulate both successful executions and error throws. Consequently, our expectations need to be adjusted, but the underlying idea remains the same: both successes and failures should be received in an ordered manner. + +## Have fun! + + + +:::info The files +The playground provided includes source files extracted from the previous sections of this tutorial. + +Inside the `src` directory, you will find various implementations of the `queue` algorithm that you encountered and attempted to defeat throughout the tutorial. For example, `src/queue.v0.js` represents the initial implementation you encountered in part 1, while `src/queue.v1.js` would pass the tests from part 1 but fail those from part 2. + +The `queue` implementation currently running in the playground is expected to pass all the tests. However, if you wish to test one of the legacy versions, you can simply edit the `src/queue.js` file and import the desired legacy implementation. + +In the `tests` directory, you will find all the suggested tests and some variations discussed throughout the tutorial. Feel free to explore and experiment with them. +::: diff --git a/website/docs/tutorials/detect-race-conditions/your-first-race-condition-test.mdx b/website/docs/tutorials/detect-race-conditions/your-first-race-condition-test.mdx new file mode 100644 index 00000000..a4c73bdc --- /dev/null +++ b/website/docs/tutorials/detect-race-conditions/your-first-race-condition-test.mdx @@ -0,0 +1,116 @@ +--- +sidebar_position: 2 +slug: /tutorials/detect-race-conditions/your-first-race-condition-test/ +description: First iteration, build your first test able to detect race conditions +--- + +import { YourFirstRace } from './Playgrounds'; + +# Your first race condition test + +## Code under test + +For the next few pages, we will focus on a function called `queue`. Its purpose is to wrap an asynchronous function and queue subsequent calls to it in two ways: + +- Promises returned by the function will resolve in order, with the first call resolving before the second one, the second one resolving before the third one, and so on. +- Concurrent calls are not allowed, meaning that a call will always wait for the previously started one to finish before being fired. + +In the context of this tutorial you'll never have to edit `queue`. The function will be provided to you. + +## Understand current test + +Fortunately, we don't have to start from scratch. The function already has a test in place that ensures queries will consistently resolve in the correct order. The test appears rather simple and currently passes. + +```js +test('should resolve in call order', async () => { + // Arrange + const seenAnswers = []; + const call = jest.fn().mockImplementation((v) => Promise.resolve(v)); + + // Act + const queued = queue(call); + await Promise.all([queued(1).then((v) => seenAnswers.push(v)), queued(2).then((v) => seenAnswers.push(v))]); + + // Assert + expect(seenAnswers).toEqual([1, 2]); +}); +``` + +If we look closer to the test, we can observe that the wrapped function is relatively straightforward in that it merely returns a resolved promise whose value corresponds to the provided input. + +```js +const call = jest.fn().mockImplementation((v) => Promise.resolve(v)); +``` + +We can also see that we assess the order of results by confirming that the values pushed into `seenAnswers` are properly ordered. It's worth noting that `seenAnswers` does not represent the same thing as `await Promise.all([queued(1), queued(2)])`. This alternative notation does not evaluate the order in which the resolutions are received, but rather only confirms that each query resolves to its expected value. + +## Towards next test + +The test above has some limitations. Among them we have the fact that the fake API we defined always resolve instantly. It has one downside: without any `queue`, calling `call(1)` and then `call(2)` would also pass the test. Indeed, when calling `call(1)` it would produce a new promise being `Promise.resolve(1)`. Instantely created, instantly resolved and instantly watched via `.then((v) => seenAnswers.push(v))`. In other words, as soon as we give back the hand to JavaScript runtime, the continuation will be called. The one registered immediately after when calling `call(2)` will have to wait its turn as it has been queued after it. + +While the test we just examined is a good starting point, it comes with some limitations. Specifically, the fake API we established in the test instantly resolves queries. As a result, it does not account for scenarios where we have multiple calls to `call` that need to be queued. For instance, if we were to call `call(1)` followed by `call(2)` without any queue, the test would still pass. This is because `call(1)` produces a new promise that resolves immediately, which is then instantly followed by `.then((v) => seenAnswers.push(v))`. Once the JavaScript runtime relinquishes control, the continuation of `call(1)` is immediately called. Consequently, the continuation registered during the call to `call(2)` will wait until the continuation of `call(1)` ends. + +In order to address this limitation, our updated test should ensure that promises resolve later rather than instantly. + +## First glance at schedulers + +When adding fast-check into a race condition test, the recommended initial step is to update the test code as follows: + + +```js +test('should resolve in call order', async () => { + await fc.assert(fc.asyncProperty(fc.scheduler(), async (s) => { // <-- added + // ...unchanged code... + })); // <-- added +}); +``` + + +This modification runs the test using the fast-check runner. By doing so, any bugs that arise during the predicate will be caught by fast-check. + +In the context of race conditions, we want fast-check to provide us with a scheduler instance that is capable of re-ordering asynchronous operations. This is why we added the `fc.scheduler()` argument: it creates an instance of a scheduler that we refer to as `s`. The first important thing to keep in mind for our new test is that we don't want to change the value returned by the API. But we want to change when it gets returned. We want to give the scheduler the responsibility of resolving API calls. To achieve this, the scheduler exposes a method called `scheduleFunction`. This method wraps a function in a scheduled or controlled version of itself. + +After pushing scheduled calls into the scheduler, we must execute and release them at some point. This is typically done using `waitAll` or `waitFor`. These APIs simply wait for `waitX` to resolve, indicating that what we were waiting for has been accomplished. + +:::info Which wait is the best? +For this first iteration, both of them will be ok, but we will see later that `waitFor` is probably a better fit in that specific example. +::: + +:::tip More +For a comprehensive list of methods exposed on the scheduler, you can checkout the [official documentation for race conditions](/docs/advanced/race-conditions/). +::: + +## Your turn! + + + +:::info What to expect? +Your test should help us to detect a bug in our current implementation of `queue`. +::: + +
+ +Hint #1 + + +You have to run the test via the `assert` runner provided by fast-check. For more details, you may refer to the section [First glance at schedulers](#first-glance-at-schedulers) of this page. + +
+ +
+ +Hint #2 + + +No need to touch `call` itself. The function should still return the inputs it received. But, instead of queueing it, we should queue a scheduled version of it. You may refer to [`scheduleFunction`](/docs/advanced/race-conditions/#schedulefunction) for more details. + +
+ +
+ +Hint #3 + + +Our test should think as a user of the API would think. It has to wait for all queued calls to be resolved before being able to assert anything on the output. You may want to use pass such condition to `waitFor` so that everything gets properly awaited before running any assertion. + +
diff --git a/website/package.json b/website/package.json index 9e82b36a..7f7dc4b0 100644 --- a/website/package.json +++ b/website/package.json @@ -13,6 +13,8 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { + "@codesandbox/sandpack-react": "^2.6.1", + "@codesandbox/sandpack-themes": "^2.0.21", "@docusaurus/core": "2.4.0", "@docusaurus/preset-classic": "2.4.0", "@docusaurus/remark-plugin-npm2yarn": "2.4.0", diff --git a/website/static/img/tutorials/autocomplete-bug-screenshot.png b/website/static/img/tutorials/autocomplete-bug-screenshot.png new file mode 100644 index 00000000..2c90b356 --- /dev/null +++ b/website/static/img/tutorials/autocomplete-bug-screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12f0a5389c41221a64861ad07b722de6e890236ce5b264c5e75292858c276d5b +size 80207 diff --git a/website/static/img/tutorials/autocomplete-bug.gif b/website/static/img/tutorials/autocomplete-bug.gif new file mode 100644 index 00000000..aee80226 --- /dev/null +++ b/website/static/img/tutorials/autocomplete-bug.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93b2045ac9c1a4c1166e52d8357400fbb86cefb4d002dc6d14792c74fba88ce2 +size 132262 diff --git a/website/static/img/tutorials/autocomplete-race-explained.png b/website/static/img/tutorials/autocomplete-race-explained.png new file mode 100644 index 00000000..5013d7db --- /dev/null +++ b/website/static/img/tutorials/autocomplete-race-explained.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ad906853d9e97a80717b1accf2d4067164a90e8726d66479d9594383fcd9706 +size 99752 diff --git a/yarn.lock b/yarn.lock index 9f7a359e..cd23d0fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1665,6 +1665,184 @@ __metadata: languageName: node linkType: hard +"@codemirror/autocomplete@npm:^6.0.0, @codemirror/autocomplete@npm:^6.4.0": + version: 6.6.0 + resolution: "@codemirror/autocomplete@npm:6.6.0" + dependencies: + "@codemirror/language": ^6.0.0 + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.6.0 + "@lezer/common": ^1.0.0 + peerDependencies: + "@codemirror/language": ^6.0.0 + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.0.0 + "@lezer/common": ^1.0.0 + checksum: 01d5d81e46d179da2c742020d7cb7c97cceb1af5772ac1fbf81abfe18408d694a78851234137fabadce1ed8f829d598a2e9c4b6fbd9632a813fa72772e458f9d + languageName: node + linkType: hard + +"@codemirror/commands@npm:^6.1.3": + version: 6.2.3 + resolution: "@codemirror/commands@npm:6.2.3" + dependencies: + "@codemirror/language": ^6.0.0 + "@codemirror/state": ^6.2.0 + "@codemirror/view": ^6.0.0 + "@lezer/common": ^1.0.0 + checksum: e6b7d07818d6df4372a272539b84a256e651cfa3416a33f9e1859f2ced0f6e3c944e0c40c2c407dcd5b13ffab2931d4e5ea5952db439837071de336e7a31c722 + languageName: node + linkType: hard + +"@codemirror/lang-css@npm:^6.0.0, @codemirror/lang-css@npm:^6.0.1": + version: 6.2.0 + resolution: "@codemirror/lang-css@npm:6.2.0" + dependencies: + "@codemirror/autocomplete": ^6.0.0 + "@codemirror/language": ^6.0.0 + "@codemirror/state": ^6.0.0 + "@lezer/common": ^1.0.2 + "@lezer/css": ^1.0.0 + checksum: d824f169083613b63f04992c24d3fecd45c718cd3deb9da3f332dd3a889a762d05ea812e31ddf7ee4b661722f8c8b49676515cb98609067c53e25ac8b469a5e4 + languageName: node + linkType: hard + +"@codemirror/lang-html@npm:^6.4.0": + version: 6.4.3 + resolution: "@codemirror/lang-html@npm:6.4.3" + dependencies: + "@codemirror/autocomplete": ^6.0.0 + "@codemirror/lang-css": ^6.0.0 + "@codemirror/lang-javascript": ^6.0.0 + "@codemirror/language": ^6.4.0 + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.2.2 + "@lezer/common": ^1.0.0 + "@lezer/css": ^1.1.0 + "@lezer/html": ^1.3.0 + checksum: 6177d19147580964ecd6910ae951201929a96e63f4f0e624c3138e2805fa87ec6d6d952a3a888c5a52af78b6dd6d04d7d8c76c6a9cd65b1921dc467b5dbaea72 + languageName: node + linkType: hard + +"@codemirror/lang-javascript@npm:^6.0.0, @codemirror/lang-javascript@npm:^6.1.2": + version: 6.1.7 + resolution: "@codemirror/lang-javascript@npm:6.1.7" + dependencies: + "@codemirror/autocomplete": ^6.0.0 + "@codemirror/language": ^6.6.0 + "@codemirror/lint": ^6.0.0 + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.0.0 + "@lezer/common": ^1.0.0 + "@lezer/javascript": ^1.0.0 + checksum: 15ce6695e7276102dbc874d178cbc4434d126b7a3e08f89aa9338c7dce5d2d6bdd5f1c6d114a744a8fa26dfc62b0dc639fe6e5c7b306bd14ed37272e75739736 + languageName: node + linkType: hard + +"@codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.3.2, @codemirror/language@npm:^6.4.0, @codemirror/language@npm:^6.6.0": + version: 6.6.0 + resolution: "@codemirror/language@npm:6.6.0" + dependencies: + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.0.0 + "@lezer/common": ^1.0.0 + "@lezer/highlight": ^1.0.0 + "@lezer/lr": ^1.0.0 + style-mod: ^4.0.0 + checksum: bb9411620e2f231653a3f0c4429e0d19a3843bff5dbc117df4649d7bf783ec4ad809c0add8bc0887a4ec3f48b4f8f941621168e47d76101d5383f0d670af1722 + languageName: node + linkType: hard + +"@codemirror/lint@npm:^6.0.0": + version: 6.2.1 + resolution: "@codemirror/lint@npm:6.2.1" + dependencies: + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.0.0 + crelt: ^1.0.5 + checksum: 0e383c6b8b0fc463f90f8ebdc71628ba39cffbe4f3667a8382b8eedb61aff9eafe947cb3db947701c8f306acdc9576a2da889d0161ccdd14c9245705cfbd4571 + languageName: node + linkType: hard + +"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.1.4, @codemirror/state@npm:^6.2.0": + version: 6.2.0 + resolution: "@codemirror/state@npm:6.2.0" + checksum: fdc99c773dc09c700dd02bf918f06132aa8d3069c262cc4eb6ca5c810ce24ae2d7e90719ae7630a8158fd263018de6d40bd78f312e6bfba754e737b64e6c6b3d + languageName: node + linkType: hard + +"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.2.2, @codemirror/view@npm:^6.6.0, @codemirror/view@npm:^6.7.1": + version: 6.10.1 + resolution: "@codemirror/view@npm:6.10.1" + dependencies: + "@codemirror/state": ^6.1.4 + style-mod: ^4.0.0 + w3c-keyname: ^2.2.4 + checksum: 3a3aaf38866c8859234de6092c2e71d1e77957492bd40dc2c4e2812887a637ef52c07bb238f8bd5f2aace769483740e822f8d683808eb46f4791b71d649a28ed + languageName: node + linkType: hard + +"@codesandbox/nodebox@npm:0.1.4": + version: 0.1.4 + resolution: "@codesandbox/nodebox@npm:0.1.4" + dependencies: + outvariant: ^1.3.0 + strict-event-emitter: ^0.4.3 + checksum: b0b57aa9f767c7265dd680139d64017c01eb3058b0f75b7fd36dfd8fe5898e911f187ce70d5bee43660efdb5cd794cc9e3d4f172f16cfb9635b2b0916bab50df + languageName: node + linkType: hard + +"@codesandbox/sandpack-client@npm:^2.6.0": + version: 2.6.0 + resolution: "@codesandbox/sandpack-client@npm:2.6.0" + dependencies: + "@codesandbox/nodebox": 0.1.4 + buffer: ^6.0.3 + dequal: ^2.0.2 + outvariant: 1.3.0 + static-browser-server: 1.0.3 + checksum: c705b12e475029955d24f5fd42e24adb0623cfb46ea407c1925c3d2fb540e42722584be9b5437f455de81709cfe25720a7f25e03d80ad26aa944e0d297de96d8 + languageName: node + linkType: hard + +"@codesandbox/sandpack-react@npm:^2.6.1": + version: 2.6.1 + resolution: "@codesandbox/sandpack-react@npm:2.6.1" + dependencies: + "@codemirror/autocomplete": ^6.4.0 + "@codemirror/commands": ^6.1.3 + "@codemirror/lang-css": ^6.0.1 + "@codemirror/lang-html": ^6.4.0 + "@codemirror/lang-javascript": ^6.1.2 + "@codemirror/language": ^6.3.2 + "@codemirror/state": ^6.2.0 + "@codemirror/view": ^6.7.1 + "@codesandbox/sandpack-client": ^2.6.0 + "@lezer/highlight": ^1.1.3 + "@react-hook/intersection-observer": ^3.1.1 + "@stitches/core": ^1.2.6 + anser: ^2.1.1 + clean-set: ^1.1.2 + codesandbox-import-util-types: ^2.2.3 + dequal: ^2.0.2 + escape-carriage: ^1.3.1 + lz-string: ^1.4.4 + react-devtools-inline: 4.4.0 + react-is: ^17.0.2 + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + checksum: dceb4d6da7e39080dd14d31bf36fca56ed2c3ff37f0f9c7a1a95a775c679fa0b5aa0b01ce8dd479c0950a9c621f81be3a9a67e5f9bfbab852e68beaba0998998 + languageName: node + linkType: hard + +"@codesandbox/sandpack-themes@npm:^2.0.21": + version: 2.0.21 + resolution: "@codesandbox/sandpack-themes@npm:2.0.21" + checksum: 1ba74c1de415344d4a5d29b61269f1401a2e4d4e447840e72a6f442f429c1f9c30e88ab316f6f27a478ad2297f520997ba4bd1f7823cae6a988e34806b398545 + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -3218,6 +3396,62 @@ __metadata: languageName: node linkType: hard +"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2": + version: 1.0.2 + resolution: "@lezer/common@npm:1.0.2" + checksum: bbcc58e07be02652bf0700d2856042ec089d5be0b95893d628b3e18192ade864fac83b61b19653e10b9f1472261a178b12318d934e9004edd5483a577c0db56b + languageName: node + linkType: hard + +"@lezer/css@npm:^1.0.0, @lezer/css@npm:^1.1.0": + version: 1.1.1 + resolution: "@lezer/css@npm:1.1.1" + dependencies: + "@lezer/highlight": ^1.0.0 + "@lezer/lr": ^1.0.0 + checksum: a7e4893aacaa7f26d5679c77a640f401b37d14155cb54863aa91b59dfd220b280360a341c0fedafc65d31101de13a5ae33cf3876c352f2da528344dafdc9b3d7 + languageName: node + linkType: hard + +"@lezer/highlight@npm:^1.0.0, @lezer/highlight@npm:^1.1.3": + version: 1.1.4 + resolution: "@lezer/highlight@npm:1.1.4" + dependencies: + "@lezer/common": ^1.0.0 + checksum: 30e848c02839bfcd9472fcd6e74d71cba12379cef38f27d0c6cab0e6831f92150cfc629d267a40cc31f84cf46ac0a935400163fdf931b2672c516bec29417485 + languageName: node + linkType: hard + +"@lezer/html@npm:^1.3.0": + version: 1.3.4 + resolution: "@lezer/html@npm:1.3.4" + dependencies: + "@lezer/common": ^1.0.0 + "@lezer/highlight": ^1.0.0 + "@lezer/lr": ^1.0.0 + checksum: 81dd134ac094edf7c40bae4c3b7126d336ce4c3c87756344bf604eff64d89b06fcb55f91618a4622eb0dae6d6015722f5bab58e2252d86e81fca8c3ced1a0c4d + languageName: node + linkType: hard + +"@lezer/javascript@npm:^1.0.0": + version: 1.4.3 + resolution: "@lezer/javascript@npm:1.4.3" + dependencies: + "@lezer/highlight": ^1.1.3 + "@lezer/lr": ^1.3.0 + checksum: 520dc2d84c84841ef554993c1c0dfe503487aa9be398ebcf617b51d06f99121841d4ca1cf25f3f53d00efa820ea778cc2271da425a606ac66e9f09a4c8cc6677 + languageName: node + linkType: hard + +"@lezer/lr@npm:^1.0.0, @lezer/lr@npm:^1.3.0": + version: 1.3.4 + resolution: "@lezer/lr@npm:1.3.4" + dependencies: + "@lezer/common": ^1.0.0 + checksum: 58bc25a9ba891dc6ca713fc8768706935e65d6e54d79a8ddb40c742cc799e87eddf4f49a6d6566a649c4726a9ab79a4200d36c9351608285a9bee6cdf3b33341 + languageName: node + linkType: hard + "@mdx-js/mdx@npm:^1.6.22": version: 1.6.22 resolution: "@mdx-js/mdx@npm:1.6.22" @@ -3535,6 +3769,13 @@ __metadata: languageName: node linkType: hard +"@open-draft/deferred-promise@npm:^2.1.0": + version: 2.1.0 + resolution: "@open-draft/deferred-promise@npm:2.1.0" + checksum: 8b14ae9c7e9936371bd489cf6541faba0cd23340b70aaa228d2a8d97f3449c738151c9f20b65002d9e3edbe9e5e34d5a27171d744711c0d11d300cd1245d9085 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -3549,6 +3790,27 @@ __metadata: languageName: node linkType: hard +"@react-hook/intersection-observer@npm:^3.1.1": + version: 3.1.1 + resolution: "@react-hook/intersection-observer@npm:3.1.1" + dependencies: + "@react-hook/passive-layout-effect": ^1.2.0 + intersection-observer: ^0.10.0 + peerDependencies: + react: ">=16.8" + checksum: 4c76b21e4f867d79ea088a8cbb6b983a72dda4a52a5f666f295d86af004a5ee5b0b121748c77f17844f558d1a461dd0f6275c563330a84f5341e210e7b8272c4 + languageName: node + linkType: hard + +"@react-hook/passive-layout-effect@npm:^1.2.0": + version: 1.2.1 + resolution: "@react-hook/passive-layout-effect@npm:1.2.1" + peerDependencies: + react: ">=16.8" + checksum: 217cb8aa90fb8e677672319a9a466d7752890cf4357c76df000b207696e9cc717cf2ee88080671cc9dae238a82f92093ab4f61ab2f6032d2a8db958fc7d99b5d + languageName: node + linkType: hard + "@rollup/plugin-commonjs@npm:^24.1.0": version: 24.1.0 resolution: "@rollup/plugin-commonjs@npm:24.1.0" @@ -3718,6 +3980,13 @@ __metadata: languageName: node linkType: hard +"@stitches/core@npm:^1.2.6": + version: 1.2.8 + resolution: "@stitches/core@npm:1.2.8" + checksum: 3a76a697b1d71ffb212e8b166f923c8f66ef99de93689dd417571f90fbba5692abb9a92220acd102baf5298896f0fdfeaa721da2d61adee1247531a26f67a15f + languageName: node + linkType: hard + "@svgr/babel-plugin-add-jsx-attribute@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:6.5.1" @@ -5213,6 +5482,13 @@ __metadata: languageName: node linkType: hard +"anser@npm:^2.1.1": + version: 2.1.1 + resolution: "anser@npm:2.1.1" + checksum: 29b99ae7c54e7107643a130a07228f853b4c28d7742db40dadca2f405263a1d30202e1c1a576644f2162e795e556591dc05fa4a6d77c49a028dfa05f73d76dae + languageName: node + linkType: hard + "ansi-align@npm:^3.0.0, ansi-align@npm:^3.0.1": version: 3.0.1 resolution: "ansi-align@npm:3.0.1" @@ -6296,6 +6572,13 @@ __metadata: languageName: node linkType: hard +"clean-set@npm:^1.1.2": + version: 1.1.2 + resolution: "clean-set@npm:1.1.2" + checksum: 1bd86cc20a1f5834b2e081f96b4336c4619b8eb842392b496b748d63e1ee58aa1959aa13cd392e96988600e105b9067d29082a13ae80f3b92d55c0474c0fda08 + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -6455,6 +6738,13 @@ __metadata: languageName: node linkType: hard +"codesandbox-import-util-types@npm:^2.2.3": + version: 2.3.0 + resolution: "codesandbox-import-util-types@npm:2.3.0" + checksum: e527fe1a0bc2de5a0a9842a70d42ad4c6de56a99a4a9e1427a81f4006ab91d85ff098e2e919cccbcd8703f9b3216b9c1f4ce5a40261051d8d1511287fbbf7d77 + languageName: node + linkType: hard + "collapse-white-space@npm:^1.0.2": version: 1.0.6 resolution: "collapse-white-space@npm:1.0.6" @@ -6857,6 +7147,13 @@ __metadata: languageName: node linkType: hard +"crelt@npm:^1.0.5": + version: 1.0.5 + resolution: "crelt@npm:1.0.5" + checksum: 04a618c5878e12a14a9a328a49ff6e37bed76abb88b72e661c56b5f161d8a9aca133650da6bcbc5224ad1f7f43a69325627f209e92a21002986d52a8f844b367 + languageName: node + linkType: hard + "cross-env@npm:^7.0.3": version: 7.0.3 resolution: "cross-env@npm:7.0.3" @@ -7147,6 +7444,16 @@ __metadata: languageName: node linkType: hard +"d@npm:1, d@npm:^1.0.1": + version: 1.0.1 + resolution: "d@npm:1.0.1" + dependencies: + es5-ext: ^0.10.50 + type: ^1.0.1 + checksum: 49ca0639c7b822db670de93d4fbce44b4aa072cd848c76292c9978a8cd0fff1028763020ff4b0f147bd77bfe29b4c7f82e0f71ade76b2a06100543cdfd948d19 + languageName: node + linkType: hard + "data-uri-to-buffer@npm:^4.0.0": version: 4.0.0 resolution: "data-uri-to-buffer@npm:4.0.0" @@ -7355,6 +7662,13 @@ __metadata: languageName: node linkType: hard +"dequal@npm:^2.0.2": + version: 2.0.3 + resolution: "dequal@npm:2.0.3" + checksum: 8679b850e1a3d0ebbc46ee780d5df7b478c23f335887464023a631d1b9af051ad4a6595a44220f9ff8ff95a8ddccf019b5ad778a976fd7bbf77383d36f412f90 + languageName: node + linkType: hard + "destroy@npm:1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -7582,6 +7896,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.0.3": + version: 16.0.3 + resolution: "dotenv@npm:16.0.3" + checksum: afcf03f373d7a6d62c7e9afea6328e62851d627a4e73f2e12d0a8deae1cd375892004f3021883f8aec85932cd2834b091f568ced92b4774625b321db83b827f8 + languageName: node + linkType: hard + "duplexer3@npm:^0.1.4": version: 0.1.5 resolution: "duplexer3@npm:0.1.5" @@ -7747,6 +8068,38 @@ __metadata: languageName: node linkType: hard +"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.50": + version: 0.10.62 + resolution: "es5-ext@npm:0.10.62" + dependencies: + es6-iterator: ^2.0.3 + es6-symbol: ^3.1.3 + next-tick: ^1.1.0 + checksum: 25f42f6068cfc6e393cf670bc5bba249132c5f5ec2dd0ed6e200e6274aca2fed8e9aec8a31c76031744c78ca283c57f0b41c7e737804c6328c7b8d3fbcba7983 + languageName: node + linkType: hard + +"es6-iterator@npm:^2.0.3": + version: 2.0.3 + resolution: "es6-iterator@npm:2.0.3" + dependencies: + d: 1 + es5-ext: ^0.10.35 + es6-symbol: ^3.1.1 + checksum: 6e48b1c2d962c21dee604b3d9f0bc3889f11ed5a8b33689155a2065d20e3107e2a69cc63a71bd125aeee3a589182f8bbcb5c8a05b6a8f38fa4205671b6d09697 + languageName: node + linkType: hard + +"es6-symbol@npm:^3, es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3": + version: 3.1.3 + resolution: "es6-symbol@npm:3.1.3" + dependencies: + d: ^1.0.1 + ext: ^1.1.2 + checksum: cd49722c2a70f011eb02143ef1c8c70658d2660dead6641e160b94619f408b9cf66425515787ffe338affdf0285ad54f4eae30ea5bd510e33f8659ec53bcaa70 + languageName: node + linkType: hard + "esbuild@npm:^0.16.3": version: 0.16.17 resolution: "esbuild@npm:0.16.17" @@ -7908,6 +8261,13 @@ __metadata: languageName: node linkType: hard +"escape-carriage@npm:^1.3.1": + version: 1.3.1 + resolution: "escape-carriage@npm:1.3.1" + checksum: 6d7613a5875977b04eb4651f3fa14eb8f54c72aa198b603c823502fcfef9f5969afc4c76d27b2f2fc008d88764fbf4c3a6e04d549d6134e2ff7f705c99e3ddb9 + languageName: node + linkType: hard + "escape-goat@npm:^2.0.0": version: 2.1.1 resolution: "escape-goat@npm:2.1.1" @@ -8252,6 +8612,15 @@ __metadata: languageName: node linkType: hard +"ext@npm:^1.1.2": + version: 1.7.0 + resolution: "ext@npm:1.7.0" + dependencies: + type: ^2.7.2 + checksum: ef481f9ef45434d8c867cfd09d0393b60945b7c8a1798bedc4514cb35aac342ccb8d8ecb66a513e6a2b4ec1e294a338e3124c49b29736f8e7c735721af352c31 + languageName: node + linkType: hard + "extend-shallow@npm:^2.0.1": version: 2.0.1 resolution: "extend-shallow@npm:2.0.1" @@ -9720,6 +10089,13 @@ __metadata: languageName: node linkType: hard +"intersection-observer@npm:^0.10.0": + version: 0.10.0 + resolution: "intersection-observer@npm:0.10.0" + checksum: 70c07db838153a29107c480e3e757c4b9c1103837b76e273616528c3f7db1e7d9c585725b3aa838f6af73ed06ef6a2a9e4892ba9dc9a8a14542fa5138b0c5c05 + languageName: node + linkType: hard + "invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" @@ -11505,7 +11881,7 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:1.52.0, mime-db@npm:>= 1.43.0 < 2": +"mime-db@npm:1.52.0, mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.52.0": version: 1.52.0 resolution: "mime-db@npm:1.52.0" checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f @@ -11874,6 +12250,13 @@ __metadata: languageName: node linkType: hard +"next-tick@npm:^1.1.0": + version: 1.1.0 + resolution: "next-tick@npm:1.1.0" + checksum: 83b5cf36027a53ee6d8b7f9c0782f2ba87f4858d977342bfc3c20c21629290a2111f8374d13a81221179603ffc4364f38374b5655d17b6a8f8a8c77bdea4fe8b + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -12350,6 +12733,20 @@ __metadata: languageName: node linkType: hard +"outvariant@npm:1.3.0": + version: 1.3.0 + resolution: "outvariant@npm:1.3.0" + checksum: ac76ca375c1c642989e1c74f0e9ebac84c05bc9fdc8f28be949c16fae1658e9f1f2fb1133fe3cc1e98afabef78fe4298fe9360b5734baf8e6ad440c182680848 + languageName: node + linkType: hard + +"outvariant@npm:^1.3.0": + version: 1.4.0 + resolution: "outvariant@npm:1.4.0" + checksum: ec32dfc315c464bb6e4906b2f450d259ce0b86caf70b70b249054359d9af21a7fccf53a8b6aa232f8d718449e31c1cfa594e6ebffaafe7bf908b502495256d7b + languageName: node + linkType: hard + "p-cancelable@npm:^1.0.0": version: 1.1.0 resolution: "p-cancelable@npm:1.1.0" @@ -13667,6 +14064,15 @@ __metadata: languageName: node linkType: hard +"react-devtools-inline@npm:4.4.0": + version: 4.4.0 + resolution: "react-devtools-inline@npm:4.4.0" + dependencies: + es6-symbol: ^3 + checksum: 18911846ecd062a7b06ffb163613678635ba9ab0cc7bf7fd0268631f8fe7479fbbb31926f766af968f128c437476e7307b57a926fe706b0489a22854aa2501ed + languageName: node + linkType: hard + "react-dom@npm:^17.0.2": version: 17.0.2 resolution: "react-dom@npm:17.0.2" @@ -13729,7 +14135,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^17.0.1": +"react-is@npm:^17.0.1, react-is@npm:^17.0.2": version: 17.0.2 resolution: "react-is@npm:17.0.2" checksum: 9d6d111d8990dc98bc5402c1266a808b0459b5d54830bbea24c12d908b536df7883f268a7868cfaedde3dd9d4e0d574db456f84d2e6df9c4526f99bb4b5344d8 @@ -13838,7 +14244,7 @@ __metadata: languageName: node linkType: hard -"react@npm:^17.0.2": +"react@npm:17.0.2": version: 17.0.2 resolution: "react@npm:17.0.2" dependencies: @@ -13857,6 +14263,16 @@ __metadata: languageName: node linkType: hard +"react@patch:react@npm%3A17.0.2#./.yarn/patches/react-npm-17.0.2-99ba37d931.patch::locator=%40fast-check%2Fmonorepo%40workspace%3A.": + version: 17.0.2 + resolution: "react@patch:react@npm%3A17.0.2#./.yarn/patches/react-npm-17.0.2-99ba37d931.patch::version=17.0.2&hash=bef5a8&locator=%40fast-check%2Fmonorepo%40workspace%3A." + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + checksum: 4a9d203492f7385f086eabfe76d986834ffd6f4e3a33d4530681a8ccc43045616971db06fca593d80d2d8b143afc6bcf6945742660d73109d25cbd4846b8b251 + languageName: node + linkType: hard + "read-cmd-shim@npm:^4.0.0": version: 4.0.0 resolution: "read-cmd-shim@npm:4.0.0" @@ -15066,6 +15482,18 @@ __metadata: languageName: node linkType: hard +"static-browser-server@npm:1.0.3": + version: 1.0.3 + resolution: "static-browser-server@npm:1.0.3" + dependencies: + "@open-draft/deferred-promise": ^2.1.0 + dotenv: ^16.0.3 + mime-db: ^1.52.0 + outvariant: ^1.3.0 + checksum: 0bb039a41d9c20954b7456010ec743836268576ae7a7c406d9b8a483ef6c6864d5f2cf76ce41d5614fc6d6abc6a207665365a44213323d7b68323c0e32860cf2 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -15087,6 +15515,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.4.3": + version: 0.4.6 + resolution: "strict-event-emitter@npm:0.4.6" + checksum: 4f4f2909613e7811de789991c06bfb770d6d6987e2ec5c66fa7485d0f07cc4e7e32eba0dcf26cee6d86af6c92946d7f4acdfaff57d0c4114df2cfa1bf0e3c091 + languageName: node + linkType: hard + "string-argv@npm:~0.3.1": version: 0.3.1 resolution: "string-argv@npm:0.3.1" @@ -15226,6 +15661,13 @@ __metadata: languageName: node linkType: hard +"style-mod@npm:^4.0.0": + version: 4.0.3 + resolution: "style-mod@npm:4.0.3" + checksum: 934556e720bd29026ff8fef43a1a35b58957813025b91f996d886e9405acf934ddb1934def4400b174bd7784c9263eb9c71f07ae83925af9271b7d921d546854 + languageName: node + linkType: hard + "style-to-object@npm:0.3.0, style-to-object@npm:^0.3.0": version: 0.3.0 resolution: "style-to-object@npm:0.3.0" @@ -15746,6 +16188,20 @@ __metadata: languageName: node linkType: hard +"type@npm:^1.0.1": + version: 1.2.0 + resolution: "type@npm:1.2.0" + checksum: dae8c64f82c648b985caf321e9dd6e8b7f4f2e2d4f846fc6fd2c8e9dc7769382d8a52369ddbaccd59aeeceb0df7f52fb339c465be5f2e543e81e810e413451ee + languageName: node + linkType: hard + +"type@npm:^2.7.2": + version: 2.7.2 + resolution: "type@npm:2.7.2" + checksum: 0f42379a8adb67fe529add238a3e3d16699d95b42d01adfe7b9a7c5da297f5c1ba93de39265ba30ffeb37dfd0afb3fb66ae09f58d6515da442219c086219f6f4 + languageName: node + linkType: hard + "typedarray-to-buffer@npm:^3.1.5": version: 3.1.5 resolution: "typedarray-to-buffer@npm:3.1.5" @@ -16414,6 +16870,13 @@ __metadata: languageName: node linkType: hard +"w3c-keyname@npm:^2.2.4": + version: 2.2.6 + resolution: "w3c-keyname@npm:2.2.6" + checksum: 59a31d23ca9953c01c99ed6695fee5b6ea36eb2412d76a21fe4302ab33a3f5cd96c006a763940b6115c3d042c16d3564eeee1156832217d028af0518098b3a42 + languageName: node + linkType: hard + "w3c-xmlserializer@npm:^4.0.0": version: 4.0.0 resolution: "w3c-xmlserializer@npm:4.0.0" @@ -16687,6 +17150,8 @@ __metadata: version: 0.0.0-use.local resolution: "website@workspace:website" dependencies: + "@codesandbox/sandpack-react": ^2.6.1 + "@codesandbox/sandpack-themes": ^2.0.21 "@docusaurus/core": 2.4.0 "@docusaurus/module-type-aliases": 2.4.0 "@docusaurus/preset-classic": 2.4.0 -- 2.51.2