diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 4177dac..72e2413 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -17,4 +17,8 @@ jobs: with: python-version: '3.14' - run: python src/website/build.py - - run: node --check src/website/site.js + - run: python src/website/validate.py + - run: | + node --check src/website/site.js + node --check src/website/theme.js + node --check src/website/docs.js diff --git a/src/website/.openai/hosting.json b/src/website/.openai/hosting.json deleted file mode 100644 index c0e67da..0000000 --- a/src/website/.openai/hosting.json +++ /dev/null @@ -1 +0,0 @@ -{"project_id":"appgprj_6aa7a5ea0a5c8191bae88aa00dac4409","static":{"directory":"dist"}} diff --git a/src/website/README.md b/src/website/README.md index 682a9d7..cbfa9c2 100644 --- a/src/website/README.md +++ b/src/website/README.md @@ -1,12 +1,22 @@ # Interprocess website -A static landing page with language examples and links to the library and protocol. No runtime framework or external dependencies. +A static landing page and developer documentation for all six language APIs. No runtime framework or external dependencies. ```sh python3 src/website/build.py python3 -m http.server --directory src/website/dist ``` -The Website workflow validates the static build on pull requests and main. The public site at cloudtoid.com uses Sites hosting; `.openai/hosting.json` identifies it. Keep registry availability and the preview/release notice accurate when publishing packages. +Cloudflare Pages publishes this site directly from `cloudtoid/interprocess`. Changes under `src/website/**` on `main` trigger production deployments; other branches receive previews linked from GitHub. Project: `cloudtoid`; build command: `python3 src/website/build.py`; output directory: `src/website/dist`. The Website GitHub Actions workflow also validates the build and JavaScript syntax. Failed builds do not replace the last successful deployment. + +Keep package availability and installation commands current when releasing packages. Benchmarks change only after new measurements. The header and footer use the official blue wordmarks from [cloudtoid/assets](https://github.com/cloudtoid/assets/tree/master/logos), served locally. The black and white variants follow the selected color theme. + +## Developer documentation + +`docs/pages.json` defines page titles, descriptions, navigation, and URLs. Edit the corresponding HTML fragments under `docs/`; `docs/template.html` supplies the shared layout. `build.py` renders the pages into `dist/docs/`, creates the sitemap and robots.txt, and adds canonical, social, and structured metadata. API content is authored against the public implementations; it is not generated from source comments. Update the reference alongside API changes, including waiting, error, ownership, and truncation behavior. Link to generated ecosystem references where available. + +Run `python3 src/website/validate.py` after building to check local links, anchors, metadata, and sitemap coverage. CI runs this check. All reference text and navigation work without JavaScript; `docs.js` enhances code blocks with highlighting and copy buttons. The homepage's language guide links point to these pages. + +The social preview uses `assets/social-card.png`; its editable SVG source is alongside it. SEO metadata uses `https://cloudtoid.com` as the canonical origin. Publishing makes the sitemap available at `/sitemap.xml`; search-engine indexing happens independently of deployment. diff --git a/src/website/assets/social-card.png b/src/website/assets/social-card.png new file mode 100644 index 0000000..a0ce71d Binary files /dev/null and b/src/website/assets/social-card.png differ diff --git a/src/website/assets/social-card.svg b/src/website/assets/social-card.svg new file mode 100644 index 0000000..1aa23ae --- /dev/null +++ b/src/website/assets/social-card.svg @@ -0,0 +1,11 @@ + + + +CLOUDTOID +Interprocess +Fast, lightweight queues +across processes and languages. + +Rust · Node.js · Go · C · Python · .NET +cloudtoid.com + diff --git a/src/website/build.py b/src/website/build.py index 24c4b92..534afae 100644 --- a/src/website/build.py +++ b/src/website/build.py @@ -1,11 +1,94 @@ -"""Build the static site for Sites, without runtime dependencies.""" +"""Build the static marketing site and developer reference, without dependencies.""" +from html import escape +import json from pathlib import Path +import re import shutil +from string import Template + root = Path(__file__).resolve().parent output = root / 'dist' output.mkdir(exist_ok=True) -for name in ('index.html', 'style.css', 'site.js', 'theme.js'): +base = 'https://cloudtoid.com' +pages = json.loads((root / 'docs/pages.json').read_text()) +home = (root / 'index.html').read_text() +favicon = re.search(r']+>', home).group() + + +def metadata(title, description, path, kind='website'): + url = base + path + tags = [f''] + for key, value in { + 'og:type': kind, 'og:site_name': 'Cloudtoid Interprocess', + 'og:title': title, 'og:description': description, 'og:url': url, + 'og:image': base + '/assets/social-card.png', + 'og:image:width': '1200', 'og:image:height': '630', + 'og:image:alt': 'Cloudtoid Interprocess: fast shared-memory queues across six languages', + }.items(): + tags.append(f'') + tags.append('') + return '\n'.join(tags) + + +home_title = re.search(r'(.*?)', home).group(1) +home_description = re.search(r']+>', metadata(home_title, home_description, '/'), home) +software = { + '@context': 'https://schema.org', '@type': 'SoftwareSourceCode', + 'name': 'Cloudtoid Interprocess', 'url': base + '/', + 'description': home_description, + 'codeRepository': 'https://github.com/cloudtoid/interprocess', + 'programmingLanguage': ['Rust', 'C', 'Python', 'JavaScript', 'Go', 'C#'], + 'runtimePlatform': ['Linux', 'macOS', 'Windows'], + 'license': 'https://github.com/cloudtoid/interprocess/blob/main/LICENSE', +} +home = home.replace('', '\n') +(output / 'index.html').write_text(home) +for name in ('style.css', 'site.js', 'theme.js', 'docs.css', 'docs.js'): shutil.copyfile(root / name, output / name) for name in ('assets', 'benchmarks', 'vendor'): shutil.copytree(root / name, output / name, dirs_exist_ok=True) -print(f'Built {output}') + + +def page_path(page): + return '/docs/' + (page['slug'] + '/' if page['slug'] else '') + + +template = Template((root / 'docs/template.html').read_text()) +for page in pages: + path = page_path(page) + content = (root / 'docs' / ((page['slug'] or 'overview') + '.html')).read_text() + navigation = ''.join( + '{}'.format(page_path(item), ' aria-current="page"' if item == page else '', escape(item['label'])) + for item in pages + ) + headings = re.findall(r'

(.*?)

