2023-06-03 08:59:04 +00:00
|
|
|
## API Report File for "@tldraw/store"
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
2023-06-20 13:31:26 +00:00
|
|
|
import { Atom } from '@tldraw/state';
|
|
|
|
import { Computed } from '@tldraw/state';
|
2024-04-15 12:53:42 +00:00
|
|
|
import { Result } from '@tldraw/utils';
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// @public
|
|
|
|
export type AllRecords<T extends Store<any>> = ExtractR<ExtractRecordType<T>>;
|
|
|
|
|
|
|
|
// @public
|
2023-05-24 11:25:41 +00:00
|
|
|
export function assertIdType<R extends UnknownRecord>(id: string | undefined, type: RecordType<R, any>): asserts id is IdOf<R>;
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// @public
|
2023-06-03 20:46:53 +00:00
|
|
|
export interface BaseRecord<TypeName extends string, Id extends RecordId<UnknownRecord>> {
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
2023-05-24 11:25:41 +00:00
|
|
|
readonly id: Id;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
|
|
|
readonly typeName: TypeName;
|
|
|
|
}
|
|
|
|
|
|
|
|
// @public
|
|
|
|
export type CollectionDiff<T> = {
|
|
|
|
added?: Set<T>;
|
|
|
|
removed?: Set<T>;
|
|
|
|
};
|
|
|
|
|
|
|
|
// @public
|
2023-05-24 11:25:41 +00:00
|
|
|
export type ComputedCache<Data, R extends UnknownRecord> = {
|
|
|
|
get(id: IdOf<R>): Data | undefined;
|
2023-04-25 11:01:25 +00:00
|
|
|
};
|
|
|
|
|
2024-05-09 09:48:01 +00:00
|
|
|
// @public
|
|
|
|
export function createComputedCache<Context extends StoreContext<any>, Result, Record extends ContextRecordType<Context> = ContextRecordType<Context>>(name: string, derive: (context: Context, record: Record) => Result | undefined, isEqual?: (a: Record, b: Record) => boolean): {
|
|
|
|
get(context: Context, id: IdOf<Record>): Result | undefined;
|
|
|
|
};
|
|
|
|
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
// @internal (undocumented)
|
|
|
|
export function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R>;
|
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
// @public
|
Bindings (#3326)
First draft of the new bindings API. We'll follow this up with some API
refinements, tests, documentation, and examples.
Bindings are a new record type for establishing relationships between
two shapes so they can update at the same time.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `feature` — New feature
### Release Notes
#### Breaking changes
- The `start` and `end` properties on `TLArrowShape` no longer have
`type: point | binding`. Instead, they're always a point, which may be
out of date if a binding exists. To check for & retrieve arrow bindings,
use `getArrowBindings(editor, shape)` instead.
- `getArrowTerminalsInArrowSpace` must be passed a `TLArrowBindings` as
a third argument: `getArrowTerminalsInArrowSpace(editor, shape,
getArrowBindings(editor, shape))`
- The following types have been renamed:
- `ShapeProps` -> `RecordProps`
- `ShapePropsType` -> `RecordPropsType`
- `TLShapePropsMigrations` -> `TLPropsMigrations`
- `SchemaShapeInfo` -> `SchemaPropsInfo`
---------
Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
2024-05-08 12:37:31 +00:00
|
|
|
export function createMigrationIds<const ID extends string, const Versions extends Record<string, number>>(sequenceId: ID, versions: Versions): {
|
2024-04-15 12:53:42 +00:00
|
|
|
[K in keyof Versions]: `${ID}/${Versions[K]}`;
|
|
|
|
};
|
|
|
|
|
|
|
|
// @public
|
|
|
|
export function createMigrationSequence({ sequence, sequenceId, retroactive, }: {
|
|
|
|
retroactive?: boolean;
|
|
|
|
sequence: Array<Migration | StandaloneDependsOn>;
|
|
|
|
sequenceId: string;
|
|
|
|
}): MigrationSequence;
|
|
|
|
|
|
|
|
// @internal (undocumented)
|
|
|
|
export function createRecordMigrationSequence(opts: {
|
|
|
|
filter?: (record: UnknownRecord) => boolean;
|
|
|
|
recordType: string;
|
|
|
|
retroactive?: boolean;
|
|
|
|
sequence: Omit<Extract<Migration, {
|
|
|
|
scope: 'record';
|
|
|
|
}>, 'scope'>[];
|
|
|
|
sequenceId: string;
|
|
|
|
}): MigrationSequence;
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// @public
|
2023-05-24 11:25:41 +00:00
|
|
|
export function createRecordType<R extends UnknownRecord>(typeName: R['typeName'], config: {
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
ephemeralKeys?: {
|
|
|
|
readonly [K in Exclude<keyof R, 'id' | 'typeName'>]: boolean;
|
|
|
|
};
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
scope: RecordScope;
|
2024-04-15 12:53:42 +00:00
|
|
|
validator?: StoreValidator<R>;
|
2023-04-25 11:01:25 +00:00
|
|
|
}): RecordType<R, keyof Omit<R, 'id' | 'typeName'>>;
|
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
// @public @deprecated (undocumented)
|
|
|
|
export function defineMigrations(opts: {
|
|
|
|
currentVersion?: number;
|
|
|
|
firstVersion?: number;
|
|
|
|
migrators?: Record<number, LegacyMigration>;
|
2023-04-25 11:01:25 +00:00
|
|
|
subTypeKey?: string;
|
2024-04-15 12:53:42 +00:00
|
|
|
subTypeMigrations?: Record<string, LegacyBaseMigrationsInfo>;
|
|
|
|
}): LegacyMigrations;
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// @public
|
|
|
|
export function devFreeze<T>(object: T): T;
|
|
|
|
|
|
|
|
// @public
|
2023-05-24 11:25:41 +00:00
|
|
|
export type HistoryEntry<R extends UnknownRecord = UnknownRecord> = {
|
2023-04-25 11:01:25 +00:00
|
|
|
changes: RecordsDiff<R>;
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
source: ChangeSource;
|
2023-04-25 11:01:25 +00:00
|
|
|
};
|
|
|
|
|
2023-05-24 11:25:41 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export type IdOf<R extends UnknownRecord> = R['id'];
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// @internal
|
|
|
|
export class IncrementalSetConstructor<T> {
|
|
|
|
constructor(
|
|
|
|
previousValue: Set<T>);
|
|
|
|
// @public
|
|
|
|
add(item: T): void;
|
|
|
|
// @public
|
|
|
|
get(): {
|
|
|
|
diff: CollectionDiff<T>;
|
2024-04-15 12:53:42 +00:00
|
|
|
value: Set<T>;
|
2023-04-25 11:01:25 +00:00
|
|
|
} | undefined;
|
|
|
|
// @public
|
|
|
|
remove(item: T): void;
|
|
|
|
}
|
|
|
|
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
// @internal
|
|
|
|
export function isRecordsDiffEmpty<T extends UnknownRecord>(diff: RecordsDiff<T>): boolean;
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// @public (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
export type LegacyMigration<Before = any, After = any> = {
|
|
|
|
down: (newState: After) => Before;
|
|
|
|
up: (oldState: Before) => After;
|
|
|
|
};
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// @public (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
export interface LegacyMigrations extends LegacyBaseMigrationsInfo {
|
|
|
|
// (undocumented)
|
|
|
|
subTypeKey?: string;
|
|
|
|
// (undocumented)
|
|
|
|
subTypeMigrations?: Record<string, LegacyBaseMigrationsInfo>;
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// @public (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
export type Migration = {
|
|
|
|
readonly dependsOn?: readonly MigrationId[] | undefined;
|
|
|
|
readonly id: MigrationId;
|
|
|
|
} & ({
|
|
|
|
readonly down?: (newState: SerializedStore<UnknownRecord>) => SerializedStore<UnknownRecord> | void;
|
|
|
|
readonly scope: 'store';
|
|
|
|
readonly up: (oldState: SerializedStore<UnknownRecord>) => SerializedStore<UnknownRecord> | void;
|
|
|
|
} | {
|
|
|
|
readonly down?: (newState: UnknownRecord) => UnknownRecord | void;
|
|
|
|
readonly filter?: (record: UnknownRecord) => boolean;
|
|
|
|
readonly scope: 'record';
|
|
|
|
readonly up: (oldState: UnknownRecord) => UnknownRecord | void;
|
|
|
|
});
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// @public (undocumented)
|
|
|
|
export enum MigrationFailureReason {
|
|
|
|
// (undocumented)
|
|
|
|
IncompatibleSubtype = "incompatible-subtype",
|
|
|
|
// (undocumented)
|
|
|
|
MigrationError = "migration-error",
|
|
|
|
// (undocumented)
|
|
|
|
TargetVersionTooNew = "target-version-too-new",
|
|
|
|
// (undocumented)
|
|
|
|
TargetVersionTooOld = "target-version-too-old",
|
|
|
|
// (undocumented)
|
|
|
|
UnknownType = "unknown-type",
|
|
|
|
// (undocumented)
|
|
|
|
UnrecognizedSubtype = "unrecognized-subtype"
|
|
|
|
}
|
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export type MigrationId = `${string}/${number}`;
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export type MigrationResult<T> = {
|
|
|
|
reason: MigrationFailureReason;
|
2024-04-15 12:53:42 +00:00
|
|
|
type: 'error';
|
2023-04-25 11:01:25 +00:00
|
|
|
} | {
|
|
|
|
type: 'success';
|
|
|
|
value: T;
|
|
|
|
};
|
|
|
|
|
|
|
|
// @public (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
export interface MigrationSequence {
|
|
|
|
retroactive: boolean;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
sequence: Migration[];
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
sequenceId: string;
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
// @internal (undocumented)
|
|
|
|
export function parseMigrationId(id: MigrationId): {
|
|
|
|
sequenceId: string;
|
|
|
|
version: number;
|
|
|
|
};
|
|
|
|
|
2023-06-03 20:46:53 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export type RecordId<R extends UnknownRecord> = string & {
|
|
|
|
__type__: R;
|
|
|
|
};
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// @public
|
2023-05-24 11:25:41 +00:00
|
|
|
export type RecordsDiff<R extends UnknownRecord> = {
|
|
|
|
added: Record<IdOf<R>, R>;
|
|
|
|
removed: Record<IdOf<R>, R>;
|
2024-04-15 12:53:42 +00:00
|
|
|
updated: Record<IdOf<R>, [from: R, to: R]>;
|
2023-04-25 11:01:25 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// @public
|
2023-05-24 11:25:41 +00:00
|
|
|
export class RecordType<R extends UnknownRecord, RequiredProperties extends keyof Omit<R, 'id' | 'typeName'>> {
|
2023-04-25 11:01:25 +00:00
|
|
|
constructor(
|
|
|
|
typeName: R['typeName'], config: {
|
|
|
|
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>;
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
readonly ephemeralKeys?: {
|
|
|
|
readonly [K in Exclude<keyof R, 'id' | 'typeName'>]: boolean;
|
|
|
|
};
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
readonly scope?: RecordScope;
|
2024-04-15 12:53:42 +00:00
|
|
|
readonly validator?: StoreValidator<R>;
|
2023-04-25 11:01:25 +00:00
|
|
|
});
|
|
|
|
clone(record: R): R;
|
|
|
|
create(properties: Pick<R, RequiredProperties> & Omit<Partial<R>, RequiredProperties>): R;
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
// @deprecated
|
2023-05-24 11:25:41 +00:00
|
|
|
createCustomId(id: string): IdOf<R>;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
|
|
|
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>;
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
createId(customUniquePart?: string): IdOf<R>;
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
// (undocumented)
|
|
|
|
readonly ephemeralKeys?: {
|
|
|
|
readonly [K in Exclude<keyof R, 'id' | 'typeName'>]: boolean;
|
|
|
|
};
|
|
|
|
// (undocumented)
|
|
|
|
readonly ephemeralKeySet: ReadonlySet<string>;
|
2023-05-24 11:25:41 +00:00
|
|
|
isId(id?: string): id is IdOf<R>;
|
|
|
|
isInstance: (record?: UnknownRecord) => record is R;
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
parseId(id: IdOf<R>): string;
|
derived presence state (#1204)
This PR adds
- A new `TLInstancePresence` record type, to collect info about the
presence state in a particular instance of the editor. This will
eventually be used to sync presence data instead of sending
instance-only state across the wire.
- **Record Scopes**
`RecordType` now has a `scope` property which can be one of three
things:
- `document`: the record belongs to the document and should be synced
and persisted freely. Currently: `TLDocument`, `TLPage`, `TLShape`, and
`TLAsset`
- `instance`: the record belongs to a single instance of the store and
should not be synced at all. It should not be persisted directly in most
cases, but rather compiled into a kind of 'instance configuration' to
store alongside the local document data so that when reopening the
associated document it can remember some of the previous instance state.
Currently: `TLInstance`, `TLInstancePageState`, `TLCamera`, `TLUser`,
`TLUserDocument`, `TLUserPresence`
- `presence`: the record belongs to a single instance of the store and
should not be persisted, but may be synced using the special presence
sync protocol. Currently just `TLInstancePresence`
This sets us up for the following changes, which are gonna be pretty
high-impact in terms of integrating tldraw into existing systems:
- Removing `instanceId` as a config option. Each instance gets a
randomly generated ID.
- We'd replace it with an `instanceConfig` option that has stuff like
selectedIds, camera positions, and so on. Then it's up to library users
to get and reinstate the instance config at persistence boundaries.
- Removing `userId` as config option, and removing the `TLUser` type
altogether.
- We might need to revisit when doing auth-enabled features like locking
shapes, but I suspect that will be separate.
2023-04-27 18:03:19 +00:00
|
|
|
// (undocumented)
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
readonly scope: RecordScope;
|
2023-04-25 11:01:25 +00:00
|
|
|
readonly typeName: R['typeName'];
|
2024-02-20 12:35:25 +00:00
|
|
|
validate(record: unknown, recordBefore?: R): R;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
2024-02-20 12:35:25 +00:00
|
|
|
readonly validator: StoreValidator<R>;
|
2023-04-25 11:01:25 +00:00
|
|
|
withDefaultProperties<DefaultProps extends Omit<Partial<R>, 'id' | 'typeName'>>(createDefaultProperties: () => DefaultProps): RecordType<R, Exclude<RequiredProperties, keyof DefaultProps>>;
|
|
|
|
}
|
|
|
|
|
|
|
|
// @public (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
export function reverseRecordsDiff(diff: RecordsDiff<any>): RecordsDiff<any>;
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// @public (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
export type SerializedSchema = SerializedSchemaV1 | SerializedSchemaV2;
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// @public (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
export interface SerializedSchemaV1 {
|
2023-04-25 11:01:25 +00:00
|
|
|
recordVersions: Record<string, {
|
|
|
|
subTypeKey: string;
|
2024-04-15 12:53:42 +00:00
|
|
|
subTypeVersions: Record<string, number>;
|
|
|
|
version: number;
|
2023-04-25 11:01:25 +00:00
|
|
|
} | {
|
|
|
|
version: number;
|
|
|
|
}>;
|
2024-04-15 12:53:42 +00:00
|
|
|
schemaVersion: 1;
|
2023-04-25 11:01:25 +00:00
|
|
|
storeVersion: number;
|
|
|
|
}
|
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export interface SerializedSchemaV2 {
|
|
|
|
// (undocumented)
|
|
|
|
schemaVersion: 2;
|
|
|
|
// (undocumented)
|
|
|
|
sequences: {
|
|
|
|
[sequenceId: string]: number;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-06-27 12:25:55 +00:00
|
|
|
// @public
|
|
|
|
export type SerializedStore<R extends UnknownRecord> = Record<IdOf<R>, R>;
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// @public
|
2023-05-24 11:25:41 +00:00
|
|
|
export function squashRecordDiffs<T extends UnknownRecord>(diffs: RecordsDiff<T>[]): RecordsDiff<T>;
|
2023-04-25 11:01:25 +00:00
|
|
|
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
// @internal
|
|
|
|
export function squashRecordDiffsMutable<T extends UnknownRecord>(target: RecordsDiff<T>, diffs: RecordsDiff<T>[]): void;
|
|
|
|
|
Bindings (#3326)
First draft of the new bindings API. We'll follow this up with some API
refinements, tests, documentation, and examples.
Bindings are a new record type for establishing relationships between
two shapes so they can update at the same time.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `feature` — New feature
### Release Notes
#### Breaking changes
- The `start` and `end` properties on `TLArrowShape` no longer have
`type: point | binding`. Instead, they're always a point, which may be
out of date if a binding exists. To check for & retrieve arrow bindings,
use `getArrowBindings(editor, shape)` instead.
- `getArrowTerminalsInArrowSpace` must be passed a `TLArrowBindings` as
a third argument: `getArrowTerminalsInArrowSpace(editor, shape,
getArrowBindings(editor, shape))`
- The following types have been renamed:
- `ShapeProps` -> `RecordProps`
- `ShapePropsType` -> `RecordPropsType`
- `TLShapePropsMigrations` -> `TLPropsMigrations`
- `SchemaShapeInfo` -> `SchemaPropsInfo`
---------
Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
2024-05-08 12:37:31 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export type StandaloneDependsOn = {
|
|
|
|
readonly dependsOn: readonly MigrationId[];
|
|
|
|
};
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// @public
|
2023-05-24 11:25:41 +00:00
|
|
|
export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
2023-04-25 11:01:25 +00:00
|
|
|
constructor(config: {
|
|
|
|
schema: StoreSchema<R, Props>;
|
2024-04-15 12:53:42 +00:00
|
|
|
initialData?: SerializedStore<R>;
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
id?: string;
|
2023-04-25 11:01:25 +00:00
|
|
|
props: Props;
|
|
|
|
});
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
// @internal (undocumented)
|
|
|
|
addHistoryInterceptor(fn: (entry: HistoryEntry<R>, source: ChangeSource) => void): () => void;
|
2023-04-25 11:01:25 +00:00
|
|
|
allRecords: () => R[];
|
|
|
|
// (undocumented)
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
applyDiff(diff: RecordsDiff<R>, { runCallbacks, ignoreEphemeralKeys, }?: {
|
|
|
|
ignoreEphemeralKeys?: boolean;
|
|
|
|
runCallbacks?: boolean;
|
|
|
|
}): void;
|
|
|
|
// @internal (undocumented)
|
|
|
|
atomic<T>(fn: () => T, runCallbacks?: boolean): T;
|
2023-04-25 11:01:25 +00:00
|
|
|
clear: () => void;
|
2024-05-09 09:48:01 +00:00
|
|
|
createComputedCache: <Result, Record extends R = R>(name: string, derive: (record: Record) => Result | undefined, isEqual?: ((a: Record, b: Record) => boolean) | undefined) => ComputedCache<Result, Record>;
|
|
|
|
createSelectedComputedCache: <Selection, Result, Record extends R = R>(name: string, selector: (record: Record) => Selection | undefined, derive: (input: Selection) => Result | undefined) => ComputedCache<Result, Record>;
|
2023-04-25 11:01:25 +00:00
|
|
|
// @internal (undocumented)
|
|
|
|
ensureStoreIsUsable(): void;
|
|
|
|
extractingChanges(fn: () => void): RecordsDiff<R>;
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
filterChangesByScope(change: RecordsDiff<R>, scope: RecordScope): {
|
|
|
|
added: { [K in IdOf<R>]: R; };
|
|
|
|
removed: { [K in IdOf<R>]: R; };
|
2024-04-15 12:53:42 +00:00
|
|
|
updated: { [K_1 in IdOf<R>]: [from: R, to: R]; };
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
} | null;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
|
|
|
_flushHistory(): void;
|
2023-05-24 11:25:41 +00:00
|
|
|
get: <K extends IdOf<R>>(id: K) => RecFromId<K> | undefined;
|
2023-06-27 12:25:55 +00:00
|
|
|
getSnapshot(scope?: 'all' | RecordScope): StoreSnapshot<R>;
|
2023-05-24 11:25:41 +00:00
|
|
|
has: <K extends IdOf<R>>(id: K) => boolean;
|
2023-04-25 11:01:25 +00:00
|
|
|
readonly history: Atom<number, RecordsDiff<R>>;
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
readonly id: string;
|
2023-04-25 11:01:25 +00:00
|
|
|
// @internal (undocumented)
|
|
|
|
isPossiblyCorrupted(): boolean;
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
listen: (onHistory: StoreListener<R>, filters?: Partial<StoreListenerFilters>) => () => void;
|
2023-06-27 12:25:55 +00:00
|
|
|
loadSnapshot(snapshot: StoreSnapshot<R>): void;
|
2023-04-25 11:01:25 +00:00
|
|
|
// @internal (undocumented)
|
|
|
|
markAsPossiblyCorrupted(): void;
|
|
|
|
mergeRemoteChanges: (fn: () => void) => void;
|
2023-09-08 17:04:53 +00:00
|
|
|
migrateSnapshot(snapshot: StoreSnapshot<R>): StoreSnapshot<R>;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
|
|
|
readonly props: Props;
|
|
|
|
put: (records: R[], phaseOverride?: 'initialize') => void;
|
|
|
|
readonly query: StoreQueries<R>;
|
2023-05-24 11:25:41 +00:00
|
|
|
remove: (ids: IdOf<R>[]) => void;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
|
|
|
readonly schema: StoreSchema<R, Props>;
|
Independent instance state persistence (#1493)
This PR
- Removes UserDocumentRecordType
- moving isSnapMode to user preferences
- moving isGridMode and isPenMode to InstanceRecordType
- deleting the other properties which are no longer needed.
- Creates a separate pipeline for persisting instance state.
Previously the instance state records were stored alongside the document
state records, and in order to load the state for a particular instance
(in our case, a particular tab) you needed to pass the 'instanceId'
prop. This prop ended up totally pervading the public API and people ran
into all kinds of issues with it, e.g. using the same instance id in
multiple editor instances.
There was also an issue whereby it was hard for us to clean up old
instance state so the idb table ended up bloating over time.
This PR makes it so that rather than passing an instanceId, you load the
instance state yourself while creating the store. It provides tools to
make that easy.
- Undoes the assumption that we might have more than one instance's
state in the store.
- Like `document`, `instance` now has a singleton id
`instance:instance`.
- Page state ids and camera ids are no longer random, but rather derive
from the page they belong to. This is like having a foreign primary key
in SQL databases. It's something i'd love to support fully as part of
the RecordType/Store api.
Tests to do
- [x] Test Migrations
- [x] Test Store.listen filtering
- [x] Make type sets in Store public and readonly
- [x] Test RecordType.createId
- [x] Test Instance state snapshot loading/exporting
- [x] Manual test File I/O
- [x] Manual test Vscode extension with multiple tabs
- [x] Audit usages of store.query
- [x] Audit usages of changed types: InstanceRecordType, 'instance',
InstancePageStateRecordType, 'instance_page_state', 'user_document',
'camera', CameraRecordType, InstancePresenceRecordType,
'instance_presence'
- [x] Test user preferences
- [x] Manual test isSnapMode and isGridMode and isPenMode
- [ ] Test indexedDb functions
- [x] Add instanceId stuff back
### Change Type
- [x] `major` — Breaking Change
### Test Plan
1. Add a step-by-step description of how to test your PR here.
2.
- [ ] Unit Tests
- [ ] Webdriver tests
### Release Notes
- Add a brief release note for your PR here.
2023-06-05 14:11:07 +00:00
|
|
|
// (undocumented)
|
|
|
|
readonly scopedTypes: {
|
|
|
|
readonly [K in RecordScope]: ReadonlySet<R['typeName']>;
|
|
|
|
};
|
2023-06-27 12:25:55 +00:00
|
|
|
serialize: (scope?: 'all' | RecordScope) => SerializedStore<R>;
|
2024-05-14 09:42:41 +00:00
|
|
|
// (undocumented)
|
|
|
|
readonly sideEffects: StoreSideEffects<R>;
|
2023-05-24 11:25:41 +00:00
|
|
|
unsafeGetWithoutCapture: <K extends IdOf<R>>(id: K) => RecFromId<K> | undefined;
|
|
|
|
update: <K extends IdOf<R>>(id: K, updater: (record: RecFromId<K>) => RecFromId<K>) => void;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
|
|
|
validate(phase: 'createRecord' | 'initialize' | 'tests' | 'updateRecord'): void;
|
|
|
|
}
|
|
|
|
|
2024-05-14 09:42:41 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export type StoreAfterChangeHandler<R extends UnknownRecord> = (prev: R, next: R, source: 'remote' | 'user') => void;
|
|
|
|
|
|
|
|
// @public (undocumented)
|
|
|
|
export type StoreAfterCreateHandler<R extends UnknownRecord> = (record: R, source: 'remote' | 'user') => void;
|
|
|
|
|
|
|
|
// @public (undocumented)
|
|
|
|
export type StoreAfterDeleteHandler<R extends UnknownRecord> = (record: R, source: 'remote' | 'user') => void;
|
|
|
|
|
|
|
|
// @public (undocumented)
|
|
|
|
export type StoreBeforeChangeHandler<R extends UnknownRecord> = (prev: R, next: R, source: 'remote' | 'user') => R;
|
|
|
|
|
|
|
|
// @public (undocumented)
|
|
|
|
export type StoreBeforeCreateHandler<R extends UnknownRecord> = (record: R, source: 'remote' | 'user') => R;
|
|
|
|
|
|
|
|
// @public (undocumented)
|
|
|
|
export type StoreBeforeDeleteHandler<R extends UnknownRecord> = (record: R, source: 'remote' | 'user') => false | void;
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export type StoreError = {
|
|
|
|
error: Error;
|
2024-04-15 12:53:42 +00:00
|
|
|
isExistingValidationIssue: boolean;
|
2023-04-25 11:01:25 +00:00
|
|
|
phase: 'createRecord' | 'initialize' | 'tests' | 'updateRecord';
|
|
|
|
recordAfter: unknown;
|
2024-04-15 12:53:42 +00:00
|
|
|
recordBefore?: unknown;
|
2023-04-25 11:01:25 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// @public
|
2023-05-24 11:25:41 +00:00
|
|
|
export type StoreListener<R extends UnknownRecord> = (entry: HistoryEntry<R>) => void;
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-05-14 09:42:41 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export type StoreOperationCompleteHandler = (source: 'remote' | 'user') => void;
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// @public (undocumented)
|
2023-05-24 11:25:41 +00:00
|
|
|
export class StoreSchema<R extends UnknownRecord, P = unknown> {
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
2023-05-24 11:25:41 +00:00
|
|
|
static create<R extends UnknownRecord, P = unknown>(types: {
|
2023-04-25 11:01:25 +00:00
|
|
|
[TypeName in R['typeName']]: {
|
|
|
|
createId: any;
|
|
|
|
};
|
|
|
|
}, options?: StoreSchemaOptions<R, P>): StoreSchema<R, P>;
|
2023-05-12 11:39:36 +00:00
|
|
|
// @internal (undocumented)
|
|
|
|
createIntegrityChecker(store: Store<R, P>): (() => void) | undefined;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
getMigrationsSince(persistedSchema: SerializedSchema): Result<Migration[], string>;
|
Automatic undo/redo (#3364)
Our undo-redo system before this diff is based on commands. A command
is:
- A function that produces some data required to perform and undo a
change
- A function that actually performs the change, based on the data
- Another function that undoes the change, based on the data
- Optionally, a function to _redo_ the change, although in practice we
never use this
Each command that gets run is added to the undo/redo stack unless it
says it shouldn't be.
This diff replaces this system of commands with a new one where all
changes to the store are automatically recorded in the undo/redo stack.
You can imagine the new history manager like a tape recorder - it
automatically records everything that happens to the store in a special
diff, unless you "pause" the recording and ask it not to. Undo and redo
rewind/fast-forward the tape to certain marks.
As the command concept is gone, the things that were commands are now
just functions that manipulate the store.
One other change here is that the store's after-phase callbacks (and the
after-phase side-effects as a result) are now batched up and called at
the end of certain key operations. For example, `applyDiff` would
previously call all the `afterCreate` callbacks before making any
removals from the diff. Now, it (and anything else that uses
`store.atomic(fn)` will defer firing any after callbacks until the end
of an operation. before callbacks are still called part-way through
operations.
## Design options
Automatic recording is a fairly large big semantic change, particularly
to the standalone `store.put`/`store.remove` etc. commands. We could
instead make not-recording the default, and make recording opt-in
instead. However, I think auto-record-by-default is the right choice for
a few reasons:
1. Switching to a recording-based vs command-based undo-redo model is
fundamentally a big semantic change. In the past, `store.put` etc. were
always ignored. Now, regardless of whether we choose record-by-default
or ignore-by-default, the behaviour of `store.put` is _context_
dependant.
2. Switching to ignore-by-default means that either our commands don't
record undo/redo history any more (unless wrapped in
`editor.history.record`, a far larger semantic change) or they have to
always-record/all accept a history options bag. If we choose
always-record, we can't use commands within `history.ignore` as they'll
start recording again. If we choose the history options bag, we have to
accept those options in 10s of methods - basically the entire `Editor`
api surface.
Overall, given that some breaking semantic change here is unavoidable, I
think that record-by-default hits the right balance of tradeoffs. I
think it's a better API going forward, whilst also not being too
disruptive as the APIs it affects are very "deep" ones that we don't
typically encourage people to use.
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
- [x] `galaxy brain` — Architectural changes
### Release Note
#### Breaking changes
##### 1. History Options
Previously, some (not all!) commands accepted a history options object
with `squashing`, `ephemeral`, and `preserveRedoStack` flags. Squashing
enabled/disabled a memory optimisation (storing individual commands vs
squashing them together). Ephemeral stopped a command from affecting the
undo/redo stack at all. Preserve redo stack stopped commands from wiping
the redo stack. These flags were never available consistently - some
commands had them and others didn't.
In this version, most of these flags have been removed. `squashing` is
gone entirely (everything squashes & does so much faster than before).
There were a couple of commands that had a special default - for
example, `updateInstanceState` used to default to being `ephemeral`.
Those maintain the defaults, but the options look a little different now
- `{ephemeral: true}` is now `{history: 'ignore'}` and
`{preserveRedoStack: true}` is now `{history:
'record-preserveRedoStack'}`.
If you were previously using these options in places where they've now
been removed, you can use wrap them with `editor.history.ignore(fn)` or
`editor.history.batch(fn, {history: 'record-preserveRedoStack'})`. For
example,
```ts
editor.nudgeShapes(..., { ephemeral: true })
```
can now be written as
```ts
editor.history.ignore(() => {
editor.nudgeShapes(...)
})
```
##### 2. Automatic recording
Previously, only commands (e.g. `editor.updateShapes` and things that
use it) were added to the undo/redo stack. Everything else (e.g.
`editor.store.put`) wasn't. Now, _everything_ that touches the store is
recorded in the undo/redo stack (unless it's part of
`mergeRemoteChanges`). You can use `editor.history.ignore(fn)` as above
if you want to make other changes to the store that aren't recorded -
this is short for `editor.history.batch(fn, {history: 'ignore'})`
When upgrading to this version of tldraw, you shouldn't need to change
anything unless you're using `store.put`, `store.remove`, or
`store.applyDiff` outside of `store.mergeRemoteChanges`. If you are, you
can preserve the functionality of those not being recorded by wrapping
them either in `mergeRemoteChanges` (if they're multiplayer-related) or
`history.ignore` as appropriate.
##### 3. Side effects
Before this diff, any changes in side-effects weren't captured by the
undo-redo stack. This was actually the motivation for this change in the
first place! But it's a pretty big change, and if you're using side
effects we recommend you double-check how they interact with undo/redo
before/after this change. To get the old behaviour back, wrap your side
effects in `editor.history.ignore`.
##### 4. Mark options
Previously, `editor.mark(id)` accepted two additional boolean
parameters: `onUndo` and `onRedo`. If these were set to false, then when
undoing or redoing we'd skip over that mark and keep going until we
found one with those values set to true. We've removed those options -
if you're using them, let us know and we'll figure out an alternative!
2024-04-24 18:26:10 +00:00
|
|
|
// @internal (undocumented)
|
|
|
|
getType(typeName: string): RecordType<R, any>;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
|
|
|
migratePersistedRecord(record: R, persistedSchema: SerializedSchema, direction?: 'down' | 'up'): MigrationResult<R>;
|
|
|
|
// (undocumented)
|
2023-06-27 13:59:07 +00:00
|
|
|
migrateStoreSnapshot(snapshot: StoreSnapshot<R>): MigrationResult<SerializedStore<R>>;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
readonly migrations: Record<string, MigrationSequence>;
|
2023-04-25 11:01:25 +00:00
|
|
|
// (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
serialize(): SerializedSchemaV2;
|
|
|
|
// @deprecated (undocumented)
|
2023-04-25 11:01:25 +00:00
|
|
|
serializeEarliestVersion(): SerializedSchema;
|
|
|
|
// (undocumented)
|
2024-04-15 12:53:42 +00:00
|
|
|
readonly sortedMigrations: readonly Migration[];
|
|
|
|
// (undocumented)
|
2023-04-25 11:01:25 +00:00
|
|
|
readonly types: {
|
|
|
|
[Record in R as Record['typeName']]: RecordType<R, any>;
|
|
|
|
};
|
|
|
|
// (undocumented)
|
|
|
|
validateRecord(store: Store<R>, record: R, phase: 'createRecord' | 'initialize' | 'tests' | 'updateRecord', recordBefore: null | R): R;
|
|
|
|
}
|
|
|
|
|
|
|
|
// @public (undocumented)
|
2023-05-24 11:25:41 +00:00
|
|
|
export type StoreSchemaOptions<R extends UnknownRecord, P> = {
|
2024-04-15 12:53:42 +00:00
|
|
|
createIntegrityChecker?: (store: Store<R, P>) => void;
|
2023-04-25 11:01:25 +00:00
|
|
|
onValidationFailure?: (data: {
|
|
|
|
error: unknown;
|
|
|
|
phase: 'createRecord' | 'initialize' | 'tests' | 'updateRecord';
|
2024-04-15 12:53:42 +00:00
|
|
|
record: R;
|
2023-04-25 11:01:25 +00:00
|
|
|
recordBefore: null | R;
|
2024-04-15 12:53:42 +00:00
|
|
|
store: Store<R>;
|
2023-04-25 11:01:25 +00:00
|
|
|
}) => R;
|
2024-04-15 12:53:42 +00:00
|
|
|
migrations?: MigrationSequence[];
|
2023-04-25 11:01:25 +00:00
|
|
|
};
|
|
|
|
|
2024-05-14 09:42:41 +00:00
|
|
|
// @public
|
|
|
|
export class StoreSideEffects<R extends UnknownRecord> {
|
|
|
|
constructor(store: Store<R>);
|
|
|
|
// @internal (undocumented)
|
|
|
|
handleAfterChange(prev: R, next: R, source: 'remote' | 'user'): void;
|
|
|
|
// @internal (undocumented)
|
|
|
|
handleAfterCreate(record: R, source: 'remote' | 'user'): void;
|
|
|
|
// @internal (undocumented)
|
|
|
|
handleAfterDelete(record: R, source: 'remote' | 'user'): void;
|
|
|
|
// @internal (undocumented)
|
|
|
|
handleBeforeChange(prev: R, next: R, source: 'remote' | 'user'): R;
|
|
|
|
// @internal (undocumented)
|
|
|
|
handleBeforeCreate(record: R, source: 'remote' | 'user'): R;
|
|
|
|
// @internal (undocumented)
|
|
|
|
handleBeforeDelete(record: R, source: 'remote' | 'user'): boolean;
|
|
|
|
// @internal (undocumented)
|
|
|
|
handleOperationComplete(source: 'remote' | 'user'): void;
|
|
|
|
// @internal (undocumented)
|
|
|
|
isEnabled(): boolean;
|
|
|
|
// @internal
|
|
|
|
register(handlersByType: {
|
|
|
|
[T in R as T['typeName']]?: {
|
|
|
|
afterChange?: StoreAfterChangeHandler<T>;
|
|
|
|
afterCreate?: StoreAfterCreateHandler<T>;
|
|
|
|
afterDelete?: StoreAfterDeleteHandler<T>;
|
|
|
|
beforeChange?: StoreBeforeChangeHandler<T>;
|
|
|
|
beforeCreate?: StoreBeforeCreateHandler<T>;
|
|
|
|
beforeDelete?: StoreBeforeDeleteHandler<T>;
|
|
|
|
};
|
|
|
|
}): () => void;
|
|
|
|
registerAfterChangeHandler<T extends R['typeName']>(typeName: T, handler: StoreAfterChangeHandler<R & {
|
|
|
|
typeName: T;
|
|
|
|
}>): () => void;
|
|
|
|
registerAfterCreateHandler<T extends R['typeName']>(typeName: T, handler: StoreAfterCreateHandler<R & {
|
|
|
|
typeName: T;
|
|
|
|
}>): () => void;
|
|
|
|
registerAfterDeleteHandler<T extends R['typeName']>(typeName: T, handler: StoreAfterDeleteHandler<R & {
|
|
|
|
typeName: T;
|
|
|
|
}>): () => void;
|
|
|
|
registerBeforeChangeHandler<T extends R['typeName']>(typeName: T, handler: StoreBeforeChangeHandler<R & {
|
|
|
|
typeName: T;
|
|
|
|
}>): () => void;
|
|
|
|
registerBeforeCreateHandler<T extends R['typeName']>(typeName: T, handler: StoreBeforeCreateHandler<R & {
|
|
|
|
typeName: T;
|
|
|
|
}>): () => void;
|
|
|
|
registerBeforeDeleteHandler<T extends R['typeName']>(typeName: T, handler: StoreBeforeDeleteHandler<R & {
|
|
|
|
typeName: T;
|
|
|
|
}>): () => void;
|
|
|
|
registerOperationCompleteHandler(handler: StoreOperationCompleteHandler): () => void;
|
|
|
|
// @internal (undocumented)
|
|
|
|
setIsEnabled(enabled: boolean): void;
|
|
|
|
}
|
|
|
|
|
2023-06-27 12:25:55 +00:00
|
|
|
// @public (undocumented)
|
|
|
|
export type StoreSnapshot<R extends UnknownRecord> = {
|
|
|
|
schema: SerializedSchema;
|
2024-04-15 12:53:42 +00:00
|
|
|
store: SerializedStore<R>;
|
2023-06-27 12:25:55 +00:00
|
|
|
};
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// @public (undocumented)
|
2023-05-24 11:25:41 +00:00
|
|
|
export type StoreValidator<R extends UnknownRecord> = {
|
2023-04-25 11:01:25 +00:00
|
|
|
validate: (record: unknown) => R;
|
2024-02-20 12:35:25 +00:00
|
|
|
validateUsingKnownGoodVersion?: (knownGoodVersion: R, record: unknown) => R;
|
2023-04-25 11:01:25 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// @public (undocumented)
|
2023-05-24 11:25:41 +00:00
|
|
|
export type StoreValidators<R extends UnknownRecord> = {
|
2023-04-25 11:01:25 +00:00
|
|
|
[K in R['typeName']]: StoreValidator<Extract<R, {
|
|
|
|
typeName: K;
|
|
|
|
}>>;
|
|
|
|
};
|
|
|
|
|
2023-05-24 11:25:41 +00:00
|
|
|
// @public (undocumented)
|
2023-06-03 20:46:53 +00:00
|
|
|
export type UnknownRecord = BaseRecord<string, RecordId<UnknownRecord>>;
|
2023-05-24 11:25:41 +00:00
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
// (No @packageDocumentation comment for this package)
|
|
|
|
|
|
|
|
```
|