After the Page Closes: Why a Simulated System Is Designed This Way

10 min readAI translationRead the Chinese original

Machine-translated from the Chinese original.

If a simulation stops when its page closes, where does the intervening history come from on reopening? Three fictional interactive examples explore how shared state, clocks, reconstruction, random streams and stable IDs keep simulated data coherent.

Contents
  1. Making “coherent” specific
  2. Across views: why should three numbers agree?
  3. Time: where does the past come from after closing?
  4. Seeds: fixing a sequence does not fix the result
  5. Responsibilities that a real service still needs
  6. What does each choice protect?

When I asked AI to walk me through a mock-data implementation, I found it interesting. Records on the page could be acted on, and numbers and charts changed with them. I wanted to know how those pieces were connected.

I kept asking: does it run on a server or on my computer? Had I simply left my local web server running? If I close the browser page, does the data keep generating? When I reopen it, does it continue or start again? And how does another server hosting the website relate to this data?

Then I wanted to go further back: how did the person designing it arrive at this structure? Can every simulation use the same approach?

The meeting rooms and three interactive sketches below were made for this article. They are fictional teaching examples, not a reconstruction of the original project or a record of me carrying out the exercise.

Making “coherent” specific

I do not know the original author's actual order of thought. Here, I use a small example to work backwards from requirements and ask what each technical choice protects.

Suppose there are three meeting rooms. The page needs a schedule, a booking list and today's occupied hours. Users can book and cancel. Later, the in-memory data source should be replaceable with a real service. Showing changes over a period also requires more than a handful of sample records.

Together, these requirements create constraints. Add an hour to the schedule and the list and totals need to agree. A meeting can become ongoing only as the clock passes its start. Rebuild history a day later, and removing an earlier day from the window should not change every record's identity. “Behaving like a system” now has conditions we can check.

All three sketches use the same small model. Time is measured in fictional numbered days, with no real calendar dates or daylight-saving changes.

Part of the modelWhy it is needed
Rooms A, B and CA booking belongs to a specific room; clashes are checked within that room
ID, start and endThe schedule, list and cancellation action must be able to refer to the same booking
bookedAt and cancelledAtA generated plan is different from something that has already happened as of now
An injected nowEvery calculation uses the same present, and a particular moment can be reproduced
History and session additionsData that can be reconstructed is distinct from changes belonging only to this session

Across views: why should three numbers agree?

If the schedule draws random blocks, the list generates its own records and the occupancy figure draws another number, each part might look plausible. There is no reason for them to agree.

One authoritative set of records gives the three queries a common basis. Here, “today's occupancy” specifically means the duration of visible, uncancelled bookings in Room A between 09:00 and 18:00. The denominator is nine hours. The total is split at now into elapsed and future hours. These numbers come from the records rather than another random draw.

That is this example's metric definition. Real metrics may have different sources, denominators and filter ranges. They still need individual definitions; a number is not necessarily the row count of the currently displayed list.

Sharing data does not yet solve cached results. Queries return copies. A successful cancellation changes internal state, but another view's copy does not become newer by itself. The update path therefore needs validation, a state change, notification of the affected room, and a new query from the relevant views.

The first sketch separates those layers. Start with independently generated results. Switch to shared records and let only the view initiating a write re-query. Finally, enable subscriptions and compare all three versions after a write. You can also cancel from the list, then click the same booking in the stale schedule: its block is still visible, but internally it can no longer be cancelled.

Example 1 / 3 · Across views

Why do three views agree?

× Records or revisions differ: schedule 2 h · list 3 h · metric 5 h; revisions 0 / 0 / 0, store 0

Day 8 10:20 · The views read bookings visible at this fixed moment; changing the source resets the data.

Data source
Data source

Room A schedulev0

Line: now 10:20 · hatch: past

Today's listv0

A-d8-9 · 09:00–10:00Ended
A-d8-12 · 12:00–13:00Upcoming
A-d8-17 · 17:00–18:00Upcoming

Occupancyv0

5 / 9 hours occupied

Past 0.33 · Future 4.67

Made for this article: fictional data calculated only in this page’s memory; refreshing resets it.

Writes also protect relationships. Two active intervals in the same room overlap when a.start < b.end && b.start < a.end. Strict less-than lets one meeting start at precisely the moment another ends. A meeting that has started cannot be cancelled, and an empty slot in the past cannot be booked. Failure returns an explicit result, without changing records or sending a success notification.