', content) + content = re.sub(r'(.*?)', + lambda match: f'{match[3]}', content) + toc = ''.join(f'{title}' for key, title in headings) + breadcrumbs = [{'@type': 'ListItem', 'position': 1, 'name': 'Documentation', 'item': base + '/docs/'}] + if page['slug']: + breadcrumbs.append({'@type': 'ListItem', 'position': 2, 'name': page['title'], 'item': base + path}) + data = [ + {'@context': 'https://schema.org', '@type': 'TechArticle', 'headline': page['title'], + 'description': page['description'], 'url': base + path, 'inLanguage': 'en', + 'author': {'@type': 'Organization', 'name': 'Cloudtoid', 'url': base + '/'}}, + {'@context': 'https://schema.org', '@type': 'BreadcrumbList', 'itemListElement': breadcrumbs}, + ] + rendered = template.substitute( + title=escape(page['title']), description=escape(page['description'], quote=True), + metadata=metadata(page['title'] + ' | Cloudtoid Interprocess', page['description'], path, 'article') + + '\n', + favicon=favicon, navigation=navigation, toc=toc, content=content, + breadcrumb=('/' + escape(page['label']) + '') if page['slug'] else '', + ) + target = output / path.strip('/') / 'index.html' + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(rendered) + +urls = [base + '/'] + [base + page_path(page) for page in pages] +(output / 'sitemap.xml').write_text('\n' + + '\n' + + '\n'.join(f'{url}' for url in urls) + '\n\n') +(output / 'robots.txt').write_text('User-agent: *\nAllow: /\n\nSitemap: ' + base + '/sitemap.xml\n') +print(f'Built {output}: homepage and {len(pages)} documentation pages') diff --git a/src/website/docs.css b/src/website/docs.css new file mode 100644 index 0000000..39e2602 --- /dev/null +++ b/src/website/docs.css @@ -0,0 +1,63 @@ +.docs-layout { display: grid; grid-template-columns: 170px minmax(0, 1fr) 145px; gap: 38px; padding-top: 40px; padding-bottom: 70px; } +.docs-sidebar, .docs-toc { align-self: start; position: sticky; top: 24px; max-height: calc(100vh - 48px); overflow-y: auto; } +.docs-sidebar .eyebrow, .docs-toc .eyebrow { font-size: 10px; margin-bottom: 16px; } +.docs-sidebar nav, .docs-toc nav { display: flex; flex-direction: column; align-items: stretch; gap: 3px; } +.docs-sidebar nav a { display: block; padding: 7px 10px; border-radius: 5px; color: var(--muted); font-size: 14px; } +.docs-sidebar a[aria-current=page] { background: var(--hero-background); color: var(--accent); font-weight: 650; } +.docs-sidebar .protocol-link { display: block; margin-top: 25px; padding-top: 18px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; } +.docs-toc a { font-size: 12px; color: var(--muted); padding: 4px 0; line-height: 1.5; } +.doc-content { min-width: 0; } +.doc-breadcrumb { display: flex; flex-wrap: wrap; gap: 9px; font-size: 12px; color: var(--muted); margin-bottom: 24px; } +.doc-content h1 { font-size: clamp(32px, 3.5vw, 45px); letter-spacing: -.045em; line-height: 1.15; margin-bottom: 20px; } +.doc-content h2 { font-size: 25px; letter-spacing: -.7px; margin: 48px 0 20px; padding-top: 8px; border-top: 1px solid var(--line); } +.doc-content h3 { font-size: 16px; font-weight: 600; margin: 26px 0 10px; line-height: 1.7; scroll-margin-top: 25px; } +.doc-content h3 code { background: var(--soft); color: var(--ink); padding: 3px 5px; box-decoration-break: clone; -webkit-box-decoration-break: clone; } +.doc-content p, .doc-content li, .doc-content dd { font-size: 15px; line-height: 1.8; color: var(--muted); } +.doc-content .doc-lead { font-size: 19px; line-height: 1.65; margin-bottom: 22px; } +.doc-content .doc-source { font-size: 12px; } +.doc-content a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; } +.doc-content code { font: .86em/1.7 var(--mono); overflow-wrap: anywhere; } +.doc-content p code, .doc-content li code, .api-list dt code { background: var(--soft); color: var(--ink); border-radius: 3px; padding: 2px 4px; } +.doc-content pre { min-height: 0; font-size: 13px; padding: 22px; margin: 20px 0; border: 1px solid var(--line); border-radius: 7px; background: var(--surface); line-height: 1.75; } +.doc-content pre code { font-size: inherit; overflow-wrap: normal; } +.doc-code { position: relative; } +.doc-code pre { padding-top: 47px; } +.doc-copy { position: absolute; top: 10px; right: 10px; border: 1px solid var(--line); border-radius: 4px; padding: 3px 9px; font-size: 11px; color: var(--muted); background: var(--soft); cursor: pointer; } +.doc-copy:hover { color: var(--accent); border-color: var(--accent); } +.doc-note { padding: 18px 20px; margin: 24px 0; border-left: 3px solid var(--accent); background: var(--hero-background); border-radius: 0 6px 6px 0; font-size: 15px; color: var(--ink); } +.doc-note strong { display: block; } +.doc-table { overflow-x: auto; border: 1px solid var(--line); border-radius: 6px; margin: 24px 0; } +.doc-table td, .doc-table th { padding: 12px 15px; } +.doc-table td { font-size: 13px; font-family: inherit; font-weight: 400; line-height: 1.6; white-space: normal; } +.api-list dt { margin-top: 15px; font-weight: 600; } +.api-list dd { margin: 5px 0 16px; } +.doc-cards { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 14px; } +.doc-cards a { display: flex; flex-direction: column; gap: 10px; padding: 20px; background: var(--surface); border: 1px solid var(--line); border-radius: 7px; text-decoration: none; } +.doc-cards a:hover { border-color: var(--accent); background: var(--soft); } +.doc-cards strong { font-size: 17px; color: var(--ink); } +.doc-cards span { font-size: 13px; color: var(--muted); } +.doc-cards code { font-size: 11px; margin-top: auto; } +.doc-bottom { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 18px; margin-top: 55px; padding-top: 20px; border-top: 1px solid var(--line); font-size: 12px; } +.docs-page header nav a { display: inline; } +@media (max-width:1100px) { + .docs-layout { grid-template-columns: 155px minmax(0,1fr); gap: 28px; } + .docs-toc { display: none; } +} +@media (max-width:800px) { + .docs-layout { grid-template-columns: 1fr; padding-top: 25px; } + .docs-sidebar { position: static; max-height: none; padding-bottom: 20px; border-bottom: 1px solid var(--line); } + .docs-sidebar nav { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 4px; } + .docs-sidebar nav a { display: block; padding: 6px; font-size: 12px; } + .docs-sidebar .protocol-link { display: none; } + .docs-sidebar .eyebrow { margin-bottom: 10px; } +} +@media (max-width:520px) { + .docs-page header nav a:nth-child(2), .docs-page header nav a:nth-child(3) { display: none; } + .doc-cards { grid-template-columns: 1fr; } + .doc-content h1 { font-size: 34px; } + .doc-content pre { font-size: 12px; padding-left: 14px; padding-right: 14px; } + .doc-content .doc-lead { font-size: 17px; } +} + +.doc-content .heading-link { color: inherit; text-decoration: none; } +.doc-content .heading-link:hover { text-decoration: underline; } diff --git a/src/website/docs.js b/src/website/docs.js new file mode 100644 index 0000000..0370b14 --- /dev/null +++ b/src/website/docs.js @@ -0,0 +1,28 @@ +document.querySelectorAll('.doc-content pre').forEach((pre, index) => { + const code = pre.querySelector('code'); + if (!code) return; + if (window.hljs) hljs.highlightElement(code); + const wrapper = document.createElement('div'); + wrapper.className = 'doc-code'; + pre.before(wrapper); + wrapper.append(pre); + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'doc-copy'; + button.textContent = 'Copy'; + button.setAttribute('aria-label', `Copy code example ${index + 1}`); + const status = document.createElement('span'); + status.className = 'sr-only'; + status.setAttribute('role', 'status'); + wrapper.append(button, status); + button.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(code.textContent); + button.textContent = 'Copied'; + status.textContent = 'Code copied to clipboard'; + } catch { + button.textContent = 'Select to copy'; + status.textContent = 'Clipboard unavailable. Select the code to copy it.'; + } + }); +}); diff --git a/src/website/docs/c.html b/src/website/docs/c.html new file mode 100644 index 0000000..dfa54e8 --- /dev/null +++ b/src/website/docs/c.html @@ -0,0 +1,69 @@ +

A compact C ABI for shared-memory messaging, with opaque handles, explicit status codes, and caller-controlled ownership.

+

Public C header ↗ · Prebuilt SDK downloads ↗

+

Install the SDK

+

Download the archive for your platform. This example uses the GitHub CLI on macOS Apple Silicon:

+
gh release download native-v3.0.1 --repo cloudtoid/interprocess --pattern "*-darwin-arm64.tar.gz"
+mkdir -p cloudtoid-sdk
+tar -xzf cloudtoid-interprocess-3.0.1-darwin-arm64.tar.gz -C cloudtoid-sdk --strip-components=1
+export PKG_CONFIG_PATH="$PWD/cloudtoid-sdk/lib/pkgconfig:$PKG_CONFIG_PATH"
+

Other archive suffixes are darwin-x64, linux-arm64, linux-x64, and win32-x64. Replace the suffix in both archive names. Linux prebuilt libraries require glibc 2.34 or later.

+

On Windows, extract the archive, set PKG_CONFIG_PATH to its lib/pkgconfig directory, and add its lib directory to PATH for the DLL. Link against the import library. The header can also be included from C++.

+

On Unix, compile a C program with pkg-config; the SDK flags include the runtime library search path:

+
cc example.c -o example $(pkg-config --cflags --libs cloudtoid-interprocess)
+./example
+

Send and receive

+
#include <interprocess.h>
+#include <stdio.h>
+
+int main(void) {
+    cip_subscriber *subscriber = NULL;
+    cip_publisher *publisher = NULL;
+    int result = 1;
+    if (cip_subscriber_open("example", NULL, 65536, &subscriber) != CIP_OK)
+        goto cleanup;
+    if (cip_publisher_open("example", NULL, 65536, &publisher) != CIP_OK)
+        goto cleanup;
+    if (cip_try_send(publisher, (const uint8_t *)"hello", 5) != CIP_OK)
+        goto cleanup;
+    cip_buffer message;
+    if (cip_receive(subscriber, 1000, &message) == CIP_OK) {
+        fwrite(message.data, 1, message.length, stdout);
+        cip_buffer_free(message);
+        result = 0;
+    }
+cleanup:
+    cip_publisher_close(publisher);
+    cip_subscriber_close(subscriber);
+    return result;
+}
+

Types and status codes

+

cip_publisher and cip_subscriber are opaque handles. The returned message type is:

+
typedef struct cip_buffer {
+    uint8_t *data;
+    size_t length;
+} cip_buffer;
+

Functions returning int32_t status use CIP_OK = 1 for success, CIP_UNAVAILABLE = 0 for full/empty/timeout or temporary recovery, and CIP_ERROR = -1 for failure. An empty message is still CIP_OK with length zero.

+

Open and close

+

int32_t cip_publisher_open(const char *name, const char *path, size_t capacity, cip_publisher **output)

+

int32_t cip_subscriber_open(const char *name, const char *path, size_t capacity, cip_subscriber **output)

+

Create or join a queue and write the handle to output on success. Strings are UTF-8 and NUL-terminated. Path may be NULL for the temporary directory; Windows ignores it. Capacity must match all participants.

+

void cip_publisher_close(cip_publisher *handle)

+

void cip_subscriber_close(cip_subscriber *handle)

+

Release a handle exactly once, after every concurrent call has returned. Close(NULL) is safe. A non-null handle cannot be reused or closed again after release.

+

Send and receive

+

int32_t cip_try_send(const cip_publisher *handle, const uint8_t *data, size_t length)

+

Copies one message. CIP_UNAVAILABLE means no room or recovery admission unavailable. Input memory must remain valid for the call; a NULL data pointer is allowed only with length zero.

+

int32_t cip_receive(const cip_subscriber *handle, int64_t timeout_ms, cip_buffer *output)

+

Copies and consumes a message into an owned buffer. Timeout is milliseconds: -1 waits indefinitely, 0 attempts once, and a positive value bounds the wait. Other negative values are invalid. CIP_UNAVAILABLE means no ready message before the timeout.

+

void cip_buffer_free(cip_buffer buffer)

+

Free each successful receive result exactly once, including empty messages. Do not free a result from an unsuccessful receive or invent a buffer to pass here. Copying the struct does not create a second ownership right.

+

int32_t cip_try_receive_into(const cip_subscriber *handle, uint8_t *data, size_t capacity, size_t *copied)

+

Tries once and copies into caller-owned storage. On success, copied is the number of bytes written. An undersized buffer truncates and consumes the message. NULL data requires capacity zero. Use the status to distinguish an empty message from no message.

+

Error details

+

const char *cip_last_error(void)

+

Diagnostic text in thread-local storage, valid until the next error on that thread. Read or copy it immediately after CIP_ERROR, on the same OS thread.

+

int32_t cip_last_error_kind(void)

+

Returns a cip_error_kind: CIP_NO_ERROR = 0, CIP_INVALID_ARGUMENT = 1, CIP_CAPACITY_MISMATCH = 2, CIP_PUBLISHER_LIMIT = 3, CIP_EXHAUSTED = 4, CIP_CORRUPT = 5, CIP_IO_ERROR = 6, or CIP_INTERNAL_ERROR = 7. Error state is diagnostic state, not a substitute for checking each function's return status.

+

Concurrency and shutdown

+

Handles support concurrent operations, but closing concurrently with an operation is unsafe. Use bounded receive timeouts, signal your worker to stop, join it, then close the handle. There is no cancellation object or batch-send function in the C ABI. Open after fork; inherited handles must not be used in the child.

+

Prebuilt SDKs contain shared libraries. For static linking, build cloudtoid-interprocess-ffi from source and define CIP_STATIC on Windows. See the source-build instructions.

diff --git a/src/website/docs/concepts.html b/src/website/docs/concepts.html new file mode 100644 index 0000000..067e7b4 --- /dev/null +++ b/src/website/docs/concepts.html @@ -0,0 +1,26 @@ +

