Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/src/Stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ export type Stack = {
transitionDuration: number;
globalTransitionState: "idle" | "loading" | "paused";
pausedEvents?: DomainEvent[];
events: DomainEvent[];
};
11 changes: 7 additions & 4 deletions core/src/activity-utils/makeStackReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
PausedEvent,
ResumedEvent,
} from "../event-types";
import type { Activity, Stack } from "../Stack";
import type { Stack } from "../Stack";
import { findTargetActivityIndices } from "./findTargetActivityIndices";
import { makeActivitiesReducer } from "./makeActivitiesReducer";
import { makeActivityReducer } from "./makeActivityReducer";
Expand Down Expand Up @@ -80,8 +80,8 @@ function withActivitiesReducer<T extends DomainEvent>(
};
}

function noop(stack: Stack) {
return stack;
function noop(stack: Stack, event: DomainEvent) {
return { ...stack, events: [...stack.events, event] };
}

export function makeStackReducer(context: { now: number; resumedAt?: number }) {
Expand All @@ -91,6 +91,7 @@ export function makeStackReducer(context: { now: number; resumedAt?: number }) {
return {
...stack,
transitionDuration: event.transitionDuration,
events: [...stack.events, event],
};
}, context),
),
Expand All @@ -110,6 +111,7 @@ export function makeStackReducer(context: { now: number; resumedAt?: number }) {
: null),
},
],
events: [...stack.events, event],
};
},
context,
Expand All @@ -120,13 +122,14 @@ export function makeStackReducer(context: { now: number; resumedAt?: number }) {
return {
...stack,
globalTransitionState: "paused",
events: [...stack.events, event],
};
}, context),
),
Resumed: withActivitiesReducer(
(stack: Stack, event: ResumedEvent): Stack => {
if (stack.globalTransitionState !== "paused" || !stack.pausedEvents) {
return stack;
return { ...stack, events: [...stack.events, event] };
}

const reducer = makeStackReducer({
Expand Down
1 change: 1 addition & 0 deletions core/src/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function aggregate(inputEvents: DomainEvent[], now: number): Stack {
globalTransitionState: "idle",
registeredActivities: [],
transitionDuration: 0,
events: [],
});

/**
Expand Down
1 change: 1 addition & 0 deletions core/src/interfaces/StackflowActions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
DomainEvent,
PausedEvent,
PoppedEvent,
PushedEvent,
Expand Down
7 changes: 7 additions & 0 deletions demo/src/stackflow/stackflow.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export const config = defineConfig({
activityName: "Main",
activityParams: {},
},
{
activityName: "Article",
activityParams: {
articleId: 60547101,
title: "울랄라",
},
},
],
},
loader: articleLoader,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createContext, useContext } from "react";
import type { NavigationProcessStatus } from "./NavigationProcess/NavigationProcess";

export const InitialSetupProcessStatusContext =
createContext<NavigationProcessStatus | null>(null);

export function useInitialSetupProcessStatus(): NavigationProcessStatus {
const status = useContext(InitialSetupProcessStatusContext);

if (!status) {
throw new Error("InitialSetupProcessStatusContext is not found");
}

return status;
}
24 changes: 24 additions & 0 deletions extensions/plugin-history-sync/src/Mutex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export class Mutex {
private latestlyBookedSession: Promise<void> = Promise.resolve();

acquire(): Promise<{ release: () => void }> {
return new Promise((resolveSessionHandle) => {
this.latestlyBookedSession = this.latestlyBookedSession.then(
() =>
new Promise((resolveSession) =>
resolveSessionHandle({ release: () => resolveSession() }),
),
);
});
}

async runExclusively<T>(thunk: () => Promise<T>): Promise<Awaited<T>> {
const { release } = await this.acquire();

try {
return await thunk();
} finally {
release();
}
}
}
30 changes: 30 additions & 0 deletions extensions/plugin-history-sync/src/NavigationEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {
DomainEvent,
PoppedEvent,
PushedEvent,
ReplacedEvent,
StepPoppedEvent,
StepPushedEvent,
StepReplacedEvent,
} from "@stackflow/core";

export type NavigationEvent =
| PushedEvent
| PoppedEvent
| ReplacedEvent
| StepPushedEvent
| StepPoppedEvent
| StepReplacedEvent;

export function isNavigationEvent(
event: DomainEvent,
): event is NavigationEvent {
return (
event.name === "Pushed" ||
event.name === "Popped" ||
event.name === "Replaced" ||
event.name === "StepPushed" ||
event.name === "StepPopped" ||
event.name === "StepReplaced"
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { NavigationEvent } from "NavigationEvent";
import type { PushedEvent, Stack, StepPushedEvent } from "@stackflow/core";
import {
type NavigationProcess,
NavigationProcessStatus,
} from "./NavigationProcess";

export class CompositNavigationProcess implements NavigationProcess {
private base: NavigationProcess;
private createDrived: (base: NavigationEvent[]) => NavigationProcess;
private derived: NavigationProcess | null;
private baseNavigationEvents: NavigationEvent[];

constructor(
base: NavigationProcess,
createDrived: (base: NavigationEvent[]) => NavigationProcess,
) {
this.base = base;
this.createDrived = createDrived;
this.derived = null;
this.baseNavigationEvents = [];
}

captureNavigationOpportunity(
stack: Stack | null,
navigationTime: number,
): (PushedEvent | StepPushedEvent)[] {
if (this.derived) {
return this.derived.captureNavigationOpportunity(stack, navigationTime);
}

const events = this.base.captureNavigationOpportunity(
stack,
navigationTime,
);

if (
events.length === 0 &&
this.base.getStatus() === NavigationProcessStatus.SUCCEEDED
) {
this.derived = this.createDrived(this.baseNavigationEvents);

return this.derived.captureNavigationOpportunity(stack, navigationTime);
}

this.baseNavigationEvents.push(...events);

return events;
}

getStatus(): NavigationProcessStatus {
const baseStatus = this.base.getStatus();

if (baseStatus === NavigationProcessStatus.SUCCEEDED) {
if (!this.derived) {
this.derived = this.createDrived(this.baseNavigationEvents);
}

const derivedStatus = this.derived.getStatus();

if (derivedStatus === NavigationProcessStatus.IDLE)
return NavigationProcessStatus.PROGRESS;

return derivedStatus;
}

return baseStatus;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { PushedEvent, Stack, StepPushedEvent } from "@stackflow/core";

export interface NavigationProcess {
captureNavigationOpportunity(
stack: Stack | null,
navigationTime: number,
): (PushedEvent | StepPushedEvent)[];

getStatus(): NavigationProcessStatus;
}

export const NavigationProcessStatus = {
IDLE: "idle",
PROGRESS: "progress",
SUCCEEDED: "succeeded",
FAILED: "failed",
} as const;

export type NavigationProcessStatus =
(typeof NavigationProcessStatus)[keyof typeof NavigationProcessStatus];

export function isTerminated(status: NavigationProcessStatus): boolean {
return (
status === NavigationProcessStatus.SUCCEEDED ||
status === NavigationProcessStatus.FAILED
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { isNavigationEvent, type NavigationEvent } from "NavigationEvent";
import type { PushedEvent, Stack, StepPushedEvent } from "@stackflow/core";
import {
isTerminated,
type NavigationProcess,
NavigationProcessStatus,
} from "./NavigationProcess";

export class SerialNavigationProcess implements NavigationProcess {
private status: NavigationProcessStatus;
private pendingNavigations: ((
navigationTime: number,
) => (PushedEvent | StepPushedEvent)[])[];
private dispatchedEvents: (PushedEvent | StepPushedEvent)[];
private baseNavigationEvents: NavigationEvent[];

constructor(
navigations: ((
navigationTime: number,
) => (PushedEvent | StepPushedEvent)[])[],
baseNavigationEvents: NavigationEvent[] = [],
) {
this.status =
navigations.length > 0
? NavigationProcessStatus.IDLE
: NavigationProcessStatus.SUCCEEDED;
this.pendingNavigations = navigations.slice();
this.dispatchedEvents = [];
this.baseNavigationEvents = baseNavigationEvents;
}

captureNavigationOpportunity(
stack: Stack | null,
navigationTime: number,
): (PushedEvent | StepPushedEvent)[] {
if (isTerminated(this.status)) return [];
if (stack !== null && stack.globalTransitionState !== "idle") return [];

if (this.pendingNavigations.length === 0) {
this.status = NavigationProcessStatus.SUCCEEDED;

return [];
}

const navigationHistory = stack
? stack.events.filter(isNavigationEvent)
: [];

if (
!(
this.verifyAllDispatchedEventsAreInNavigationHistory(
navigationHistory,
) && this.verifyNoUnknownNavigationEvents(navigationHistory)
)
) {
this.pendingNavigations = [];
this.status = NavigationProcessStatus.FAILED;

return [];
}

const nextNavigation = this.pendingNavigations.splice(0, 1);
const nextNavigationEvents = nextNavigation.flatMap((navigation) =>
navigation(navigationTime),
);

this.dispatchedEvents.push(...nextNavigationEvents);
this.status = NavigationProcessStatus.PROGRESS;

return nextNavigationEvents;
}

getStatus(): NavigationProcessStatus {
return this.status;
}

private verifyAllDispatchedEventsAreInNavigationHistory(
navigationHistory: NavigationEvent[],
): boolean {
return this.dispatchedEvents.every((event) =>
navigationHistory.some((e) => e.id === event.id),
);
}

private verifyNoUnknownNavigationEvents(
navigationHistory: NavigationEvent[],
): boolean {
return navigationHistory.every((event) =>
[...this.baseNavigationEvents, ...this.dispatchedEvents].some(
(e) => e.id === event.id,
),
);
}
}
Loading
Loading