The page can face a small interface:

type Result<T> = { ok: true; value: T } | { ok: false; error: string };
type Booking = { id: string; roomId: string; start: number; end: number };
interface BookingSource {
  list(room: string, from: number, to: number): Promise<Result<Booking[]>>;
  book(room: string, start: number, end: number): Promise<Result<Booking>>;
  cancel(id: string): Promise<Result<Booking>>;
  subscribe(onChange: (room: string) => void): () => void;
}

This is an interface sketch: an agreement about capabilities and return shapes, not a promise that every call sends an HTTP request. An implementation can read and write an array held in a factory's closure, returning copies through structuredClone. Views must receive the same instance to share its state. Calling the factory separately creates independent data sets.

Waiting, failure and notification also define the boundary around a data source. The page handles results without reaching directly into the generator's array. That boundary has a cost: queries, cache invalidation and subscription cleanup need to be maintained. Shared state alone does not finish the work of keeping views in step.

Time: where does the past come from after closing?

Back to the concrete question I asked: if closing the page stops the simulation, why might reopening show data for the intervening hours?

First, distinguish the server providing website files from the place executing the simulation. A remote server can deliver a website whose downloaded code generates data locally in the browser. A deployment address alone does not establish where a simulation lives.

In this example, no code keeps accumulating data for the session after the page closes. On reopening, its past is calculated again from the seed and the clock.

The model separates generating history from deciding what is visible now. The generator creates hypothetical booking facts by room, day and slot: when a booking is made, when it starts, and whether it will be cancelled. A projection then uses a single now to determine visible records and status. A booking is hidden until its booking time. A cancellation is shown only once it has happened. Otherwise, start and end determine whether a meeting is upcoming, ongoing or ended.

For example, suppose a booking is made at 11:30 on day 8 for a meeting starting at 14:00. It is invisible when the page opens at 10:20. Reopen at 13:20, calculate the same rules up to 13:20, and that upcoming booking can appear. No program needs to have written a log every minute while the page was closed.

const facts = generate(seed, window, rules);
const visible = project(facts, now);

The clock is supplied from outside; individual functions do not separately read system time. We can hold one moment fixed, compare results, then explicitly advance to another. Passing time does not itself execute code: a caller must still trigger reconstruction or state progression. Browsers can also throttle timers in background tabs, so counting timer callbacks is not a reliable substitute for elapsed time.

In the second sketch, the outer article and clock remain present while the simulation component is actually unmounted and remounted. Advance time to let the session add temporary bookings, or cancel an upcoming booking. Close the session, advance three hours, inspect generator and addition counts for that interval, then reopen and reconcile the results.

Example 2 / 3 · Time and lifecycle

After reopening, where does the past come from?

Session mounted · Day 8 10:20

Closing the session unmounts only this simulation. The article and tab stay open. Closing the tab would also lose this comparison snapshot.

Day 8 10:20
OpenDay 8 10:20

History 65 · visible 65 · generator calls 1 · live additions 0 · your cancellations 0

A-d8-15Upcoming
A-d8-17Upcoming
Made for this article: fictional data calculated only in this page’s memory; refreshing resets it.

Two kinds of data are deliberately separated. The historical base can be rebuilt from the seed and a new now. Temporary bookings and your cancellations form an overlay belonging to this session. Reopening may reveal more historical records or different statuses, while the old operations do not return with them.

Seeing the past again and saving the past are different promises. Preserving cancellations requires storing the current result, or storing actions and replaying them after reconstruction. Replay, in turn, needs a reliable ID for the booking being cancelled. The article's comparison snapshot is also temporary state held by its outer component; closing the entire tab would not preserve that evidence for this example.

Seeds: fixing a sequence does not fix the result

The explanation in that conversation initially connected a fixed seed with consistent data. It later added a qualification: a seed fixes the pseudorandom sequence. The final data also depends on how that sequence is consumed.

That distinction deserves a concrete example. This generator visits day, room and slot in order. It first draws for the presence of a booking. If one exists, it draws duration, booking lead time and whether it is cancelled. A cancellation requires another draw for its time. A two-hour booking covers the next slot, which takes no draws of its own.

Branch taken by a slotRandom draws consumed
Empty1
Booking without cancellation4
Booking with cancellation5
Covered by the previous booking, or skipped for maintenance0

