@relayfile/core 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ import type { FileSemantics } from "./storage.js";
2
+ import type { WriteFileRequest } from "./files.js";
3
+ export declare const DEFAULT_FORK_TTL_SECONDS: number;
4
+ export declare const MAX_FORK_TTL_SECONDS: number;
5
+ export interface ForkOptions {
6
+ workspaceId: string;
7
+ proposalId: string;
8
+ ttlSeconds?: number;
9
+ }
10
+ export interface ForkHandle {
11
+ forkId: string;
12
+ proposalId: string;
13
+ workspaceId: string;
14
+ expiresAt: string;
15
+ parentRevision: string;
16
+ }
17
+ export interface ForkWriteFileRequest extends Omit<WriteFileRequest, "ifMatch"> {
18
+ contentType?: string;
19
+ content: string;
20
+ encoding?: string;
21
+ semantics?: FileSemantics;
22
+ }
23
+ export type ForkOverlayEntry = {
24
+ type: "write";
25
+ file: ForkWriteFileRequest;
26
+ revision: string;
27
+ } | {
28
+ type: "delete";
29
+ deletedAt: string;
30
+ };
31
+ export interface ForkState extends ForkHandle {
32
+ createdAt: string;
33
+ overlay: Record<string, ForkOverlayEntry>;
34
+ overlayRevisionCounter: number;
35
+ }
36
+ export interface CommitForkResponse {
37
+ revision: string;
38
+ writtenCount: number;
39
+ deletedCount: number;
40
+ }
41
+ export interface ParentMovedErrorPayload {
42
+ error: "parent_moved";
43
+ currentRevision: string;
44
+ }
45
+ export interface ForkExpiredErrorPayload {
46
+ error: "fork_expired";
47
+ }
48
+ export declare function normalizeForkTTLSeconds(ttlSeconds?: number): number;
49
+ export declare function computeForkExpiresAt(createdAt: string | Date, ttlSeconds?: number): string;
50
+ export declare function isForkExpired(fork: Pick<ForkState, "expiresAt">, now?: string | Date): boolean;
51
+ export declare function nextForkOverlayRevision(forkId: string, nextCounter: number): string;
52
+ export declare function createForkState(input: {
53
+ forkId: string;
54
+ workspaceId: string;
55
+ proposalId: string;
56
+ parentRevision: string;
57
+ createdAt?: string | Date;
58
+ ttlSeconds?: number;
59
+ }): ForkState;
60
+ export declare function writeForkOverlay(fork: ForkState, path: string, file: ForkWriteFileRequest): ForkOverlayEntry;
61
+ export declare function deleteForkOverlay(fork: ForkState, path: string, deletedAt?: string | Date): ForkOverlayEntry;
package/dist/forks.js ADDED
@@ -0,0 +1,62 @@
1
+ import { normalizePath } from "./files.js";
2
+ export const DEFAULT_FORK_TTL_SECONDS = 7 * 24 * 60 * 60;
3
+ export const MAX_FORK_TTL_SECONDS = 30 * 24 * 60 * 60;
4
+ export function normalizeForkTTLSeconds(ttlSeconds) {
5
+ if (ttlSeconds === undefined) {
6
+ return DEFAULT_FORK_TTL_SECONDS;
7
+ }
8
+ if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
9
+ return DEFAULT_FORK_TTL_SECONDS;
10
+ }
11
+ return Math.min(Math.floor(ttlSeconds), MAX_FORK_TTL_SECONDS);
12
+ }
13
+ export function computeForkExpiresAt(createdAt, ttlSeconds) {
14
+ const created = typeof createdAt === "string" ? new Date(createdAt) : createdAt;
15
+ const ttlMs = normalizeForkTTLSeconds(ttlSeconds) * 1000;
16
+ return new Date(created.getTime() + ttlMs).toISOString();
17
+ }
18
+ export function isForkExpired(fork, now = new Date()) {
19
+ const nowMs = typeof now === "string" ? Date.parse(now) : now.getTime();
20
+ return Number.isFinite(nowMs) && nowMs >= Date.parse(fork.expiresAt);
21
+ }
22
+ export function nextForkOverlayRevision(forkId, nextCounter) {
23
+ return `fork:${forkId}:${nextCounter}`;
24
+ }
25
+ export function createForkState(input) {
26
+ const createdAt = input.createdAt instanceof Date
27
+ ? input.createdAt.toISOString()
28
+ : input.createdAt ?? new Date().toISOString();
29
+ return {
30
+ forkId: input.forkId,
31
+ workspaceId: input.workspaceId,
32
+ proposalId: input.proposalId,
33
+ parentRevision: input.parentRevision,
34
+ createdAt,
35
+ expiresAt: computeForkExpiresAt(createdAt, input.ttlSeconds),
36
+ overlay: {},
37
+ overlayRevisionCounter: 0,
38
+ };
39
+ }
40
+ export function writeForkOverlay(fork, path, file) {
41
+ const nextCounter = fork.overlayRevisionCounter + 1;
42
+ const revision = nextForkOverlayRevision(fork.forkId, nextCounter);
43
+ const entry = {
44
+ type: "write",
45
+ file: {
46
+ ...file,
47
+ path: normalizePath(path),
48
+ },
49
+ revision,
50
+ };
51
+ fork.overlayRevisionCounter = nextCounter;
52
+ fork.overlay[normalizePath(path)] = entry;
53
+ return entry;
54
+ }
55
+ export function deleteForkOverlay(fork, path, deletedAt = new Date()) {
56
+ const entry = {
57
+ type: "delete",
58
+ deletedAt: deletedAt instanceof Date ? deletedAt.toISOString() : deletedAt,
59
+ };
60
+ fork.overlay[normalizePath(path)] = entry;
61
+ return entry;
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { DEFAULT_FORK_TTL_SECONDS, MAX_FORK_TTL_SECONDS, computeForkExpiresAt, createForkState, deleteForkOverlay, isForkExpired, nextForkOverlayRevision, normalizeForkTTLSeconds, writeForkOverlay, } from "./forks.js";
3
+ describe("fork helpers", () => {
4
+ it("normalizes ttl seconds with the documented default and max", () => {
5
+ expect(normalizeForkTTLSeconds()).toBe(DEFAULT_FORK_TTL_SECONDS);
6
+ expect(normalizeForkTTLSeconds(0)).toBe(DEFAULT_FORK_TTL_SECONDS);
7
+ expect(normalizeForkTTLSeconds(MAX_FORK_TTL_SECONDS + 1)).toBe(MAX_FORK_TTL_SECONDS);
8
+ });
9
+ it("computes expiresAt from creation time", () => {
10
+ expect(computeForkExpiresAt("2026-04-21T00:00:00.000Z", 60)).toBe("2026-04-21T00:01:00.000Z");
11
+ });
12
+ it("tracks overlay revisions without colliding with parent revisions", () => {
13
+ const fork = createForkState({
14
+ forkId: "fork_123",
15
+ workspaceId: "ws_1",
16
+ proposalId: "prop_1",
17
+ parentRevision: "rev_1",
18
+ createdAt: "2026-04-21T00:00:00.000Z",
19
+ });
20
+ const first = writeForkOverlay(fork, "docs/a.md", {
21
+ path: "docs/a.md",
22
+ content: "# a",
23
+ });
24
+ const second = writeForkOverlay(fork, "/docs/b.md", {
25
+ path: "/docs/b.md",
26
+ content: "# b",
27
+ });
28
+ expect(first).toMatchObject({ type: "write", revision: "fork:fork_123:1" });
29
+ expect(second).toMatchObject({ type: "write", revision: "fork:fork_123:2" });
30
+ expect(nextForkOverlayRevision("fork_123", 3)).toBe("fork:fork_123:3");
31
+ expect(Object.keys(fork.overlay)).toEqual(["/docs/a.md", "/docs/b.md"]);
32
+ });
33
+ it("stores delete tombstones by normalized path", () => {
34
+ const fork = createForkState({
35
+ forkId: "fork_123",
36
+ workspaceId: "ws_1",
37
+ proposalId: "prop_1",
38
+ parentRevision: "rev_1",
39
+ });
40
+ deleteForkOverlay(fork, "docs/a.md", "2026-04-21T00:00:00.000Z");
41
+ expect(fork.overlay["/docs/a.md"]).toEqual({
42
+ type: "delete",
43
+ deletedAt: "2026-04-21T00:00:00.000Z",
44
+ });
45
+ });
46
+ it("detects expired forks", () => {
47
+ const fork = createForkState({
48
+ forkId: "fork_123",
49
+ workspaceId: "ws_1",
50
+ proposalId: "prop_1",
51
+ parentRevision: "rev_1",
52
+ createdAt: "2026-04-21T00:00:00.000Z",
53
+ ttlSeconds: 60,
54
+ });
55
+ expect(isForkExpired(fork, "2026-04-21T00:00:59.000Z")).toBe(false);
56
+ expect(isForkExpired(fork, "2026-04-21T00:01:00.000Z")).toBe(true);
57
+ });
58
+ });
package/dist/index.d.ts CHANGED
@@ -16,3 +16,4 @@ export * from "./writeback.js";
16
16
  export * from "./webhooks.js";
17
17
  export * from "./export.js";
18
18
  export * from "./dedup.js";
19
+ export * from "./forks.js";
package/dist/index.js CHANGED
@@ -16,3 +16,4 @@ export * from "./writeback.js";
16
16
  export * from "./webhooks.js";
17
17
  export * from "./export.js";
18
18
  export * from "./dedup.js";
19
+ export * from "./forks.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Shared business logic for relayfile — file operations, ACL, queries, events, and writeback lifecycle",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,6 +14,7 @@
14
14
  "prepublishOnly": "npm run build"
15
15
  },
16
16
  "devDependencies": {
17
+ "@types/node": "^22.0.0",
17
18
  "typescript": "^5.7.3",
18
19
  "vitest": "^3.0.0"
19
20
  },