One queue, multiple publishers, competing subscribers. These rules apply across every language binding.

+

Queue identity

+

All participants must use the same name, capacity, and Unix backing directory. Windows ignores the path; its mapping and lease objects are session-local, so participants must share a Windows session and compatible permissions.

+

Use an explicit absolute path for cross-language Unix applications: runtime temporary directories can differ. Names must also be unique across directories because the notification semaphore uses the name, not the backing path.

+ +

Transient lifetime

+

A queue remains usable while at least one publisher or subscriber is attached. Closing a reader does not destroy a queue retained by other participants. After all endpoints close or their processes exit, unread messages are lost. Opening the same name again creates a fresh, empty queue.

+ +

On Unix, stale files may remain after a crash until the next opener cleans them up. Their presence does not mean that messages can be resumed.

+

Delivery and ordering

+

Subscribers compete for messages: under ordinary operation, each message is consumed by one subscriber. This is a work-sharing queue, not broadcast. To deliver a copy to every consumer, use separate queues.

+

Delivery follows reservation order. An unfinished earlier reservation can delay later messages. Successful sequential sends preserve order; concurrent calls have no defined relative order. Batch sends commit a prefix and may interleave with other publishers; a batch is not a transaction.

+

The queue transports bytes, without a serializer. Agree on encoding, schema, and byte order in your application. Empty messages are valid. Distinguish an empty payload from the API's “no message” result.

+

Capacity and backpressure

+

Capacity is the circular message buffer size in bytes. It must exceed 16 and be divisible by 8. Total mapped storage is 262400 + capacity bytes, including the header and publisher table. Each queue supports up to 2,048 connected publisher objects.

+

Records include metadata and alignment, so the entire buffer is not available for payload bytes. A nonblocking send can report unavailable because space is exhausted or recovery temporarily blocks admission. Choose a retry deadline, bounded backoff, or an application-level overflow policy. Retrying cannot make an inherently oversized message fit.

+

Receive-into APIs copy at most the supplied buffer length. An undersized buffer truncates and consumes the message; the remainder cannot be read later. Size reusable buffers for the largest payload you accept.

+

Waiting and cancellation

+

“Try” methods do not wait for a message or free space. An unavailable receive may also mean a reader owns consumption or the next record is not ready. Open, close, recovery checks, and wrapper synchronization can still involve operating-system work.

+

Rust and C offer blocking receives with timeouts. Python waits with the GIL released and checks Python signals between bounded waits. .NET accepts a cancellation token on blocking Dequeue. Node.js and Go check immediately, then use adaptive timer backoff from 1 to 10 ms while idle. Their idle-to-active latency can include that interval plus scheduling delay. Prefer one receive loop per subscriber.

+

Crashes and recovery

+

Recovery reclaims abandoned work only after checking participant liveness. Paused live owners are not expired just because time passes. Messages can be lost during recovery, including completed messages behind an abandoned reservation. The queue does not provide durable acknowledgements or exactly-once effects.

+

Publisher reservations use native 64-bit atomics. The complete queue is not formally lock-free: readers serialize consumption, live paused owners can block progress, and creation and destruction use OS locks. Open native endpoints after fork(); do not use inherited endpoints in the child.

+

Upgrades and long-lived queues

+

v3 is incompatible with the v1/v2 shared-memory layout. Drain the old queue, stop all participants, and upgrade them together using a fresh queue. The physical buffer wraps; logical counters do not. Counter exhaustion is an explicit error and requires a fresh queue after participants finish.

+

For layout offsets, atomic ordering, and recovery details, read the full protocol v3 specification.

diff --git a/src/website/docs/dotnet.html b/src/website/docs/dotnet.html new file mode 100644 index 0000000..b96183f --- /dev/null +++ b/src/website/docs/dotnet.html @@ -0,0 +1,50 @@ +

Send ReadOnlySpan<byte> messages and receive into reusable memory. Use CancellationToken when waiting for work.

+

NuGet package ↗ · Public contracts ↗

+

Install

+

Requires .NET 10 or later and a 64-bit process on a supported platform.

+
dotnet add package Cloudtoid.Interprocess
+

Send and receive

+
using Cloudtoid.Interprocess;
+
+var options = new QueueOptions("example", capacity: 65536);
+var factory = new QueueFactory();
+using var subscriber = factory.CreateSubscriber(options);
+using var publisher = factory.CreatePublisher(options);
+
+if (!publisher.TryEnqueue("hello"u8))
+    throw new InvalidOperationException("Queue is full or recovering");
+
+byte[] buffer = new byte[256];
+using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(1));
+ReadOnlyMemory<byte> message = subscriber.Dequeue(buffer, cancellation.Token);
+Console.WriteLine(System.Text.Encoding.UTF8.GetString(message.Span));
+

QueueOptions

+

QueueOptions(string queueName, long capacity)

+

QueueOptions(string queueName, string path, long capacity)

+

Immutable queue configuration. The first overload uses Path.GetTempPath(). Windows ignores the backing path. Capacity is message-buffer bytes, greater than 16 and divisible by 8. Read-only properties are string QueueName, string Path, and long Capacity.

+

QueueFactory and IQueueFactory

+

QueueFactory() / QueueFactory(ILoggerFactory loggerFactory)

+

Constructs the factory with default logging or your own logger factory.

+

IPublisher CreatePublisher(QueueOptions options)

+

ISubscriber CreateSubscriber(QueueOptions options)

+

Create or join the transient queue as a publisher or subscriber. Both returned interfaces implement IDisposable. At most 2,048 publisher objects may be connected to a queue.

+

IServiceCollection AddInterprocessQueue(this IServiceCollection services)

+

Registers IQueueFactory with the service collection. Call services.AddInterprocessQueue() and resolve IQueueFactory through dependency injection.

+

IPublisher

+

bool TryEnqueue(ReadOnlySpan<byte> message)

+

Copies the bytes into shared memory without waiting for space. Returns false when the message does not fit or recovery temporarily closes admission; true means the message was committed. A notification semaphore reaching its limit does not fail an already committed send.

+

ISubscriber

+

bool TryDequeue(out ReadOnlyMemory<byte> message)

+

Tries once, allocating a byte array for a received message. False means no ready message, an active reader owns consumption, or the next message is unfinished.

+

bool TryDequeue(Memory<byte> buffer, out ReadOnlyMemory<byte> message)

+

Copies into your buffer. On success the returned memory references the filled part of that buffer; consume it before reusing or modifying the buffer. An undersized buffer truncates and consumes the message. A true result with zero length is a real empty message.

+

ReadOnlyMemory<byte> Dequeue(CancellationToken cancellation)

+

Blocks until a message arrives, cancellation is requested, disposal is observed, or a failure occurs. Allocates a result array. Use CancellationToken.None to wait without application cancellation.

+

ReadOnlyMemory<byte> Dequeue(Memory<byte> buffer, CancellationToken cancellation)

+

Blocking receive into caller-owned storage, with the same truncation and buffer-lifetime rules as TryDequeue with a buffer.

+

Failures and cancellation

+

Cancellation and subscriber disposal throw OperationCanceledException from receive operations. Sending through a disposed publisher throws ObjectDisposedException. Invalid options are rejected by argument validation. A publisher-limit failure throws InvalidOperationException; counter exhaustion throws OverflowException. Queue opening and access can also fail because of capacity mismatch, permissions, I/O, or invalid shared state.

+

False from TryEnqueue/TryDequeue is not an exception. Apply a bounded retry policy if the application needs to wait. TryDequeue has no cancellation token because it does not wait for a message.

+

Cleanup and concurrency

+

Use using for each endpoint. Disposal stops new calls and waits for admitted calls to finish before releasing resources; waiting readers observe disposal on retry. Consumption is serialized across subscribers. A stalled live participant is not treated as crashed simply because a deadline passes.

+

The API is synchronous; there is no DequeueAsync or batch-enqueue API. Reusing receive buffers avoids per-message result-array allocation. See queue lifetime before handing off between processes.

diff --git a/src/website/docs/go.html b/src/website/docs/go.html new file mode 100644 index 0000000..6cff76a --- /dev/null +++ b/src/website/docs/go.html @@ -0,0 +1,69 @@ +

Use Go byte slices and context cancellation over the native C ABI. Endpoints support concurrent calls and explicit cleanup.

+

Generated Go reference ↗ · API source ↗

+

Install

+

Requires Go 1.24 or later, cgo enabled, a C compiler, and pkg-config. Install the C SDK first and set PKG_CONFIG_PATH to its lib/pkgconfig directory. On Windows, add the SDK's lib directory to PATH and use a cgo-compatible compiler.

+
go get github.com/cloudtoid/interprocess/src/go/v3@latest
+

The module's package name is interprocess. On Unix, the pkg-config flags include the installed library's runtime search path.

+

Send and receive

+
package main
+
+import (
+    "context"
+    "fmt"
+    "time"
+
+    "github.com/cloudtoid/interprocess/src/go/v3"
+)
+
+func main() {
+    if err := run(); err != nil { panic(err) }
+}
+
+func run() error {
+    options := interprocess.Options{Name: "example", Capacity: 65536}
+    subscriber, err := interprocess.OpenSubscriber(options)
+    if err != nil { return err }
+    defer subscriber.Close()
+    publisher, err := interprocess.OpenPublisher(options)
+    if err != nil { return err }
+    defer publisher.Close()
+
+    sent, err := publisher.TrySend([]byte("hello"))
+    if err != nil { return err }
+    if !sent { return fmt.Errorf("queue is full or recovering") }
+    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+    defer cancel()
+    message, err := subscriber.Receive(ctx)
+    if err != nil { return err }
+    fmt.Println(string(message))
+    return nil
+}
+

Options

+
type Options struct {
+    Name, Path string
+    Capacity   int
+}
+

Name identifies the queue. Empty Path uses the OS temporary directory; Windows ignores it. Capacity is message-buffer bytes, greater than 16 and divisible by 8. Options must match all other participants.

+

Publisher

+

OpenPublisher(o Options) (*Publisher, error)

+

Creates or joins the transient queue and registers a publisher. Defer Close after a successful open.

+

(*Publisher).TrySend(message []byte) (bool, error)

+

Copies a message without waiting for space. (false, nil) means full or recovering; (true, nil) means committed.

+

(*Publisher).Close() error

+

