npm.io
0.3.0 • Published yesterday

react-native-watch-connectivity-pro

Licence
MIT
Version
0.3.0
Deps
0
Size
219 kB
Vulns
0
Weekly
0

React Native Watch Connectivity Pro

npm npm

A React Native library for seamless communication between iOS apps and Apple Watch applications.

For a terser, machine-readable description of the real API surface and its quirks (written for LLM coding agents), see llms.txt. Where the two disagree, trust llms.txt — it is the more tightly-maintained source of truth.

Features

  • Two-way messaging between iPhone and Apple Watch
  • Application context synchronization
  • Reliable file transfers with progress tracking
  • Complication data updates
  • Session state monitoring (reachability, etc.)
  • Automatic message queuing when watch is unavailable
  • Support for handling responses from the watch
  • Robust error handling & recovery
  • Enhanced reachability detection when app returns from background
  • Active ping mechanism to wake up watch connectivity
  • Comprehensive TypeScript definitions
  • Queue management API for monitoring and controlling message delivery

Current Status

This package works for basic messaging between React Native iOS apps and Apple Watch. The following features are currently working:

  • Session initialization and activation
  • Sending messages from phone to watch
  • Receiving messages from watch to phone
  • Application context synchronization
  • Session state monitoring
  • Message queueing when watch is unavailable
  • Automatic queue processing when watch becomes available
  • Queue management (status checking, manual processing, clearing)
  • Enhanced reachability detection for background/foreground transitions

See TODO.md for a complete overview of implemented and planned features.

Installation

npm install react-native-watch-connectivity-pro
# or
yarn add react-native-watch-connectivity-pro
iOS
cd ios && pod install

Setup Requirements

  1. Create a watchOS App Extension Your iOS app must include a watchOS app extension. See Apple's documentation on Creating a watchOS App.

  2. App Groups Configure app groups for your main app and watch extension to share data. See Configuring App Groups.

  3. Initialize Early in App Lifecycle For best results, initialize the connectivity service early in your app's lifecycle, ideally in your main App.js/App.tsx file.

  4. Use NativeEventEmitter Always use React Native's NativeEventEmitter to set up event listeners.

Usage

Initializing the Watch Connectivity Service
import React, { useEffect } from 'react';
import { NativeEventEmitter, NativeModules } from 'react-native';
import WatchConnectivity from 'react-native-watch-connectivity-pro';

function App() {
  useEffect(() => {
    // Initialize watch connectivity early in the app lifecycle
    const initWatchConnectivity = async () => {
      try {
        const status = await WatchConnectivity.initSession();
        console.log('Watch connectivity initialized:', status);
      } catch (error) {
        console.error('Failed to initialize watch connectivity:', error);
      }
    };
    
    initWatchConnectivity();
    
    // Set up event listeners using NativeEventEmitter
    const eventEmitter = new NativeEventEmitter(NativeModules.RNWatchConnectivityPro);
    
    const messageSubscription = eventEmitter.addListener(
      'messageReceived',
      (event) => {
        console.log('Received message from watch:', event.message);
      }
    );
    
    // Listen for queue updates
    const queueUpdateSubscription = eventEmitter.addListener(
      'messageQueueUpdated',
      (queueStatus) => {
        console.log('Message queue updated:', queueStatus);
      }
    );
    
    return () => {
      // Clean up subscriptions
      messageSubscription.remove();
      queueUpdateSubscription.remove();
    };
  }, []);
  
  // Rest of your app
  return (
    // ...
  );
}
Sending Messages to Watch

sendMessage resolves void on success. If the session isn't activated or the watch isn't reachable, it rejects with error code 'QUEUED' — and as of the reliability fixes, the message really has been added to the in-memory retry queue in that case (previously the plain sendMessage path silently dropped it; only the reply-handler path enqueued). Treat QUEUED as "accepted for later delivery", not a hard failure:

// Send a simple message to the watch
const sendMessageToWatch = async () => {
  try {
    await WatchConnectivity.sendMessage(
      {
        type: 'test',
        message: 'Hello from React Native!',
        timestamp: Date.now() / 1000
      },
      (response) => {
        console.log('Watch replied:', response);
      }
    );
    console.log('Message sent successfully');
  } catch (error: any) {
    if (error?.code === 'QUEUED') {
      console.log('Watch unavailable — message was queued for later delivery');
    } else {
      console.error('Failed to send message:', error);
    }
  }
};

