From eb739202703e568368715c4d6357407f3ad4edfa Mon Sep 17 00:00:00 2001 From: Kevin Whinnery Date: Sun, 3 Sep 2023 10:55:38 -0500 Subject: [PATCH] add dedicated section of the docs for KV --- docusaurus.config.js | 19 ++- kv/index.md | 3 + kv/manual/index.md | 210 ++++++++++++++++++++++++++ kv/manual/key_space.md | 244 ++++++++++++++++++++++++++++++ kv/manual/on_deploy.md | 79 ++++++++++ kv/manual/operations.md | 262 +++++++++++++++++++++++++++++++++ kv/manual/secondary_indexes.md | 183 +++++++++++++++++++++++ kv/manual/transactions.md | 100 +++++++++++++ kv/tutorials/index.md | 48 ++++++ sidebars/kv.js | 93 ++++++++++++ sidebars/runtime.js | 24 +-- src/css/custom.css | 57 ++++++- src/pages/index.jsx | 2 +- static/server.ts | 30 +++- 14 files changed, 1329 insertions(+), 25 deletions(-) create mode 100644 kv/index.md create mode 100644 kv/manual/index.md create mode 100644 kv/manual/key_space.md create mode 100644 kv/manual/on_deploy.md create mode 100644 kv/manual/operations.md create mode 100644 kv/manual/secondary_indexes.md create mode 100644 kv/manual/transactions.md create mode 100644 kv/tutorials/index.md create mode 100644 sidebars/kv.js diff --git a/docusaurus.config.js b/docusaurus.config.js index 18ac59171..c92e931ae 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -12,7 +12,7 @@ const config = { favicon: "img/favicon.ico", // Set the production url of your site here - url: "https://docs.deno.land", + url: "https://docs.deno.com", // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' baseUrl: "/", @@ -93,6 +93,15 @@ const config = { sidebarPath: require.resolve("./sidebars/deploy.js"), }, ], + [ + "@docusaurus/plugin-content-docs", + { + id: "kv", + path: "kv", + routeBasePath: "/kv", + sidebarPath: require.resolve("./sidebars/kv.js"), + }, + ], async function tailwindPlugin(context, options) { return { name: "docusaurus-tailwindcss", @@ -134,9 +143,15 @@ const config = { label: "Deploy", activeBaseRegex: `^/deploy`, }, + { + to: "/kv/manual", + position: "left", + label: "KV", + activeBaseRegex: `^/kv`, + }, { href: "https://www.deno.land/std", - label: "Standard Library", + label: "Std. Library", }, /* { diff --git a/kv/index.md b/kv/index.md new file mode 100644 index 000000000..59ede40e4 --- /dev/null +++ b/kv/index.md @@ -0,0 +1,3 @@ +# Deno KV + +_This URL redirected in production to [/kv/manual](/kv/manual)._ diff --git a/kv/manual/index.md b/kv/manual/index.md new file mode 100644 index 000000000..ce99a65d5 --- /dev/null +++ b/kv/manual/index.md @@ -0,0 +1,210 @@ +# Deno KV Quick Start + +Since version 1.32, Deno has a built in key-value store that durably persists +data on disk, allowing for data storage and access across service and system +restarts. + +The key-value store is designed to be fast and easy to use. Keys are sequences +(arrays) of JavaScript types like `string`, `number`, `bigint`, `boolean`, and +`Uint8Array`. Values are arbitrary JavaScript primitives, objects, and arrays. + +The store supports seven different operations that can be composed together to +support many use-cases and enable persistence for most common patterns in modern +web applications. Atomic operations are available that allow grouping of any +number of modification operations into a single atomic transaction. + +All data in the KV store is versioned, which allows atomic operations to be +conditional on versions in storage matching the value that user code expected. +This enables optimistic locking, enabling virtual asynchronous transactions. + +All writes to the KV store are strongly consistent and immediately durably +persisted. Reads are strongly consistent by default, but alternative consistency +modes are available to enable different performance tradeoffs. + +> ⚠️ Deno KV is currently **experimental** and **subject to change**. While we do +> our best to ensure data durability, data loss is possible, especially around +> Deno updates. We recommend that you backup your data regularly and consider +> storing data in a secondary store for the time being. + +## Getting started + +> ⚠️ Because Deno KV is currently **experimental** and **subject to change**, it +> is only available when running with `--unstable` flag in Deno CLI. + +All operations on the key-value store are performed via the `Deno.Kv` API. + +A database can be opened using the `Deno.openKv()` function. This function +optionally takes a database path on disk as the first argument. If no path is +specified, the database is persisted in a global directory, bound to the script +that `Deno.openKv()` was called from. Future invocations of the same script will +use the same database. + +Operations can be called on the `Deno.Kv`. The three primary operations on the +database are `get`, `set`, and `delete`. These allow reading, writing, and +deleting individual keys. + +```tsx +// Open the default database for the script. +const kv = await Deno.openKv(); + +// Persist an object at the users/alice key. +await kv.set(["users", "alice"], { name: "Alice" }); + +// Read back this key. +const res = await kv.get(["users", "alice"]); +console.log(res.key); // [ "users", "alice" ] +console.log(res.value); // { name: "Alice" } + +// Delete the key. +await kv.delete(["users", "alice"]); + +// Reading back the key now returns null. +const res2 = await kv.get(["users", "alice"]); +console.log(res2.key); // [ "users", "alice" ] +console.log(res2.value); // null +``` + +The `list` operation can be used to list out all keys matching a specific +selector. In the below example all keys starting with some prefix are selected. + +```tsx,ignore +await kv.set(["users", "alice"], { birthday: "January 1, 1990" }); +await kv.set(["users", "sam"], { birthday: "February 14, 1985" }); +await kv.set(["users", "taylor"], { birthday: "December 25, 1970" }); + +// List out all entries with keys starting with `["users"]` +for await (const entry of kv.list({ prefix: ["users"] })) { + console.log(entry.key); + console.log(entry.value); +} +``` + +> Note: in addition to prefix selectors, range selectors, and constrained prefix +> selectors are also available. + +In addition to individual `get`, `set`, and `delete` operations, the key-value +store supports `atomic` operations that allow multiple modifications to take +place at once, optionally conditional on the existing data in the store. + +In the below example, we insert a new user only if it does not yet exist by +performing an atomic operation that has a check that there is no existing value +for the given key: + +```tsx,ignore +const key = ["users", "alice"]; +const value = { birthday: "January 1, 1990" }; +const res = await kv.atomic() + .check({ key, versionstamp: null }) // `null` versionstamps mean 'no value' + .set(key, value) + .commit(); +if (res.ok) { + console.log("User did not yet exist. Inserted!"); +} else { + console.log("User already exists."); +} +``` + +## Examples + +**Multi-player Tic-Tac-Toe** + +- GitHub authentication +- Saved user state +- Real-time sync using BroadcastChannel +- [Source code](https://github.com/denoland/tic-tac-toe) +- [Live preview](https://tic-tac-toe-game.deno.dev/) + +**Pixelpage** + +- Persistent canvas state +- Multi-user collaboration +- Real-time sync using BroadcastChannel +- [Source code](https://github.com/denoland/pixelpage) +- [Live preview](https://pixelpage.deno.dev/) + +**Todo list** + +- Zod schema validation +- Built using Fresh +- Real-time collaboration using BroadcastChannel +- [Source code](https://github.com/denoland/showcase_todo) +- [Live preview](https://showcase-todo.deno.dev/) + +**Sketch book** + +- Stores drawings in KV +- GitHub authentication +- [Source code](https://github.com/hashrock/kv-sketchbook) +- [Live preview](https://hashrock-kv-sketchbook.deno.dev/) + +**Deno KV OAuth** + +- High-level OAuth 2.0 powered by Deno KV +- [Source code](https://github.com/denoland/deno_kv_oauth) +- [Live preview](https://kv-oauth.deno.dev/) + +**Deno SaaSKit** + +- Modern SaaS template built on Fresh. +- [Hacker News](https://news.ycombinator.com/)-like demo entirely built on KV. +- Uses Deno KV OAuth for GitHub OAuth 2.0 authentication +- [Source code](https://github.com/denoland/saaskit) +- [Live preview](https://hunt.deno.land/) + +## Reference + +- [API Reference](https://deno.land/api?unstable&s=Deno.Kv) +- [Key Space](./key_space.md) +- [Operations](./operations.md) + +## Patterns + +- [Transactions](./transactions.md) +- [Secondary Indexes](./secondary_indexes.md) +- Real-time data (TODO) +- Counters (TODO) + + diff --git a/kv/manual/key_space.md b/kv/manual/key_space.md new file mode 100644 index 000000000..7835e06c2 --- /dev/null +++ b/kv/manual/key_space.md @@ -0,0 +1,244 @@ +# Key Space + +> ⚠️ Deno KV is currently **experimental** and **subject to change**. While we do +> our best to ensure data durability, data loss is possible, especially around +> Deno updates. We recommend that you backup your data regularly and consider +> storing data in a secondary store for the time being. + +Deno KV is a key value store. The key space is a flat namespace of +key+value+versionstamp pairs. Keys are sequences of key parts, which allow +modeling of hierarchical data. Values are arbitrary JavaScript objects. +Versionstamps represent when a value was inserted / modified. + +## Keys + +Keys in Deno KV are sequences of key parts, which can be `string`s, `number`s, +`boolean`s, `Uint8Array`s, or `bigint`s. + +Using a sequence of parts, rather than a single string eliminates the +possibility of delimiter injection attacks, because there is no visible +delimiter. + +> A key injection attack occurs when an attacker manipulates the structure of a +> key-value store by injecting delimiters used in the key encoding scheme into a +> user controlled variable, leading to unintended behavior or unauthorized +> access. For example, consider a key-value store using a slash (/) as a +> delimiter, with keys like "user/alice/settings" and "user/bob/settings". An +> attacker could create a new user with the name "alice/settings/hacked" to form +> the key "user/alice/settings/hacked/settings", injecting the delimiter and +> manipulating the key structure. In Deno KV, the injection would result in the +> key `["user", "alice/settings/hacked", "settings"]`, which is not harmful. + +Between key parts, invisible delimiters are used to separate the parts. These +delimiters are never visible, but ensure that one part can not be confused with +another part. For example, the key parts `["abc", "def"]`, `["ab", "cdef"]`, +`["abc", "", "def"]` are all different keys. + +Keys are case sensitive and are ordered lexicographically by their parts. The +first part is the most significant, and the last part is the least significant. +The order of the parts is determined by both the type and the value of the part. + +### Key Part Ordering + +Key parts are ordered lexicographically by their type, and within a given type, +they are ordered by their value. The ordering of types is as follows: + +1. `Uint8Array` +1. `string` +1. `number` +1. `bigint` +1. `boolean` + +Within a given type, the ordering is: + +- `Uint8Array`: byte ordering of the array +- `string`: byte ordering of the UTF-8 encoding of the string +- `number`: -NaN < -Infinity < -1.0 < -0.5 < -0.0 < 0.0 < 0.5 < 1.0 < Infinity < + NaN +- `bigint`: mathematical ordering, largest negative number first, largest + positive number last +- `boolean`: false < true + +This means that the part `1.0` (a number) is ordered before the part `2.0` (also +a number), but is greater than the part `0n` (a bigint), because `1.0` is a +number and `0n` is a bigint, and type ordering has precedence over the ordering +of values within a type. + +### Key Examples + +```js +["users", 42, "profile"]; // User with ID 42's profile +["posts", "2023-04-23", "comments"]; // Comments for all posts on 2023-04-23 +["products", "electronics", "smartphones", "apple"]; // Apple smartphones in the electronics category +["orders", 1001, "shipping", "tracking"]; // Tracking information for order ID 1001 +["files", new Uint8Array([1, 2, 3]), "metadata"]; // Metadata for a file with Uint8Array identifier +["projects", "openai", "tasks", 5]; // Task with ID 5 in the OpenAI project +["events", "2023-03-31", "location", "san_francisco"]; // Events in San Francisco on 2023-03-31 +["invoices", 2023, "Q1", "summary"]; // Summary of Q1 invoices for 2023 +["teams", "engineering", "members", 1n]; // Member with ID 1n in the engineering team +``` + +### Universally Unique Lexicographically Sortable Identifiers (ULIDs) + +Key part ordering allows keys consisting of timestamps and ID parts to be listed +chronologically. Typically, you can generate a key using the following: +[`Date.now()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) +and +[`crypto.randomUUID()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID): + +```js +async function setUser(user) { + await kv.set(["users", Date.now(), crypto.randomUUID()], user); +} +``` + +Run multiple times sequentially, this produces the following keys: + +```js +["users", 1691377037923, "8c72fa25-40ad-42ce-80b0-44f79bc7a09e"]; // First user +["users", 1691377037924, "8063f20c-8c2e-425e-a5ab-d61e7a717765"]; // Second user +["users", 1691377037925, "35310cea-58ba-4101-b09a-86232bf230b2"]; // Third user +``` + +However, having the timestamp and ID represented within a single key part may be +more straightforward in some cases. You can use a +[Universally Unique Lexicographically Sortable Identifier (ULID)](https://github.com/ulid/spec) +to do this. This type of identifier encodes a UTC timestamp, is +lexicographically sortable and is cryptographically random by default: + +```js +import { ulid } from "https://deno.land/x/ulid/mod.ts"; + +const kv = await Deno.openKv(); + +async function setUser(user) { + await kv.set(["users", ulid()], user); +} +``` + +```js +["users", "01H76YTWK3YBV020S6MP69TBEQ"]; // First user +["users", "01H76YTWK4V82VFET9YTYDQ0NY"]; // Second user +["users", "01H76YTWK5DM1G9TFR0Y5SCZQV"]; // Third user +``` + +Furthermore, you can generate ULIDs monotonically increasingly using a factory +function: + +```js +import { monotonicFactory } from "https://deno.land/x/ulid/mod.ts"; + +const ulid = monotonicFactory(); + +async function setUser(user) { + await kv.set(["users", ulid()], user); +} +``` + +```js +// Strict ordering for the same timestamp by incrementing the least-significant random bit by 1 +["users", "01H76YTWK3YBV020S6MP69TBEQ"]; // First user +["users", "01H76YTWK3YBV020S6MP69TBER"]; // Second user +["users", "01H76YTWK3YBV020S6MP69TBES"]; // Third user +``` + +## Values + +Values in Deno KV can be arbitrary JavaScript values that are compatible with +the [structured clone algorithm][structured clone algorithm]. This includes: + +- `undefined` +- `null` +- `boolean` +- `number` +- `string` +- `bigint` +- `Uint8Array` +- `Array` +- `Object` +- `Map` +- `Set` +- `Date` +- `RegExp` + +Objects and arrays can contain any of the above types, including other objects +and arrays. `Map`s and `Set`s can contain any of the above types, including +other `Map`s and `Set`s. + +Circular references within values are supported. + +Objects with a non-primitive prototype are not supported (such as class +instances or Web API objects). Functions and symbols can also not be serialized. + +### `Deno.KvU64` type + +In addition to structured serializable values, the special value `Deno.KvU64` is +also supported as a value. This object represents a 64-bit unsigned integer, +represented as a bigint. It can be used with the `sum`, `min`, and `max` KV +operations. It can not be stored within an object or array. It must be stored as +a top-level value. + +It can be created with the `Deno.KvU64` constructor: + +```js +const u64 = new Deno.KvU64(42n); +``` + +### Value Examples + +```js,ignore +undefined; +null; +true; +false; +42; +-42.5; +42n; +"hello"; +new Uint8Array([1, 2, 3]); +[1, 2, 3]; +{ a: 1, b: 2, c: 3 }; +new Map([["a", 1], ["b", 2], ["c", 3]]); +new Set([1, 2, 3]); +new Date("2023-04-23"); +/abc/; + +// Circular references are supported +const a = {}; +const b = { a }; +a.b = b; + +// Deno.KvU64 is supported +new Deno.KvU64(42n); +``` + +## Versionstamp + +All data in the Deno KV key-space is versioned. Every time a value is inserted +or modified, a versionstamp is assigned to it. Versionstamps are monotonically +increasing, non-sequential, 12 byte values that represent the time that the +value was modified. Versionstamps do not represent real time, but rather the +order in which the values were modified. + +Because versionstamps are monotonically increasing, they can be used to +determine whether a given value is newer or older than another value. This can +be done by comparing the versionstamps of the two values. If versionstamp A is +greater than versionstamp B, then value A was modified more recently than value +B. + +```js +versionstampA > versionstampB; +"000002fa526aaccb0000" > "000002fa526aacc90000"; // true +``` + +All data modified by a single transaction are assigned the same versionstamp. +This means that if two `set` operations are performed in the same atomic +operation, then the versionstamp of the new values will be the same. + +Versionstamps are used to implement optimistic concurrency control. Atomic +operations can contain checks that ensure that the versionstamp of the data they +are operating on matches a versionstamp passed to the operation. If the +versionstamp of the data is not the same as the versionstamp passed to the +operation, then the transaction will fail and the operation will not be applied. + +[structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm diff --git a/kv/manual/on_deploy.md b/kv/manual/on_deploy.md new file mode 100644 index 000000000..054cd5080 --- /dev/null +++ b/kv/manual/on_deploy.md @@ -0,0 +1,79 @@ +# KV on Deno Deploy + +Deno Deploy now offers a built-in serverless key-value database called Deno KV, +which is currently in closed beta testing. To join the waitlist for this +exclusive beta, please [sign up here.](https://dash.deno.com/kv). While in +closed beta, Deno KV will not charge for storage and includes up to 1GB of +storage per user. + +Additionally, Deno KV is available within Deno itself, utilizing SQLite as its +backend. This feature has been accessible since Deno v1.32 with the `--unstable` +flag. + +[Discover how to effectively use the Deno KV database by referring to the Deno Runtime user guide.](/runtime/manual/runtime/kv) + +## Getting started + +Upon receiving an invitation from the waitlist, a new “KV” tab will appear in +all your projects. This tab displays basic usage statistics and a data browser. + +For GitHub projects, two separate databases are generated: one for the +production branch (usually `main`) and another for all other branches. For +playground projects, a single database is created. + +No additional configuration is required. If a Deno project utilizing KV works on +a local setup, it will seamlessly function on Deploy without any modifications. + +## Consistency + +Deno KV, by default, is a strongly-consistent database. It provides the +strictest form of strong consistency called _external consistency_, which +implies: + +- **Serializability**: This is the highest level of isolation for transactions. + It ensures that the concurrent execution of multiple transactions results in a + system state that would be the same as if the transactions were executed + sequentially, one after another. In other words, the end result of + serializable transactions is equivalent to some sequential order of these + transactions. +- **Linearizability**: This consistency model guarantees that operations, such + as read and write, appear to be instantaneous and occur in real-time. Once a + write operation completes, all subsequent read operations will immediately + return the updated value. Linearizability ensures a strong real-time ordering + of operations, making the system more predictable and easier to reason about. + +Meanwhile, you can choose to relax consistency constraints by setting the +`consistency: "eventual"` option on individual read operations. This option +allows the system to serve the read from global replicas and caches for minimal +latency. + +Below are the latency figures observed in our top regions: + +| Region | Latency (Eventual Consistency) | Latency (Strong Consistency) | +| -------------------------- | ------------------------------ | ---------------------------- | +| North Virginia (us-east4) | 7ms | 7ms | +| Frankfurt (europe-west3) | 7ms | 94ms | +| Netherlands (europe-west4) | 13ms | 95ms | +| California (us-west2) | 72ms | 72ms | +| Hong Kong (asia-east2) | 42ms | 194ms | + +## Data distribution + +Deno KV databases are replicated across at least 6 data centers, spanning 3 +regions (US, Europe, and Asia). Once a write operation is committed, its +mutations are persistently stored in a minimum of two data centers within the +primary region. Asynchronous replication typically transfers these mutations to +the other two regions in under 10 seconds. + +The system is designed to tolerate most data center-level failures without +experiencing downtime or data loss. Recovery Point Objectives (RPO) and Recovery +Time Objectives (RTO) help quantify the system's resilience under various +failure modes. RPO represents the maximum acceptable amount of data loss +measured in time, whereas RTO signifies the maximum acceptable time required to +restore the system to normal operations after a failure. + +- Loss of one data center in the primary region: RPO=0 (no data loss), RTO<5s + (system restoration in under 5 seconds) +- Loss of any number of data centers in a replica region: RPO=0, RTO<5s +- Loss of two or more data centers in the primary region: RPO<60s (under 60 + seconds of data loss) diff --git a/kv/manual/operations.md b/kv/manual/operations.md new file mode 100644 index 000000000..fd9b5aeef --- /dev/null +++ b/kv/manual/operations.md @@ -0,0 +1,262 @@ +# Operations + +> ⚠️ Deno KV is currently **experimental** and **subject to change**. While we do +> our best to ensure data durability, data loss is possible, especially around +> Deno updates. We recommend that you backup your data regularly and consider +> storing data in a secondary store for the time being. + +The Deno KV API provides a set of operations that can be performed on the key +space. + +There are two operations that read data from the store, and five operations that +write data to the store. + +Read operations can either be performed in strong or eventual consistency mode. +Strong consistency mode guarantees that the read operation will return the most +recently written value. Eventual consistency mode may return a stale value, but +is faster. + +Write operations are always performed in strong consistency mode. + +## `get` + +The `get` operation returns the value and versionstamp associated with a given +key. If a value does not exist, get returns a `null` value and versionstamp. + +There are two APIs that can be used to perform a `get` operation. The +[`Deno.Kv.prototype.get(key, options?)`][get] API, which can be used to read a +single key, and the [`Deno.Kv.prototype.getMany(keys, options?)`][getMany] API, +which can be used to read multiple keys at once. + +Get operations are performed as a "snapshot read" in all consistency modes. This +means that when retrieving multiple keys at once, the values returned will be +consistent with each other. + +```ts,ignore +const res = await kv.get(["config"]); +console.log(res); // { key: ["config"], value: "value", versionstamp: "000002fa526aaccb0000" } + +const res = await kv.get(["config"], { consistency: "eventual" }); +console.log(res); // { key: ["config"], value: "value", versionstamp: "000002fa526aaccb0000" } + +const [res1, res2, res3] = await kv.getMany<[string, string, string]>([ + ["users", "sam"], + ["users", "taylor"], + ["users", "alex"], +]); +console.log(res1); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } +console.log(res2); // { key: ["users", "taylor"], value: "taylor", versionstamp: "0059e9035e5e7c5e0000" } +console.log(res3); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } +``` + +## `list` + +The `list` operation returns a list of keys that match a given selector. The +associated values and versionstamps for these keys are also returned. There are +2 different selectors that can be used to filter the keys matched. + +The `prefix` selector matches all keys that start with the given prefix key +parts, but not inclusive of an exact match of the key. The prefix selector may +optionally be given a `start` OR `end` key to limit the range of keys returned. +The `start` key is inclusive, and the `end` key is exclusive. + +The `range` selector matches all keys that are lexographically between the given +`start` and `end` keys. The `start` key is inclusive, and the `end` key is +exclusive. + +> Note: In the case of the prefix selector, the `prefix` key must consist only +> of full (not partial) key parts. For example, if the key `["foo", "bar"]` +> exists in the store, then the prefix selector `["foo"]` will match it, but the +> prefix selector `["f"]` will not. + +The list operation may optionally be given a `limit` to limit the number of keys +returned. + +List operations can be performed using the +[`Deno.Kv.prototype.list(selector, options?)`][list] method. This method +returns a `Deno.KvListIterator` that can be used to iterate over the keys +returned. This is an async iterator, and can be used with `for await` loops. + +```ts,ignore +// Return all users +const iter = kv.list({ prefix: ["users"] }); +const users = []; +for await (const res of iter) users.push(res); +console.log(users[0]); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } +console.log(users[1]); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } +console.log(users[2]); // { key: ["users", "taylor"], value: "taylor", versionstamp: "0059e9035e5e7c5e0000" } + +// Return the first 2 users +const iter = kv.list({ prefix: ["users"] }, { limit: 2 }); +const users = []; +for await (const res of iter) users.push(res); +console.log(users[0]); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } +console.log(users[1]); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } + +// Return all users lexicographically after "taylor" +const iter = kv.list({ prefix: ["users"], start: ["users", "taylor"] }); +const users = []; +for await (const res of iter) users.push(res); +console.log(users[0]); // { key: ["users", "taylor"], value: "taylor", versionstamp: "0059e9035e5e7c5e0000" } + +// Return all users lexicographically before "taylor" +const iter = kv.list({ prefix: ["users"], end: ["users", "taylor"] }); +const users = []; +for await (const res of iter) users.push(res); +console.log(users[0]); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } +console.log(users[1]); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } + +// Return all users starting with characters between "a" and "n" +const iter = kv.list({ start: ["users", "a"], end: ["users", "n"] }); +const users = []; +for await (const res of iter) users.push(res); +console.log(users[0]); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } +``` + +The list operation reads data from the store in batches. The size of each batch +can be controlled using the `batchSize` option. The default batch size is 500 +keys. Data within a batch is read in a single snapshot read, so the values are +consistent with each other. Consistency modes apply to each batch of data read. +Across batches, data is not consistent. The borders between batches is not +visible from the API as the iterator returns individual keys. + +The list operation can be performed in reverse order by setting the `reverse` +option to `true`. This will return the keys in lexicographically descending +order. The `start` and `end` keys are still inclusive and exclusive +respectively, and are still interpreted as lexicographically ascending. + +```ts,ignore +// Return all users in reverse order, ending with "sam" +const iter = kv.list({ prefix: ["users"], start: ["users", "sam"] }, { + reverse: true, +}); +const users = []; +for await (const res of iter) users.push(res); +console.log(users[0]); // { key: ["users", "taylor"], value: "taylor", versionstamp: "0059e9035e5e7c5e0000" } +console.log(users[1]); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } +``` + +> Note: in the above example we set the `start` key to `["users", "sam"]`, even +> though the first key returned is `["users", "taylor"]`. This is because the +> `start` and `end` keys are always evaluated in lexicographically ascending +> order, even when the list operation is performed in reverse order (which +> returns the keys in lexicographically descending order). + +## `set` + +The `set` operation sets the value of a key in the store. If the key does not +exist, it is created. If the key already exists, its value is overwritten. + +The `set` operation can be performed using the +[`Deno.Kv.prototype.set(key, value)`][set] method. This method returns a +`Promise` that resolves to a `Deno.KvCommitResult` object, which contains the +`versionstamp` of the commit. + +Set operations are always performed in strong consistency mode. + +```ts,ignore +const res = await kv.set(["users", "alex"], "alex"); +console.log(res.versionstamp); // "00a44a3c3e53b9750000" +``` + +## `delete` + +The `delete` operation deletes a key from the store. If the key does not exist, +the operation is a no-op. + +The `delete` operation can be performed using the +[`Deno.Kv.prototype.delete(key)`][delete] method. + +Delete operations are always performed in strong consistency mode. + +```ts,ignore +await kv.delete(["users", "alex"]); +``` + +## `sum` + +The `sum` operation atomically adds a value to a key in the store. If the key +does not exist, it is created with the value of the sum. If the key already +exists, its value is added to the sum. + +The `sum` operation can only be performed as part of an atomic operation. The +[`Deno.AtomicOperation.prototype.mutate({ type: "sum", value })`][mutate] method +can be used to add a sum mutation to an atomic operation. + +The sum operation can only be performed on values of type `Deno.KvU64`. Both the +operand and the value in the store must be of type `Deno.KvU64`. + +If the new value of the key is greater than `2^64 - 1` or less than `0`, the sum +operation wraps around. For example, if the value in the store is `2^64 - 1` and +the operand is `1`, the new value will be `0`. + +Sum operations are always performed in strong consistency mode. + +```ts,ignore +await kv.atomic() + .mutate({ + type: "sum", + key: ["accounts", "alex"], + value: new Deno.KvU64(100n), + }) + .commit(); +``` + +## `min` + +The `min` operation atomically sets a key to the minimum of its current value +and a given value. If the key does not exist, it is created with the given +value. If the key already exists, its value is set to the minimum of its current +value and the given value. + +The `min` operation can only be performed as part of an atomic operation. The +[`Deno.AtomicOperation.prototype.mutate({ type: "min", value })`][mutate] method +can be used to add a min mutation to an atomic operation. + +The min operation can only be performed on values of type `Deno.KvU64`. Both the +operand and the value in the store must be of type `Deno.KvU64`. + +Min operations are always performed in strong consistency mode. + +```ts,ignore +await kv.atomic() + .mutate({ + type: "min", + key: ["accounts", "alex"], + value: new Deno.KvU64(100n), + }) + .commit(); +``` + +## `max` + +The `max` operation atomically sets a key to the maximum of its current value +and a given value. If the key does not exist, it is created with the given +value. If the key already exists, its value is set to the maximum of its current +value and the given value. + +The `max` operation can only be performed as part of an atomic operation. The +[`Deno.AtomicOperation.prototype.mutate({ type: "max", value })`][mutate] method +can be used to add a max mutation to an atomic operation. + +The max operation can only be performed on values of type `Deno.KvU64`. Both the +operand and the value in the store must be of type `Deno.KvU64`. + +Max operations are always performed in strong consistency mode. + +```ts,ignore +await kv.atomic() + .mutate({ + type: "max", + key: ["accounts", "alex"], + value: new Deno.KvU64(100n), + }) + .commit(); +``` + +[get]: https://deno.land/api?s=Deno.Kv&p=prototype.get&unstable +[getMany]: https://deno.land/api?s=Deno.Kv&p=prototype.getMany&unstable +[list]: https://deno.land/api?s=Deno.Kv&p=prototype.list&unstable +[set]: https://deno.land/api?s=Deno.Kv&p=prototype.set&unstable +[delete]: https://deno.land/api?s=Deno.Kv&p=prototype.delete&unstable +[mutate]: https://deno.land/api?s=Deno.AtomicOperation&p=prototype.mutate&unstable diff --git a/kv/manual/secondary_indexes.md b/kv/manual/secondary_indexes.md new file mode 100644 index 000000000..32158749d --- /dev/null +++ b/kv/manual/secondary_indexes.md @@ -0,0 +1,183 @@ +# Secondary Indexes + +> ⚠️ Deno KV is currently **experimental** and **subject to change**. While we do +> our best to ensure data durability, data loss is possible, especially around +> Deno updates. We recommend that you backup your data regularly and consider +> storing data in a secondary store for the time being. + +Key-value stores like Deno KV organize data as collections of key-value pairs, +where each unique key is associated with a single value. This structure enables +easy retrieval of values based on their keys but does not allow for querying +based on the values themselves. To overcome this constraint, you can create +secondary indexes, which store the same value under additional keys that include +(part of) that value. + +Maintaining consistency between primary and secondary keys is crucial when using +secondary indexes. If a value is updated at the primary key without updating the +secondary key, the data returned from a query targeting the secondary key will +be incorrect. To ensure that primary and secondary keys always represent the +same data, use atomic operations when inserting, updating, or deleting data. +This approach ensures that the group of mutation actions are executed as a +single unit, and either all succeed or all fail, preventing inconsistencies. + +## Unique indexes (one-to-one) + +Unique indexes have each key in the index associated with exactly one primary +key. For example, when storing user data and looking up users by both their +unique IDs and email addresses, store user data under two separate keys: one for +the primary key (user ID) and another for the secondary index (email). This +setup allows querying users based on either their ID or their email. The +secondary index can also enforce uniqueness constraints on values in the store. +In the case of user data, use the index to ensure that each email address is +associated with only one user - in other words that emails are unique. + +To implement a unique secondary index for this example, follow these steps: + +1. Create a `User` interface representing the data: + + ```tsx + interface User { + id: string; + name: string; + email: string; + } + ``` + +2. Define an `insertUser` function that stores user data at both the primary and + secondary keys: + + ```tsx,ignore + async function insertUser(user: User) { + const primaryKey = ["users", user.id]; + const byEmailKey = ["users_by_email", user.email]; + const res = await kv.atomic() + .check({ key: primaryKey, versionstamp: null }) + .check({ key: byEmailKey, versionstamp: null }) + .set(primaryKey, user) + .set(byEmailKey, user) + .commit(); + if (!res.ok) { + throw new TypeError("User with ID or email already exists"); + } + } + ``` + + > This function performs the insert using an atomic operation that checks + > that no user with the same ID or email already exists. If either of these + > constraints is violated, the insert fails and no data is modified. + +3. Define a `getUser` function to retrieve a user by their ID: + + ```tsx,ignore + async function getUser(id: string): Promise { + const res = await kv.get(["users", id]); + return res.value; + } + ``` + +4. Define a `getUserByEmail` function to retrieve a user by their email address: + + ```tsx,ignore + async function getUserByEmail(email: string): Promise { + const res = await kv.get(["users_by_email", email]); + return res.value; + } + ``` + + This function queries the store using the secondary key + (`["users_by_email", email]`). + +5. Define a deleteUser function to delete users by their ID: + + ```tsx,ignore + async function deleteUser(id: string) { + let res = { ok: false }; + while (!res.ok) { + const getRes = await kv.get(["users", id]); + if (getRes.value === null) return; + res = await kv.atomic() + .check(getRes) + .delete(["users", id]) + .delete(["users_by_email", getRes.value.email]) + .commit(); + } + } + ``` + + + > This function first retrieves the user by their ID to get the users email + > address. This is needed to retrieve the email that is needed to construct + > the key for the secondary index for this user address. It then performs an + > atomic operation that checks that the user in the database has not changed, + > and then deletes both the primary and secondary key pointing to the user + > value. If this fails (the user has been modified between query and delete), + > the atomic operation aborts. The entire procedure is retried until the + > delete succeeds. + > + > The check is required to prevent race conditions where + > value may have been modified between the retrieve and delete. This race can + > occur if an update changes the user's email, because the secondary index + > moves in this case. The delete of the secondary index then fails, because + > the delete is targeting the old secondary index key. + +## Non-Unique Indexes (One-to-Many) + +Non-unique indexes are secondary indexes where a single key can be associated +with multiple primary keys, allowing you to query for multiple items based on a +shared attribute. For example, when querying users by their favorite color, +implement this using a non-unique secondary index. The favorite color is a +non-unique attribute since multiple users can have the same favorite color. + +To implement a non-unique secondary index for this example, follow these steps: + +1. Define the `User` interface: + + ```ts + interface User { + id: string; + name: string; + favoriteColor: string; + } + ``` + +2. Define the `insertUser` function: + + + ```ts,ignore + async function insertUser(user: User) { + const primaryKey = ["users", user.id]; + const byColorKey = ["users_by_favorite_color", user.favoriteColor, user.id]; + await kv.atomic() + .check({ key: primaryKey, versionstamp: null }) + .set(primaryKey, user) + .set(byColorKey, user) + .commit(); + } + ``` + +3. Define a function to retrieve users by their favorite color: + + ```ts,ignore + async function getUsersByFavoriteColor(color: string): Promise { + const iter = kv.list({ prefix: ["users_by_favorite_color", color] }); + const users = []; + for await (const { value } of iter) { + users.push(value); + } + return users; + } + ``` + +This example demonstrates the use of a non-unique secondary index, +`users_by_favorite_color`, which allows querying users based on their favorite +color. The primary key remains the user `id`. + +The primary difference between the implementation of unique and non-unique +indexes lies in the structure and organization of the secondary keys. In unique +indexes, each secondary key is associated with exactly one primary key, ensuring +that the indexed attribute is unique across all records. In the case of +non-unique indexes, a single secondary key can be associated with multiple +primary keys, as the indexed attribute may be shared among multiple records. To +achieve this, non-unique secondary keys are typically structured with an +additional unique identifier (e.g., primary key) as part of the key, allowing +multiple records with the same attribute to coexist without conflicts. diff --git a/kv/manual/transactions.md b/kv/manual/transactions.md new file mode 100644 index 000000000..2c264fca9 --- /dev/null +++ b/kv/manual/transactions.md @@ -0,0 +1,100 @@ +# Transactions + +> ⚠️ Deno KV is currently **experimental** and **subject to change**. While we do +> our best to ensure data durability, data loss is possible, especially around +> Deno updates. We recommend that you backup your data regularly and consider +> storing data in a secondary store for the time being. + +> A database transaction, in the context of a key-value store like Deno KV, +> refers to a sequence of data manipulation operations executed as a single, +> atomic unit of work to ensure data consistency, integrity, and durability. +> These operations, typically comprising read, write, update, and delete actions +> on key-value pairs, adhere to the ACID (Atomicity, Consistency, Isolation, and +> Durability) properties, which guarantee that either all operations within the +> transaction are successfully completed, or the transaction is rolled back to +> its initial state in the event of a failure, leaving the database unchanged. +> This approach allows multiple users or applications to interact with the KV +> store concurrently, while maintaining the database's consistency, reliability +> and stability. + +The Deno KV store utilizes _optimistic concurrency control transactions_ rather +than _interactive transactions_ like many SQL systems like PostgreSQL or MySQL. +This approach employs versionstamps, which represent the current version of a +value for a given key, to manage concurrent access to shared resources without +using locks. When a read operation occurs, the system returns a versionstamp for +the associated key in addition to the value. + +To execute a transaction, one performs an atomic operations that can consist of +multiple mutation actions (like set or delete). Along with these actions, +key+versionstamp pairs are provided as a condition for the transaction's +success. The optimistic concurrency control transaction will only commit if the +specified versionstamps match the current version for the values in the database +for the corresponding keys. This transaction model ensures data consistency and +integrity while allowing concurrent interactions within the Deno KV store. + +Because OCC transactions are optimistic, they can fail on commit because the +version constraints specified in the atomic operation were violated. This occurs +when an agent updates a key used within the transaction between read and commit. +When this happens, the agent performing the transaction must retry the +transaction. + +To illustrate how to use OCC transactions with Deno KV, this example shows how +to implement a `transferFunds(from: string, to: string, amount: number)` +function for an account ledger. The account ledger stores the balance for each +account in the key-value store. The keys are prefixed by `"account"`, followed +by the account identifier: `["account", "alice"]`. The value stored for each key +is a number that represents the account balance. + +Here's a step-by-step example of implementing this `transferFunds` function: + + +```ts,ignore +async function transferFunds(sender: string, receiver: string, amount: number) { + if (amount <= 0) throw new Error("Amount must be positive"); + + // Construct the KV keys for the sender and receiver accounts. + const senderKey = ["account", sender]; + const receiverKey = ["account", receiver]; + + // Retry the transaction until it succeeds. + let res = { ok: false }; + while (!res.ok) { + // Read the current balance of both accounts. + const [senderRes, receiverRes] = await kv.getMany([senderKey, receiverKey]); + if (senderRes.value === null) throw new Error(`Account ${sender} not found`); + if (receiverRes.value === null) throw new Error(`Account ${receiver} not found`); + + const senderBalance = senderRes.value; + const receiverBalance = receiverRes.value; + + // Ensure the sender has a sufficient balance to complete the transfer. + if (senderBalance < amount) { + throw new Error( + `Insufficient funds to transfer ${amount} from ${sender}`, + ); + } + + // Perform the transfer. + const newSenderBalance = senderBalance - amount; + const newReceiverBalance = receiverBalance + amount; + + // Attempt to commit the transaction. `res` returns an object with + // `ok: false` if the transaction fails to commit due to a check failure + // (i.e. the versionstamp for a key has changed) + res = await kv.atomic() + .check(senderRes) // Ensure the sender's balance hasn't changed. + .check(receiverRes) // Ensure the receiver's balance hasn't changed. + .set(senderKey, newSenderBalance) // Update the sender's balance. + .set(receiverKey, newReceiverBalance) // Update the receiver's balance. + .commit(); + } +} +``` + +In this example, the `transferFunds` function reads the balances and +versionstamps of both accounts, calculates the new balances after the transfer, +and checks if there are sufficient funds in account A. It then performs an +atomic operation, setting the new balances with the versionstamp constraints. If +the transaction is successful, the loop exits. If the version constraints are +violated, the transaction fails, and the loop retries the transaction until it +succeeds. diff --git a/kv/tutorials/index.md b/kv/tutorials/index.md new file mode 100644 index 000000000..f70a71d2d --- /dev/null +++ b/kv/tutorials/index.md @@ -0,0 +1,48 @@ +# Deno KV Tutorials & Examples + +Check out these examples showing real-world usage of Deno KV. + +**Multi-player Tic-Tac-Toe** + +- GitHub authentication +- Saved user state +- Real-time sync using BroadcastChannel +- [Source code](https://github.com/denoland/tic-tac-toe) +- [Live preview](https://tic-tac-toe-game.deno.dev/) + +**Pixelpage** + +- Persistent canvas state +- Multi-user collaboration +- Real-time sync using BroadcastChannel +- [Source code](https://github.com/denoland/pixelpage) +- [Live preview](https://pixelpage.deno.dev/) + +**Todo list** + +- Zod schema validation +- Built using Fresh +- Real-time collaboration using BroadcastChannel +- [Source code](https://github.com/denoland/showcase_todo) +- [Live preview](https://showcase-todo.deno.dev/) + +**Sketch book** + +- Stores drawings in KV +- GitHub authentication +- [Source code](https://github.com/hashrock/kv-sketchbook) +- [Live preview](https://hashrock-kv-sketchbook.deno.dev/) + +**Deno KV OAuth** + +- High-level OAuth 2.0 powered by Deno KV +- [Source code](https://github.com/denoland/deno_kv_oauth) +- [Live preview](https://kv-oauth.deno.dev/) + +**Deno SaaSKit** + +- Modern SaaS template built on Fresh. +- [Hacker News](https://news.ycombinator.com/)-like demo entirely built on KV. +- Uses Deno KV OAuth for GitHub OAuth 2.0 authentication +- [Source code](https://github.com/denoland/saaskit) +- [Live preview](https://hunt.deno.land/) diff --git a/sidebars/kv.js b/sidebars/kv.js new file mode 100644 index 000000000..838891f80 --- /dev/null +++ b/sidebars/kv.js @@ -0,0 +1,93 @@ +// Include main doc categories on most pages +const mainMenu = [ + // https://docusaurus.io/docs/sidebar/items + { + type: "link", + href: "/kv/manual", + label: "Manual", + className: "icon-menu-option icon-menu-user-guide", + }, + { + type: "link", + label: "Tutorials & Examples", + href: "/kv/tutorials", + className: "icon-menu-option icon-menu-tutorials", + }, + { + type: "link", + label: "API Reference", + href: "https://deno.land/api?unstable=true&s=Deno.Kv", + className: "icon-menu-option icon-menu-api __no-external", + }, +]; + +const sidebars = { + kv: mainMenu, + + kvGuideHome: mainMenu.concat([ + { + type: "html", + value: "
Deno KV Manual
", + className: "section-header", + }, + { + type: "doc", + label: "Quick Start", + id: "manual/index", + }, + "manual/key_space", + "manual/operations", + "manual/secondary_indexes", + "manual/transactions", + "manual/on_deploy", + ]), + + kvTutorialsHome: mainMenu.concat([ + { + type: "html", + value: "
Tutorials & Examples
", + className: "section-header", + }, + { + type: "doc", + label: "Overview", + id: "tutorials/index", + }, + { + type: "link", + label: "TODO List", + href: "https://github.com/denoland/showcase_todo", + }, + { + type: "link", + label: "Multiplayer Tic-Tac-Toe", + href: "https://github.com/denoland/tic-tac-toe", + }, + { + type: "link", + label: "Real-time Pixel Canvas", + href: "https://github.com/denoland/pixelpage", + }, + { + type: "link", + label: "KV-powered oAuth2", + href: "https://github.com/denoland/deno_kv_oauth", + }, + { + type: "link", + label: "SaaSKit", + href: "https://github.com/denoland/saaskit", + }, + { + type: "link", + label: "More on Deno by Example", + href: "https://examples.deno.land", + }, + { + type: "html", + value: '
', + }, + ]), +}; + +module.exports = sidebars; diff --git a/sidebars/runtime.js b/sidebars/runtime.js index b402059d1..14a03428f 100644 --- a/sidebars/runtime.js +++ b/sidebars/runtime.js @@ -85,7 +85,7 @@ const sidebars = { runtimeBasicsHome: mainMenu.concat([ { type: "html", - value: `
Manual > Deno Basics
`, + value: `
Home > Deno Basics
`, className: "section-header", }, "manual/getting_started/installation", @@ -141,7 +141,7 @@ const sidebars = { runtimeRuntimeHome: mainMenu.concat([ { type: "html", - value: `
Manual > Runtime APIs
`, + value: `
Home > Runtime APIs
`, className: "section-header", }, "manual/runtime/builtin_apis", @@ -151,16 +151,6 @@ const sidebars = { "manual/runtime/ffi_api", "manual/runtime/program_lifecycle", "manual/runtime/stability", - { - type: "html", - value: "
Deno KV
", - className: "section-header", - }, - "manual/runtime/kv/index", - "manual/runtime/kv/key_space", - "manual/runtime/kv/operations", - "manual/runtime/kv/secondary_indexes", - "manual/runtime/kv/transactions", { type: "html", value: `
Web Platform APIs
`, @@ -192,7 +182,7 @@ const sidebars = { { type: "html", value: - `
Manual > Work with Node.js & npm
`, + `
Home > Work with Node.js & npm
`, className: "section-header", }, "manual/node/index", @@ -228,8 +218,7 @@ const sidebars = { runtimeToolsHome: mainMenu.concat([ { type: "html", - value: - `
Manual > Developer Tools
`, + value: `
Home > Developer Tools
`, className: "section-header", }, "manual/tools/index", @@ -300,8 +289,7 @@ const sidebars = { runtimeAdvancedHome: mainMenu.concat([ { type: "html", - value: - `
Manual > Advanced Topics
`, + value: `
Home > Advanced Topics
`, className: "section-header", }, "manual/advanced/continuous_integration", @@ -361,7 +349,7 @@ const sidebars = { runtimeReferencesHome: mainMenu.concat([ { type: "html", - value: `
Manual > References
`, + value: `
Home > References
`, className: "section-header", }, "manual/references/index", diff --git a/src/css/custom.css b/src/css/custom.css index c279294b4..af9e4bc79 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -9,6 +9,7 @@ @import url("https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,700;1,400;1,700&display=swap"); /* You can override the default Infima variables here. */ +/* :root { --ifm-color-primary: #2e8555; --ifm-color-primary-dark: #29784c; @@ -29,7 +30,7 @@ "Liberation Mono", "Courier New", monospace, Menlo, Monaco, "Lucida Console", Consolas, "Liberation Mono", "Courier New", monospace; } - +*/ /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme="dark"] { --ifm-color-primary: #25c2a0; @@ -44,10 +45,12 @@ html, body { + position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; - overflow-x: hidden; - overflow-y: auto; + overscroll-behavior: none; font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } @@ -71,8 +74,28 @@ h3 { line-height: 1.2; } +.navbar { + position: fixed; + width: 100%; +} + +#__docusaurus, .main-wrapper { + overscroll-behavior-y: none; +} + .container { max-width: 1140px !important; + min-height: 75%; +} + +.container article { + padding: 60px 0; +} + +@media only screen and (max-width: 996px) { + .container { + min-height: auto; + } } /* Hide breadcrumbs */ @@ -80,6 +103,18 @@ h3 { display: none; } +.theme-doc-sidebar-container { + position: relative; +} + +.theme-doc-sidebar-container div:first-child { + position: fixed; +} + +.theme-doc-sidebar-container div:first-child div:first-child { + position: relative; +} + .section-header { padding: 5px 0; margin: 10px var(--ifm-menu-link-padding-horizontal); @@ -96,8 +131,19 @@ h3 { font-size: 0.8rem; } +.navbar__item { + padding: 0 2px; +} + +.navbar__link { + margin-left: 10px; + margin-right: 10px; +} + .navbar__link--active { font-weight: bold; + margin-top: 3px; + border-bottom: 3px solid var(--ifm-color-primary); } .navbar__logo { @@ -105,6 +151,11 @@ h3 { margin-right: 0.7rem; } +.navbar__logo img { + height: 24px; + width: 24px; +} + .navbar__title { font-size: 1.2rem; } diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 6be05f9d5..4751cab1f 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -9,7 +9,7 @@ export default function Home() { title={`Deno: the easiest, most secure JavaScript runtime`} description="Reference documentation for the Deno runtime and Deno Deploy" > -
+

Deno Documentation

diff --git a/static/server.ts b/static/server.ts index dddbc6eb7..a05616673 100644 --- a/static/server.ts +++ b/static/server.ts @@ -4,11 +4,36 @@ import { serveStatic } from "https://deno.land/x/hono@v3.5.5/middleware.ts"; const app = new Hono(); // Configure redirects +app.get("/", (c) => c.redirect("/runtime/manual")); app.get("/manual", (c) => c.redirect("/runtime/manual")); app.get("/runtime", (c) => c.redirect("/runtime/manual")); app.get("/deploy", (c) => c.redirect("/deploy/manual")); app.get("/deploy/docs", (c) => c.redirect("/deploy/manual")); +// KV redirects +app.get("/kv", (c) => c.redirect("/kv/manual")); +app.get("/runtime/manual/runtime/kv", (c) => c.redirect("/kv/manual")); +app.get( + "/runtime/manual/runtime/kv/key_space", + (c) => c.redirect("/kv/manual/key_space"), +); +app.get( + "/runtime/manual/runtime/kv/operations", + (c) => c.redirect("/kv/manual/operations"), +); +app.get( + "/runtime/manual/runtime/kv/secondary_indexes", + (c) => c.redirect("/kv/manual/secondary_indexes"), +); +app.get( + "/runtime/manual/runtime/kv/transactions", + (c) => c.redirect("/kv/manual/transactions"), +); +app.get( + "/deploy/manual/kv", + (c) => c.redirect("/kv/manual/on_deploy"), +); + // Redirect all manual paths - most should work app.all("/manual.*", (c) => { const unversionedPath = c.req.path.split("/").slice(2); @@ -62,7 +87,10 @@ app.all("/deploy/docs.*", (c) => { app.use("*", serveStatic({ root: "./" })); // 404s -app.use("*", serveStatic({ root: "./", path: "./404.html" })); +app.notFound((c) => { + console.error("404 error returned for path: ", c.req.path); + return c.redirect("/404.html", 404); +}); // Serve on port 8000 Deno.serve(app.fetch);