Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/web-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Web test

# Runs the browser tests for the web implementation of objectbox
# (objectbox/lib/src/web/, tests in web_test/) with dart2js and dart2wasm.
# Avoid duplicate builds for pull requests, allow manual trigger.
on:
push:
branches:
- main
- dev
- web-support
pull_request:
workflow_dispatch:

# Minimal access by default
permissions:
contents: read

defaults:
run:
shell: bash

env:
# Keep aligned with test.yml.
DART_VERSION: 3.12.1 # Available versions: https://dart.dev/get-dart/archive

jobs:
web-test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: dart-lang/setup-dart@v1
with:
sdk: ${{ env.DART_VERSION }}
- name: Install dependencies
working-directory: web_test
run: dart pub get
- name: Generate binding code
working-directory: web_test
run: dart run build_runner build
- name: Test with dart2js (Chrome)
working-directory: web_test
run: dart test -p chrome
- name: Test with dart2wasm (Chrome)
working-directory: web_test
run: dart test -p chrome -c dart2wasm
40 changes: 5 additions & 35 deletions flutter_libs/lib/objectbox_flutter_libs.dart
Original file line number Diff line number Diff line change
@@ -1,39 +1,9 @@
/// This package contains platform-specific native libraries for flutter.
/// See the actual library implementation in package "objectbox".
library objectbox_flutter_libs;

import 'dart:io';

import 'package:flutter/services.dart';
import 'package:objectbox/objectbox.dart';
import 'package:path_provider/path_provider.dart';

/// Returns the default database directory inside this Flutter app's
/// `getApplicationDocumentsDirectory()`.
///
/// Note: on desktop platforms this returns a directory in the users documents
/// directory. It is advised to not use this then and instead create a directory
/// named specifically for your app.
Future<Directory> defaultStoreDirectory() async => Directory(
'${(await getApplicationDocumentsDirectory()).path}/${Store.defaultDirectoryPath}',
);
/// On the web platform there are no native libraries; the web variant provides
/// compatible no-op implementations so that generated code compiles.
library;

const _platform = MethodChannel("objectbox_flutter_libs");

/// If your Flutter app runs on Android 6 (or older) devices, call this before
/// using any ObjectBox APIs, to fix loading the native ObjectBox library.
///
/// If the device is running Android 6 (or older) this will try to load the
/// native library using Java APIs. Afterwards, calling ObjectBox APIs should
/// load the library successfully on the Dart/Flutter side.
///
/// See the [GitHub issue for details](https://github.com/objectbox/objectbox-dart/issues/369).
Future<void> loadObjectBoxLibraryAndroidCompat() async {
if (!Platform.isAndroid) {
// To support calling this in multi-platform Flutter apps
// do nothing if not Android (plugins for other platforms do not
// implement method below).
return;
}
await _platform.invokeMethod<String>('loadObjectBoxLibrary');
}
export 'src/objectbox_flutter_libs_native.dart'
if (dart.library.js_interop) 'src/objectbox_flutter_libs_web.dart';
35 changes: 35 additions & 0 deletions flutter_libs/lib/src/objectbox_flutter_libs_native.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'dart:io';

import 'package:flutter/services.dart';
import 'package:objectbox/objectbox.dart';
import 'package:path_provider/path_provider.dart';

/// Returns the default database directory inside this Flutter app's
/// `getApplicationDocumentsDirectory()`.
///
/// Note: on desktop platforms this returns a directory in the users documents
/// directory. It is advised to not use this then and instead create a directory
/// named specifically for your app.
Future<Directory> defaultStoreDirectory() async => Directory(
'${(await getApplicationDocumentsDirectory()).path}/${Store.defaultDirectoryPath}',
);

const _platform = MethodChannel("objectbox_flutter_libs");

/// If your Flutter app runs on Android 6 (or older) devices, call this before
/// using any ObjectBox APIs, to fix loading the native ObjectBox library.
///
/// If the device is running Android 6 (or older) this will try to load the
/// native library using Java APIs. Afterwards, calling ObjectBox APIs should
/// load the library successfully on the Dart/Flutter side.
///
/// See the [GitHub issue for details](https://github.com/objectbox/objectbox-dart/issues/369).
Future<void> loadObjectBoxLibraryAndroidCompat() async {
if (!Platform.isAndroid) {
// To support calling this in multi-platform Flutter apps
// do nothing if not Android (plugins for other platforms do not
// implement method below).
return;
}
await _platform.invokeMethod<String>('loadObjectBoxLibrary');
}
22 changes: 22 additions & 0 deletions flutter_libs/lib/src/objectbox_flutter_libs_web.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:objectbox/objectbox.dart';