Outbound payloads (this call, updateApplicationContext, transferUserInfo, transferCurrentComplicationUserInfo, and replies via replyToMessage) are automatically passed through the package's plistSafe sanitizer before they reach native — see Payload sanitization below. You do not need to strip undefined/null/NaN yourself.

Receiving Messages from Watch

messageReceived fires for more than just interactive watch→phone messages — it's also re-emitted for every received user-info transfer and every received application context, so you can listen to a single event for "something arrived from the watch." Every event body carries a source field telling you which channel it came from: 'message', 'userInfo', or 'applicationContext'. In Debug builds with file logging enabled (the default in Debug), a one-off diagnostic message with source: 'test' is also emitted the first time checkWatchConnectivityStatus() runs — filter it out, or rely on it never appearing in a Release build (file logging defaults to OFF there).

// Using the NativeEventEmitter
const eventEmitter = new NativeEventEmitter(NativeModules.RNWatchConnectivityPro);

// Listen for messages from the watch
const messageSubscription = eventEmitter.addListener(
  'messageReceived',
  (event) => {
    if (event.source === 'test') return; // debug-only diagnostic, ignore

    // Check if the message has a reply handler
    if (event.hasReplyHandler && event.handlerId) {
      // Get the message content
      const message = event.message;
      console.log(`Received message from watch (source=${event.source}):`, message);
      
      // Reply to the watch if needed
      WatchConnectivity.replyToMessage(
        event.handlerId,
        { 
          acknowledged: true,
          message: 'Message received!',
          timestamp: Date.now() / 1000
        }
      ).then(() => {
        console.log('Reply sent successfully');
      }).catch(error => {
        console.error('Failed to send reply:', error);
      });
    } else {
      // Process message without reply handler
      console.log('Received message from watch (no reply):', event.message);
    }
  }
);

// Don't forget to remove the listener when not needed
// messageSubscription.remove();

Note on reply handlers: if you receive a message with hasReplyHandler / handlerId and never call replyToMessage, the handler now expires after 30 seconds and the watch automatically gets a default reply ({"error": "reply timeout", "acknowledged": false}) instead of the callback leaking forever.

Managing the Message Queue

When the watch is not reachable, messages sent via sendMessage are automatically added to an in-memory retry queue owned by the native module instance. The queue is automatically drained when the watch becomes reachable again (on session activation, on a sessionWatchStateDidChange or sessionReachabilityDidChange callback reporting reachable, or via a manual processMessageQueue() call), in batches of 5 messages, 1 second apart. You can also inspect and manage it directly:

// Get the current queue status
const checkQueueStatus = async () => {
  try {
    const status = await WatchConnectivity.getQueueStatus();
    console.log('Queue status:', status);
    // status: { count: number, isProcessing: boolean, oldestMessage: number, newestMessage: number }
    // oldestMessage/newestMessage are epoch-SECOND timestamps of when those
    // messages were enqueued (0 when the queue is empty) — plain numbers,
    // not objects.

    if (status.count > 0) {
      console.log(`${status.count} messages waiting for watch to become available`);
      console.log(`Oldest message queued at: ${new Date(status.oldestMessage * 1000)}`);
    }
  } catch (error) {
    console.error('Failed to get queue status:', error);
  }
};

// Manually force-drain the queue (normally happens automatically when the watch becomes reachable)
const processQueue = async () => {
  try {
    const result = await WatchConnectivity.processMessageQueue();
    console.log(`Processed ${result.processed} queued messages`);
    // result is just { processed: number } — if the session isn't ready or
    // the queue is empty, `processed` is simply 0; there's no `reason` field.
  } catch (error) {
    console.error('Failed to process queue:', error);
  }
};

// Clear the message queue
const clearQueue = async () => {
  try {
    const cleared = await WatchConnectivity.clearMessageQueue();
    // Resolves a boolean (true), not a count — the queue was reset.
    console.log('Message queue cleared:', cleared);
  } catch (error) {
    console.error('Failed to clear queue:', error);
  }
};