Waits for the current native calls, releases the endpoint, and returns nil. Repeated calls are safe.

+

Subscriber

+

OpenSubscriber(o Options) (*Subscriber, error)

+

Creates or joins the queue as a competing subscriber.

+

(*Subscriber).TryReceive() ([]byte, error)

+

(nil, nil) means no ready message. A received empty message is a non-nil zero-length slice. Returned bytes are owned by Go.

+

(*Subscriber).TryReceiveInto(buffer []byte) (int, bool, error)

+

Returns bytes copied, whether a message was consumed, and an error. The boolean distinguishes an empty queue from an empty message. An undersized buffer truncates and consumes the entire message.

+

(*Subscriber).Receive(ctx context.Context) ([]byte, error)

+

Waits for a message, closure, or context cancellation. Returns ctx.Err() for cancellation or deadline expiry. Pass context.Background() for an indefinite wait; do not pass a nil context.

+

(*Subscriber).Close() error

+

Waits for current native calls before releasing the handle. Outstanding receives observe ErrClosed on their next attempt. Repeated close is safe.

+

Errors

+

Use errors.Is with ErrInvalidArgument, ErrCapacityMismatch, ErrPublisherLimit, ErrExhausted, ErrCorrupt, ErrIO, ErrInternal, or ErrClosed. Full queues are reported through the send boolean, not a sentinel error. Context errors are context.Canceled or context.DeadlineExceeded.

+

Concurrency and scheduling

+

Do not copy endpoints after first use. Always close them; concurrent operations are protected against handle release. Each idle Receive uses a Go timer with 1–10 ms adaptive backoff, interrupted by context cancellation. It does not keep an OS thread blocked in cgo while idle. Prefer one receive loop per subscriber and distribute work after receiving.

+

This binding has no batch-send method. For lifetime and delivery guarantees, see queue concepts.

diff --git a/src/website/docs/node.html b/src/website/docs/node.html new file mode 100644 index 0000000..54b20bb --- /dev/null +++ b/src/website/docs/node.html @@ -0,0 +1,47 @@ +

Send typed byte arrays and receive Node Buffers, with promises and AbortSignal for waiting.

+

npm package ↗ · TypeScript declarations ↗ · JavaScript API source ↗

+

Install

+

Requires Node.js 18 or later. Platform binaries install as optional packages; do not omit optional dependencies. TypeScript users need TypeScript 5.2 or later for the Symbol.dispose declarations. Linux prebuilt binaries require glibc 2.34 or later.

+
npm install @cloudtoid/interprocess
+

Send and receive

+

Save as example.mjs and run with node example.mjs. CommonJS can instead use require('@cloudtoid/interprocess').

+
import { Publisher, Subscriber } from '@cloudtoid/interprocess';
+
+const subscriber = new Subscriber('example', 65536);
+const publisher = new Publisher('example', 65536);
+try {
+  if (!publisher.trySend(Buffer.from('hello'))) {
+    throw new Error('Queue is full or recovering');
+  }
+  const message = await subscriber.receive({
+    signal: AbortSignal.timeout(1000)
+  });
+  console.log(message.toString('utf8'));
+} finally {
+  publisher.close();
+  subscriber.close();
+}
+

Publisher

+

new Publisher(name: string, capacity: number, path?: string)

+

Creates or joins a queue. The optional Unix directory defaults to the OS temporary directory and is ignored on Windows. All participants must use matching identity and capacity.

+

trySend(data: Uint8Array): boolean

+

Accepts Uint8Array, including Buffer. Returns true after committing the message, or false when full or recovering. Other failures throw.

+

trySendBatch(messages: Uint8Array[]): number

+

Returns the committed prefix length. A short count can mean full capacity, recovery, or a mid-batch error. Retry the unsent suffix to surface a persistent error. An error before any commit throws immediately. The batch is not atomic.

+

close(): void / [Symbol.dispose](): void

+

Releases the publisher. Repeated close is safe; operations after close throw ERR_CLOSED.

+

Subscriber

+

new Subscriber(name: string, capacity: number, path?: string)

+

Opens a competing subscriber with the same configuration rules as Publisher.

+

tryReceive(): Buffer | null

+

Consumes a ready message and returns an owned Buffer. null means no message is ready. An empty Buffer is a real message.

+

receive(options?: { signal?: AbortSignal }): Promise<Buffer>

+

Checks immediately, then waits until a message arrives, the signal aborts, the endpoint closes, or an error occurs. Cancellation rejects with signal.reason. Use AbortSignal.timeout(milliseconds) for a deadline.

+

close(): void / [Symbol.dispose](): void

+

Releases the endpoint. Pending receives reject when they next observe closure. Repeated close is safe.

+

Errors

+

Queue failures expose an error.code: ERR_INVALID_ARGUMENT, ERR_CAPACITY_MISMATCH, ERR_PUBLISHER_LIMIT, ERR_EXHAUSTED, ERR_CORRUPT, ERR_IO, or ERR_CLOSED. Binding-level type errors can also occur for invalid JavaScript arguments.

+

ERR_NATIVE_UNAVAILABLE means the native addon could not load. Check the platform package, optional dependencies, and Linux glibc requirement, or build from source. Cancellation uses the signal's reason rather than a queue error code.

+

Scheduling and cleanup

+

Idle receives use timer backoff of 1, 2, 4, 8, then 10 ms, without occupying libuv workers. A new receive starts with an immediate check. Scheduling can delay timers and cancellation beyond those intervals. Prefer one receive loop per subscriber instead of many pending receives.

+

Always close both endpoints in a finally block or use explicit resource management where your runtime supports it. No public receive-into API is exposed by this binding. See transient queue lifetime before splitting publishers and subscribers into separate processes.

diff --git a/src/website/docs/overview.html b/src/website/docs/overview.html new file mode 100644 index 0000000..83eaf2d --- /dev/null +++ b/src/website/docs/overview.html @@ -0,0 +1,26 @@ +

Move bytes between processes with a shared-memory queue. Pick the API that fits your language; every implementation speaks the same v3 protocol.

+

Choose your language

+
+RustOwned bytes, reusable buffers, and batch sends.cargo add cloudtoid-interprocess +Node.js & TypeScriptBuffers, promises, and AbortSignal.npm install @cloudtoid/interprocess +GoByte slices, context cancellation, and cgo.go get github.com/cloudtoid/interprocess/src/go/v3@latest +CA small ABI with explicit buffer ownership.#include <interprocess.h> +PythonBytes, timeouts, and context managers. Build from source.from cloudtoid_interprocess import Publisher +.NETSpans, reusable memory, and CancellationToken.dotnet add package Cloudtoid.Interprocess +
+

Your first queue

+
  1. Install the package for your language. Go also needs the C SDK.
  2. Choose a short queue name and a capacity, such as 65536 bytes. On Unix, choose an explicit shared directory when processes use different runtimes.
  3. Open a subscriber and a publisher using the same identity and capacity.
  4. Send bytes. Check the result: a full queue needs an application-level retry or backpressure policy.
  5. Receive bytes and close each endpoint when its work is finished.
+ +

Which receive should I use?

+

Use a nonblocking receive when your application already controls scheduling. Use a waiting receive when you want the library to wait for work. Reuse caller-owned buffers in Rust, Go, C, or .NET to avoid allocating a result buffer on each receive.

+
+ + + + + + +
LanguageTry onceWait for work
Rusttry_recv()recv() / recv_timeout(Duration)
Node.jstryReceive()await receive({ signal })
GoTryReceive()Receive(ctx)
Ccip_receive(handle, 0, &buffer)cip_receive(handle, timeout_ms, &buffer)
Pythontry_receive()receive(timeout=seconds)
.NETTryDequeue(out message)Dequeue(cancellation)
+

Platforms and compatibility

+

The protocol targets little-endian, 64-bit Linux, macOS, and Windows on x86-64 and ARM64. Prebuilt package availability varies: check your language's installation section. All connected processes must use protocol v3. Package versions can differ while remaining compatible with that protocol.

+

Rust provides the native core behind C, Node.js, Python, and Go. .NET implements the same protocol independently. See queue concepts for delivery guarantees and benchmarks for measured throughput and latency.

diff --git a/src/website/docs/pages.json b/src/website/docs/pages.json new file mode 100644 index 0000000..558e90e --- /dev/null +++ b/src/website/docs/pages.json @@ -0,0 +1,10 @@ +[ + {"slug":"", "label":"Overview", "title":"Developer documentation", "description":"Build fast cross-process messaging with Cloudtoid Interprocess. Installation guides and API references for Rust, Node.js, Go, C, Python, and .NET."}, + {"slug":"concepts", "label":"Queue concepts", "title":"Queue lifetime, delivery, and interoperability", "description":"Understand transient queue lifetime, competing subscribers, capacity, message ordering, crash recovery, and cross-language compatibility in protocol v3."}, + {"slug":"rust", "label":"Rust", "title":"Rust API reference", "description":"Install cloudtoid-interprocess and use Options, Publisher, and Subscriber. Reference for batch sends, reusable receive buffers, blocking waits, and errors."}, + {"slug":"node", "label":"Node.js", "title":"Node.js & TypeScript API reference", "description":"Install @cloudtoid/interprocess. Send Uint8Array messages, receive Buffers, cancel with AbortSignal, and close endpoints using the Node.js API."}, + {"slug":"go", "label":"Go", "title":"Go API reference", "description":"Use Cloudtoid Interprocess from Go with cgo. Configure Options, send and receive byte slices, reuse buffers, and cancel receives with context.Context."}, + {"slug":"c", "label":"C", "title":"C API reference", "description":"Install the Cloudtoid Interprocess C SDK. Reference for handles, status codes, receive timeouts, owned buffers, and safe shutdown."}, + {"slug":"python", "label":"Python", "title":"Python API reference", "description":"Build Cloudtoid Interprocess for Python. Send bytes and buffer objects, receive with timeouts, use context managers, and handle queue exceptions."}, + {"slug":"dotnet", "label":".NET", "title":".NET API reference", "description":"Install Cloudtoid.Interprocess from NuGet. Use QueueFactory, QueueOptions, IPublisher, and ISubscriber with reusable buffers and CancellationToken."} +] diff --git a/src/website/docs/python.html b/src/website/docs/python.html new file mode 100644 index 0000000..ea9f621 --- /dev/null +++ b/src/website/docs/python.html @@ -0,0 +1,39 @@ +