/// Stand-in for `dart:io`'s `Directory` on the web platform, where there is no
/// real file system. Only carries the [path] that generated `openStore()` code
/// passes to the [Store] constructor, which uses it as a logical database
/// name on web.
class WebStoreDirectory {
/// The logical database name.
final String path;

/// Wrap a logical database name.
const WebStoreDirectory(this.path);
}

/// Returns the default database location on web: there are no directories,
/// so this is just [Store.defaultDirectoryPath] used as a logical name.
Future<WebStoreDirectory> defaultStoreDirectory() async =>
const WebStoreDirectory(Store.defaultDirectoryPath);

/// Does nothing on web (there is no native library to load). See the native
/// variant of this function for details.
Future<void> loadObjectBoxLibraryAndroidCompat() async {}
26 changes: 19 additions & 7 deletions generator/lib/src/code_chunks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class CodeChunks {

import 'dart:typed_data';

import 'package:flat_buffers/flat_buffers.dart' as fb;
import 'package:objectbox/flatbuffers.dart' as fb;
import 'package:objectbox/internal.dart' as $obxInt; // generated code can access "internal" functionality
import 'package:objectbox/objectbox.dart' as $obx;${pubspec?.obxFlutterImport}

Expand Down Expand Up @@ -85,14 +85,16 @@ class CodeChunks {
bool queriesCaseSensitiveDefault = true,
String? macosApplicationGroup})${obxFlutter ? ' async' : ''} {
${obxFlutter ? 'await loadObjectBoxLibraryAndroidCompat();' : ''}
return $obx.Store(getObjectBoxModel(),
final store = $obx.Store(getObjectBoxModel(),
directory: directory${obxFlutter ? ' ?? (await defaultStoreDirectory()).path' : ''},
maxDBSizeInKB: maxDBSizeInKB,
maxDataSizeInKB: maxDataSizeInKB,
fileMode: fileMode,
maxReaders: maxReaders,
queriesCaseSensitiveDefault: queriesCaseSensitiveDefault,
macosApplicationGroup: macosApplicationGroup);
${obxFlutter ? '// On web the store loads persisted data asynchronously.\n await store.ready;' : ''}
return store;
}''';
}

Expand All @@ -114,20 +116,30 @@ class CodeChunks {
lastIndexId: ${createIdUid(model.lastIndexId)},
lastRelationId: ${createIdUid(model.lastRelationId)},
lastSequenceId: ${createIdUid(model.lastSequenceId)},
retiredEntityUids: const ${model.retiredEntityUids},
retiredIndexUids: const ${model.retiredIndexUids},
retiredPropertyUids: const ${model.retiredPropertyUids},
retiredRelationUids: const ${model.retiredRelationUids},
retiredEntityUids: ${createUidList(model.retiredEntityUids)},
retiredIndexUids: ${createUidList(model.retiredIndexUids)},
retiredPropertyUids: ${createUidList(model.retiredPropertyUids)},
retiredRelationUids: ${createUidList(model.retiredRelationUids)},
modelVersion: ${model.modelVersion},
modelVersionParserMinimum: ${model.modelVersionParserMinimum},
version: ${model.version});
''';
}

static String createIdUid(IdUid value) {
return 'const $obxInt.IdUid(${value.id}, ${value.uid})';
// Emitted as a string parsed at runtime: UIDs are 64-bit values that
// regularly exceed 2^53 and can't be written as integer literals in code
// that is compiled to JavaScript (dart2js). See issue #185 (web support).
return "$obxInt.IdUid.fromString('${value.id}:${value.uid}')";
}

/// Like [createIdUid]: retired UID lists are 64-bit values emitted as
/// runtime-parsed strings so generated code compiles with dart2js.
static String createUidList(List<int> uids) =>
uids.isEmpty
? 'const []'
: "[${uids.map((uid) => "int.parse('$uid')").join(', ')}]";