Reliability notes:

  • The queue is in-memory only — it does NOT survive app termination or a crash. "Offline queueing" means "queued until the watch is reachable or the app process ends, whichever comes first." For data that absolutely must arrive (auth tokens, critical one-off payloads), prefer transferUserInfo, which is OS-guaranteed and survives your app dying.
  • A drain that fails partway through (e.g. the watch drops reachability mid-batch) now requeues the failed messages instead of losing them, and stops draining rather than hot-looping against an unreachable watch.
  • The bare native sendMessage path and the sendMessage-with-reply-handler path both enqueue on failure — there's no longer a silent-drop difference between them.
Detecting Watch Reachability and Queue Processing

You can monitor watch reachability and queue status to update your UI accordingly. Note that initSession() resolves the full WatchState object, not a bare boolean, and watchStateUpdated nests its payload under watchState — there is no separate queueProcessingUpdate event; queue changes are reported via messageQueueUpdated (body: {count, isProcessing, oldestMessage, newestMessage}):

useEffect(() => {
  // Initialize the watch connectivity service
  const initWatchConnectivity = async () => {
    try {
      const state = await WatchConnectivity.initSession();
      setIsWatchReachable(state.isReachable);

      // If watch is reachable, check if there are queued messages to process
      if (state.isReachable) {
        const queueStatus = await WatchConnectivity.getQueueStatus();
        if (queueStatus.count > 0) {
          console.log(`Found ${queueStatus.count} queued messages, processing...`);
          await WatchConnectivity.processMessageQueue();
        }
      }
    } catch (error) {
      console.error('Failed to initialize watch connectivity:', error);
    }
  };
  
  initWatchConnectivity();
  
  // Listen for watch reachability / session-state changes.
  // Fires on activation, deactivate/inactive, sessionWatchStateDidChange, AND
  // (since the reliability fixes) sessionReachabilityDidChange — so a pure
  // reachability flip with no other state change now reaches JS promptly and
  // triggers a queue drain, instead of requiring a poll.
  const eventEmitter = new NativeEventEmitter(NativeModules.RNWatchConnectivityPro);

  const watchStateSubscription = eventEmitter.addListener(
    'watchStateUpdated',
    (event) => {
      const { isReachable, isPaired, isComplicationEnabled, queuedMessages } = event.watchState;
      setIsWatchReachable(isReachable);

      // Watch just became reachable - the library will automatically
      // process any queued messages, but you can also handle it yourself
      if (isReachable) {
        console.log('Watch is now reachable, messages will be delivered');
      } else {
        console.log(`Watch is not reachable, ${queuedMessages ?? 0} message(s) queued`);
      }
    }
  );

  // Listen for queue changes directly
  const queueUpdateSubscription = eventEmitter.addListener(
    'messageQueueUpdated',
    ({ count, isProcessing }) => {
      setIsProcessingQueue(isProcessing);
      console.log(`Queue changed: ${count} message(s) pending, processing=${isProcessing}`);
    }
  );

  return () => {
    watchStateSubscription.remove();
    queueUpdateSubscription.remove();
  };
}, []);
Best Practices for Message Queueing
  1. Message Format: When sending messages, always include a timestamp and message type for easier management:
await WatchConnectivity.sendMessage({
  type: 'DATA_UPDATE', 
  payload: yourData,
  timestamp: Date.now() / 1000
});
  1. Prioritize Important Messages: getQueueStatus() only returns aggregate info (count, isProcessing, and the enqueue timestamps of the oldest/newest entries) — it does NOT expose the contents or type of individual queued messages, so you cannot selectively clear "messages of this type." If a queue backlog means older messages are now stale, the practical pattern is to clear the whole queue and re-send the current state:
// When sending a new critical message that supersedes everything queued so far
const sendCriticalUpdate = async (data) => {
  try {
    const status = await WatchConnectivity.getQueueStatus();

    if (status.count > 3) {
      // Backlog is large enough that older queued messages are probably stale.
      await WatchConnectivity.clearMessageQueue();
      console.log('Cleared stale messages from queue');
    }

    // Now send the latest critical update
    await WatchConnectivity.sendMessage({
      type: 'CRITICAL_UPDATE',
      payload: data,
      timestamp: Date.now() / 1000
    });

    console.log('Critical update sent (or queued if watch unreachable)');
  } catch (error: any) {
    if (error?.code !== 'QUEUED') {
      console.error('Failed to send critical update:', error);
    }
  }
};
  1. Handling Long-Disconnected Watches: If your app needs to handle scenarios where the watch might be disconnected for extended periods:
