Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove the need to install @starbeam/peer as a peer #35

Merged
merged 2 commits into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/peer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ This package provides an extremely stable API for getting:
- The current timestamp as a number
- The value of the `UNINITIALIZED` symbol

Apps shouldn't use the exports of this dependency directly. Instead, installing it as a peer
dependency allows two versions of Starbeam to coexist in the same process and **to share reactivity
between them**.
Apps shouldn't use the exports of this dependency directly. Instead, separating the most fundamental
parts of Starbeam's composition into a separate package allows two versions of Starbeam to coexist
in the same process and **to share reactivity between them**.

In other words, if you access a Cell from version 1 of Starbeam in the context of a formula
created in version 2 of Starbeam, updating the cell will invalidate the formula.

This package uses `Symbol.for` to ensure that only a single copy of the fundamental symbols and
constants exists in a single process. As a result, it is not necessary to install this package as a
peer dependency.
2 changes: 1 addition & 1 deletion packages/peer/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { REACTIVE, UNINITIALIZED } from "./src/constants.js";
export { NOW, REACTIVE, UNINITIALIZED } from "./src/constants.js";
export { bump, now } from "./src/now.js";
19 changes: 18 additions & 1 deletion packages/peer/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
/**
* The `UNINITIALIZED` symbol represents a special internal value that can be used to differentiate
* between any user-supplied value and the state of being uninitialized.
*
* You do not **need** to import `@starbeam/peer` to get this symbol, as it is specified using
* `Symbol.for`.
*/
const UNINITIALIZED = Symbol.for("starbeam.UNINITIALIZED");
type UNINITIALIZED = typeof UNINITIALIZED;

/**
* The `REACTIVE` symbol is the protocol entry point for reactive values. Implementations of
* the `ReactiveProtocol` interface specify their reactive behavior under this symbol.
*/
const REACTIVE: unique symbol = Symbol.for("starbeam.REACTIVE");
type REACTIVE = typeof REACTIVE;

export { REACTIVE, UNINITIALIZED };
/**
* The `NOW` symbol is the name on `globalThis` that is used to store the current timestamp.
*/
const NOW: unique symbol = Symbol.for("starbeam.NOW");
type NOW = typeof NOW;

export { NOW, REACTIVE, UNINITIALIZED };
11 changes: 11 additions & 0 deletions packages/peer/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { NOW } from "./constants.js";

export interface Clock {
timestamp: number;
}

export interface GlobalWithNow {
[NOW]: {
timestamp: number;
};
}
31 changes: 26 additions & 5 deletions packages/peer/src/now.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
const NOW = {
timestamp: 0,
};
import { NOW } from "./constants.js";
import type { GlobalWithNow } from "./env.js";

/**
* The `CLOCK` constant is a universal monotonically increasing clock. The `Timestamp` class is used
* in `@starbeam/timeline` and `@starbeam/core`, but `Timestamp` defers to this constant. This means
* that multiple copies of `@starbeam/timeline` will still see the same monotonically increasing clock.
*
* The term "timestamp" is used in this context to refer to a monotonically increasing number, where
* each number represents a different moment in time.
*/
let CLOCK = (globalThis as unknown as GlobalWithNow)[NOW];

if (!CLOCK) {
CLOCK = (globalThis as unknown as GlobalWithNow)[NOW] = {
timestamp: 0,
};
}

/**
* Get the current timestamp.
*/
export function now(): number {
return NOW.timestamp;
return CLOCK.timestamp;
}

/**
* Increment the current timestamp, and return the new one.
*/
export function bump(): number {
NOW.timestamp = NOW.timestamp + 1;
CLOCK.timestamp = CLOCK.timestamp + 1;
return now();
}
60 changes: 33 additions & 27 deletions packages/peer/tests/constants.spec.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,52 @@
import { UNINITIALIZED } from "@starbeam/peer";
import { NOW, REACTIVE, UNINITIALIZED } from "@starbeam/peer";
import { describe, expect, test } from "vitest";