With one stream shared by every room, skipping branches in Room A can move Room B to a different position in the sequence. B's rules have not changed, but its result can. Moving the window forward a day can have a similar effect: generating one fewer earlier day changes the draw positions that follow.

The third sketch compares generated facts for the same slots. It does not count a meeting ending as the clock advances as a change in random generation. Try moving the window and introducing maintenance in Room A, switch between global and split streams, and select a cell to see which draw positions it used before and after.

Example 3 / 3 · Seed and identity

Why can data change with a fixed seed?

Outside Room A, 59 of 72 slots changed.

Random stream
Random stream
Change
Change
Changed generated factsHatch: removed by maintenanceSelect or use arrows, then Enter for details
Room / day91011121314151617
A · d5
A · d6
A · d7
A · d8
B · d5
B · d6
B · d7
B · d8
C · d5
C · d6
C · d7
C · d8

Global ordinal IDs: 48 now point to another meeting · business keys: 0 redirected, 28 disappeared with their records, 27 appeared

Before · No booking
mock-world-4 draw 165 → [0.88]

After · No booking
mock-world-4 draw 107 → [0.60]

This compares complete generated facts, including future plans, not what is visible as of now or time-derived status. A business key locates a slot but does not freeze its properties. Split streams separate draw positions; business rules such as two-hour bookings can still affect neighbouring slots in the same room. Changing the algorithm, draw order, or generator version can change data under the same seed; save generated examples or version the generator when long-term stability matters.

Made for this article: fictional data calculated only in this page’s memory; refreshing resets it.

Splitting streams by key is one way to narrow the dependency:

const streamKey = `${seed}/${roomId}/${day}/${hour}`;
const random = createRandom(streamKey);

One room's branches no longer move the numbers another room draws. This does not make everything independent: a two-hour meeting still occupies its room's next slot, because that relationship belongs to the business rule. Changing the random algorithm, field draw order or generator version can also change results under the same seed. Examples that must remain fixed for a long time can store their generated results, or record the generator version and configuration as well.

IDs need separate attention. Number records by position, remove some from the front of the window, and a familiar ordinal may point to another meeting. Even with split streams preserving identical data in the overlapping range, the ordinal IDs can redirect. The sketch keeps both numbering schemes to separate these two problems.

A business key such as A-d8-11 identifies the 11:00 slot in Room A on day 8. It stabilises how identity is located, not duration, booking time or every other property. In real workflows allowing multiple records per slot, rescheduling or merging, this key would be insufficient and identity rules would need further design.

Responsibilities that a real service still needs

A consistent interface helps the page keep calling a data source in the same way. It does not automatically transfer an in-memory model's guarantees to a server.

Within one JavaScript runtime, a synchronous conflict check and write can finish without an intervening wait. Concurrent server requests may both pass a check. Protecting against overlapping bookings requires a transaction with appropriate locking or a database constraint. Permissions must be checked against a real session, records must actually be saved, and network failures must end loading and allow recovery. Changes made by other users also need polling or push to reach this page.

The first sketch permits new bookings only against a visible snapshot at a fixed clock. The second moves time and uses history projections, temporary additions and cancellations. These demonstrate separate boundaries; they do not combine into a booking service with every concurrency problem solved.

What does each choice protect?

I also asked whether every simulation could work this way. Looking back over the discussion, a useful starting point is to make the requirements specific. A fixed screenshot can use a few fixed records. Linked interactions, historical trends and reproducible problem states gradually introduce the need for more machinery.

Condition to protectChoice used hereObservable consequence of removing it
Agreement across schedule, list and metricShared records and explicitly defined queriesEach part looks plausible, but they disagree
New results after a writeNotify after success and re-query affected viewsData changes while the screen remains stale
A reproducible momentSupply the clock from outsideDifferent calculations use different presents
An explainable source for history on reopeningGenerate facts, then project at a momentReconstruction is confused with continued execution
Less coupling between unrelated dataSplit random streams by keyFewer draws in one place change another
Actions refer back to the same recordDesign stable identity separatelyThe ordinal survives but its target changes

This gets closer to what I was asking about how a design is arrived at: given a requirement, how do you identify relationships that must hold, then express them through interfaces, state, clocks and rules? The example can show one concrete derivation. The original author's actual choices still need their own account.