const syncWithWatch = async () => {
  try {
    // Check watch state first (checkWatchConnectivityStatus re-activates the
    // session if needed and drains the queue if the watch turns out reachable)
    const watchState = await WatchConnectivity.checkWatchConnectivityStatus();

    if (!watchState.isPaired) {
      // Watch is not paired at all - nothing to do
      console.log('No watch paired with this iPhone');
      return;
    }
    
    if (!watchState.isReachable) {
      // Watch is paired but not reachable
      console.log('Watch is paired but not reachable');
      
      // Check queue status
      const queueStatus = await WatchConnectivity.getQueueStatus();
      
      if (queueStatus.count > 10) {
        // Too many queued messages, consider clearing old ones
        console.log('Queue has too many messages, clearing old messages');
        await WatchConnectivity.clearMessageQueue();
        
        // Send only the latest app state instead
        await WatchConnectivity.updateApplicationContext({
          lastSyncTime: Date.now() / 1000,
          criticalData: getLatestAppData(),
          syncStatus: 'QUEUE_CLEARED_RESYNCING'
        });
        
        console.log('Updated application context with latest state');
      }
    } else {
      // Watch is reachable, we can send messages normally
      console.log('Watch is reachable, sending updates');
      // Regular message sending...
    }
  } catch (error) {
    console.error('Error during watch sync:', error);
  }
};
Application Context
// Update application context
const updateContext = async () => {
  try {
    await WatchConnectivity.updateApplicationContext({
  currentScreen: 'home',
  lastUpdated: Date.now(),
  userSettings: { theme: 'dark', notifications: true }
    });
    console.log('Context updated successfully');
  } catch (error) {
    console.error('Failed to update context:', error);
  }
};

// Get current application context
const getContext = async () => {
  try {
    const context = await WatchConnectivity.getApplicationContext();
    console.log('Current context:', context);
  } catch (error) {
    console.error('Failed to get context:', error);
  }
};
Watch Complications

Complications allow your app to display information directly on the Apple Watch face. The WatchConnectivity framework provides methods to update complications with high-priority transfers.

// Check if your app's complication is enabled on any watch face
const isEnabled = await WatchConnectivity.isComplicationEnabled();

// Check how many updates you have left for the day
// Apple limits the number of updates per day to preserve battery life
const remainingTransfers = await WatchConnectivity.getRemainingComplicationTransfers();

// Update the complication with new data
// This is delivered with higher priority than regular messages
const result = await WatchConnectivity.transferCurrentComplicationUserInfo({
  value: 75,           // Display value
  unit: 'kW',          // Display unit
  trend: 'up',         // Trend direction
  timestamp: Date.now() // When the data was updated
});

// The result contains:
// {
//   id: '1234-5678-...',              // Unique transfer ID
//   timestamp: 1234567890,            // When the transfer was initiated
//   isCurrentComplicationInfo: true,  // Confirmation this is a complication update
//   userInfo: {...}                   // The data you sent
// }

Your WatchKit app will need to implement the proper delegate methods to receive these updates and refresh the complication:

// In your ComplicationController.swift (watchOS app)
func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
    // List all your complications here
    let descriptors = [
        CLKComplicationDescriptor(identifier: "com.yourapp.complication", 
                                 displayName: "YourApp", 
                                 supportedFamilies: [.modularSmall, .graphicCircular])
    ]
    handler(descriptors)
}

// In your ExtensionDelegate.swift (watchOS app)
func didReceiveComplicationUserInfo(_ userInfo: [String : Any]) {
    // Request an update for all complications
    let server = CLKComplicationServer.sharedInstance()
    for complication in server.activeComplications ?? [] {
        server.reloadTimeline(for: complication)
    }
}

API Reference

The package has a single default export — there is no named WatchConnectivity export, so import it as a default:

import WatchConnectivity from 'react-native-watch-connectivity-pro';

// Activate/query the session — there is no separate isAvailable()/getSessionState().
// initSession() and checkWatchConnectivityStatus() both resolve the full WatchState.
const state = await WatchConnectivity.initSession();
console.log(state.isPaired, state.isReachable, state.isWatchAppInstalled);