static String createModelEntity(ModelEntity entity) {
var additionalArgs = '';
if (entity.externalName != null) {
Expand Down
53 changes: 53 additions & 0 deletions objectbox/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,58 @@
## latest

* Web platform support, phase 3 ([#185](https://github.com/objectbox/objectbox-dart/issues/185)):
queries. `box.query()` now works on web: a pure-Dart evaluator over the
condition tree running against the in-memory records. Supported: all
property conditions (string with case-sensitivity flags, integer, double,
bool, date/dateNano, byte vector, string vector `containsElement`,
`isNull`/`notNull`, `oneOf`/`notOneOf`, `between`), and/or groups,
`order()` (descending, caseSensitive, nullsLast, nullsAsZero, unsigned),
offset/limit, `find`/`findFirst`/`findUnique`/`findIds`/`count`/`remove`/
`stream` (+ async variants), query parameters (`param()` with alias
support), property queries (min/max/sum/average/count/find with distinct
and case sensitivity), relation links (`link`, `backlink`, `linkMany`,
`backlinkMany`, nested links, `relationCount`), `watch()` streams, and
vector search (`nearestNeighborsF32` with `findWithScores`/
`findIdsWithScores`) evaluated as an exact brute-force scan - Euclidean
(squared), cosine and dot-product distances; Geo is not supported on web.
Note: on web queries scan the in-memory records; indexes are not used yet.
* Web platform support, phase 2 ([#185](https://github.com/objectbox/objectbox-dart/issues/185)):
a working database on web. `Store` and `Box` are now fully functional in the
browser: an in-memory engine (keeping the synchronous ObjectBox API) that is
persisted to IndexedDB with a write-behind queue. Supported: all CRUD
operations and `PutMode` semantics, monotonic ID assignment, `@Unique`
enforcement (including `ConflictStrategy.replace`), `ToOne`/`ToMany`
relations and backlinks, `runInTransaction` with rollback on error,
`store.watch<T>()`/`entityChanges` streams, `Store.attach`, in-memory
databases via `memory:` directories, and persistence across sessions
(including the unique index and ID sequences). Not yet supported on web:
queries (phase 3), `Store.fromReference`, isolates (`runAsync` runs on the
same thread), Sync and Admin.
- New: `Store.ready`, a future that completes when the store has loaded its
persisted data; completes immediately on native platforms. The generated
`openStore()` of Flutter apps awaits it automatically; pure Dart apps on
web should `await store.ready` after constructing the store.
- Durability note: on web, writes are applied to memory synchronously and
flushed to IndexedDB within a microtask; a browser crash may lose the very
last writes. `Store.close()` flushes the queue.
- The minimum Dart SDK is now 3.4 (dart:js_interop / package:web).
- FlatBuffers on web use a vendored copy of `flat_buffers` with
JavaScript-safe 64-bit integer handling (dart2js does not support
`ByteData.get/setInt64`); native platforms keep using the
`flat_buffers` package unchanged via the new conditional export
`package:objectbox/flatbuffers.dart`, which generated code now imports.
* Web platform support, phase 1 ([#185](https://github.com/objectbox/objectbox-dart/issues/185)):
the package (including code generated by `objectbox_generator` and the
`objectbox_flutter_libs` helpers) now compiles for the web platform, with both
dart2js and dart2wasm. There is no database implementation on web yet: all
database operations throw `UnsupportedError` for now. `Admin.isAvailable()`,
`Sync.isAvailable()` and `Store.isOpen()` return `false` on web so existing
guard code keeps working. Native platforms are unchanged.
* Generator: emit model IDs/UIDs as `IdUid.fromString(...)` and retired UID
lists as runtime-parsed strings instead of integer literals, which cannot be
compiled to JavaScript for values above 2^53 (UIDs are random 64-bit
values).

## 5.3.2 (2026-05-20)

* Update ObjectBox database for Flutter Linux/Windows, Dart Native apps to [5.3.2-2026-05-05](https://github.com/objectbox/objectbox-c/releases/tag/v5.3.2)
Expand Down
10 changes: 10 additions & 0 deletions objectbox/lib/flatbuffers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// The FlatBuffers implementation used by ObjectBox and its generated code.
///
/// On native platforms this is `package:flat_buffers` as-is. On web it is a
/// vendored copy with JavaScript-safe 64-bit integer handling, because
/// dart2js does not support `ByteData.get/setInt64` (see
/// `src/web/flatbuffers/`).
library;

export 'package:flat_buffers/flat_buffers.dart'
if (dart.library.js_interop) 'src/web/flatbuffers/flat_buffers.dart';
10 changes: 6 additions & 4 deletions objectbox/lib/internal.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/// This library serves as an entrypoint for generated code and objectbox tools.
/// Don't import into your own code, use 'objectbox.dart' instead.
library objectbox_internal;
library;

// Note: OBXVectorDistanceType and OBXHnswFlags are now exported via
// modelinfo/index.dart (enums.dart) instead of the ffigen bindings, and
// InternalStoreAccess via the platform-conditional store facade, so that this
// library never pulls dart:ffi into a web build.
export 'src/modelinfo/index.dart';
export 'src/native/bindings/flatbuffers_readers.dart';
export 'src/native/bindings/flexbuffers.dart';
export 'src/native/bindings/objectbox_c.dart'
show OBXVectorDistanceType, OBXHnswFlags;
export 'src/native/store.dart' show InternalStoreAccess;
export 'src/relations/info.dart';
export 'src/relations/to_many.dart'
show InternalToManyAccess, InternalToManyTestAccess;
export 'src/store.dart' show InternalStoreAccess;
4 changes: 2 additions & 2 deletions objectbox/lib/objectbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
/// with strong ACID semantics.
///
/// Read the [Getting Started](https://docs.objectbox.io/getting-started) guide.
library objectbox;
library;

export 'src/admin.dart' show Admin;
export 'src/annotations.dart';
export 'src/box.dart' show Box, PutMode;
export 'src/common.dart';
export 'src/modelinfo/enums.dart' show OBXSyncFlags;
export 'src/native/query/vector_search_results.dart';
export 'src/query.dart'
show
Query,
Expand Down Expand Up @@ -55,3 +54,4 @@ export 'src/sync.dart'
SyncState,
SyncLoginEvent;
export 'src/transaction.dart' show TxMode;
export 'src/vector_search_results.dart';
2 changes: 1 addition & 1 deletion objectbox/lib/src/admin.dart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export 'native/admin.dart' if (dart.library.html) 'web/admin.dart';
export 'native/admin.dart' if (dart.library.js_interop) 'web/admin.dart';
14 changes: 13 additions & 1 deletion objectbox/lib/src/box.dart
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
export 'native/box.dart' if (dart.library.html) 'web/box.dart';
export 'native/box.dart' if (dart.library.js_interop) 'web/box.dart';

/// Box put (write) mode.
enum PutMode {
/// Insert (if given object's ID is zero) or update an existing object.
put,

/// Insert a new object.
insert,

/// Update an existing object, fails if the given ID doesn't exist.
update,
}
2 changes: 1 addition & 1 deletion objectbox/lib/src/model.dart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export 'native/model.dart' if (dart.library.html) 'web/model.dart';
export 'native/model.dart' if (dart.library.js_interop) 'web/model.dart';
14 changes: 1 addition & 13 deletions objectbox/lib/src/modelinfo/entity_definition.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:ffi';
import 'dart:typed_data';

import 'package:flat_buffers/flat_buffers.dart' as fb;
import '../../flatbuffers.dart' as fb;

import '../relations/info.dart';
import '../relations/to_many.dart';
Expand Down Expand Up @@ -31,15 +30,4 @@ class EntityDefinition<T> {
required this.toManyRelations});

Type type() => T;

/// Shortcut that creates a [ByteData] view and passes it to [objectFromFB].
T objectFromData(Store store, Pointer<Uint8> data, int size) {
// There has been a performance improvement in the past using memcpy for
// small buffers, but this is not longer faster than asTypedList.
// See /benchmark/bin/native_pointers.dart.
final uInt8List = data.asTypedList(size);
final byteData =
ByteData.view(uInt8List.buffer, uInt8List.offsetInBytes, size);
return objectFromFB(store, byteData);
}
}
Loading