Send Python bytes and buffer objects through the shared Rust engine. Use context managers to keep endpoint lifetimes explicit.

+

Python package source ↗ · Binding implementation ↗

+

Install from source

+

The Python package is not yet published on PyPI. Requires Python 3.9 or later, Git, Rust, and a native linker. Run in an activated virtual environment:

+
python -m pip install "git+https://github.com/cloudtoid/interprocess.git@native-v3.0.1#subdirectory=src/python"
+

Send and receive

+
from cloudtoid_interprocess import Publisher, Subscriber
+
+with Subscriber("example", 65536) as subscriber:
+    with Publisher("example", 65536) as publisher:
+        if not publisher.try_send(b"hello"):
+            raise RuntimeError("Queue is full or recovering")
+        message = subscriber.receive(timeout=1.0)
+        if message is None:
+            raise TimeoutError("No message arrived")
+        print(message.decode("utf-8"))
+

Publisher

+

Publisher(name, capacity, path=None)

+

Creates or joins the queue. name is a string, capacity is integer message-buffer bytes, and path is an optional filesystem path for Unix storage. The default is the OS temporary directory; Windows ignores path.

+

try_send(data) -> bool

+

Accepts bytes and objects implementing the buffer protocol, including bytearray and memoryview. Non-bytes inputs are snapshotted before sending. Returns False when full or recovering, True on commit; other failures raise.

+

try_send_batch(messages) -> int

+

Pass a list of bytes-like messages. Returns the committed prefix length. A short count can mean full capacity, recovery, or a mid-batch error. Retry only the unsent suffix. Errors before any commit are raised immediately; the batch is not transactional.

+

close() -> None

+

Releases the endpoint. Repeated close is safe. Publisher supports with via __enter__ and __exit__; leaving the block closes it without suppressing exceptions.

+

Subscriber

+

Subscriber(name, capacity, path=None)

+

Creates or joins the queue with the same configuration rules as Publisher.

+

try_receive() -> bytes | None

+

Consumes one ready message and returns bytes. None means no message is ready. b"" is a valid empty message, so test message is not None rather than truthiness.

+

receive(timeout=None) -> bytes | None

+

Blocks until delivery or timeout. Timeout is a finite nonnegative number of seconds; None waits indefinitely and zero attempts once. Returns None on timeout. Negative, infinite, and NaN timeout values raise ValueError.

+

close() -> None

+

Releases the subscriber. Repeated close is safe. Subscriber also supports with. A concurrent close waits for the current bounded native wait before releasing the handle.

+

Exceptions

+
InterprocessError
Base for queue-specific exceptions below, not for every possible API failure.
CapacityMismatchError
Existing capacity differs; also a ValueError.
PublisherLimitError
No publisher slot is available; also a RuntimeError.
CorruptQueueError
Shared state is invalid; also a RuntimeError.
ValueError
Invalid configuration, invalid timeout, or a closed endpoint.
OverflowError
Counter exhaustion or an integer outside a native argument's range.
OSError
Operating-system failure.
TypeError
Invalid argument type or unsupported buffer object.
+

Waiting, signals, and lifetime

+

Receive releases the GIL while waiting and checks Python signals between native waits of at most 100 ms, subject to scheduling. A received message is returned rather than discarded to report a later signal. There is no native asyncio API; use a worker thread with bounded timeouts for orderly shutdown. Cancelling an asyncio wrapper does not stop its already-running blocking call.

+

No public receive-into method is exposed. Close endpoints with context managers and keep their lifetimes overlapping across processes; see transient lifetime.

diff --git a/src/website/docs/rust.html b/src/website/docs/rust.html new file mode 100644 index 0000000..2c5053e --- /dev/null +++ b/src/website/docs/rust.html @@ -0,0 +1,46 @@ +

Use the native Rust core directly: send byte slices, receive owned vectors, or reuse your own storage.

+

Generated crate documentation ↗ · API source ↗

+

Install

+

Requires Rust 1.87 or later on a supported little-endian 64-bit platform.

+
cargo add cloudtoid-interprocess
+

Send and receive

+

This complete example keeps both endpoints alive. Separate processes use the same options; on Unix, add .with_path("/absolute/shared/directory") when their temporary directories differ.

+
use cloudtoid_interprocess::{Options, Publisher, Subscriber};
+
+fn main() -> cloudtoid_interprocess::Result<()> {
+    let options = Options::new("example", 65536);
+    let subscriber = Subscriber::open(&options)?;
+    let publisher = Publisher::open(&options)?;
+    publisher.try_send(b"hello")?;
+    let message = subscriber.try_recv()?.expect("message is ready");
+    assert_eq!(message, b"hello");
+    Ok(())
+}
+

Options

+

Options::new(name: impl Into<String>, capacity: usize) -> Self

+

Creates configuration using the OS temporary directory. Validation happens when opening an endpoint. Public fields are name: String, path: PathBuf, and capacity: usize. The struct is non-exhaustive; use the constructor rather than a struct literal.

+

with_path(self, path: impl Into<PathBuf>) -> Self

+

Sets the Unix backing directory. Windows ignores this option. Capacity is bytes, greater than 16 and divisible by 8; see identity rules.

+

Publisher

+

Publisher::open(options: &Options) -> Result<Self>

+

Creates or joins the queue and registers a publisher. MAX_PUBLISHERS is 2048.

+

try_send(&self, message: &[u8]) -> Result<()>

+

Copies a message into the queue without waiting for space. Ok(()) means committed; Error::Full means full or temporarily recovering. Other errors must be handled separately.

+

try_send_batch(&self, messages: &[&[u8]]) -> Result<usize>

+

Returns the committed prefix length. A short count, including zero, can indicate full capacity, recovery, or a mid-batch error. Retry only the unsent suffix to observe persistent errors. Errors before any commit are returned immediately. Batches can interleave with other publishers.

+

Subscriber

+

Subscriber::open(options: &Options) -> Result<Self>

+

Creates or joins the queue as a competing subscriber.

+

try_recv(&self) -> Result<Option<Vec<u8>>>

+

Copies and consumes one ready message into an owned vector. None means no message is ready; Some(vec![]) is a valid empty message.

+

try_recv_into(&self, buffer: &mut [u8]) -> Result<Option<usize>>

+

Copies into your buffer and returns the byte count. Some(0) still means a message was consumed. Oversized messages are truncated and consumed.

+

recv(&self) -> Result<Vec<u8>>

+

Blocks the calling thread until a message is received or an error occurs.

+

recv_timeout(&self, timeout: Duration) -> Result<Option<Vec<u8>>>

+

Blocks for up to the timeout, subject to scheduling and operation overhead. None means timeout. A zero duration attempts once. Received vectors belong to the caller.

+

Errors

+

Result<T> aliases std::result::Result<T, Error>. Error is non-exhaustive and implements Display and std::error::Error. Use error.is_full() for retryable admission failures.