// Send a message (rejects with code 'QUEUED' if the watch isn't reachable —
// the message is added to the retry queue in that case, not dropped)
await WatchConnectivity.sendMessage({
  command: 'update',
  value: 42
});

// Update application context
await WatchConnectivity.updateApplicationContext({
  lastUpdated: Date.now(),
  settings: { /* ... */ }
});

// Transfer file
const fileTransfer = await WatchConnectivity.transferFile(
  '/path/to/file.jpg',
  { type: 'image', timestamp: Date.now() }
);

// Watch complications
const isEnabled = await WatchConnectivity.isComplicationEnabled();
const remaining = await WatchConnectivity.getRemainingComplicationTransfers();
await WatchConnectivity.transferCurrentComplicationUserInfo({
  value: 75,
  unit: 'kW', 
  timestamp: Date.now()
});

// Toggle native file logging (Documents/watch_connectivity.log). Defaults to
// ON in Debug builds, OFF in Release — most apps never need to call this.
await WatchConnectivity.setFileLoggingEnabled(true);
Payload sanitization

Every outbound send path — sendMessage, updateApplicationContext, transferUserInfo, transferCurrentComplicationUserInfo, replyToMessage (and sendAuthTokensToWatch, which calls transferUserInfo under the hood) — automatically runs your payload through the package's plistSafe sanitizer before it reaches native. You do not need to hand-roll your own stripping logic. plistSafe is also exported directly if you want to pre-sanitize a payload yourself (e.g. before diffing it against a previous context):

import { plistSafe } from 'react-native-watch-connectivity-pro';

const safe = plistSafe({ a: 1, b: undefined, c: NaN, d: [1, null, Infinity, 2] });
// => { a: 1, d: [1, 2] }

Semantics: undefined/null are stripped everywhere (object values and array elements); non-finite numbers (NaN, Infinity, -Infinity) are stripped, including when they appear as array elements; Date instances are preserved as-is (a supported plist/NSDate type); other primitives pass through untouched. Arrays keep their remaining elements densely packed — dropped entries are removed, not left as holes — because WCSession payloads cannot contain NSNull in an array either.

Common Issues & Troubleshooting

Messages not being received

If you're sending messages from the watch but not receiving them in your React Native app:

  1. Ensure you're using the NativeEventEmitter and not directly calling addListener on the module
  2. Initialize the watch connectivity early in your app lifecycle
  3. Make sure the delegate methods are properly implemented in the native module (they are in this package)
  4. Check that your watch app is properly sending messages
Handling Background/Foreground Transitions

Watch connectivity can report false negatives when your app transitions from background to foreground - meaning the watch may be reachable but the iOS API initially reports it as unreachable. This library includes enhanced functionality to handle this:

  1. Enhanced Reachability Detection: The getReachability method now includes an active ping mechanism that attempts to wake up the WCSession when the app returns from background. This only works if your watch app actually replies to incoming messages — the ping is a real sendMessage call with a 300ms reply timeout, and it's the watch's reply (any reply, content doesn't matter) that flips pingSucceeded to true. If your watchOS WCSessionDelegate doesn't implement session(_:didReceiveMessage:replyHandler:) (or never calls the reply handler), the ping mechanism can never succeed and getReachability/checkWatchConnectivityStatus will just fall back to the raw isReachable value after the timeout (fromTimeout: true).
// Check watch reachability with enhanced detection
const checkWatchStatus = async () => {
  try {
    const status = await WatchConnectivity.getReachability();
    console.log('Watch reachability status:', status);
    
    if (status.pingSucceeded) {
      console.log('Watch detected through ping mechanism!');
      // The watch was initially reported as unreachable, but our ping found it
    }
    
    return status.isReachable;
  } catch (error) {
    console.error('Failed to check watch status:', error);
    return false;
  }
};
  1. Force Reactivation When App Resumes: Implement this pattern in your app to ensure reliable watch connectivity:
import { AppState } from 'react-native';

// In your component
useEffect(() => {
  const handleAppStateChange = async (nextAppState) => {
    // When app comes to foreground
    if (nextAppState === 'active') {
      console.log('App came to foreground, checking watch connectivity');
      
      // Force a connectivity check which will use the enhanced ping mechanism
      const status = await WatchConnectivity.checkWatchConnectivityStatus();
      
      if (status.isReachable) {
        console.log('Watch is reachable after app resumed');
        // Refresh your watch data here
      } else {
        console.log('Watch not reachable after resuming app');
      }
    }
  };
  
  // Subscribe to app state changes
  const subscription = AppState.addEventListener('change', handleAppStateChange);
  
  return () => {
    subscription.remove();
  };
}, []);
  1. Multiple Retry Strategy: For the most robust implementation, use a retry pattern:
const sendDataToWatch = async (data) => {
  // Try up to 3 times
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      // First check reachability - this uses our enhanced ping mechanism
      const status = await WatchConnectivity.getReachability();
      
      if (!status.isReachable) {
        console.log(`Watch not reachable on attempt ${attempt + 1}, waiting...`);
        // Wait for 300ms before retrying
        await new Promise(resolve => setTimeout(resolve, 300));
        continue;
      }
      
      // Send the data
      await WatchConnectivity.sendMessage({
        type: 'apiData',
        data: data,
        timestamp: Date.now()
      });
      
      console.log('Successfully sent data to watch');
      return true;
    } catch (error) {
      console.log(`Error on attempt ${attempt + 1}:`, error);
      if (attempt < 2) {
        // Wait before retrying
        await new Promise(resolve => setTimeout(resolve, 300));
      }
    }
  }
  
  console.log('Failed to send data to watch after multiple attempts');
  return false;
};
Handling Intermittent Connectivity