describe("UNINITALIZED", () => {
testSymbol(UNINITIALIZED, "UNINITIALIZED");
testSymbol(REACTIVE, "REACTIVE");
testSymbol(NOW, "NOW");
});

function testSymbol(symbol: symbol, description: string) {
test("is a symbol", () => {
expect(typeof UNINITIALIZED).toBe("symbol");
expect(UNINITIALIZED.description).toBe("starbeam.UNINITIALIZED");
expect(typeof symbol).toBe("symbol");
expect(symbol.description).toBe(`starbeam.${description}`);
});

test("is the same value each time (i.e. not an export let)", () => {
expect(UNINITIALIZED).toBe(UNINITIALIZED);
expect(symbol).toBe(symbol);
});

test("is registered at Symbol.for('starbeam.UNINITIALIZED')", () => {
expect(Symbol.for("starbeam.UNINITIALIZED")).toBe(UNINITIALIZED);
test(`is registered at Symbol.for('starbeam.${description}')`, () => {
expect(Symbol.for(`starbeam.${description}`)).toBe(symbol);
});

test("isn't one of the builtin symbols", () => {
expect(UNINITIALIZED).not.toBe(Symbol.iterator);
expect(UNINITIALIZED).not.toBe(Symbol.toStringTag);
expect(UNINITIALIZED).not.toBe(Symbol.unscopables);
expect(UNINITIALIZED).not.toBe(Symbol.hasInstance);
expect(UNINITIALIZED).not.toBe(Symbol.isConcatSpreadable);
expect(UNINITIALIZED).not.toBe(Symbol.match);
expect(UNINITIALIZED).not.toBe(Symbol.replace);
expect(UNINITIALIZED).not.toBe(Symbol.search);
expect(UNINITIALIZED).not.toBe(Symbol.species);
expect(UNINITIALIZED).not.toBe(Symbol.split);
expect(UNINITIALIZED).not.toBe(Symbol.toPrimitive);
expect(symbol).not.toBe(Symbol.iterator);
expect(symbol).not.toBe(Symbol.toStringTag);
expect(symbol).not.toBe(Symbol.unscopables);
expect(symbol).not.toBe(Symbol.hasInstance);
expect(symbol).not.toBe(Symbol.isConcatSpreadable);
expect(symbol).not.toBe(Symbol.match);
expect(symbol).not.toBe(Symbol.replace);
expect(symbol).not.toBe(Symbol.search);
expect(symbol).not.toBe(Symbol.species);
expect(symbol).not.toBe(Symbol.split);
expect(symbol).not.toBe(Symbol.toPrimitive);

// it's not node's inspect symbol
expect(UNINITIALIZED).not.toBe(Symbol.for("nodejs.util.inspect.custom"));
expect(symbol).not.toBe(Symbol.for("nodejs.util.inspect.custom"));

// other react symbols
expect(UNINITIALIZED).not.toBe(Symbol.for("react.element"));
expect(UNINITIALIZED).not.toBe(Symbol.for("react.forward_ref"));
expect(UNINITIALIZED).not.toBe(Symbol.for("react.fragment"));
expect(UNINITIALIZED).not.toBe(Symbol.for("react.profiler"));
expect(UNINITIALIZED).not.toBe(Symbol.for("react.provider"));
expect(UNINITIALIZED).not.toBe(Symbol.for("react.context"));
expect(UNINITIALIZED).not.toBe(Symbol.for("react.concurrent_mode"));
expect(symbol).not.toBe(Symbol.for("react.element"));
expect(symbol).not.toBe(Symbol.for("react.forward_ref"));
expect(symbol).not.toBe(Symbol.for("react.fragment"));
expect(symbol).not.toBe(Symbol.for("react.profiler"));
expect(symbol).not.toBe(Symbol.for("react.provider"));
expect(symbol).not.toBe(Symbol.for("react.context"));
expect(symbol).not.toBe(Symbol.for("react.concurrent_mode"));

// observable symbol, casting Symbol to avoid TS error
expect(UNINITIALIZED).not.toBe(Symbol.for("rxjs.internal.observable"));
expect(symbol).not.toBe(Symbol.for("rxjs.internal.observable"));
});
});
}