+
Full
No space or recovery admission unavailable.
Invalid(&'static str)
Invalid options or message length.
CapacityMismatch
Existing queue capacity differs.
PublisherLimit
All publisher registrations are occupied.
Exhausted
A lifetime counter cannot advance; use a fresh queue.
Corrupt
Invalid shared queue state.
Io(std::io::Error)
An OS operation failed; available through the error source.
+

Waiting and cleanup

+

Endpoints release registrations on Drop. Keep at least one endpoint alive to retain the queue. Open endpoints after fork(). Blocking receives have no async-runtime integration: use a blocking worker with bounded timeouts if you need controlled shutdown from an async application. Aborting an async task does not cancel a Rust blocking receive already running on a worker.

diff --git a/src/website/docs/template.html b/src/website/docs/template.html new file mode 100644 index 0000000..1e851ed --- /dev/null +++ b/src/website/docs/template.html @@ -0,0 +1,23 @@ + + + + +$title | Cloudtoid Interprocess + +$metadata +$favicon + + + + + + + +
CloudtoidCloudtoid / interprocess
+
+ +
Interprocess/Docs$breadcrumb

$title

$content
+ +
+ + diff --git a/src/website/index.html b/src/website/index.html index df10b0f..86595ab 100644 --- a/src/website/index.html +++ b/src/website/index.html @@ -5,14 +5,14 @@ Cloudtoid Interprocess — Fast queues across processes and languages - + -
CloudtoidCloudtoid / interprocess
+
CloudtoidCloudtoid / interprocess
@@ -21,7 +21,7 @@

Interprocessv3

Fast, lightweight queues
across processes and languages.

Rust · C · Python · Node.js · Go · .NET

Connect multiple publisher and subscriber processes through a shared circular buffer. Native atomic reservations, reusable memory, and no broker service. Built for high throughput on a single machine.

- +

Linux · macOS · Windows MIT licensed

@@ -76,8 +76,8 @@

Send + receive

01

Keep allocations out
of the hot path.

Preallocate queue storage and reuse receive buffers. The measured .NET send-and-receive path allocates 0 bytes per operation, reducing garbage-collection pressure.

02

Move bytes with
very little work.

Native 64-bit atomic reservations and coalesced wakeups keep coordination lean. Send your existing binary format without a required serializer or wire protocol.

03

Keep the operating
cost small.

No additional messaging service, broker machine, or per-message license fee. Run your processes on the same host and choose how much memory the queue uses.

-

LANGUAGE INTERFACES

One queue.
Six language interfaces.

Rust, C, Python, Node.js, Go, and .NET can exchange messages through the same open v3 protocol. Pick the right language for each process.

Available now: .NET on NuGet, Rust on crates.io, Node.js on npm, Go, and the C SDK. Python is available from source.

Read the Rust guide →
-
Install

+

LANGUAGE INTERFACES

One queue.
Six language interfaces.

Rust, C, Python, Node.js, Go, and .NET can exchange messages through the same open v3 protocol. Pick the right language for each process.

Read the Rust guide →
+
Install

PROTOCOL V3

Open source.
Documented in full.

Inspect the memory layout, atomic ordering, resource lifetime, and crash recovery. Build on an MIT-licensed implementation with tests across every publisher/subscriber language pair.

Built for volatile queues on one machine. For durable jobs, add application-level acknowledgements and persistence.

Read the protocol
diff --git a/src/website/site.js b/src/website/site.js index f9a8aca..fda2f89 100644 --- a/src/website/site.js +++ b/src/website/site.js @@ -91,9 +91,10 @@ function select(tab, focus = false) { code.innerHTML = hljs.highlight(source, { language: grammar }).value; document.querySelector('#example').setAttribute('aria-label', `${language} example`); const link = document.querySelector('#language-docs'); - link.href = `https://github.com/cloudtoid/interprocess/tree/main/src/${folder}`; + link.href = `/docs/${folder}/`; link.textContent = `Read the ${language} guide →`; document.querySelector('#copy-status').textContent = ''; + document.querySelector('#install-copy-status').textContent = ''; if (focus) tab.focus(); } tabs.forEach((tab, index) => { @@ -111,4 +112,14 @@ document.querySelector('#copy').addEventListener('click', async () => { try { await navigator.clipboard.writeText(code.textContent); document.querySelector('#copy-status').textContent = 'Copied'; } catch { document.querySelector('#copy-status').textContent = 'Select the code to copy'; } }); +document.querySelector('#copy-install').addEventListener('click', async () => { + const command = document.querySelector('#install-command').textContent; + const status = document.querySelector('#install-copy-status'); + try { + await navigator.clipboard.writeText(command); + if (document.querySelector('#install-command').textContent === command) status.textContent = 'Copied'; + } catch { + if (document.querySelector('#install-command').textContent === command) status.textContent = 'Select the command to copy'; + } +}); select(tabs[0]); diff --git a/src/website/style.css b/src/website/style.css index 3978e5d..6ccc2ae 100644 --- a/src/website/style.css +++ b/src/website/style.css @@ -1,43 +1,48 @@ :root { color-scheme: light; - --background: #f7faf9; + --background: #fcfcfd; --surface: #ffffff; - --heading-row: #edf4f2; - --highlight: #e1f4ec; - --button-text: #fff; - --button-hover: #30435a; - --syntax-keyword: #a62664; - --syntax-string: #287446; - --syntax-function: #1558c0; - --syntax-number: #805100; - --ink: #122f3b; - --muted: #526a70; - --line: #cddeda; - --soft: #edf4f2; - --blue: #006b5c; - --hero: #0d2937; - --mint: #81efc5; - --amber: #f5c86b; - --mono: ui-monospace, SFMono-Regular, Consolas, monospace; + --heading-row: #f0f0f3; + --highlight: #f0f1fe; + --button-text: #ffffff; + --button-hover: #5151cd; + --syntax-keyword: #8e4ec6; + --syntax-string: #b32665; + --syntax-function: #3451b2; + --syntax-number: #ad3d28; + --ink: #1c2024; + --muted: #60646c; + --line: #d9d9e0; + --soft: #f5f5f7; + --accent: #5753c6; + --rose: #ca244d; + --hero-background: #eeedf7; + --section-background: #f0f1f5; + --mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; } [data-theme="dark"] { color-scheme: dark; - --background: #0b1820; - --surface: #112630; - --heading-row: #15323c; - --highlight: #123c35; - --ink: #e8f2ef; - --muted: #9db7b8; - --line: #2e4b53; - --soft: #10252e; - --blue: #6de0b8; - --button-text: #11161d; - --button-hover: #b9cee6; - --syntax-keyword: #f3a0cf; - --syntax-string: #9ad7a0; - --syntax-function: #8ab8ff; - --syntax-number: #eac589; + --background: #111113; + --surface: #18191b; + --heading-row: #212225; + --highlight: #27264a; + --ink: #edeef0; + --muted: #b0b4ba; + --line: #363a3f; + --soft: #1c1d20; + --accent: #b1a9ff; + --rose: #ff92ad; + --hero-background: #1a1926; + --section-background: #191a20; + --button-text: #111113; + --button-hover: #7773e6; + --syntax-keyword: #d1afff; + --syntax-string: #ffb7cd; + --syntax-function: #9eb1ff; + --syntax-number: #ffb4a4; } +[data-theme="dark"] .button { background: var(--button-fill, #5b5bd6); } +[data-theme="dark"] .button:hover { background: var(--button-hover); } * { box-sizing: border-box; } html { scroll-behavior: smooth; scroll-padding-top: 24px; } body { margin: 0; background: var(--background); color: var(--ink); font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Avenir Next", "Segoe UI", sans-serif; } @@ -48,7 +53,7 @@ strong { font-weight: 650; } .wrap { max-width: 1240px; margin: 0 auto; padding-left: 40px; padding-right: 40px; } .section { padding-top: 76px; padding-bottom: 76px; } .eyebrow { font: 500 12px/1.6 var(--mono); letter-spacing: 1.2px; margin: 0 0 20px; color: var(--muted); } -.status-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--blue); margin-right: 8px; } +.status-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); margin-right: 8px; } .brand { display: inline-flex; align-items: center; gap: 10px; font-size: 22px; font-weight: 750; letter-spacing: -.7px; white-space: nowrap; } .brand span { font: 14px var(--mono); color: var(--muted); letter-spacing: -.5px; } header { display: flex; align-items: center; justify-content: space-between; gap: 24px; min-height: 84px; border-bottom: 1px solid var(--line); } @@ -62,14 +67,14 @@ h1 { font-size: clamp(56px, 6vw, 78px); letter-spacing: -4px; line-height: 1.05; .button { display: inline-flex; align-items: center; justify-content: center; gap: 28px; padding: 12px 20px; background: var(--ink); color: var(--button-text); border-radius: 6px; font-size: 14px; font-weight: 600; line-height: 1.5; } .button:hover { background: var(--button-hover); text-decoration: none; } .button span { font-size: 19px; } -.text-link { font-size: 14px; font-weight: 600; color: var(--blue); } +.text-link { font-size: 14px; font-weight: 600; color: var(--accent); } .availability { font: 12px/1.8 var(--mono); color: var(--muted); margin: 0; } .availability span { margin-left: 12px; padding-left: 12px; border-left: 1px solid var(--line); } .speed-panel { border: 1px solid var(--line); border-top: 3px solid var(--ink); padding: 24px 26px 0; background: var(--surface); } .panel-heading { display: flex; gap: 12px; justify-content: space-between; align-items: center; } .panel-heading .eyebrow { margin: 0; font-size: 12px; letter-spacing: .6px; } .chip { font: 12px/1.5 var(--mono); padding: 3px 7px; color: var(--muted); border: 1px solid var(--line); border-radius: 2px; white-space: nowrap; } -.speed-value { font: 500 clamp(64px, 6.8vw, 86px)/1.25 var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -6px; margin-top: 22px; color: var(--blue); white-space: nowrap; } +.speed-value { font: 500 clamp(64px, 6.8vw, 86px)/1.25 var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -6px; margin-top: 22px; color: var(--accent); white-space: nowrap; } .speed-value > span { font-size: 29px; font-weight: 400; letter-spacing: -1px; margin-left: 8px; } .speed-panel h2 { font-size: 22px; font-weight: 600; letter-spacing: -.5px; margin: 8px 0; } .speed-panel > p { font-size: 15px; line-height: 1.6; color: var(--muted); margin: 0 0 20px; } @@ -80,7 +85,7 @@ h1 { font-size: clamp(56px, 6vw, 78px); letter-spacing: -4px; line-height: 1.05; .payload-results dd { margin: 0; font: 14px/1.65 var(--mono); } .measurement-label { display: grid; font-size: 12px; line-height: 1.8; color: var(--muted); } .measurement-label strong { font-size: 13px; color: var(--ink); font-weight: 500; } -.panel-link { display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--line); margin: 21px -26px 0; padding: 13px 26px; font-size: 13px; color: var(--blue); } +.panel-link { display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--line); margin: 21px -26px 0; padding: 13px 26px; font-size: 13px; color: var(--accent); } .proof-strip { display: grid; grid-template-columns: 1.2fr 1fr 1fr; gap: 35px; padding-top: 23px; padding-bottom: 23px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } .proof-strip > div { display: flex; align-items: center; gap: 18px; } .proof-strip strong { font: 500 37px/1.1 var(--mono); letter-spacing: -2px; white-space: nowrap; } @@ -102,7 +107,7 @@ th { font-size: 12px; font-weight: 500; color: var(--muted); white-space: nowrap td:nth-last-child(-n+2) { white-space: nowrap; font: 13px/1.6 var(--mono); font-variant-numeric: tabular-nums; } td:nth-last-child(2) { font-weight: 650; } tr.highlight { background: var(--highlight); } -tr.highlight td:nth-last-child(2) { color: var(--blue); font-size: 16px; } +tr.highlight td:nth-last-child(2) { color: var(--accent); font-size: 16px; } .table-note { font-size: 12px; color: var(--muted); padding: 14px 24px; margin: 0; border-top: 1px solid var(--line); } .platform-benchmarks { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-top: 24px; } .platform-benchmarks th, .platform-benchmarks td { padding: 15px 18px; } @@ -114,13 +119,13 @@ tr.highlight td:nth-last-child(2) { color: var(--blue); font-size: 16px; } .methodology[open] summary > span { transform: rotate(45deg); } .methodology > div { padding-bottom: 20px; max-width: 970px; } .methodology p { margin: 15px 0; } -.methodology a { text-decoration: underline; text-underline-offset: 3px; color: var(--blue); } +.methodology a { text-decoration: underline; text-underline-offset: 3px; color: var(--accent); } .architecture { border-top: 1px solid var(--line); } .queue-diagram { display: grid; grid-template-columns: 1fr 45px 1.9fr 45px 1fr; gap: 12px; align-items: center; border: 1px solid var(--line); padding: 28px 22px; background: var(--soft); } .process-group h3 { font-size: 15px; font-weight: 600; margin: 0 0 16px; } .process { display: flex; align-items: center; gap: 8px; background: var(--surface); border: 1px solid var(--line); padding: 10px; font-size: 13px; margin-top: 8px; white-space: nowrap; } .process small { margin-left: auto; font: 12px var(--mono); color: var(--muted); } -.process-dot { height: 5px; width: 5px; border-radius: 50%; background: var(--blue); flex-shrink: 0; } +.process-dot { height: 5px; width: 5px; border-radius: 50%; background: var(--accent); flex-shrink: 0; } .process-group > p { font-size: 12px; color: var(--muted); margin: 13px 0 0; } .connector { text-align: center; color: var(--muted); } .connector span { font: 12px var(--mono); } @@ -129,7 +134,7 @@ tr.highlight td:nth-last-child(2) { color: var(--blue); font-size: 16px; } .queue-title { font: 12px var(--mono); letter-spacing: 1px; color: var(--muted); margin-bottom: 21px; } .messages { display: flex; gap: 4px; border: 1px solid var(--line); padding: 6px; } .messages span { flex: 1; min-width: 0; padding: 12px 3px; background: var(--soft); border: 1px solid var(--line); font: 13px var(--mono); color: var(--muted); } -.messages span:first-child { background: var(--blue); border-color: var(--blue); color: var(--button-text); } +.messages span:first-child { background: var(--accent); border-color: var(--accent); color: var(--button-text); } .reuse { color: var(--muted); font-size: 12px; border-bottom: 1px solid var(--line); margin: 11px 0 16px; padding-bottom: 14px; } .queue-core > strong { font-size: 17px; font-weight: 600; } .queue-core > p { font-size: 13px; color: var(--muted); margin: 5px 0 0; } @@ -145,11 +150,10 @@ tr.highlight td:nth-last-child(2) { color: var(--blue); font-size: 16px; } .start { display: grid; grid-template-columns: .8fr 1.2fr; gap: 65px; align-items: center; } .start h2 { margin-bottom: 23px; } .start p:not(.eyebrow) { font-size: 16px; color: var(--muted); } -.start .release { font-size: 14px!important; border-left: 2px solid var(--line); padding-left: 14px; margin: 25px 0; } .code-window { min-width: 0; background: var(--soft); color: var(--ink); overflow: hidden; border: 1px solid var(--line); border-radius: 3px; } .tabs { display: flex; padding: 9px 10px 0; gap: 1px; border-bottom: 1px solid var(--line); overflow-x: auto; } .tabs button { border: 0; border-bottom: 2px solid transparent; background: none; color: var(--muted); font-size: 14px; white-space: nowrap; padding: 10px 11px; cursor: pointer; } -.tabs [aria-selected=true] { color: var(--blue); border-bottom-color: var(--blue); } +.tabs [aria-selected=true] { color: var(--accent); border-bottom-color: var(--accent); } pre { margin: 0; min-height: 295px; padding: 27px; overflow: auto; font: 13px/1.9 var(--mono); } .code-footer { padding: 12px 17px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; border-top: 1px solid var(--line); font: 12px/1.5 var(--mono); color: var(--muted); } .code-footer button { margin-left: auto; background: none; border: 1px solid var(--muted); color: var(--ink); font-size: 13px; cursor: pointer; padding: 6px 9px; border-radius: 2px; } @@ -270,109 +274,42 @@ footer .brand { color: var(--ink); } h1 { font-size: clamp(42px, 10vw, 49px); } } -/* Navy and mint carry the identity; color also separates the queue's roles. */ -header { min-height: 88px; border-bottom: 0; } -header .brand { color: var(--blue); letter-spacing: -.8px; } -header nav a { font-weight: 550; } -.hero { background: var(--hero); color: #f0f8f5; box-shadow: 0 0 0 100vmax var(--hero); clip-path: inset(0 -100vmax); padding-top: 84px; padding-bottom: 76px; gap: 72px; } -.hero .eyebrow { color: var(--mint); letter-spacing: 1.5px; } -.hero h1 { font-weight: 700; letter-spacing: -.055em; margin-bottom: 30px; } -.hero .version { color: var(--mint); border-color: #3c786d; background: #183e43; border-radius: 5px; } -.hero .status-dot { background: var(--mint); } -.hero-languages { color: var(--mint); font-size: 1rem; line-height: 1.8; margin: 16px 0; } -.adoption { padding-top: 32px; padding-bottom: 32px; text-align: center; color: var(--muted); } -.adoption p { margin: 0; font-size: 1.125rem; } -.adoption strong { color: var(--ink); font-weight: 650; } -.hero .intro { font-size: clamp(28px, 3vw, 35px); line-height: 1.3; font-weight: 450; } -.hero-description { color: #c0d4d7; font-size: 17px; } -.hero .button { background: var(--mint); color: #0b302c; padding: 14px 23px; } -.hero .button:hover { background: #acf7d8; } -.hero .text-link { color: #e1f4ee; } -.hero .availability { color: #a8c5ca; } -.hero .availability span { border-color: #3a5964; } -.speed-panel { color: #eafaf3; background: #163d43; border: 1px solid #3a6b68; border-top: 4px solid var(--mint); border-radius: 12px; padding: 27px 30px 0; box-shadow: 0 22px 55px #00131a55; } -.speed-panel .chip { color: #c3dfd6; border-color: #4a7772; } -.speed-panel .panel-heading .eyebrow { color: #c3dfd6; letter-spacing: .5px; } -.speed-panel .speed-value { color: var(--mint); font-size: clamp(70px, 7vw, 91px); margin-top: 25px; } -.speed-panel h2 { color: #f0faf6; font-size: 24px; } -.speed-panel > p, .speed-panel .payload-results dt, .speed-panel .measurement-label { color: #b9d9d2; } -.speed-panel .payload-results > div { border-color: #3e6564; } -.speed-panel .payload-results dd { color: #f3d48f; } -.speed-panel .measurement-label strong { color: #e2f3ed; } -.speed-panel .panel-link { border-color: #3e6564; color: var(--mint); margin-left: -30px; margin-right: -30px; padding: 16px 30px; background: #103138; border-radius: 0 0 11px 11px; } -.proof-strip { background: #123341; color: #ecf5f1; box-shadow: 0 0 0 100vmax #123341; clip-path: inset(0 -100vmax); border: 0; padding-top: 30px; padding-bottom: 30px; } -.proof-strip strong { color: var(--mint); } -.proof-strip small { color: #abc7cb; } +/* Slate surfaces and restrained iris accents, based on Radix Colors. */ +header { border-bottom: 1px solid var(--line); } +.hero { padding-top: 88px; padding-bottom: 80px; gap: 64px; background: var(--hero-background); box-shadow: 0 0 0 100vmax var(--hero-background); clip-path: inset(0 -100vmax); } +.hero h1 { font-weight: 700; letter-spacing: -.055em; } +.hero .eyebrow { color: var(--accent); } +.hero-languages { color: var(--muted); font-size: 15px; margin: 18px 0; } +.version { border-radius: 4px; background: var(--soft); } +.button { background: var(--accent); color: #fff; border-radius: 5px; } +.button:hover { background: var(--button-hover); } +.adoption { padding-top: 30px; padding-bottom: 30px; text-align: center; color: var(--muted); } +.adoption p { margin: 0; font-size: 17px; } +.adoption strong { color: var(--ink); } +.speed-panel { border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 8px 32px #00000006; } +.speed-value { color: var(--ink); letter-spacing: -.065em; } +.speed-value > span { color: var(--muted); } +.speed-panel .panel-link { background: var(--soft); border-radius: 0 0 9px 9px; } +.proof-strip strong { color: var(--ink); } +.proof-strip > div { border-left: 2px solid var(--line); padding-left: 18px; } +.section-heading h2, .start h2, .open h2 { font-weight: 650; } +.benchmark-block { border-radius: 8px; background: var(--surface); } +.benchmark-header h3 { font-size: 21px; } +.methodology { padding: 0 18px; background: var(--soft); border: 1px solid var(--line); border-radius: 6px; } +.queue-diagram { border-radius: 8px; } +.process { border-radius: 4px; } +.process-group .process { border-left: 2px solid var(--accent); } +.process-group:last-child .process { border-left-color: var(--rose); } +.process-group:last-child .process-dot { background: var(--rose); } +.queue-core { border-color: var(--line); border-radius: 6px; } +.benefits .feature-index { color: var(--accent); } +.code-window { border-radius: 8px; background: var(--surface); } +.tabs, .code-footer { background: var(--soft); } +.tabs [aria-selected=true] { background: var(--surface); } +.efficiency, .open { background: var(--section-background); } +.open-inner { align-items: center; } @media (max-width:800px) { - .hero { gap: 40px; padding-top: 56px; padding-bottom: 50px; } - .speed-panel { max-width: none; } -} -@media (max-width:520px) { - header { min-height: 74px; } - .hero { padding-top: 44px; padding-bottom: 40px; } - .hero .intro { font-size: 27px; } - .speed-panel { padding: 24px 20px 0; } - .speed-panel .speed-value { font-size: 68px; } - .speed-panel .panel-link { margin-left: -20px; margin-right: -20px; padding-left: 20px; padding-right: 20px; } - .proof-strip > div + div { border-color: #355764; } -} -.section { padding-top: 88px; padding-bottom: 88px; } -.section-heading .eyebrow, .start > div > .eyebrow { color: var(--blue); padding-left: 12px; border-left: 3px solid var(--blue); letter-spacing: 1.7px; } -.section-heading h2, .start h2, .open h2 { font-weight: 650; letter-spacing: -.04em; } -.benchmark-block { background: var(--surface); border-radius: 10px; } -.benchmark-header { background: var(--soft); padding-top: 23px; padding-bottom: 23px; } -.benchmark-header h3 { color: var(--blue); font-size: 24px; } -.benchmark-header .chip { border: 0; background: var(--surface); padding: 6px 10px; border-radius: 5px; } -tr.highlight td:first-child { border-left: 4px solid var(--blue); padding-left: 20px; font-weight: 650; } -tr.highlight td:nth-last-child(2) { font-size: 18px; } -.methodology { padding: 0 18px; background: var(--soft); border: 1px solid var(--line); border-radius: 7px; } -.queue-diagram { border-radius: 12px; background: var(--surface); padding-top: 36px; padding-bottom: 36px; } -.process-group h3 { color: #7350b0; } -.process-group:last-child h3 { color: #996315; } -.process-group .process { border-left: 3px solid #9876cd; border-radius: 5px; background: var(--soft); } -.process-group .process-dot { background: #9876cd; } -.process-group:last-child .process { border-left-color: #c89535; } -.process-group:last-child .process-dot { background: #c89535; } -.queue-core { background: #0f4945; color: #edf9f3; border: 1px solid #287869; border-radius: 9px; } -.queue-title { color: #a4dccb; } -.queue-title .status-dot { background: var(--mint); } -.messages { border-color: #468579; border-radius: 5px; } -.messages span { color: #d2eee3; background: #205a53; border-color: #397267; border-radius: 3px; } -.messages span:first-child { background: var(--mint); color: #123d33; border-color: var(--mint); } -.queue-core .reuse { color: #b5d9cd; border-color: #3e756a; } -.queue-core > p { color: #b5d9cd; } -.efficiency { background: var(--soft); } -.benefits .feature-index { display: inline-block; color: var(--blue); background: var(--surface); border: 1px solid var(--line); padding: 5px 9px; border-radius: 5px; } -.benefits h3 { font-weight: 650; } -.code-window { - --ink: #e4f2ed; --muted: #adc5c6; --line: #35555f; --blue: #81efc5; - --syntax-keyword: #e9a0d3; --syntax-string: #91e5bb; --syntax-function: #87c9f0; --syntax-number: #f2cf89; - background: #102d3a; border: 1px solid #35555f; border-radius: 10px; -} -.tabs { background: #0c2330; padding: 12px 12px 0; } -.tabs [aria-selected=true] { background: #173b46; border-radius: 5px 5px 0 0; } -pre { padding: 28px; min-height: 320px; } -.code-footer { background: #0c2330; } -.open { background: #0d2937; color: #eef8f2; } -.open .eyebrow { color: var(--mint); } -.open p:not(.eyebrow) { color: #c0d4d7; } -.open .button { background: var(--mint); color: #0b302c; } -.open .button:hover { background: #acf7d8; } -.open-inner { padding-top: 65px; padding-bottom: 65px; align-items: center; } -footer { border: 0; padding-top: 30px; padding-bottom: 30px; } -footer .brand { color: var(--blue); } -[data-theme=dark] .process-group h3 { color: #c7a8f1; } -[data-theme=dark] .process-group:last-child h3 { color: #efc47b; } -@media (max-width:800px) { - .section { padding-top: 60px; padding-bottom: 60px; } - .hero { gap: 40px; } -} -@media (max-width:520px) { - .section { padding-top: 48px; padding-bottom: 48px; } - .section-heading h2, .start h2, .open h2 { font-size: 30px; } - .queue-diagram { padding: 24px 14px; } - pre { padding: 22px 16px; font-size: 12px; } - .open-inner { padding-top: 45px; padding-bottom: 45px; } + .hero { padding-top: 52px; padding-bottom: 48px; gap: 36px; } } .brand img { display: block; width: 145px; height: 28px; object-fit: contain; } @@ -382,4 +319,25 @@ footer .brand { color: var(--blue); } .installation { padding: 20px 24px; border-bottom: 1px solid var(--line); } .installation p { margin: 6px 0 12px; color: var(--muted); font-size: 14px; } -.installation pre { margin: 0; padding: 12px; font-size: 14px; overflow-x: auto; } +.installation pre { margin: 0; min-height: 0; padding: 12px; font-size: 14px; overflow-x: auto; } + +[data-palette="cobalt"] { --accent: #3557c7; --button-hover: #2947b0; --hero-background: #eaf0fc; --section-background: #f1f4fa; --highlight: #eaf0fc; } +[data-palette="cobalt"][data-theme="dark"] { --accent: #a6bcff; --button-fill: #3557c7; --button-hover: #2947b0; --hero-background: #161e30; --section-background: #191f2a; --highlight: #161e30; } +[data-palette="rose"] { --accent: #b82b5e; --button-hover: #a11f4e; --hero-background: #f9eaf0; --section-background: #faf2f5; --highlight: #f9eaf0; } +[data-palette="rose"][data-theme="dark"] { --accent: #ffabc5; --button-fill: #b82b5e; --button-hover: #a11f4e; --hero-background: #291820; --section-background: #231b20; --highlight: #291820; } +[data-palette="terracotta"] { --accent: #a6442a; --button-hover: #91381f; --hero-background: #f8ece4; --section-background: #f8f3ee; --highlight: #f8ece4; } +[data-palette="terracotta"][data-theme="dark"] { --accent: #f8b49e; --button-fill: #a6442a; --button-hover: #91381f; --hero-background: #291c17; --section-background: #241e1a; --highlight: #291c17; } +[data-palette="plum"] { --accent: #8542a0; --button-hover: #74368e; --hero-background: #f3eaf7; --section-background: #f7f2f9; --highlight: #f3eaf7; } +[data-palette="plum"][data-theme="dark"] { --accent: #dfaff2; --button-fill: #8542a0; --button-hover: #74368e; --hero-background: #25182c; --section-background: #211b26; --highlight: #25182c; } +[data-palette="teal"] { --accent: #14766d; --button-hover: #0c625b; --hero-background: #e7f2ef; --section-background: #f0f6f4; --highlight: #e7f2ef; } +[data-palette="teal"][data-theme="dark"] { --accent: #8ddbd0; --button-fill: #14766d; --button-hover: #0c625b; --hero-background: #142623; --section-background: #18221f; --highlight: #142623; } +[data-palette="olive"] { --accent: #626b2d; --button-hover: #515b21; --hero-background: #eff0e3; --section-background: #f5f5ef; --highlight: #eff0e3; } +[data-palette="olive"][data-theme="dark"] { --accent: #c8d29b; --button-fill: #626b2d; --button-hover: #515b21; --hero-background: #222418; --section-background: #202119; --highlight: #222418; } + +.install-code { display: flex; align-items: flex-start; gap: 8px; } +.install-code pre { flex: 1; min-width: 0; } +#copy-install { display: inline-flex; flex-shrink: 0; align-items: center; justify-content: center; margin-top: 8px; padding: 8px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface); color: var(--muted); cursor: pointer; } +#copy-install:hover { color: var(--accent); border-color: var(--accent); } +#install-copy-status { font-size: 12px; color: var(--muted); } + +nav a[href="/docs/"] { display: inline; } diff --git a/src/website/theme.js b/src/website/theme.js index c87ebdb..2f55b81 100644 --- a/src/website/theme.js +++ b/src/website/theme.js @@ -1,11 +1,14 @@ (() => { const system = window.matchMedia('(prefers-color-scheme: dark)'); let preference = 'system'; + const palettes = ['iris', 'cobalt', 'rose', 'terracotta', 'plum', 'teal', 'olive']; + const palette = palettes[Math.floor(Math.random() * palettes.length)]; try { const saved = localStorage.getItem('cloudtoid-theme'); if (saved === 'light' || saved === 'dark') preference = saved; } catch { /* System preference still works when storage is unavailable. */ } function apply() { + document.documentElement.dataset.palette = palette; document.documentElement.dataset.theme = preference === 'system' ? (system.matches ? 'dark' : 'light') : preference; } diff --git a/src/website/validate.py b/src/website/validate.py new file mode 100644 index 0000000..2fecdd8 --- /dev/null +++ b/src/website/validate.py @@ -0,0 +1,89 @@ +"""Check rendered page metadata, structured data, local links, and fragment targets.""" +from html.parser import HTMLParser +import json +from pathlib import Path +from urllib.parse import urljoin, urlparse, unquote +import xml.etree.ElementTree as ET + +root = Path(__file__).resolve().parent / 'dist' +base = 'https://cloudtoid.com' + + +class Page(HTMLParser): + def __init__(self, path): + super().__init__(convert_charrefs=True) + self.path, self.ids, self.links, self.meta = path, set(), [], {} + self.canonicals, self.titles, self.h1s, self.json_data = [], [], [], [] + self.capture, self.text = None, '' + self.feed(path.read_text()) + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if 'id' in attrs: + assert attrs['id'] not in self.ids, f'{self.path}: duplicate id {attrs["id"]}' + self.ids.add(attrs['id']) + if tag == 'a' and 'href' in attrs: + self.links.append(attrs['href']) + if tag in ('img', 'script') and 'src' in attrs: + self.links.append(attrs['src']) + if tag == 'link': + if attrs.get('rel') == 'canonical': + self.canonicals.append(attrs['href']) + elif attrs.get('rel') == 'stylesheet': + self.links.append(attrs['href']) + if tag == 'meta': + key = attrs.get('name', attrs.get('property')) + if key: + self.meta[key] = attrs.get('content') + if tag in ('title', 'h1') or (tag == 'script' and attrs.get('type') == 'application/ld+json'): + self.capture, self.text = tag, '' + + def handle_data(self, data): + if self.capture: + self.text += data + + def handle_endtag(self, tag): + if tag == self.capture: + if tag == 'title': + self.titles.append(self.text) + elif tag == 'h1': + self.h1s.append(self.text) + else: + self.json_data.append(json.loads(self.text)) + self.capture = None + + +pages = {p: Page(p) for p in root.rglob('*.html')} +expected = ['index.html', 'docs/index.html'] + [f'docs/{slug}/index.html' for slug in ('concepts', 'rust', 'node', 'go', 'c', 'python', 'dotnet')] +assert all(root / path in pages for path in expected), 'Missing documentation pages' +titles, descriptions, canonicals = set(), set(), set() +for path, page in pages.items(): + relative = path.relative_to(root).as_posix() + url = base + '/' + relative.removesuffix('index.html') + assert page.canonicals == [url], f'{path}: incorrect canonical' + assert len(page.titles) == len(page.h1s) == 1, f'{path}: title/h1 missing or duplicated' + assert page.titles[0] not in titles, f'{path}: duplicate title' + titles.add(page.titles[0]) + description = page.meta.get('description') + assert description and description not in descriptions, f'{path}: missing/duplicate description' + descriptions.add(description) + assert page.meta.get('og:url') == url and page.meta.get('og:title'), f'{path}: missing social metadata' + assert page.json_data, f'{path}: missing structured data' + assert page.meta.get('robots') != 'noindex', f'{path}: accidentally excluded from indexing' + canonicals.add(url) + for link in page.links + [page.meta['og:image']]: + target = urlparse(urljoin(url, link)) + if target.netloc != 'cloudtoid.com' or target.scheme not in ('http', 'https'): + continue + local = root / unquote(target.path).lstrip('/') + if local.is_dir(): + local /= 'index.html' + assert local.is_file(), f'{path}: broken local link {link}' + if target.fragment and local in pages: + assert unquote(target.fragment) in pages[local].ids, f'{path}: missing anchor {link}' + +sitemap = ET.parse(root / 'sitemap.xml') +locations = [item.text for item in sitemap.findall('.//{http://www.sitemaps.org/schemas/sitemap/0.9}loc')] +assert len(locations) == len(set(locations)) and set(locations) == canonicals, 'Sitemap does not match pages' +assert 'Sitemap: https://cloudtoid.com/sitemap.xml' in (root / 'robots.txt').read_text() +print(f'Validated {len(pages)} pages: metadata, sitemap, structured JSON, local assets, links, and anchors')