This library handles intermittent connectivity automatically by:

  1. Queueing messages when the watch is not reachable
  2. Automatically sending queued messages when the watch becomes reachable
  3. Providing APIs to monitor and manage the queue

To handle watch connectivity changes in your UI:

// Listen for watch state changes
const watchStateSubscription = eventEmitter.addListener(
  'watchStateUpdated',
  (event) => {
    const { watchState } = event;
    console.log('Watch state changed:', watchState);
    
    // Update UI based on watch state
    if (watchState.isReachable) {
      // Show "Connected" status
    } else {
      // Show "Disconnected" status
    }
    
    // Check if there are queued messages
    if (watchState.queuedMessages > 0) {
      console.log(`${watchState.queuedMessages} messages queued`);
    }
  }
);
Implementation Details of Enhanced Reachability Detection

For developers interested in how the enhanced reachability detection works:

  1. The Problem: Apple's WatchConnectivity framework's isReachable property can report false negatives when an app returns from background to foreground, even when the watch app is active and running.

  2. Our Solution:

    a. Active Ping Approach: Instead of relying only on the passive isReachable property, our enhanced implementation:

    • First checks the standard isReachable property
    • If it returns false, we attempt to send a small "ping" message to the watch
    • If the ping succeeds, we know the watch is actually reachable despite what isReachable reports
    • We use a short timeout to prevent hanging the app if the watch is truly unreachable

    b. Reactivation Logic: When detecting that the app has come to the foreground from background:

    • We force the WCSession to reactivate if needed
    • We proactively check connectivity status using our ping approach
    • We adjust a short delay to allow the WCSession to properly establish
  3. Internal Sequence:

    1. App comes to foreground
    2. AppState change is detected
    3. checkWatchConnectivityStatus is called
    4. Enhanced getReachability method is triggered
    5. If isReachable is false, a ping message attempts to wake up the connection
    6. The result includes information about whether the ping succeeded
    7. The app can then make an informed decision about sending data

This approach solves the common issue where the watch appears to be unreachable immediately after the app returns from background, even though the watch app is open and running.

Session not activating

If the session isn't activating properly:

  1. Make sure both the iOS app and Watch app are installed and paired
  2. The Watch app must be installed from the app running on the phone
  3. Check watch reachability status before sending messages

Versioning policy

This package follows semver strictly on its public contract, not just its type signatures: any change to a method name, an event name, an event payload shape, or a rejection error code is a MAJOR version bump. Adding new optional fields to an existing event body or result object, or adding a new method/event, is MINOR. Bug fixes that don't change the shape of the public contract (e.g. the queue-drain reliability fixes) are PATCH. See CHANGELOG.md for the release history and llms.txt for the exhaustive, quirk-by-quirk description of current behavior.

License

MIT

Keywords