Skip to content

Added modules for IndexedDb#1240

Open
SandroMaglione wants to merge 2 commits intoEffect-TS:mainfrom
SandroMaglione:sandromaglione/indexed-db
Open

Added modules for IndexedDb#1240
SandroMaglione wants to merge 2 commits intoEffect-TS:mainfrom
SandroMaglione:sandromaglione/indexed-db

Conversation

@SandroMaglione
Copy link

Type

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update

Description

Porting of the IndexedDb PR from v3 to v4 (original source).

@SandroMaglione
Copy link
Author

A few adjustments are still needed for the v4 migrations.

@tim-smart what's the Effectable (CommitPrototype) equivalent in v4? What about Effect.runtime() and Fiber.RuntimeFiber (inside IndexedDbDatabase.ts)?

@gcanti IndexedDbQuery was using Schema.Struct.Constructor (source) and IndexedDbTable was defined from Schema.TypeId, can you point me to the equivalents in v4?

Copy link

@lewxdev lewxdev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a non-exhaustive feedback on the current changes.
@tim-smart please feel free to review these comments before @SandroMaglione makes any changes

Comment on lines +94 to +111
/** @internal */
const IDBFlatKey = Schema.Union([
Schema.String,
Schema.Number,
Schema.Date,
Schema.declare(
(input): input is BufferSource =>
input instanceof ArrayBuffer || ArrayBuffer.isView(input),
),
]);

/**
* Schema for `IDBValidKey` (`number | string | Date | BufferSource | IDBValidKey[]`).
*
* @since 1.0.0
* @category schemas
*/
export const IDBValidKey = Schema.Union([IDBFlatKey, Schema.Array(IDBFlatKey)]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This can be simplified by using Schema.suspend to define the recursive schema.
  • According to the standard (IndexedDB API 3.0), only a subset of the types provided are valid keys. Notably these types differ:
    • number (excluding NaN)
    • Date (excluding any invalid Date)
  • Effect 4.0 does not currently have an equivalent for Schema.NonNaN, a Schema.isNonNaN filter could be added to further simplify this check.
  • Schema.mutable is used because the built-in DOM declaration defines IDBValidKey with a mutable type IDBValidKey[]. Alternatively, a type may be defined with readonly IDBValidKey[] (Readonly<IDBValidKey> errors)
Suggested change
/** @internal */
const IDBFlatKey = Schema.Union([
Schema.String,
Schema.Number,
Schema.Date,
Schema.declare(
(input): input is BufferSource =>
input instanceof ArrayBuffer || ArrayBuffer.isView(input),
),
]);
/**
* Schema for `IDBValidKey` (`number | string | Date | BufferSource | IDBValidKey[]`).
*
* @since 1.0.0
* @category schemas
*/
export const IDBValidKey = Schema.Union([IDBFlatKey, Schema.Array(IDBFlatKey)]);
/**
* @since 1.0.0
* @category schemas
*/
export const IDBValidKey: Schema.Schema<IDBValidKey> = Schema.Union([
Schema.String,
Schema.Number.check(Schema.makeFilter((input) => !Number.isNaN(input))),
Schema.DateValid,
Schema.declare((input): input is BufferSource =>
input instanceof ArrayBuffer ||
ArrayBuffer.isView(input) && input.buffer instanceof ArrayBuffer
),
Schema.mutable(Schema.Array(Schema.suspend(() => IDBValidKey))),
]);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use Schema.Finite for finite numbers. I think it excludes nan too

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid using suspend here and keep it as it was.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tim-smart

You can use Schema.Finite for finite numbers. I think it excludes nan too

Schema.Finite would be too restrictive since it excludes Infinity and -Infinity, which are valid keys according to the standard.

I would avoid using suspend here and keep it as it was.

No suspend is fine as it's functionally equivalent.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK if infinite is permitted then the nan check is fine.

suspend works, but you end up with a better schema ast without it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IndexedDB is effectively a NoSQL database, and I think using vocabulary like "Table" is incorrect. This module would be more accurate renamed to IndexedDbObjectStore. ("Table" makes sense if you have columns)

Note that the "Table" vocabulary is also used throughout and should be updated as well.

Comment on lines +88 to +92
export const AutoIncrement = Schema.Number.annotate({
identifier: "AutoIncrement",
title: "autoIncrement",
description: "Defines a valid autoIncrement key path for the IndexedDb table",
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the standard (IndexedDB API 3.0), the autoIncrement key generator must be a positive integer not greater than 253. That check has been added here.

Suggested change
export const AutoIncrement = Schema.Number.annotate({
identifier: "AutoIncrement",
title: "autoIncrement",
description: "Defines a valid autoIncrement key path for the IndexedDb table",
});
export const AutoIncrement = Schema.Number
.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 2 ** 53 }))
.annotate({
identifier: "AutoIncrement",
title: "autoIncrement",
description: "Defines a valid autoIncrement key path for the IndexedDb table",
});

Copy link
Collaborator

@tim-smart tim-smart Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 2 ** 53 }))

Comment on lines +29 to +33
export interface IndexedDb {
readonly [TypeId]: TypeId;
readonly indexedDB: globalThis.IDBFactory;
readonly IDBKeyRange: typeof globalThis.IDBKeyRange;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on other Effect modules, it may make more sense to expose effectful service functions rather than the built-in interface. For example, as a rough sketch:

export interface IndexedDbFactory {
  readonly open: (name: string, version: number) => Effect.Effect<...>;
  readonly deleteDatabase: (name: string) => Effect.Effect<...>;
  readonly cmp: (first: IDBValidKey, second: IDBValidKey) => Effect.Effect<...>;
  readonly databases: Effect.Effect<...>;
}

export interface IndexedDbKeyRange {
  readonly only: (value: IDBValidKey) => Effect.Effect<...>;
  readonly lowerBound: (lower: IDBValidKey, open?: boolean) => Effect.Effect<...>;
  readonly upperBound: (upper: IDBValidKey, open?: boolean) => Effect.Effect<...>;
  readonly bound: (lower: IDBValidKey, upper: IDBValidKey, lowerOpen?: boolean, upperOpen?: boolean) => Effect.Effect<...>;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep the service lean and then potentially expose another service that further extends it with effectful methods. Keeps it easily injectable for tests etc

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The effectful service could be added at a later date.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but with the current implementation it's not possible to create an index or key on a deeply nested field.
This is a big limitation that could be addressed by taking an annotation-driven approach to defining ObjectStore schemas (although this may not work well with the query builder unless some TypeScript wizardry is employed, but maybe that's an okay tradeoff).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants